Lilith Metaverse · Architecture

Tier-1 UE5 Client: Modules, Game Feature Plugins, and Platforms

A focused page within the Lilith Metaverse Architecture documentation. The full map and every sibling page live in the Architecture hub.

6sections14 minread1diagram3tables

On this page

The Tier-1 client is the canonical V3 ("Lilith") runtime: one Unreal Engine 5.5 project at V3/ue/ that drives every premium surface — native desktop, console, mobile, VR, and the browser (the same binary, server-rendered over Pixel Streaming). It is the high-fidelity end of the tiered client stack: Lumen GI, Nanite geometry, MetaHuman-class avatars, and Sequencer-driven concerts can only run inside UE5, so rather than fork a per-platform client, V3 ships a single project that the build pipeline cooks eleven different ways and a Game Feature plugin system that hot-loads tenants and event modes without an engine rebuild. The shape of that project is not aspirational prose: V3/ue/V3.uproject pins "EngineAssociation": "5.5" and "DisableEnginePluginsByDefault": true, declares 17 C++ modules with explicit loading phases, and enables the exact plugin set the design needs (PixelStreaming, GameplayAbilities, Niagara, Metasound, Mover/PoseSearch, the Iris replication system, OpenXR/OpenXRHandTracking, OnlineServicesEOS, Gauntlet). This page is the module-, plugin-, and platform-level map of that client; the section hub is ../V3_ARCHITECTURE.md.

The reason the client is organised this way is operational. A metaverse that must boot a cold editor in ≤ 25 s, cook for phones and an Apple Vision Pro from one source tree, and load a "Saraswati concert" or a "Tara live class" as a self-contained content bundle cannot be a monolith of always-on code. So V3 splits its concerns into narrow modules (each with its own *.Build.cs and a small FV3…ModuleContract identity), declares everything tenant- or event-specific as an ExplicitlyLoaded Game Feature plugin that ships in the Registered state and only activates on join, and centralises the platform-matrix knowledge in one validated registry of cook profiles in V3Core. The same source even produces a second build target — V3PixelStreamingWorker.Target.cs, a monolithic headless cook — so the browser tier is literally this client running server-side. Everything below is grounded in that tree.

What ships, honestly#

The module split, the platform cook-profile registry, the Game Feature plugin pipeline, and the cross-language wire codec are real and tested. The 17 modules all build into both the V3 game target and the V3PixelStreamingWorker target (V3/ue/Source/V3.Target.cs, V3PixelStreamingWorker.Target.cs); V3Core is the IMPLEMENT_PRIMARY_GAME_MODULE (V3/ue/Source/V3Core/Private/V3Core.cpp:852) and carries a real eleven-platform cook-target registry whose per-platform Validate() enforces RHI, shader-format, resolution, FPS, and XR-runtime rules; the Game Feature generator and verifier are real editor commandlets; and V3Net is a hand-written Protobuf varint/zig-zag codec that agrees byte-for-byte with the TypeScript and Rust bindings of the same .proto. There are 32 automation specs under V3/ue/Source/V3Tests/, and they assert specific values (e.g. "Win64 shader format is SM6", "Quest 3 per-eye 90 fps budget"), not just truthiness.

