Metal was drawn with metalness 1 in the room environment at half strength with one sun, so metal groups came out nearly black. The game's metal keeps the vertex color (lit, plus a reflection tinted by it), so metal is now part metal with a stronger reflection, the environment is brighter and a sky over ground fill lights the sides away from the sun. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
91 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 becomesUGCSERVERDIR/UGCC<datacenter>/(the datacenter id isDATACENTERIDinboot.cfg) and files are<folder><type folder><datacenter><blueprint id><.lxfml|.nif|.hkx|.dds>.gz: the file, gzip compressed. The type folder is3DOPTIMIZED/for NIF and HKX,IMAGE128DDS/for DDS and nothing for LXFML.- the same name with
.checksuminstead 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.
UGCUSE3DSERVICES=7:0(the client's default, without 3D services). The client asks its world for each file's MD5 and size (REQUEST_UGC_MANIFEST_INFO, answered withUGC_MANIFEST_RESPONSE, below) and downloadsUGCSERVERDIR/BrickModels/UserMade/<id % 1000, 3 digits>/<id, 20 digits><.lxfml|.nif|.hkx|.dds>.sd0: the file as an sd0 stream (the bytessd00x01 0xff, then chunks of a u32 size and zlib data, each inflating to at most 256 KiB). It inflates it, saves it asres/BrickModels/UserMade/<id % 1000>/<id><ext>and checks its MD5 against the world's answer; a file it has already is only downloaded again when its MD5 differs. HTTP 408 makes it try again later. The worlds answer from checksums the UGC server stores (ugc_file_checksums), and the UGC server serves the.sd0files; see "Without 3D services" below for what keeps this off by default.
The manifest packets (checked in the client)
Both are plain packets (not game messages); offsets are after the 0x53 byte and the header (connection type, message id, a padding byte).
| Packet | Layout |
|---|---|
REQUEST_UGC_MANIFEST_INFO (world message 27), client to world |
u64 blueprint id, u8 resource type (16 bytes after the 0x53). Sent by SendRequestUGCManifestInfoPacket when the client needs a blueprint's file and 3D services are off. |
UGC_MANIFEST_RESPONSE (client message 60), world to client |
u64 blueprint id, u8 resource type, then 21 bytes of manifest info: u8 valid, u32 size of the inflated file, 16 bytes MD5 of it. The client ignores an answer that isn't exactly 37 bytes after the 0x53 (PacketHandler_MSG_CLIENT_UGC_MANIFEST_RESPONSE). |
The answer is cached per blueprint and type (the same message the 3D services .checksum download produces). With
it, the client uses a file it has when its MD5 matches and downloads it otherwise; with valid 0 it uses a file it has as
it is and downloads it only when it has none. Structs: WorldPackets::RequestUgcManifestInfo,
ClientPackets::UgcManifestResponse (eUgcResourceType).
The blueprint id of an inventory item's icon (LWOInventoryComponent_Client::LoadBlueprintIcon) is the item's
blueprintid config when it has one (Brick-by-Brick models, LOT 6662), else its subkey (cars and rockets: their
ugc_modular_build id, which DLU gives them as subkey when they are built). Cars and rockets from before builds were
stored had subkey 0 and no build row, so the client never asked for their icons: see "Cars and rockets from before
builds were stored" below.
A placed model (LOT 14) always loads its blueprint's NIF, HKX and LXFML through these requests; see "Models without 3D services" for how the worlds answer them.
Client settings (boot.cfg) for a server at 203.0.113.5 with the default port, with 3D services:
UGCUSE3DSERVICES=7:1,
UGCSERVERIP=0:203.0.113.5,
UGCSERVERPORT=1:2008,
UGCSERVERDIR=0:/ugc,
DATACENTERID=1:150,
and without them (the client's default mode; ugc_manifest=1 on the server):
UGCSERVERIP=0:203.0.113.5,
UGCSERVERPORT=1:2008,
UGCSERVERDIR=0:/ugc,
Every line of boot.cfg needs its trailing comma. One line without it (e.g. AUTHSERVERPORT=1:1500) makes the
client reject the whole file: its log says "No boot configuration found; it's likely that the working directory is
incorrect." and it silently uses its built-in defaults for everything (the patch and UGC servers at
http://127.0.0.1:80/lwoclient). The client reads boot.cfg only when it starts. UGCSERVERIP, UGCSERVERPORT and
UGCSERVERDIR default to PATCHSERVERIP, PATCHSERVERPORT and PATCHSERVERDIR + /UserBrickModels
(LWOResMgr2Interface::DownloadThread_Run, 0x01058aa0). A UGC server on the same address and port as the patch server
shares its connection ("UGC site Info: Sharing main download connection" in the client's log); otherwise the log shows
"UGC site Info: Host '...' - Connected '...'".
What the 1.10.64 client does in practice (checked in the client)
- With
UGCUSE3DSERVICES=7:1the client downloads a property's models (.lxfml.checksum,3DOPTIMIZED/*.nif.checksumand*.hkx.checksumfor each). Every failed download is reported to the world asUgcDownloadFailed(world message 120), and the models then don't show (the server makes no.hkx, so this mode isn't usable for models). - With
UGCUSE3DSERVICES=7:0(the client's own default) the client builds the models itself from the LXFML the world sends, and property models load. Opening the backpack's models asks the world for each car's, rocket's and model's icon manifest. Withugc_manifest=1the world answers (checked: the answers are read and the client goes on to download each icon fromBrickModels/UserMade/<bucket>/<id>.dds.sd0under itsUGCSERVERDIR). - Earlier tests concluded that the client ignores the
UGCSERVER*andPATCHSERVER*lines ofboot.cfgand always downloads fromhttp://127.0.0.1:80/lwoclient. That was wrong: the test clients'boot.cfghad a line without its trailing comma, so the client rejected the whole file and used its defaults (see the settings above). The UGC server still serves/lwoclient/UserBrickModels/...as well asclient_path, for clients left on the defaults. - A download that can't connect (HTTP status 0) counts as a UGC connection failure, and the client logs the player out
for it ("connection failed downloading UGC assets",
MainThread_LogoutDueToConnectionFailures, 0x0102b930). Any other failure (404, ...) only loses that file and is reported asUgcDownloadFailed. - A NIF the UGC server makes, put in place of one of the game's own models and spawned, renders in the client with its
colors: the NIFs are written like the game's
res/BrickModels/ndmadefiles (nif.xml's 20.3.0.9, user version 0; every shape with a material, alpha blending by the vertex alpha, specular off and vertex colors, in that order). - The server logs every client download with its answer (
Client download <path> -> <status>), and answers the LXFML from the database, so it is never waited for or evicted.
Processing
Player models (ugc rows) are made the way LU Toolbox (the Blender add-on the community makes LU models with) makes
them: its importer, Process Model, Bake Lighting (AO only) and its icon renderer, with its defaults as the settings'
defaults. The table below goes through it step by step.
At a glance, one model: queued in the database (after the owner's quiet period, staff requests first) -> read the
LXFML -> build each level of detail from the client's brick primitives -> colors, transparency and variation ->
remove faces nobody can see -> bake ambient occlusion into the vertex colors -> sort bricks into shader groups
(plastic, transparent, metal, brushed steel, glow, glitter) -> write the NIF -> draw the icon from that NIF -> store
the files (.gz/.checksum and .sd0), their checksums and the stats -> tell the worlds (UGC_MODELS_MADE), which
tell the clients showing the model. A worker thread does steps 1 to 7; the main thread does the queue, the database
and the notices (see Threads). Cars and rockets are made once per combination of modules (icon only).
-
The LXFML is read from
ugc.lxfml(an sd0 stream). Parts come fromBricks/Brick/Part(LXFML 5: row-major rotation and translation per bone) orScene/Model/Group/Part(LXFML 4: axis angle). -
Each level of detail in
lods(default0,2, as LU Toolbox imports) is built from the client's LDD primitives,res/brickprimitives/lod<n>/<design>.g,.g1, ... (read like the game does: loose files first, then the client's packs, so packed clients and bricks added to them work) (sub-partiuses the part'si-th material, material 0 meaning the part's first). Colors come from LU Toolbox's palette (color_palette=lu_toolbox;brickdbuses the client'sMaterials.xml): LU's colors, the LDD colors LU doesn't have mapped onto the nearest LU one, colors LU Toolbox doesn't know but the client'sMaterials.xmlhas (colors added to the brick database) fromMaterials.xml, unknown ones black. A brick is transparent only when all of its materials are; transparent bricks gettransparent_opacity(58.82%).transparent_colors(default 129;none: no colors) names colors that are transparent whateverMaterials.xmlsays (129, "Tr. Bright Bluish Violet with Glitter", has alpha 255 there); a color named there getstransparent_opacity.color_brightness(percent, default 100: unchanged) scales the models' colors after the variation below, not the icons'. -
Color variation: each material of each brick has its brightness shifted like LU Toolbox's "Apply Color Variation": the color's HSV value is taken to a 1/2.224 gamma, moved by a random amount of up to
color_variation/200 (5%: 0.025) either way, times the color's own amount (black 0.4, orange 1.5, ...), clamped and taken back; hue and saturation stay. The random number comes from the model's id, the brick's index and the material, so making a model again gives the same colors, and every LOD the same (LU Toolbox restarts its random sequence per LOD). -
Faces nobody can see are removed from the opaque bricks (they're rendered from 42 directions and triangles that never show are dropped; transparent bricks hide nothing and aren't touched; see "Hidden faces" below).
hsr_ground_plane=1also drops what can only be seen from below. -
Ambient occlusion is baked like LU Toolbox's Bake Lighting with AO Only: 64 rays per vertex (
ao_samples) over the hemisphere around the vertex normal (cosine weighted, the same pattern every time, so a model made again comes out the same), each blocked when it hits an opaque triangle within 5 (ao_distance); the vertex's occlusion is the share of rays that get out, and its light1 - ao_strength * (1 - occlusion)(default strength 1), multiplied into the vertex color in linear space (UgcRender::BakeAo). It is ray-cast visibility, not path tracing: no light bounces, no color bleeding, as Cycles' AO-only bake. Transparent bricks are neither baked nor occlude; glowing colors add their glow times 6 (Glow Strength 3 x Glow Multiplier 2). The model alone is used: other models on the property and the terrain don't occlude it. The light is multiplied into the vertex colors (the NIF has one color set; LU Toolbox keeps it in a "Lit" layer beside "Col"). -
The meshes are written as a Gamebryo 20.3.0.9 NIF (user version 0, the client's own version) laid out like LU Toolbox's exports and the game's own brick models (
res/BrickModels/ndmade): the rootSceneNode_Model, anNiLODNodeS01_Opaque_Model(andS01_Alpha_Modelfor transparent bricks) withNiRangeLODDataholding each level's distances (LU Toolbox's: with LODs 0 and 2, 0-100 and 100-10000), a nodeLOD_<n>per level and its shapes under it, named like the group. Bricks with another look get groups of their own, drawn with the client's shaders (S<id>_Metal_Model,_Brushed_Model,_Glow_Model,_Glitter_Model,_GlitterAlpha_Model; satin stays inS01_Alphawith its own opacity and whitening): see "Metal and glow", "Glitter" and "Satin" below. Shapes have vertex colors, a white material and, when transparent, alpha blending. Opaque shapes are divided at 65535 vertices along their longest side (LU Toolbox's divide_mesh); transparent bricks are one shape each unlesscombine_transparent=1. Vertices are in LDD's Y-up space with identity transforms, like the game's own brick models. -
The icon is drawn from the finished
.nif: it is read back with the same reader as the client's files (NifFile, LOD 0) and rasterized, so it shows exactly what the game shows, with the color variation, the faces that were removed and the lighting baked into the vertex colors (so the icon adds no occlusion of its own). The camera and framing is LU Toolbox's icon renderer's (its UGC render add-on's BrickBuild scene: a 50 mm lens, 39.6 degrees, from 53.4 degrees around and 19.5 above, framed at 1.03, the sun from 21 degrees around and 50.3 above). The light (world light, a fill from the camera, the sun with soft shadows, a highlight, exposure and contrast) is set so the icons are as bright as the game's own model icons (res/textures/ui/inventory/models: mean luminance 120 of 255 over 150 of them; ours 118 on a set of player models). Drawn by a software rasterizer (no GPU, no display), 4x4 supersampled, on a transparent background:icon.pngfor the dashboard and anicon.ddsfor the client, written like the client's own 128x128 icons (DXT5, no mipmaps, header flags 0x81007 with the linear size, caps 0x1000).The light settings are
icon_world_light,icon_sun_light,icon_fill,icon_specular,icon_shininess,icon_exposure,icon_contrast,icon_shadow_strengthandicon_ao_strength(new names: the oldericon_ambient,icon_sun_strengthandicon_shadowslines of existing ugcconfig.ini files, with the darker values, are no longer read). Every framing and light value (key,icon_*setting, range, default) is listed once inUgcIconParams; the settings, the dashboard's settings page and its icon editor are built from that list. Values come from the settings, then the kind's preset (a car or rocket build type from the client'sModularBuildComponent; player models have none: each is a different size and shape, so its icon is fitted to it from the settings, and the dashboard refuses a player model preset), then the item's own (a model, or a combination of car or rocket modules), the last two inugc_icon_settings.The pose. The camera, the model's turn and the crop are worked out in
UgcIconPose(shared with the dashboard's editor, see below), in this order: the model is turned about its origin bymodelYaw(around +Y), thenmodelPitch(around +X), thenmodelRoll(around +Z), i.e. R = Ry * Rx * Rz (three.js's Euler orderYXZ; settingsicon_model_yaw,icon_model_pitch,icon_model_roll, all 0 by default so icons stay as they were). A car or rocket is first turned by its build type'sAdditionalModelRotation. The camera then looks at the centre of the turned model's bounds fromyaw(around +Y, from +Z towards +X) andpitch(up), as far away as makes the bounding sphere fill the field of viewfov(a perspective projection, the camera's up is +Y). Last the picture is cropped to the model's projected bounds: scaled so their larger side fills the icon divided bymargin(1 fills it, more leaves a border), centred, then moved byoffsetXandoffsetY(shares of the icon's width and height). The camera's distance follows from the field of view and the border, and the shift is the target moved in the picture, so these parameters describe every pose the icon can have. -
stats.jsonrecords the bricks, each LOD's triangles before and after hidden faces were removed, vertices, shapes, how long each step took and the settings used;model.noao.nifis LOD 0 before the lighting bake. Both, and the icon and mesh of the version before (previous.*), are for the dashboard's viewer.
Matching LU Toolbox
| LU Toolbox step (default) | UGC server |
|---|---|
Import LXFML: LXFML 4/5, brickprimitives/lod<n>, sub-part materials, 0 = the part's first, missing bricks skipped |
Same |
| Import LODs 0, 2 (LOD 1 off, LOD 3 doesn't exist in the client) | Same (lods=0,2); a design missing from a level uses the next more detailed one |
| Import: flex parts (several bones) bent per bone | Not done: flex parts are placed by their first bone |
Import: custom normals from the .g files |
Same |
| Import: LU palette colors, LDD colors mapped to LU ones, unknown ones black (26) | Same (color_palette=lu_toolbox) |
| Import: random scale for seams (its factor is 0, so none) | Same (none) |
| Keep UVs off (no UVs; decorations aren't imported) | Same: no UVs, no decorations |
| Combine Objects on (opaque bricks joined per LOD), Combine Transparent off | Same (combine_transparent=0) |
| Reset Orientation / Correct Orientation (Blender's Z-up) | Equivalent: LDD's Y-up with identity transforms, as the game's own brick models |
| Correct Colors off (the importer's colors are the palette already) | Same |
| Apply Color Variation on, 5%, per color amounts (CUSTOM_VARIATION) | Same (color_variation=5), stable per model, brick and material |
| Transparent Opacity 58.82% | Same (transparent_opacity) |
| Vertex color layers Col, Lit, Alpha (1), Glow | One color set: Col times Lit (glow added to Lit); alpha is the opacity |
| Setup Bake Material (VertexColor / VertexColorTransparent) | Equivalent: white NiMaterialProperty, vertex colors as ambient and diffuse, NiAlphaProperty on transparent shapes |
| Remove Hidden Faces: Cycles bakes with an overexposed world (VC pre-pass 32 samples, tris to quads, 5 pixels between vertices, 8 samples, threshold 0.01), autoremove, transparent bricks hidden | Not the same: depth renders from 42 directions (hsr_resolution), transparent bricks hidden and untouched; faces LU Toolbox keeps because only bounced light reaches them (insides seen through openings, recesses) are removed (see "Hidden faces") |
| Use Ground Plane off | Same (hsr_ground_plane=0) |
| Split objects over 65536 vertices (divide_mesh, along the longest side, linked parts together) | Same, also keeping each shape under 65535 triangles (the format's limit) |
Setup LOD data: SceneNode, NiLODNode per shape name, LOD nodes, near/far by the levels there are, S01_Opaque_/S01_Alpha_ names cut at 60 |
Same (lod_distance_0..3, lod_cull, shader_opaque); LU Toolbox's glow, metal and superemissive shader settings are unused by it too; the UGC server's own metal and glow groups are opt in (see below) |
| Bake Lighting, AO Only: 64 AO samples, distance 5, transparent skipped, glow strength 3 x 2, smooth vertex colors | Same (ao_samples, ao_distance, glow_strength); smoothing averages a vertex's corners, and the occlusion is per vertex already |
| NifTools export for LU: 20.3.0.9, user version 0 | Same |
Physics (.hkx) |
Intentionally not done: .hkx requests answer 404, so the client makes its own |
| Icon: LOD 0 imported again with its own color corrections (white and black toned down) and no color variation | Different on purpose: the icon is drawn from the generated .nif (LOD 0), so it matches the game, variation and baked lighting included; no icon-only color corrections |
| Icon: Bevel Edges and Subdivide | Not done (the rasterizer draws the bricks as they are) |
| Icon: principled materials (roughness 0.16), hashed transparency, Cycles | Approximated: world light, camera fill, sun with soft shadow-mapped shadows and a highlight, exposure and contrast, matched to the game's own icons' brightness; no bounced light; transparent bricks sorted and blended |
| Icon scene BrickBuild / Car: 50 mm lens, camera 53.4 / 19.5 degrees, sun at 21 / 50.3, 128 px, framing 1.03, transparent film | Same framing (icon_*); the light is brighter, to match the game's icons |
| Icon scene Rocket: 35 mm lens, other angles, two suns | Not built in; a preset for the rocket build type can be set in the icon editor |
Hidden faces
UgcHsr::RemoveHiddenFaces (remove_hidden_faces=1, default) renders each LOD's opaque mesh from 42 directions around
the whole model (an icosahedron's corners and edge centres, UgcRender::VisibleFromAround), hsr_resolution pixels
square (default 1024), and removes the triangles that show in none of the renders. Faces turned away from a direction
(by their vertex normals) aren't drawn in it; triangles too small or thin to cover a pixel centre are kept when their
centre isn't behind what was drawn, so small visible ones stay. hsr_ground_plane=1 leaves out the directions from
below. Transparent bricks aren't drawn (they hide nothing) and aren't touched.
It sees what is in direct view from outside the model only: faces that only bounced light reaches (insides seen through small openings, deep recesses) are removed, where LU Toolbox's Remove Hidden Faces (a Cycles bake with an overexposed world) keeps them. A version that traced LU Toolbox's paths was tried and removed: on the same models it took 17 to 100 times as long (for example 45 s instead of 2.6 s for a 140-brick model), too heavy for the server.
Processing options
Ways of making models to try and compare, none replacing another. The settings pick the defaults; staff can make
models again with others (the UGC page's options next to Make again, /api/ugc/reprocess with options,
/reprocessproperty [embree|hiprt|embree-gpu] [off|oidn] [native|toolbox-blender] in game, UgcServer --make-model <file> <folder> [options]). The choices are written as their names in any order (UgcProcessOptions in
dCommon/UgcKeys.h); one left out is the setting's. Options stored by earlier versions still read: builtin (the UGC
server's own ray hierarchies, which Embree replaced) is embree, and toolbox and fast (hidden-face methods) are
skipped (toolbox is not toolbox-blender).
| Setting | Choices | What it changes |
|---|---|---|
ray_backend |
embree (default), hiprt, embree-gpu |
What traces the occlusion rays of the bake (and the denoised icons', UgcRays) |
denoise |
off (default), oidn |
How a model's icon gets its occlusion |
processor |
native (default), toolbox-blender |
What makes a player model's files: the UGC server, or LU Toolbox itself in Blender (below) |
embree: Intel Embree 4 (Apache-2.0), fetched and built with the servers (SSE2, AVX and AVX2 kernels picked by the CPU, its own task scheduler, no TBB), on the worker's thread only; Intel and AMD x86 CPUs.hiprt: AMD's HIPRT on AMD and NVIDIA GPUs (below).embree-gpu: Embree on Intel GPUs through SYCL (below). They find the same hits but for rounding; the tests compare the GPU backends with Embree (the same nearest triangle for 99.9% of rays, the same distances, occlusion within 0.002 on average), Embree with rays whose hits are known, and the occlusion with what the hierarchies Embree replaced worked out.oidn: a model's icon is drawn frommodel.noao.nif(its colors before the occlusion bake) with the occlusion traced per pixel of the supersampled icon (denoise_samplesrays from each, default 4, 64 per icon pixel; the bake's distance and strength) and denoised with Intel Open Image Denoise 2 (Apache-2.0), guided by the colors and normals, instead of showing the per-vertex bake. The model keeps its baked occlusion: a denoiser only removes noise that differs from pixel to pixel, and the bake is per vertex (checked: OIDN takes white noise from 17% to 0.6% spread and leaves per-vertex blocks as they are), so there is no image of the bake to denoise and it can't use fewer samples. The denoiser works on a thread of its own; its time is counted as the worker's CPU time. Icons drawn again from stored files use the storedmodel.noao.nifthe same way.
hiprt, embree-gpu and oidn are optional in the build (CMake options, off by default; without them, or without
the GPU, the setting falls back to embree and off, and the UGC server logs why at start; --make-model prints it):
-DDLU_OIDN=ON: an installed OIDN 2 is used when CMake finds it, else Intel's release package (Linux x86-64 and Windows, pinned by hash, with its TBB) is downloaded and its libraries copied next to the servers.-DDLU_HIPRT=ON: needs HIPRT's headers (HIPRT_ROOT, else ROCm's/opt/rocm/include); HIPRT's library is loaded when first used, and HIP (AMD) or CUDA (NVIDIA; when the CUDA toolkit is found at build time) by Orochi (MIT, fetched). AMD RDNA 1 or newer or NVIDIA Maxwell or newer;hiprt_devicepicks the GPU (0: the first; restart to change). The trace kernels are compiled from the headers copied next to the servers the first time (a second or two) and kept incache/hiprt. One GPU context for the process, the workers take turns on it; GPU time isn't CPU time, somax_cpu_percentdoesn't hold it back. A GPU wants many rays at once, so the occlusion rays go in batches.-DDLU_EMBREE_SYCL=ON: Intel Arc and Xe GPUs (Xe-HPG and newer, with Intel's GPU compute runtime) through Embree's SYCL support. Needs a SYCL compiler at build time: Intel oneAPI DPC++ (icpx, found throughONEAPI_ROOTor the path) or the open source DPC++ (clang++,DPCPP_ROOT), orDLU_SYCL_CXXset to one.dUgcServer/EmbreeSyclis built by it as a project of its own (Embree 4.4 with SYCL, linked in statically and bound inside, and the GPU kernels) intolibdlu_embree_syclnext to the servers, which the UGC server loads the first time it is asked for; nothing else is built by the SYCL compiler, and the servers don't link oneAPI. At run time it needs the SYCL runtime (oneAPI'slibsycl, or the one of the open source DPC++ it was built with). The kernels are compiled for the GPU found when first used.embree_gpu_devicepicks the GPU (0: the first Embree supports; restart to change). The same way as hiprt: one GPU for the process, the workers take turns, the occlusion rays in batches.
Every make records what made it: stats.json (settings.rays, denoise, after fallbacks), ugc.made_options and a
row in ugc_process_runs with its times. The UGC page shows each model's in the List view's Options column (and what
its next make will use), and Processing options compared averages per combination: makes, models, time, CPU,
hidden faces', occlusion's and icon's time, bricks and share of triangles removed (GET /api/ugc/options). Compare
combinations on the same models (Make again on a set of models with each); the averages mix whatever models each was
used on. A native make is recorded as before (embree off: the processor isn't named), a Toolbox make as
toolbox-blender.
LU Toolbox in Blender (processor=toolbox-blender)
A way to compare with LU Toolbox exactly: the model is made by LU Toolbox itself, running in a headless Blender, not by a re-implementation of it. Blender, LU Toolbox, the niftools add-on and LU-Toolbox-Standalone stay external programs that the UGC server starts; nothing of them is built into or linked with the servers. Only player models: cars and rockets are made natively whatever the option.
How a model is made (UgcJobs::ProcessModelToolbox, dUgcServer/Toolbox/):
- The worker thread writes the model's LXFML into
toolbox_work_dirand hands it to the Blender worker, which runs LU-Toolbox-Standalone'slu_batch_driver.pysteps (its functions, imported, not copied): LU Toolbox's importer with the LODs inlods, Process Model (its defaults: colors, color variation, Remove Hidden Faces by its Cycles bakes, LOD setup), Bake Lighting, and the niftools.nifexport for LEGO Universe. Each model starts from a fresh Blender scene (factory settings read again, about 0.1 s). - The
.nifis read back withNifFile(every LOD) to check it and count its triangles; the triangles before are the bricks' own meshes built from the same files (as native).model.nifis written with its downloads as a native one. - The icon is drawn from LU Toolbox's
.nifby the UGC server's icon renderer (not denoised: there is no.nifbefore the bake, so nomodel.noao.nifeither). LU-Toolbox-UGC-Render (the icon add-on) isn't used: it needs Blender's user interface (no-b), so a display, which a server doesn't have. stats.jsonhassettings.processortoolbox-blender, the Blender, LU Toolbox and niftools versions and device, and LU Toolbox's steps' times inms:build(import),hiddenSurfaces(all of Process Model),ambientOcclusion(Bake Lighting),export,reset,icon,waited(for Blender),blenderCpu,total. So the comparison table's Hidden faces and Occlusion columns are Process Model and Bake Lighting for these rows.
The Blender worker (UgcToolbox::Worker, dlu_toolbox_worker.py copied next to the servers into ugc-toolbox/) is
started when the first Toolbox model comes and stays up, making one model after another; the UGC workers take turns on
it (one model at a time). It talks JSON lines over its stdin and its original stdout (UgcToolboxProtocol; everything
Blender and the add-ons print goes to toolbox_work_dir/blender.log). It is started again after it crashes (the model
fails with the reason and the log's last lines, and is tried again like any failed model), after a model takes longer
than toolbox_timeout_seconds, after 100 models, and when its settings change. Three failed starts in a row: no new
start for 10 minutes. It runs at worker_nice, with toolbox_threads threads, and its CPU time counts as the worker's
(the make's CPU time, and max_cpu_percent: the waiting worker pauses Blender with SIGSTOP while the budget is
overdrawn). pause_hours and draining hold it as any job (no new models start). It stops with the UGC server (and is
killed if the server dies). /status has toolbox: running, pid, models, starts, versions, last error and why it
can't be used.
When toolbox-blender is asked for (the setting or one make's options) and can't be used — a setting or program
missing (UgcToolbox::Problem), or Windows (not supported yet) — the model is made natively: the UGC server logs why
at start (and when it changes) and for each such make, the make's note says so, and it is recorded as native.
Setting (ugcconfig.ini, dashboard: UGC) |
Default | |
|---|---|---|
processor |
native |
toolbox-blender makes every model with LU Toolbox |
toolbox_blender |
(none) | The Blender executable |
toolbox_standalone_dir |
(none) | LU-Toolbox-Standalone (the folder with lu_batch_driver.py) |
toolbox_scripts_dir |
(none) | A Blender scripts folder whose addons/ has lu_toolbox and io_scene_niftools (passed as BLENDER_USER_SCRIPTS); empty: the add-ons installed in Blender's own user folder |
toolbox_brickdb_dir |
toolbox-brickdb |
LU Toolbox's brick folder. Made from the client the first time: brickdb.zip unpacked into it and brickprimitives/ linked file by file. Given the client's res folder itself, LU Toolbox unpacks brickdb.zip into it, which this avoids |
toolbox_work_dir |
toolbox-work |
The model being made and blender.log |
toolbox_device |
cpu |
Cycles' device for LU Toolbox's bakes: cpu, cuda, optix, hip, auto (Blender's preferences) |
toolbox_threads |
4 | Blender's -t |
toolbox_timeout_seconds |
1800 | A model taking longer fails and Blender is started again |
Relative paths are next to the server binaries. UgcServer --make-model <file> <folder> toolbox-blender makes one model
with it (Blender started for it and stopped after).
Setting it up
- Blender 3.1 (what LU Toolbox 2.x and LU-Toolbox-Standalone are made for): the portable Linux build
(
blender-3.1.2-linux-x64.tar.xzfrom Blender's release archive) unpacked anywhere; pointtoolbox_blenderat itsblender. - A scripts folder with
addons/lu_toolbox(LU Toolbox) andaddons/io_scene_niftools(the niftools add-on, v0.1.1, the first with LEGO Universe export), andtoolbox_scripts_dirpointing at it. - LU-Toolbox-Standalone, and
toolbox_standalone_dirpointing at it. processor=toolbox-blender, ortoolbox-blenderin Make again's options for some models.
niftools v0.1.1 (the release with LEGO Universe export) doesn't write LU Toolbox's models as they are; three changes
are needed, all in modules/nif_export/ (checked with Blender 3.1.2 and 5.2.1, by reading the .nifs back):
geometry/mesh/__init__.py,set_ni_geom_data: reset theuv_setsfield whether or not there are UVs (without, every model without UV maps fails withValidation failed on NiTriShapeData.uv_sets).- the same file, where
n_tris = len(b_mesh.loop_triangles): callb_mesh.calc_loop_triangles()first when it's empty (Blender before 3.6 doesn't fill it on its own: the.nifis written with no triangles at all). types.py,create_ninode: use the object's"type"custom property as the node type when it names one (LU Toolbox makes itsNiLODNodes that way, as niftools read them before v0.1; the node type property has only NiNode and BSFadeNode, so without it the.nifhas plain nodes and every LOD is drawn at once).
Blender 5.2 (Python 3.14) works too, with more changes in both add-ons, and gives somewhat different results (the
bakes are Cycles', which changed): LU Toolbox's calc_normals_split/use_auto_smooth (gone in 4.1) and
bpy.ops.object.bake(context_override) (context dicts gone in 4.0: context.temp_override); niftools'
calc_normals_split, Object.face_maps (gone in 4.0) and its updater's cls.__dict__['__annotations__'] (Python
3.14 makes annotations lazy).
How it compares
Measured with --make-model on five player models (Blender 3.1.2 on the CPU, toolbox_threads=6, nice 10; a Blender
started per make, about 1 s of the Toolbox time):
| Bricks | Native | Toolbox (Blender CPU) | Triangles before | Native after | Toolbox after |
|---|---|---|---|---|---|
| 12 | 0.9 s | 9.2 s (26 s) | 12344 | 6992 | 7355 |
| 40 | 4.7 s | 11.9 s (41 s) | 11808 | 2491 | 3712 |
| 110 | 3.9 s | 33.1 s (143 s) | 53264 | 2655 | 2733 |
| 300 | 2.5 s | 104.7 s (213 s) | 75344 | 22378 | 25979 |
| 600 (72000 transparent) | 5.9 s | 112 s (124 s) | 48800 opaque | 47771 | 47788 |
Most of the Toolbox's time is Process Model (its Cycles bakes for hidden faces: 29 s of the 110-brick model's 33 s, 67 s of the 300-brick one's 105 s); the export grows with the shapes (38 s for the 600-brick model's 401 transparent shapes). Blender's start is about 1 s and each model's fresh scene 0.1 s, so the warm worker saves little per model; its reason is the add-ons' setup and the brick folder. LU Toolbox keeps more faces (those only bounced light reaches, see Hidden faces): 0 to 49% more triangles after. The baked colors are close: the average vertex color of each model's most detailed level is within about 10% of the native one (the Toolbox's slightly darker).
LU Toolbox's .nif (as niftools writes it) has the root SceneNode_Collection.001 turned 90 degrees about X, the
NiLODNodes turned back and each shape turned again (Blender's Z up); the UGC server's own models have no turns. The
client never sees the root's turn: its render component puts the object's own position and rotation on the root node
it loads, over the stored ones (its scale stays). NifFile does the same (a root's rotation and translation are left
out), so the NiLODNode's and the shapes' turns cancel and the model stands up in its icon and the dashboard's views.
The game's own .nifs all have roots that aren't turned or moved, so they read as before.
Metal and glow (on by default, not how live looked)
Live's models, LU Toolbox's exports and the client's own builder (LUNifBuilder_BK, which writes only S01_Opaque
and S01_Alpha) all draw every brick with the LEGO shader, so metal colors look like grey plastic, glowing colors
like bright plastic and glitter like plain transparent plastic. The UGC server gives them the client's metal,
emissive and animated UV shaders by default. Set the four shader ids to 0 (and satin_colors to none) for live's
look: off writes exactly the files it wrote before these settings existed (the same bytes, tested).
How the client picks the shader (checked in the 1.10.64 client; Ghidra bookmarks under "UGCShaders"): player models
(LOT 14, and 6662) have RenderComponent shader 100, mapShaders "Multishader" (gameValue 9999). For a downloaded model
LWOBaseRenderComponent::WrapMultishaderNodes (0x00c0d370) wraps each NiLODNode (or bare NiGeometry) of the
.nif, and AddObjectToRenderPipe (0x00cfbdb0) reads the wrapped node's name with sscanf("S%d") (else "_S%d"
after the first _S): the number is a mapShaders id, and its gameValue is the shader. A gameValue outside 3..108
falls back to 5 (LEGO) and logs "Multishaded node ... malformed name". There is one shader per NiLODNode, shared by
all of its levels, so each look needs a group of its own.
Setting (ugcconfig.ini, dashboard: UGC models) |
Default | What it writes |
|---|---|---|
shader_metal |
88 | S<id>_Metal_Model for metal colors: 88 is Polished Metal (gameValue 98). The client loads textures/metal/metal_reflection_polished.dds itself and tints it by the vertex color (Metallic.fx, Technique_Lighting_PolishedMetal_VertColor). |
shader_brushed |
89 | S<id>_Brushed_Model for brushed steel colors: 89 is Brushed Steel (gameValue 99; it loads metal_reflection_brushed.dds and _noise.dds, the noise in object space). The textures are registered by the client (RegisterBrushedSteelTextures, 0x00467090) as global shader textures 6 and 7, so the .nif needs none. The client's Materials.xml has no brushed types, so this needs brushed_colors or a Materials.xml that names them. |
shader_glow |
46 | S<id>_Glow_Model for opaque glowing colors: 46 is LEGO-Emissive (gameValue 53), which draws lerp(lit, vertex color, vertex alpha * material emissive red), opaque. |
glow_emissive |
1 | The glow shapes' NiMaterialProperty emissive (grey): how far the shader goes from lit to the plain color. |
metal_material_types |
shinySteel |
Materials.xml MaterialTypes that are metal (empty: the default; none: none). |
brushed_material_types |
brushedSteel,matteSteel |
Materials.xml MaterialTypes that are brushed steel. |
brushed_colors |
298,300,1002,1004 (the drum lacquered colors) | LEGO color ids that are brushed steel whatever their type; they win over the metal and glow colors. Empty: the default; none: no colors. |
shader_glitter |
21 | S<id>_Glitter_Model (opaque) and S<id>_GlitterAlpha_Model (transparent) for glitter colors: 21 is LEGO-AnimUV (gameValue 30), see Glitter below. |
glitter_material_types |
glitter |
Materials.xml MaterialTypes that are glitter. |
glitter_colors |
114,117 | LEGO color ids that are glitter whatever their type (as brushed_colors). The default: the two colors LEGO's own color data (Studio's color categories, "Glitter Colors") files as glitter that the client's Materials.xml types shinyPlastic (114 Tr. Medium Reddish-Violet w. Glitter, 117 Transparent Glitter). |
glitter_size |
1.6 | The glitter texture's tile, in model units (a stud is 0.8): with glitter_density, the flecks' spacing. |
glitter_density |
80 | Flecks in one tile. |
glitter_fleck_size |
0.05 | A fleck's diameter in model units (LDD units are centimetres: half a millimetre, about LEGO's glitter). |
glitter_fleck_opacity |
80 | Percent: how white the brightest flecks are over the brick's color; most are dimmer. |
shader_glitter_sparkle |
79 | S<id>_GlitterSparkle_Model, the glitter bricks' sparkles (only with shader_glitter): 79 is Distortion Directional (Ocean) (gameValue 89), whose layers the client moves on its own; 0: no sparkles. See Glitter below. |
glitter_sparkle_size |
0.1 | A sparkle's diameter in model units. |
glitter_sparkle_amount |
5 | Percent of each moving layer covered by sparkles (about its square's share of a brick sparkles at once). |
glitter_speed |
1 | How fast sparkles flash and go out (0.1 to 4; 1: about half a second each): the sparkle tile is 75 sparkle sizes times it. It used to be how fast the flecks drift, which never showed in game. |
glitter_sparkle_tint |
30 | Percent of the brick's color the sparkles take (0: white). |
glitter_sparkle_brightness |
100 | Percent: the sparkles' vertex color. |
glitter_random |
1 | Each glitter brick its own fleck pattern (turned and moved by the brick); 0: the same pattern on every brick. |
satin_colors |
360,362,363,364,365,366,367,376 | Satin (opal) colors, see Satin below. The default: LEGO's color data's "Satin Colors" category (the Transparent ... Opal colors). Empty: the default; none: off. |
satin_opacity |
75 | Percent: the vertex alpha of transparent satin bricks, instead of transparent_opacity or the Materials.xml alpha. |
satin_whiten |
20 | Percent: how far satin colors are moved towards white (in linear RGB, after the color variation). |
Which color has which look is data, not a list in the code (UgcModel::LookOf): glow is LU Toolbox's glow table
(UgcPalette::Glow: 50, 294, 329, 9000-9027), metal is the Materials.xml types above (the clients checked have 8 or
14 shinySteel colors, and 1 or 3 glitter ones: 129, 341, 351), glitter is the glitter type plus
glitter_colors. LU Toolbox's metallic table only colors bricks, it gives no look: most of it is shinyPlastic in the
client's Materials.xml (131, the grey of many baseplates, is plastic in the client's brick colors too), and it put
whole baseplates in S88_Metal_Model. Pearl stays plastic (the client has no shader for it).
Only opaque bricks get metal and glow: a transparent glowing color (294 with the brick database palette, alpha 150)
stays in S01_Alpha_Model. Transparent bricks can be glitter (every glitter color the clients have is transparent:
341 and 351 have alpha 150, 129 is in transparent_colors).
What is written with a group on: per LOD, the opaque bricks are split by look before being divided at 65535 vertices,
and the .nif gets, in order, S01_Opaque_Model, S88_Metal_Model, S89_Brushed_Model, S46_Glow_Model,
S21_Glitter_Model, S01_Alpha_Model and S21_GlitterAlpha_Model (transparent glitter bricks, one shape per brick
like the other transparent ones, or one with combine_transparent), each only when it has triangles, and each with every LOD level (an empty LOD_<n> node where it has
none there), like the plastic groups. Metal shapes are like plastic ones (white material, no textures, the brick color
as vertex color with the lighting baked in). Glow shapes get a material of their own with emissive glow_emissive,
vertex alpha 1 (the shader's mask) and their plain color, not the baked one: the shader lights them itself, and the
glow added by the bake would glow twice. stats.json lists each LOD's triangles per group. model.noao.nif has the
same groups.
Turning a setting on or off changes only models made afterwards: the ones made already keep their look until they are made again, with the UGC page's Make everything again (or Reprocess on one model); nothing is remade on its own.
The icon renderer and the dashboard know the groups: the icon reads each shape's tag back (the settings' ids and the
client's 88, 89 and 46) and draws glow at its plain color (by glow_emissive, unlit) and metal with a dimmed diffuse
light, a sky over dark ground reflection tinted by its color and a sun highlight (sharp for polished, broad for
brushed). This is an approximation of the game's environment maps. NifFile::ShaderLookFor gives 98 REFLECTIVE,
99 REFLECTIVE | BRUSHED and 53 EMISSIVE; the UGC page's 3D view gets each mesh's look (/api/ugc/mesh, "look")
and draws metal as the game does, its color kept (part metal: metalness 0.5 polished, 0.4 brushed, with a stronger
reflection of the view's environment; a metalness of 1 in the view's dim room was nearly black) and glow unlit, under
a sun and a sky over ground fill so no side of a model is black, and the zone views draw
LEGO-Emissive objects going to their vertex color by its alpha (metal there stays lit like the rest).
Glitter
Glitter is two layers: still flecks in the brick (LEGO-AnimUV) and sparkles over it that flash and go out (Distortion Directional). The client has no glitter shader, and nothing in a placed model's .nif can move:
Why a placed model never animates (checked in the 1.10.64 client; Ghidra comments at the addresses). Player models
(LOT 14) have RenderComponentWrapper 9845 (animations\pets\weeble\weeblewobble.kfm), so
ObjectLoader2::LoadRenderComponent (0x010536b0) always makes them an LWOSkinnedRenderComponent with the UGC .nif as
the wrapped node. Its per-frame Run (0x00d6d3d0) calls NiAVObject::Update (the only update of the object's scene
graph, and of its property controllers) only when the position changed or ShouldAnimate (0x00bd3860) is true, which
needs animationEnabled. LWOModelBehaviorComponent::EnableAnimation (0x00be2740, on render ready and whenever the
serialized model type changes) sends SetAnimationEnabled(modelType != 2), and every placed property model is
modelType 2 (ModelComponent::Serialize writes 2, as live did). So an NiTextureTransformController in the file
never runs, whatever the node flags (LWOBaseRenderComponent::Run's selective update check is not used for these
objects). Glitter made with texture controllers (and root flags 0x102) before this was still in game.
What does move on its own are shader globals that a shader class's own Run sets every frame for all its objects:
Distortion Directional (Ocean) (mapShaders 79, gameValue 89, class at vtable 0x015695a0, Run 0x010b90c0) adds
dt/4/6, dt/4/12 and dt/4/18 to the U of g_vDirectionalMotionLayer1..3 every frame (wrapping at 1; the V of
layers 2 and 3 swing back and forth), which its vertex shader adds to the layers' UVs (Ocean.fx
Technique_Ocean_Distort_Directional_2Layers: uv * 0.75 + layer1, uv + layer2; _3Layers: uv * 0.5, * 0.75,
* 1; the class's constructor 0x00464500 names the 2-layer technique twice and the 3-layer one once among its six
technique slots, which the graphics settings pick between). Its pixel shader averages the layers' texels (each later layer's
UV moved by (earlier texel's rg) * 0.2 - 0.5), multiplied by (N.L * sun + ambient) * vertex color; alpha =
average alpha * vertex alpha * fade. The game's own pond ripples use it the same way
(S79__pond_ripplesShape, mesh/env/env_won_gnar_croc_pondfx.nif).
Flecks. LEGO-AnimUV (mapShaders 21, gameValue 30, LEGOPPLighting.fx and its _low, _noenv,
_noenv_nospec versions) is the LEGO lighting with the UVs multiplied by TEXTRANSFORMBASE (the base map's texture
transform) in the vertex shader. A shape with vertex colors and a base texture gets
Technique_LEGOPPLightingVertColorTextured_AnimUV (technique names set up at 0x010ac110), whose pixel shader
(LEGOPPLighting_PS_VertColorTextured) is lerp(vertex color, texture rgb, texture alpha), then the LEGO lighting
(LEGOPP_PixelCommon4), alpha = vertex alpha times the fade. So a white texture with flecks in its alpha puts white
flecks on a brick that is otherwise lit as plastic. What a glitter shape has, beside what plastic shapes have (white
material, alpha, specular, vertex colors):
- A UV set: each vertex's position on the axis plane its normal faces most, divided by
glitter_size(UgcGlitter::Uv), so the flecks are as dense on every brick and every side, then turned by an angle and moved by an offset under a tile that the brick picks for each plane (glitter_random, on by default): each brick's number (UgcGlitter::BrickSeed, from the model's id and the brick's index, kept per vertex inMesh::brickSeeds), so no two bricks have the same pattern, every LOD of a brick has its own, and a model made again gets the same. The icon draws the flecks on the UVs the .nif has (Mesh::uvs, read back byUgcModel::FromNif). - An
NiTexturingProperty(one per file, shared by both glitter groups): apply mode decal (fixed function would do what the shader does), 9 slots, the base map only: wrap S and T, trilinear, UV set 0, a texture transform (translation 0, scale 1, Maya method, center 0.5). No controllers. - Its source, stored in the file as the client's own stored textures (
res/mesh/env/env_ag_ocean-maelstrom.nif):NiSourceTexture(use external 0, nameugc_glitter.dds, pixel layout 6, mipmaps 2, alpha 3, static, persist render data) andNiPersistentSrcTextureRendererData: RGBA 32 bit, channels blue, green, red, alpha, platform DX9, square with every mipmap down to 1 (128 at the defaults; the power of two up to 512 that keeps a fleck 3 pixels wide,Params::TextureSize). RGB is white; the alpha isglitter_densityflat flakes (UgcGlitter::FleckAlpha): 0.7 to 1.3 timesglitter_fleck_sizeacross with a pixel's worth of edge, each as bright as its facet happens to catch the light (0.3 to 1 ofglitter_fleck_opacity, weighted towards dim: LEGO's glitter bricks show many faint flecks and a few bright ones), at places from a fixed seed, wrapping at the edges; each mipmap the 2x2 mean of the one above.
Transparent glitter: every UGC shape has the same NiAlphaProperty (blend source alpha over one minus source alpha)
and transparent bricks are transparent by their vertex alpha; the LEGO-AnimUV techniques declare
UsesNiRenderState = true and their pixel shader outputs the vertex alpha, the same as the LEGO shader's that
S01_Alpha_Model is drawn with, so transparent glitter gets a group of its own.
Sparkles (shader_glitter_sparkle, 79; 0: none; only with shader_glitter on). A group
S79_GlitterSparkle_Model after all the others, with every LOD, whose shapes are the glitter bricks' pieces (opaque
and transparent) again:
- Lifted off the brick along the normals by 0.005 (
UgcGlitter::SPARKLE_LIFT), so they are in front of its surface: a transparent brick, drawn later in the blended phase, doesn't cover them, and they don't fight it for the depth. - Vertex colors: white taking
glitter_sparkle_tintpercent of the brick's color, timesglitter_sparkle_brightness, alpha 1 (UgcGlitter::SparkleColor). - UVs as the flecks' but on the sparkle tile and placed apart from them (
UgcGlitter::eLayer::SPARKLES). - Material white, alpha 1. An
NiAlphaPropertywith the test bit (flags 0x1A00: test, GREATEREQUAL; threshold 127):ShaderCommon::GetAlphaFlags(0x0109f5a0) puts a shape whose alpha property has the test bit in the alpha test phase, whose states (ShaderCommon__SetupPhaseRenderStates0x00463300) are blending off, alpha test GREATEREQUAL 0x7f, depth test and write. - An
NiTexturingProperty: the base map only, wrapping, trilinear, no texture transform (as the pond ripples), its sourceugc_sparkle.ddsstored like the flecks'. Its alpha (UgcGlitter::SparkleAlpha): flat sparkles ofglitter_sparkle_sizeat 230, coveringglitter_sparkle_amountpercent. Averaged over 2 layers one sparkle alone is 115 and over 3 it is 77, under the test's 127; two sparkles meeting are 230 or 153. So a sparkle shows only where two moving layers' sparkles cross: it appears, grows, shrinks and goes out as the layers slide past each other at different speeds. The first two mipmaps take the brightest of each 2x2 (the sparkles keep their alpha a little further away), the rest the mean. - The sparkle tile (
UgcGlitter::Params::SparkleTile) is75 * glitter_sparkle_size * glitter_speedmodel units: the layers move a fixed share of a tile a second, so a bigger tile crosses sparkles faster; at speed 1 each flash lasts about half a second. The texture is the power of two (128 to 1024) that keeps a sparkle 3 pixels wide (256 at the defaults).
The icon draws the flecks where they are (the same texture and the .nif's UVs, before the light; glitter_size and
glitter_density), opaque and transparent, and leaves the sparkles out (Shaders::OverlayTags: alpha tested shapes
tagged shader_glitter_sparkle or 79). The UGC page's 3D view marks glitter meshes (/api/ugc/mesh: look
GLITTER 512, a mesh with a stored texture in a group tagged shader_glitter or 21) and draws flecks on their UVs,
and marks the sparkles (look SPARKLE 1024, alpha tested and tagged shader_glitter_sparkle or 79), which it draws
flashing on theirs as the client does (addGlitter in static/js/scenery-core.js: two layers of sparkles in cells,
one at three quarters the scale sliding a tile in 24 s, the other a tile in 48 s, shown where both have one). The
LXFML views (the UGC page's second view, the property and zone views) draw flecks and sparkles on the glitter colors
from their positions, by window.LDD_GLITTER (/api/bricks/materials.js: the glitter colors and every glitter
setting as they are now, not cached): a live preview of the settings without making the model again (a brick's own
placement, glitter_random, is only in the made model).
Satin
The client has no satin shader either: Clear Plastic (mapShaders 3) has no vertex color, so it can't show a colored
satin. Satin bricks stay in S01_Alpha_Model and are made to look satin when their colors are made: a transparent
brick of a satin_colors color gets satin_opacity as its vertex alpha, and its color (any brick's) is moved
satin_whiten percent towards white, milky. These colors are only in a client whose Materials.xml has them (the
opal colors, 360 to 376, are not in the 1.10.64 client's).
Modular builds (ugc_modular_build rows, ldf_config like 1:4713+1:4714+1:4715):
- Each module LOT's
ModuleComponent(component type 28) gives its part code and build type; the build type'sModularBuildComponent.xmlgives the topology (root part, and which part connects to which named location) andPlacement/AdditionalModelRotation. - Each module's mesh is its render asset (
RenderComponent.render_asset, the NIF the client assembles in game). The modules have no textures: none of the 399 render assets of the client'sModuleComponentLOTs (1.10.64) has anNiSourceTextureorNiTexturingProperty; their look is their vertex colors andNiMaterialProperty, which the icon draws (the client's own module icons,textures/ui/rebuilding/, show the same colors). 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'sconnectiontranslation is used when the parent's NIF has no such node. (Module LXFMLs inres/BrickModelsexist for only some modules and are authored in different spaces, so they aren't used.) - The assembled mesh gets an icon like a model's (with the build type's preset and the combination's own values). No
mesh is stored: the client assembles modular builds itself. For the dashboard's icon editor the assembled mesh is
made on request (
/admin/assembly, below).
Cars and rockets are put together from a fixed set of modules, so the icon is made once per combination of modules
and shared by every build of it. A build's combination is its ldf_config's LOTs sorted (UgcModularKey::Normalize,
e.g. 4713-4714-4715: each LOT belongs to one slot of one build type), stored under an id hashed from it. A build whose
combination is made already is marked made right away; builds of a combination being made wait for it and all get its
outcome. The client's downloads stay per blueprint id: the server looks up the build's combination and serves the
shared files. combo.json in the combination's folder says its LOTs and build type.
Storage
Files live under ugc_output_dir (default ugc next to the server binaries):
ugc/models/<id % 1000>/<id>/model.nif.gz, model.nif.checksum, model.nif.sd0, icon.dds.gz, icon.dds.checksum,
icon.dds.sd0, icon.png, model.noao.nif.gz, stats.json, previous.icon.png,
previous.model.nif.gz, previous.stats.json
ugc/modular/<combination id % 1000>/<combination id>/icon.dds.gz, icon.dds.checksum, icon.dds.sd0, icon.png, combo.json
ugc/.checksums-stored
Every file the client downloads is written three ways: .gz and .checksum for 3D services and .sd0 (dCommon's
Sd0::Compress) without them. When a worker writes an item, the checksums of its .sd0 files (MD5 and size of the
inflated file, from the .checksum) go back to the main thread, which stores them in ugc_file_checksums for the
worlds. Items made before this get their icon.dds.sd0 (from icon.dds.gz) and their icon's checksum once, a few per
tick on the main thread when the server starts; .checksums-stored marks that done. Their .nif gets its .sd0 and
checksum when the model is made again; until then the worlds send such a model's LXFML (no model.nif checksum, see
"Models without 3D services").
A model's files are written to a temporary folder and renamed into place, so a half written model is never served.
Meshes are only stored compressed (a model took about 18 MB when the .nif was kept uncompressed beside its .gz, so the
2 GB cap held about 110 models and the server kept evicting and remaking them); the dashboard's copies are inflated
when it asks. When an item is made again, its icon, mesh and stats of the version before are kept as previous.*.
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. A combination's files are deleted like any others even while builds share them: the next request for any of
those builds makes them again.
Deleting and purging
The dashboard can delete stored files (the UGC server does it on its main thread and says how many bytes it freed):
one item's, every item matching a filter (kind, state, owner, made more than N days ago, not asked for in N days) or
all of a kind (typing PURGE ALL). Afterwards the rows stay made (made again when a game client asks), are queued to
be made now, or are marked failed with "Deleted from the dashboard". Items being made at that moment are skipped, so a
worker never writes into a folder being deleted.
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, 3 when there was nothing to make (the model has no bricks; not a failure, never retried, counted and shown as
"empty", answered with 404 like HKX so the client doesn't wait; migrations 87/70 move the rows that had failed only for
that). The names come from IUgc::eProcessState (IUgc::ProcessStateName). 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.
Migrations dlu/mysql/88_ugc_model_stats.sql and dlu/sqlite/71_ugc_model_stats.sql: ugc.brick_count and
ugc.triangle_count, what the UGC server counted when it made a model (its bricks, and the triangles of the made
mesh's most detailed level, from stats.json; 0 until it has), so the dashboard can sort models by size.
Migrations dlu/mysql/86_ugc_debounce_icon_settings.sql and dlu/sqlite/69_ugc_debounce_icon_settings.sql:
ugc.process_after and the table ugc_icon_settings (target: kind:<kind>, model:<id> or combo:<key>; params
JSON; updated_at).
Migrations dlu/mysql/89_ugc_file_checksums.sql and dlu/sqlite/72_ugc_file_checksums.sql: the table
ugc_file_checksums (kind 0 a player model, 1 a combination of car or rocket modules; storage_id the model's ugc
id or the combination's id; file icon.dds or model.nif; md5, 32 lowercase hex digits, and size of the inflated
file), written by the UGC server whenever it writes a file, and ugc_modular_build.combination_id, the combination a
build shares its files with (0 until the UGC server has seen the build: it fills them in for existing builds when it
starts, -1 when the modules can't be told). IUgc::GetUgcFileChecksum(blueprint, file) looks a blueprint up as a model
first, then as a build through its combination.
Migrations dlu/mysql/90_ugc_process_time.sql, 91_ugc_process_diagnostics.sql and dlu/sqlite/73, 74:
ugc.process_ms (wall time of the last make), ugc.process_cpu_ms (the worker thread's CPU time) and
ugc.process_memory_kb (the estimated memory it needed), and the same on ugc_modular_build; the dashboard's Took,
CPU and RAM (est.) columns. A make's time includes its icon's. When only a model's icon is drawn again (the icon
editor's Draw all icons of this type again), its stats.json gets the new icon's time (ms.icon, and ms.total
changed by the difference) and process_ms and process_cpu_ms change by the same difference (the icon is drawn on
one thread, so its time is taken as its CPU time).
Migrations dlu/mysql/92_ugc_triangles_before.sql and dlu/sqlite/75_ugc_triangles_before.sql:
ugc.triangle_count_before, LOD 0's triangles before hidden faces were removed (the dashboard's Saved column).
Migrations dlu/mysql/94_ugc_priority.sql and dlu/sqlite/77_ugc_priority.sql: ugc.priority, 1 for models staff
asked to be made again (made before any other, cleared once made).
Migrations dlu/mysql/99_ugc_process_options.sql and dlu/sqlite/82_ugc_process_options.sql (see "Processing
options"): ugc.process_options, the options staff picked for a model's next make (empty: the settings'; cleared once
it isn't pending), ugc.made_options, what made its current files, and the table ugc_process_runs, one row per
successful make of a model (ugc_id, options, made_at, process_ms, process_cpu_ms, hsr_ms, ao_ms,
icon_ms from its stats.json, bricks, triangles_before, triangles).
Cars and rockets from before builds were stored
Migrations dlu/mysql/98_modular_build_ids.sql and dlu/sqlite/81_modular_build_ids.sql (run by
ModularBuildIdMigration, after the SQL migrations): every saved item with modules (x@ma, assemblyPartLOTs; only
modular builds have them) and no subkey gets what a new build gets (ModularBuildFinish): a persistent id (from
object_id_tracker, with the character bit, as ObjectIDManager::GetPersistentID) as its subkey (sk) and a
ugc_modular_build row with its modules and the character as owner. The character's XML is written with
UpdateCharacterXml (a world still holding an older copy can't save over it). The UGC server then makes their icons
like any other build's. Characters that load later with such an item (a database from before the migration, restored
XML) get the same when they load (InventoryComponent, AssignModularBuildId). Mail attachments are not changed.
Without 3D services (UGCUSE3DSERVICES=7:0)
UgcManifest (dGame/dUtilities) answers REQUEST_UGC_MANIFEST_INFO when ugc_manifest=1 (sharedconfig.ini,
default 0):
- The world's main thread handles the packet (
g_WorldHandlers), looks the checksum up with one indexed query and sendsUGC_MANIFEST_RESPONSEto that client in the same tick. - Icons (DDS): an icon that isn't made yet isn't answered: the request waits (at most 512, for 15 minutes) and
UgcManifest::Update(world tick) looks again every 5 seconds, answering once the UGC server has stored the checksum. A model still in its quiet period after a save is made right away (ExpediteUgcModel), as when a client asks the UGC server directly. Waiting requests are dropped when the client disconnects. - Player models' files (NIF, HKX, LXFML) are always answered at once, see "Models without 3D services".
- No worker threads, HTTP or file reads in the world: the UGC server precomputes the checksums into the database (the
LXFML's is worked out from the
ugcrow on the main thread and kept).
The UGC server serves BrickModels/UserMade/<bucket>/<id>.<ext>.sd0 under client_path, under
/<any folder>/UserBrickModels (the client's default UGCSERVERDIR, lwoclient/UserBrickModels with its built-in
patch folder) and at the root; .dds is a model's icon or a car or rocket build's combination icon, .nif a model's
mesh, .lxfml the model's LXFML from the database, .hkx 404.
It is off by default because a client whose boot.cfg doesn't point at the UGC server downloads from its defaults (the
patch server's address, http://127.0.0.1:80/lwoclient when that isn't set either) and is logged out when it can't
connect there. Turn it on when the players' boot.cfg has UGCSERVERIP, UGCSERVERPORT and UGCSERVERDIR for the
UGC server (see the settings above), or when the patch server's address is the UGC server.
Models without 3D services (ugc_manifest_models)
What the 1.10.64 client does with a placed player model (LOT 14, spawned with blueprintid), checked in the client:
- The model's BlueprintComponent (component 42) sets
renderUserGen=1andphysicsUserGen=1in the spawn data itself when it has ablueprintidand nonif_name/hkx_name(LWOBlueprintComponent::PrepareConfigData, 0x00c736f0), so the render component always loads the blueprint's NIF (resource type 1) and the physics its HKX (type 2) by blueprint id (ObjectLoader2::LoadRenderComponent, 0x010536b0;LWOBasePhysComponent::LoadHkxDataFromConfig, 0x00c75220). There's no choice between LXFML and NIF on the render side, and the world needn't sendrenderUserGen. The ModelBehaviorComponent loads the blueprint's LXFML (type 0) as well (RequestBlueprintData, 0x00c24640). - Every such request first needs the blueprint's manifest info (
UGCManifest_Base::GetOrRequestManifestInfo, 0x0101e0e0): a cached entry is used at once (an entry with valid 1 never expires; one with valid 0 expires after 300 seconds,UGCManifest_Client::IsEntryExpired, 0x010186d0), else the request waits and the client sendsREQUEST_UGC_MANIFEST_INFO. There is no timeout: a manifest request the world never answers leaves that file, and the model, waiting for good (LWOResMgr2Interface::RequestBlueprintManifestThenLoad, 0x0105a910). - With the answer, the client uses the file it has when its MD5 matches (or the answer's valid is 0 and it has one),
else downloads it (
LoadBlueprintResource, 0x01056700). A failed download (404) leaves the model without that file (no fallback to the LXFML) and is reported asUgcDownloadFailed(world message 120, with the status); status 0 logs the player out. - The LXFML the world sends when a property loads (
BlueprintSaveResponsewith local id 0) makes the client build the NIF and HKX itself and cache the manifest info of the LXFML, NIF and HKX with its own files' MD5s (LWOBBBInterface::MainThread_ProcessModelResponse, 0x00b5a1e0), which answers its own requests, so nothing is downloaded. That's how DLU always showed models. The build runs on the client's BBB thread and writesBrickModels/UserMade/<bucket>/<id>.lxfml,.nifand.hkx, the paths a download uses (LWOBBBInterface::GenerateModelFromLxfml, 0x00b6c220). So every LXFML sent to a client replaces a served.nifit downloaded, on disk and in its manifest cache, once that build is done; a mesh already drawn stays. - A single-asset flush (ResMgr2 0x2b06, what
NotifyClientUGCModelReadysends) erases the path from every resource cache at once (ResourceCache::FlushCachedAsset, 0x010372a0); objects already drawn keep their mesh. - A download is tried 3 times; after that the client uses the file it has, if any (its own build), else the model has
no file (
LWOResMgr2Interface::GetResource, 0x0105ca50). NotifyClientUGCModelReady(game message 909, the blueprint id only) to a model: its BlueprintComponent flushes the cached NIF, HKX and LXFML of that blueprint and requests the NIF and HKX again (LWOBlueprintComponent::OnNotifyClientUGCModelReady, 0x00ca6430). It doesn't clear the manifest cache, so the new checksum has to reach the client first: aUGC_MANIFEST_RESPONSEthe client didn't ask for updates its cache (PacketHandler_MSG_CLIENT_UGC_MANIFEST_RESPONSEcaches whatever arrives).- Live packet captures of a property load weren't available to compare with.
With ugc_manifest=1 and ugc_manifest_models=1 (sharedconfig.ini, default 0; the dashboard shows it under UGC
serving), the worlds:
-
Property load: send the LXFML (one
BlueprintSaveResponse) only for the models whose mesh the UGC server hasn't made (nomodel.nifinugc_file_checksums). Made ones are left out: the client asks for their files, downloads the served mesh and draws it. Before the models are constructed for the player, the world sends them the served NIF checksum of every made model placed there (UgcManifest::OnPropertyLoading): after an earlier visit the client's cached NIF checksum is its own build's (from the LXFML it got for the HKX, or before the model was made), which it would use without asking; with the served one it downloads the served mesh again. Tried and dropped: sending every model's LXFML (the client builds its own NIF and HKX) and switching to the served mesh 3 s later with the served NIF's checksum,NotifyClientUGCModelReadyand the model constructed again: the client kept drawing its own build (most likely because its builds of those LXFMLs finished after the switch and cached their own NIFs again; see "Waiting for the client's own build"). -
NIF of a made model: the UGC server's checksum; the client downloads
<id>.nif.sd0from the UGC server. -
LXFML of a made model: the MD5 and size of the stored LXFML inflated (what the UGC server serves as
<id>.lxfml.sd0), worked out once per model and kept. -
HKX of any model: the model's LXFML (the UGC server makes no physics). The client builds the model from it and loads its own HKX, so served models have collision; the mesh already drawn stays the served one. The build also replaces the served
.nifon disk and its cached checksum (see above), so the next load needs the served checksum again (Property load). The HKX's checksum stays cached (valid entries never expire), so the client asks for the HKX, and rebuilds, only while it has no entry for it. -
Any model file of a model that isn't made (e.g. a model someone else just placed, or
ugc_manifest_models=0): the model's LXFML to that client (once per 10 seconds for the three requests), which builds it itself, so a model is never left waiting. A blueprint that isn't a player model gets valid 0. -
Made again: when the UGC server writes a model's mesh with a different checksum than before (made for the first time, or remade after a change), it sends
UGC_MODELS_MADE(master message 37, the blueprint ids) to the master, which passes it to every world. A world with that model placed switches each player the model is shown to (UgcManifest::SwitchClient): the served NIF's checksum,NotifyClientUGCModelReadyto each placed copy (the client flushes its cached NIF, HKX and LXFML and downloads the served NIF), and 1.5 s later each copy taken down for that player and constructed again 1 s after that; the new object draws the served NIF and loads the client's own HKX (still cached), so it keeps collision. -
Waiting for the client's own build: a player sent the model's LXFML less than 30 s before (
BUILD_SETTLE: the property load, a request for a model not made yet, the owner's brick by brick save) may still be building it, and that build would put its own NIF and checksum back after the switch. Such a player is switched 30 s after the last LXFML sent to them (UgcManifest::ServedMeshSwitches); another LXFML sent meanwhile starts the 30 s again. The others are switched at once. This is the common case: another player's client asks for a model the owner just placed, gets its LXFML, and the UGC server is told to make that model now (ExpediteUgcModel). A model made again unchanged (after eviction) isn't sent. Nothing polls: one message per batch of made models. -
/reprocessproperty [options](GM 8): every model placed on the property the player is on goes back to the UGC server's queue (ResetPropertyUgcModelProcessing), with the processing options given for that make (see "Processing options"; none: the settings'). The world checks every 5 seconds; once none is pending (or after 15 minutes) it sends every player in the world the newmodel.nifchecksums and transfers them back into the same zone and clone. The client loads the property again and downloads the new meshes (its manifest cache has the new checksums, which its files don't match). Models of a reprocess skip the "made again" switch. The models are queued as priority (ugc.priority, also set by the dashboard's Reprocess all models): the UGC server takes them before any other model, polls them even when its queue is full and puts them at its front (as cars and rockets); the flag clears once a model is made.
Waiting while the owner is still building
A saved model isn't made right away: InsertNewUgcModel stores process_after = now + ugc_debounce_seconds
(sharedconfig.ini, default 120, 0: right away) and moves the owner's other waiting models to that time too, so each
save starts the wait again. The UGC server only takes models whose process_after has passed. The wait ends early when
a game client asks for the model's files, when the owner leaves the world or logs out, or when it is made again from
the dashboard. Every save is a new blueprint: versions deleted during the wait are never made. It survives restarts.
The database is the queue: the UGC server looks for rows with is_optimized = 0 every poll_interval_ms (default
2000), poll_batch (32) or enough to keep every worker busy at a time: priority rows first (ugc.priority, set when
staff reprocess a property: /reprocessproperty or the dashboard's Reprocess all models), then the least tried, then
the newest. Cars and rockets and priority models are polled even when the queue is full and go to its front. 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_optimizeddefaults to 0 (InsertNewUgcModelwrites 0, and itsprocess_after). - When a model's LXFML changes,
UpdateUgcModelDatasetsis_optimized = 0, process_attempts = 0, process_error = ''in the same statement. Anything that writesugc.lxfmlanother way must do the same, or callResetUgcModelProcessing(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(InsertUgcBuildrows 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 (including the files' checksums, parsed by the worker from the .checksum it wrote).
Brick geometry and materials are loaded once and shared read-only (the cache has its own lock). The start-up backfill
of old items' .sd0 icons and checksums and of builds' combination ids runs on the main thread, a few items per tick.
CPU and memory
Settings in ugcconfig.ini (and the dashboard's settings page), picked up while running when the config is reloaded:
worker_threads(restart): how many models are made at once.max_cpu_percent: the workers together average at most this share of all CPU cores. The long loops (the renders for hidden faces, the occlusion rays, icons) account each thread's CPU time every few milliseconds against a budget that fills at that rate and sleep while it's overdrawn (UgcThrottle), so it holds for long jobs too and whateverworker_threadsis. Short bursts (a quarter second) aren't slowed. 0: no limit.worker_nice: the workers' Linux scheduling priority (0 normal to 19), so the game servers go first.max_memory_mb: each job's memory is estimated from its brick count before it starts (the renders' buffers plus about 40 KB per brick per LOD); a job waits while the running ones and it together would be over the limit, and one bigger than the limit alone runs when nothing else does. 0: no limit. After each job the workers give freed memory back to the system.max_model_bricks: models with more bricks fail with "the model has N bricks, more than max_model_bricks (M)". 0: no limit.pause_hours: local hours in which no new jobs start, e.g.18-23or22-6(running ones finish).
/status reports the process's CPU use (percent of one core, and the core count), resident memory, the running jobs'
estimated memory and how often a job waited for memory, whether the workers were throttled in the last 5 seconds and
for how long in all, whether it is paused, and the limits. The traffic report (Diagnostics page) carries the gauges
cpu_percent, memory_mb, job_memory_mb and throttled besides the workers' ones.
Measured on a 16 core machine with 4 workers working through 15 items (models of 100 to 1500 bricks and cars): with
no limit the process used about 400% of one core (all 4 workers) and finished in about 27 seconds; with
max_cpu_percent=12 (1.92 cores) it stayed at 190-195% in every 3 second sample through the backlog (one sample at
227% while a job finished) and finished in about 45 seconds. With max_memory_mb=400 the running jobs' estimates
stayed under 375 MB (two jobs waited for memory) and the process peaked at 283 MB resident, back to 55 MB when idle.
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|.checksumfor the client (client_path, default/ugc, is the client'sUGCSERVERDIR), and the same under/lwoclient/UserBrickModels(the path the client really uses, see above). HKX answers 404. The LXFML comes from the database. 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.<client_path>/BrickModels/UserMade/<id % 1000>/<id, 20 digits>.<lxfml|nif|hkx|dds>.sd0, the same under/<folder>/UserBrickModelsand at the root, for clients without 3D services (see "Without 3D services"); 408 and 404 as above./files/model/<id>/<file>and/files/modular/<id>/<file>for the dashboard (icon.png,model.nifandmodel.noao.nif(inflated),stats.json,combo.jsonandprevious.versions), not cached by browsers./admin/preview,/admin/assembly,/admin/regenerate-icons,/admin/delete(POST, JSON) for the dashboard only: they need the headerX-Ugc-Admin-Keywith the master password. A preview draws an icon with given values (the whole pose included) on a worker (ahead of the queue, within the CPU and memory budgets) and returns the PNG without storing it.assembly({modules}) returns a combination's assembled mesh as a .nif, put together exactly as for its icon and already turned by the build type'sAdditionalModelRotation, made on a worker the same way and kept in a small cache (the last 32 combinations, at most 64 MB), so the editor can show what the icon renderer draws. regenerate-icons draws every stored icon of a kind again (player models' from their stored .nif, nothing else is made); delete is described under Storage./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> [options] and UgcServer --make-modular "1:4713+1:4714+1:4715" <folder> [options] make one item's files into a folder without a database, for trying settings; the options are
processing options over the settings (e.g. embree fast), and it prints the time, the CPU time and what made it.
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_UgcServer_<start time>_<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, UGC in the menu under Properties; properties_view to look, the new ugc_manage permission to make
things again and to save icon values) reads the database: counts per state for models and for cars and rockets, and the
items as a gallery of their icons or a list. Player models are listed one by one (owner, state, attempts, last attempt,
bricks and triangles, file name, failure reason). Cars and rockets are listed as assemblies, one per combination of
modules however many builds use it (the UGC server makes one icon per combination). The List view gives both the same
columns and sorts (assemblies have no Saved): Icon, ID and Owner (an assembly's newest build and its creator, and how
many other owners), State (an assembly is made when any build of it is), Made, Took, CPU and RAM (an assembly's: the
latest make of any of its builds, and the cost of the make the UGC server did for it; builds that shared the made icon
cost nothing), Size (bricks and triangles; an assembly's module count), File (the file name; an assembly's build type,
named after the type's assembly object in ModularBuildComponent, and its modules) and Where: for a model where it is
(below), for an assembly how many builds and owners use it. Owners link to their character and, with accounts_view,
account.
Both lists are paged on the server (GET /api/ugc?kind=model|modular&q=&state=&type=&sort=&page=&size=, with the
total; where=1 adds each model's whereabouts; while searching, matches says how many of each kind match, shown on
the kind buttons), with numbered pages, first and last, a page to jump to and a page size kept per user. The search box takes
plain text (names, owners, ids) or field prefixes: owner:, account:, property:, name:, lot: or module: (a
LOT, or for assemblies a module's name), id:, state: and kind: (a car or rocket type). Models sort by newest,
oldest, most bricks, most triangles, recently made, slowest, most CPU, most RAM, most triangles saved, owner or file
name; assemblies the same (most modules for most bricks, no triangles or saved) and by most builds. The kind,
search, filters, sort, page and view are kept in the address, so Back and Forward and shared links work. The search
is UgcLookupSql (the same on MySQL and SQLite, IUgcLookup::ListUgc); assemblies are grouped
from the builds (UgcAssemblies). Buttons make one item, the failed ones or everything again (these only reset the
columns; the UGC server picks the rows up), for models with the processing options picked next to them (see
"Processing options").
An assembly opens with its modules, the icon editor and References: the builds that use it (GET /api/ugc/assembly/builds?modules=&q=&page=), with owner character and account, state and where each is (placed on a
property, in a mail, in its creator's inventories; the same lookup as the models' Where), paged and searchable. A link
to a build, /ugc?item=<build id>&kind=modular (what the property and character pages link to), opens its
assembly (GET /api/ugc/assembly/of/<id>) with that build highlighted in References.
The icon editor (on every opened item, and per type under Icon presets per type, which opens each type on an
example: the newest made player model, the most used combination of each car or rocket type) is one panel for
everything an icon's look can be set to, all from the parameter list (GET /api/ugc/icon/params, with each
parameter's group):
- A 3D view (three.js) of the player model's made .nif (
/api/ugc/mesh/<id>?lod=0, what its icon is drawn from) or of the assembly (GET /api/ugc/assembly?modules=, the UGC server's assembled .nif converted by the dashboard), seen through the icon renderer's own camera:static/js/ugc-pose-math.jsisUgcIconPosein JavaScript (the same perspective, turn order, framing and crop; both are checked against one fixture,tests/dUgcTests/ ugc-pose-fixture.json, by gtest and by node). The view shows a little more than the icon, with the icon's square outlined, so it matches the preview beside it. Dragging moves the camera around the model, turns the model (Ctrl+ drag, or the Turn model mode; Alt+wheel rolls it), moves the sun (Alt+drag, shown as an arrow) or shifts the model in the icon (Shift+drag or right drag); the wheel changes the border. The light in the view is close to the icon's (world light, sun, fill, exposure, contrast) but has no shadows or highlights: the preview is the real thing. - Sliders for every parameter, grouped (camera, border and shift, model turn, sun, light, look), kept in step with the view both ways.
- A live preview drawn by the UGC server (
POST /api/ugc/icon/preview, sent a moment after the last change). - Save as the preset for the item's type, save for this model or combination of modules, go back to the type's
preset or the default settings, remove the item's own values, and draw every icon of the type again. Saving needs
ugc_manageand is written to the audit log.
Settings for the UGC server have their own category on the Settings page (serving, processing, models, storage,
icons) and the UGC page shows each section beside what it changes (processing, models and serving under the server
status, storage with the purge tools, icon defaults with the presets), for those with the settings permission.
Both read the settings catalog and save the same way (the servers reload at once; settings marked restart are read
when the UGC server starts), and each links to its entry on the Settings page (/settings#ugcconfig.ini/<name>).
Everything from the UGC server goes through the dashboard, which reaches it at ugc_internal_url
(dashboardconfig.ini; empty: http://127.0.0.1:2008, the same machine), so the page works from wherever the browser
is: /api/ugc/server/status (its /status, kept 2 seconds), /api/ugc/files/<kind>/<id>/<file> (kept 15 seconds)
/api/ugc/mesh/<id>?lod=&version=current|previous&ao=0|1 and /api/ugc/assembly?modules= (its NIFs converted by the
dashboard's worker threads with the scenery viewer's NifFile conversion). The fetches run on the dashboard's worker threads. ugc_public_url is
only used for an "open on the UGC server" link.
The status box shows the queue, workers, CPU (with its limit), memory, the jobs' estimated memory (with its limit) and whether the workers are throttled or paused. Clicking an item opens the viewer: the generated NIF in 3D (any LOD, now or before it was made again, with wireframe, vertex colors and baked lighting switches), the LXFML as built beside it, the icon now and before, and the stats (triangles per LOD before and after hidden faces were removed, how many were removed, vertices, shapes, timings, with the change since the version before).
Finding creations and showing them elsewhere
The UGC page's search is the one search for creations (it replaced the UGC Search page; /ugc_search?q= redirects
to /ugc?view=list&q=). A number matches the UGC / blueprint id, a placed model's object id, the property it is placed
on, the creator's character or account id, or a LOT (a model placed as that LOT, or a car or rocket with that module);
text matches the creator's character and account names, property names, the name and description a player gave a
placed model and the upload's file name. A model's Where (UgcLinks::Whereabouts) is where it is: placed on a property
(from properties_contents, with the name the player gave it there and a link to that model in the property's 3D view),
attached to a mail (a model item's blueprint in the attachment's config; for a car's or rocket's build, its subkey) or
in its creator's inventories (their saved XML, looked at for the 25 first creators on a page); anything else is "Not
found" (traded, sold or deleted). GET /api/ugc_links/search?q= (API) answers the same search for both kinds at once.
The property page and the character page link to a creation on the UGC page as
/ugc?item=<id>&kind=model|modular, for the page to open that item.
What the UGC server made shows on the pages that show a creation, to whoever may view that page:
- The property page: each player-built model's icon, state and link (
GET /api/ugc_links/property/<property id>). - The property 3D view: made models are drawn from their NIF (
GET /api/ugc_links/mesh/<ugc id>?property=), switchable with Generated models (see docs/Dashboard.md); the rest from their LXFML. - The character page's inventories: creations get their icon and link (
GET /api/ugc_links/character/<character id>). A model item is known by its blueprint (blueprintid, which the item now keeps in its saved config asx@bp); a car or rocket by its subkey (itsugc_modular_buildid).
Icons come from GET /api/ugc_links/icon/model|modular/<id> (the UGC server's icon.png, fetched by the dashboard
like the /ugc page's files). It is served with properties_view, for a creation one of your own characters made, or
with ?property=<id> / ?character=<id> naming a page you may view (properties: properties_view, or the owner with
own_properties; characters: characters_view, or your own) that holds it. Until the UGC server has made an icon the
pages show the item's own icon.
Not done yet
- Served models (
ugc_manifest_models) are not checked in game yet: that the served mesh shows, thatNotifyClientUGCModelReadyswaps a model a client built itself for the served one while it's shown, and how models without collision behave. - HKX (physics) is not generated, so models downloaded from the UGC server have no collision for clients that never built them.
- A model's
.nifmade before.sd0files were written has none (and no checksum, so clients keep building that model from its LXFML) until the model is made again (Reprocess on the dashboard).