But the client is a logic-and-manifest skeleton, not a content-complete game. This is the part the monolith's prose overstates, and the honest position matters:

  • Six of the 17 modules are module-contract stubs. V3Gameplay, V3Animation, V3Cinematics, V3Persistence, V3Telemetry, and V3VFX are each ~49 lines — a DECLARE_LOG_CATEGORY, an FV3…ModuleContract, and an IMPLEMENT_MODULE. The monolith's "GAS abilities (interaction, asana lock, gesture, applause), attribute sets" for V3Gameplay is not implemented in C++; the only ability references in the repo are asset-path strings inside Game Feature manifests (e.g. /Game/Saraswati/Abilities/AS_ConcertAudience), and those .uasset ability sets are not committed.
  • There is no UV3NetDriver, no UV3WorldSubsystem, and no V3NetTransport cxx-bridge. The monolith's "World-Server ↔ UE Network Integration" section describes a UNetDriver subclass and a local-mirror world subsystem; a grep of V3/ue/Source finds none of those symbols. What exists is the wire codec (V3Net) and a voice client model (V3Voice); wiring them to live actors is not in the tree. Treat that section as design intent. See Netcode Protocol and Physics.
  • Zero .umap files are committed. Every cook profile targets /Game/World/Atrium/L_LilithCommonsAtrium, but that map exists only as a JSON scene descriptor (V3/ue/Content/World/Atrium/L_LilithCommonsAtrium.v3scene.json), not an authored level. The committed binary content is generated: 21 GameFeatureData.uasset (one per plugin, machine-built from manifests), ~107 design-token Slate brush/style assets (built by a second commandlet), and one ConcertMaster.uasset Saraswati template.

So: the buildable engineering scaffold — modules, targets, cook profiles, plugin registration, the wire codec, the VR/avatar/UI subsystems with real logic — is genuinely here and under test. The authored game content — levels, ability blueprints, Niagara systems, MetaSound graphs, avatar meshes — is declared in manifests and scene descriptors but largely not committed. Where a module is a stub or a claim is unbacked, this page says so.

The 17-module split#

Each module owns one concern, builds with its own *.Build.cs, and exposes a tiny FV3…ModuleContract (GetModuleName / GetOwnedSurfaceTag / SupportsRuntimeLoad) so the split is introspectable. The runtime fifteen are listed in V3.Target.cs ExtraModuleNames; V3Editor (Editor type) and V3Tests (DeveloperTool) round out the seventeen in V3.uproject. The table maps each module to what is actually in the tree today, not the aspirational charter:

Module Files State What the code actually contains
V3Core 9 substantive Primary game module, 120 fps bootstrap frame policy, 11-platform cook-profile registry, UV3GameFeatureData, AV3LilithGameMode/Pawn.
V3Gameplay 3 skeleton Module contract only (~49 LOC). No GAS classes, abilities, or attribute sets in C++.
V3World 5 thin Cold-join phase-budget fixtures, Atrium placeholder-scene validation, V3InWorldCameraProp. Not a replicated world subsystem.
V3Avatar 12 substantive FV3MetaHumanRetargetTable (Oshun60 bindings), OpenXR hand-IK, V3HandIkSolver, V3PostureStateMachine, V3TaraEyesOpenPolicy.
V3Animation 3 skeleton Module contract only (~49 LOC).
V3Input 9 substantive V3HandGestureRegistry, V3VrFloorRecalibration, V3VrTimewarp (the [V3.OpenXR] runtime module).
V3Net 4 substantive Hand-written Protobuf wire codec (FV3NetMultiplayerProtocolCodec); packet structs for snapshot-delta, presence, voice, interaction.
V3Voice 4 substantive FV3VoiceRealtimeGatewayClient SFU model — Opus 24 kbps, DTLS-SRTP, Resonance spatialization, mouth-to-ear budget.
V3UI 15 substantive V3DesignTokenLibrary, V3AvatarNameplateLod, V3ActivityState, VR comfort/calibration, the Tara physical-adjustment consent dialog.
V3Audio 5 partial V3SteamAudioVr spatialization (~200 LOC) atop the module contract.
V3VFX 3 skeleton Module contract only (~49 LOC).
V3Cinematics 3 skeleton Module contract only (~49 LOC).
V3Persistence 3 skeleton Module contract only (~49 LOC).
V3OnlineServices 3 substantive EOS / V1-identity model packed into the module file (~403 LOC); covered by V3OnlineServicesIdentityTests.
V3Telemetry 3 skeleton Module contract only (~49 LOC).
V3Editor 11 substantive The Game Feature + design-token authoring commandlets and the token-reloader widget.
V3Tests 35 substantive 32 automation specs: cook profiles, net codec, voice latency, OpenXR, avatar, VR comfort.

The honest reading of this table: the client-side subsystems that have nothing to do with authored 3D content — networking codec, voice routing, VR comfort, avatar retarget math, design tokens, identity — are real and tested, while the modules that exist mainly to host authored gameplay/animation/VFX/cinematics content are still empty shells waiting on that content.

Module loading order and the primary game module#

V3.uproject assigns loading phases deliberately: V3Core and V3Gameplay load PreDefault (so the cook policy and gameplay tags are up before world actors), everything else loads Default, V3Editor is Editor-only, and V3Tests is a DeveloperTool. V3Core's module is the project's IMPLEMENT_PRIMARY_GAME_MODULE(FV3CoreModule, V3Core, "V3"). On startup it applies a frame policy — t.MaxFPS = 120, bUseFixedFrameRate = false, and a re-application on OnPostEngineInit so a late device-profile cannot clobber it (V3Core.cpp:808831) — matching DefaultEngine.ini's t.MaxFPS=120. The default game mode is wired in config, not code: DefaultEngine.ini sets GlobalDefaultGameMode=/Script/V3Core.V3LilithGameMode, and that AV3LilithGameMode simply sets DefaultPawnClass = AV3LilithPawn::StaticClass() with no HUD (V3LilithGameMode.cpp).

The ≤ 25 s cold-start budget is enforced from two directions. V3.uproject sets DisableEnginePluginsByDefault: true so nothing loads unless explicitly enabled, and DefaultEngine.ini then carries a long +DisabledPlugins= block (Android*, AppleARKit, Bridge, the DMX family, nDisplay, VirtualProductionUtilities, WebBrowser*) plus a [V3.EditorPerformance] section with ColdStartBudgetSeconds=25 and DisablePluginListSource=V3.UE.ModuleSplit. The OpenXR posture is config-pinned too: [V3.OpenXR] names RuntimeModule=V3Input and +RequiredBackends= Quest3 / VisionPro / PSVR2 / ValveIndex / ViveFocus3.

The wire codec, concretely#

V3Net is the one place the Tier-1 client touches the world server, and it is a genuine engineering artifact rather than a placeholder. V3NetProtocol.cpp implements Protobuf wire format by hand — WriteVarint, ZigZagEncode32, length-delimited keys — with no dependency on a protobuf runtime (the V3Net.Build.cs deps are just Core/CoreUObject/Engine/GameplayTags/ V3Core). Transforms are packed for the bandwidth budget: position in millimetres (FV3NetVector3Mm), rotation in milli-degrees (FV3NetRotationMilliDegrees), expression intensity in basis points — which is how the "≤ 32 bytes per avatar" target is met. The codec encodes and decodes snapshot-delta, presence, voice-control, interaction, and client/server envelopes, and BuildInEngineProtocolExchangeReport() produces the fixture that V3NetProtocolTests checks against the canonical hex. That same .proto is implemented three times (TS, Rust, this C++), and the cross-language golden-hex parity test is the guard — see Netcode Protocol and Physics and World Server and Gateway.

Game Feature Plugins: tenants and event modes#

Everything tenant- or event-specific is shipped as a Game Feature plugin so it can be hot-loaded on join, unloaded on leave, and included or excluded per platform/region at cook time without an engine rebuild. There are 21 plugins under V3/ue/Plugins/GameFeatures/3 tenants and 18 event modes:

Kind Plugins
Tenant V3Tenant_TaraStudio, V3Tenant_SaraswatiStage, V3Tenant_LilithCommons
Tara V3Mode_TaraLiveClass, V3Mode_TaraOnDemand, V3Mode_TaraPrivate, V3Mode_TaraCohort
Saraswati V3Mode_SaraswatiConcert, V3Mode_SaraswatiClub, V3Mode_SaraswatiListening, V3Mode_SaraswatiDrop, V3Mode_SaraswatiFestival
Commons V3Mode_CommonsAtrium, V3Mode_CommonsObservatory, V3Mode_CommonsDebateHall, V3Mode_CommonsLectureHall, V3Mode_CommonsStacks, V3Mode_CommonsRitualRoom, V3Mode_CommonsAretAtrium, V3Mode_CommonsLanternHall, V3Mode_CommonsSolitaryCell

Every *.uplugin carries the same three flags that make hot-loading work: "EnabledByDefault": false, "ExplicitlyLoaded": true, and "BuiltInInitialFeatureState": "Registered" (verified across the tenant and mode plugins). Registered means the plugin's assets are discoverable but inert until the Game Features subsystem transitions it to Active; ExplicitlyLoaded keeps it out of the default mount so cold start stays cheap. Each plugin also declares "Plugins": [GameFeatures, ModularGameplay] as dependencies.

The manifest → asset → runtime pipeline#

The interesting part is that the registration data is authored as JSON and compiled into a UAsset by a commandlet, so the source of truth is reviewable text and the binary is reproducible. Each plugin ships a Content/<Plugin>/GameFeatureData_<Plugin>.v3asset.json manifest. For V3Mode_SaraswatiConcert it reads:

json
{
  "assetType": "GameFeatureData",
  "plugin": "V3Mode_SaraswatiConcert",
  "activation": "join-saraswati-concert",
  "gameplayTags": [
    "Mode.Saraswati.Concert",
    "Saraswati.Sequencer",
    "Audience.BandedSeating"
  ],
  "abilitySets": [
    "/Game/Saraswati/Abilities/AS_ConcertAudience",
    "/Game/Saraswati/Abilities/AS_ConcertOperator"
  ],
  "worldTemplates": [
    "/Game/Venues/Saraswati/Concert/LVL_SaraswatiConcert_Template"
  ],
  "niagaraSystems": [
    "/Game/VFX/Saraswati/NS_ConcertStage",
    "/Game/VFX/Saraswati/NS_AudienceLightWave"
  ],
  "metaSounds": [
    "/Game/Audio/Saraswati/MS_ConcertMainMix",
    "/Game/Audio/Saraswati/MS_AudienceBandMix"
  ]
}

The target of that compilation is UV3GameFeatureData (V3/ue/Source/V3Core/Public/V3GameFeatureData.h), a UGameFeatureData subclass with seven EditDefaultsOnly fields — FeatureName, ActivationPolicy, and arrays of gameplay tags, ability-set ids, world-template ids, Niagara-system ids, and MetaSound ids. Its HasCompleteRegistrationManifest() refuses an incomplete manifest field-by-field ("Game Feature manifest is missing required field: {0}"), and under WITH_EDITOR that check is wired into IsDataValid() so a malformed Game Feature fails Data Validation rather than shipping inert.

flowchart TB json["GameFeatureData_*.v3asset.json<br/><sub>authored manifest (per plugin)</sub>"] gen["V3GenerateGameFeatureDataCommandlet<br/><sub>editor commandlet</sub>"] tags["Config/Tags/*Tags.ini<br/><sub>generated gameplay tags</sub>"] asset["Content/GameFeatureData.uasset<br/><sub>UV3GameFeatureData (committed)</sub>"] verify["V3VerifyGameFeaturesCommandlet<br/><sub>load → Active → unload → Registered</sub>"] runtime["GameFeaturesSubsystem<br/><sub>activates on join</sub>"] json --> gen gen --> tags gen --> asset asset --> verify verify -->|round-trip passes| runtime

V3GenerateGameFeatureDataCommandlet::Main (V3/ue/Source/V3Editor/Private/) walks every *.uplugin under Plugins/GameFeatures, reads and strictly validates the JSON (every array must be non-empty; assetType must equal GameFeatureData; the manifest's plugin must match the descriptor filename), writes a <Plugin>Tags.ini of GameplayTagList= entries, then idempotently creates-or-updates the GameFeatureData.uasset via SavePackage — re-running is a no-op when AssetMatchesManifest holds. That is why all 21 plugins have a committed Tags.ini and a committed GameFeatureData.uasset that exactly mirror their manifests.

The companion V3VerifyGameFeaturesCommandlet proves the plugins actually hot-load. For each descriptor it drives the real UGameFeaturesSubsystem::ChangeGameFeatureTargetState to Active, ticks the editor while polling for completion (10 s timeout), asserts IsGameFeaturePluginActive, then transitions back to Registered — a genuine load/unload round-trip per plugin, not a flag check. Caveat: because the manifests point at /Game/... abilities, levels, Niagara, and MetaSounds that are not committed, this verifier exercises the registration and activation-state machinery, not a fully-populated feature.

Platform cook target profiles#

The platform matrix lives in one validated registry in V3Core: FV3CookTargetProfileRegistry (V3Core.h / V3Core.cpp) exposes a Build… factory for each of eleven EV3CookPlatform values and a BuildCookRunCommand that renders the profile's argument vector into a RunUAT BuildCookRun … string. Each profile is a FV3CookTargetProfile whose Validate(OutReason) is a real gate, and the rules are platform-aware rather than generic:

Profile (factory) RHI / shader Target res / FPS Reference HW Renderer rule enforced by Validate()
BuildWin64LumenNaniteProfile DX12 / PCD3D_SM6 2560×1440 @ 60 RTX 3060 Lumen + Nanite + VSM required; HW ray tracing on.
BuildMacAppleSiliconProfile Metal / SF_METAL_SM6 2560×1440 @ 60 M2 Pro Lumen + Nanite + VSM; HW RT off; arm64.
BuildLinuxLumenNaniteProfile Vulkan / SF_VULKAN_SM6 2560×1440 @ 60 RTX 3060 Desktop renderer; x64.
BuildSteamDeckMobileProfile Vulkan / SF_VULKAN_SM5 1280×720 @ 40 Steam Deck LCD Desktop renderer features must be off; 720p/40 fps floor.
BuildIosForwardPlusProfile Metal / SF_METAL 2556×1179 @ 60 iPhone 15 Pro Mobile shaders; Lumen/Nanite/VSM off.
BuildAndroidVulkanForwardPlusProfile Vulkan / SF_VULKAN_ES31_ANDROID 2992×1344 @ 60 Pixel 8 Pro Mobile Vulkan ES3.1; ASTC cook flavor.
BuildQuest3OpenXrProfile Vulkan / SF_VULKAN_ES31_ANDROID 2064×2208 @ 90 (per eye) Quest 3 dev kit Android Vulkan + XR runtime profile (hand tracking, foveation, passthrough).
BuildVisionProOpenXrProfile Metal / SF_METAL 3660×3200 @ 90 (per eye) Vision Pro simulator visionOS Metal + XR profile (hand + eye gaze, foveation).
BuildPsvr2OpenXrProfile GNM / SF_PS5 2000×2040 @ 90 (per eye) PSVR 2 dev kit Nanite on, Lumen/VSM/RT off; XR profile (eye gaze, foveation).
BuildPs5LumenNaniteProfile GNM / SF_PS5 3840×2160 @ 60 PS5 dev kit 4K60; HW RT off; must carry a PSVR2 90 fps secondary target.
BuildXboxSeriesXLumenNaniteProfile D3D12 / SF_XSX 3840×2160 @ 60 Xbox Series X dev kit 4K60; HW RT off for the post-GA budget.

The validator is not cosmetic. A desktop profile that drops Lumen, Nanite, or virtual shadow maps fails; a "constrained mobile" profile (Steam Deck, iOS, Android, Quest 3, Vision Pro) that enables any desktop-only feature fails; an XR profile that targets < 90 fps, omits per-eye dimensions, or claims a required capability without the matching input profile fails via FV3CookXrRuntimeProfile::Validate (e.g. "XR runtime profile must drive avatar hand IK", "must enable foveated rendering"). Every profile must cook the Atrium map, declare archive + manifest paths, and supply a BuildCookRunArgs vector of at least eight tokens. These rules are the subject of one automation spec per platform under V3/ue/Source/V3Tests/ (e.g. V3Win64CookProfileTests.cpp asserting Rendering.ShaderFormat == "PCD3D_SM6", render dimensions, and that the rendered command contains -platform=Win64 and the Atrium -map=).

The whole project is built with deterministic floating point — V3.Target.cs appends /fp:strict /fp:except- on Win64 and -fno-fast-math -ffp-contract=off elsewhere, with bUseUnityBuild = false and bBuildDeveloperTools = false, plus ProjectDefinitions V3_LILITH=1 and V3_ENGINE_UE55=1.

The Pixel Streaming worker is the same client, server-side#

The browser tier is not a separate codebase: V3PixelStreamingWorker.Target.cs compiles the identical fifteen runtime modules as a monolithic, headless, shipping-with-logging target (LinkType = Monolithic, bBuildWithEditorOnlyData = false, bUseLoggingInShipping = true) and adds the defines V3_PIXEL_STREAMING_WORKER=1, V3_HEADLESS_PIXEL_STREAMING=1, and (on Win64) V3_PIXEL_STREAMING_WIN_WORKER=1. A GPU worker runs this build, renders frames, and streams them over WebRTC to a browser that sends input back — so a browser attendee is, from the world server's perspective, just another Tier-1 client. How sessions are matched to workers and POPs, and how that tier is chosen, belongs to Tier Routing and Pixel Streaming; the point here is that the client binary is shared, and the only delta is a target file plus four cook profiles' worth of headless flags.

How the client connects to its neighbours#

  • To the world server / gateway. V3Net's codec speaks the @oshun/multiplayer-protocol wire format (the canonical .proto lives in libs/v3/multiplayer-protocol/); the Rust world server and gateway are the authority. See World Server and Gateway and Netcode Protocol and Physics.
  • Voice. V3Voice's FV3VoiceRealtimeGatewayClient models a LiveKit-compatible SFU at wss://lilith-realtime-gateway.internal/v3/realtime/sfu — Opus 24 kbps mono, DTLS-SRTP with replay protection, an 80 ms mouth-to-ear budget, and Resonance-Audio spatialization gated behind an honest IsResonanceAudioRuntimeAvailable() capability check. Detail lives in Avatar, Animation, and Audio.
  • Identity. AV3LilithPawn carries the V1 bridge at the actor level: ApplyV1IdentityNameplate runs SanitizeV1DisplayNameForNameplate (trim, collapse whitespace, 32-char clamp, "Guest" fallback) and flips bBoundToV1Identity. The TypeScript side is @oshun/lilith-identity-bridge (libs/v3/lilith-identity-bridge/). See V1 Integration and Identity Bridge.
  • Avatars and UI. V3Avatar holds the Oshun60 MetaHuman retarget table and posture/hand-IK/eyes-open logic (note its deps are engine-only — the retarget is a binding-and-validation model, not a live MetaHumanRuntime rig); V3UI carries the design-token Slate library (built into the ~107 brush/style .uasset by V3GenerateDesignTokenAssetsCommandlet), avatar-nameplate LOD, VR comfort/calibration, and the Tara physical-adjustment consent dialog. These feed Tara Classes, Aja, and Commons and the Saraswati Stage Pipeline.
  • Contracts. The launch-decision, tenant, and event-mode contracts the client honours are typed in libs/contracts/src/v3/ (lilith.ts, saraswati.ts, commons.ts, tara.ts) — the same registry the glossary and layout catalogues.