Server Authority and Replication
In Luduvo’s networked architecture, the server remains the sole authority over the virtual world. It runs all physics simulations, computes and applies damage, spawns and destroys entities, and transmits the resulting world state to connected clients 30 times a second.
Each client renders the replicated state, predicts local character locomotion, and communicates intent back to the server using structured event tables.
How the engine does this inside: Replication and Events.
Two distinct scripting environments#
A script’s file extension dictates its execution domain. Naming a script Hotbar.client.lua instructs the engine to load it on the client; any other naming convention, such as Tools.server.lua or Spawner.lua, runs exclusively on the server. The API reference clearly distinguishes what each environment permits:
| Marking | Server script | Client script |
|---|---|---|
| method, server only | callable | absent; calling it raises an error: 'Destroy' can only be called from the server |
| property, server write | read and write | read-only; attempting a write raises an error: 'Material' can only be set from the server |
| property, client only | a write errors with 'InputMove' is client-only | read and write |
Methods like Destroy, ApplyForce, SetLinearVelocity, TakeDamage, and PlayAnimation are strictly server-only. Properties such as Anchored, Health, or Velocity are globally readable, but only the server can mutate them. Conversely, properties driving local control (such as InputMove, InputSteer, and ViewSource) belong exclusively to the client camera and local input systems.
What the client simulates#
To eliminate perceived latency, the client simulates and predicts only the local player character. This keeps controls responsive without waiting for network round-trips. At every 1/60 s physics step, the client samples local input, records that input together with the predicted state (torso position, rotation, linear velocity, and angular velocity) in a 128-entry history, and sends the input to the authoritative server.
When the server’s authoritative snapshot arrives over the network, the client initiates reconciliation:
The client rolls back its state to match the authoritative snapshot once the torso position deviates by more than 0.01 studs, or velocity deviates by more than 0.1 studs/s. From there it replays the inputs it recorded since that snapshot, up to 90 steps, back to the present moment. If network conditions or desyncs create an error gap exceeding 4 studs, the client bypasses replay entirely and snaps directly to the server position to avoid visual artifacting.
The client never simulates remote entities. Instead, the runtime smoothly interpolates remote bodies between incoming server snapshots, introducing an adaptive interpolation delay tuned dynamically between 2 and 8 network ticks. Similarly, UI elements and the camera exist solely on the client: signals such as Activated, Hovered, and DragMoved fire exclusively within client scripts.
What replicates across the wire#
Whenever an entity possesses a network id, the engine replicates its core state: Position, Rotation, Scale, Name, Parent, Anchored, physics properties, color, transparency, material, collider geometry, meshes, surface appearances, welds, mechanical joints, character components (Health, team, backpack, locomotion, humanoid state, appearance), tags, and the attr table.
The attr table stores arbitrary key-value pairs without a declared schema, and replicates them along with the rest of the entity.
For character rigs, the engine replicates limb attachments as motor joints. Rather than relying on abstract transform frames, motor joints link limbs to the torso using explicit joint geometry: Joint (the torso-relative offset to the joint hinge) and Pivot (the limb-relative offset to the joint hinge, negated). The physics solver then constrains both coordinate frames to rotate cleanly around this shared pivot point.
Custom components replicate according to their declared schema scope: server-scoped components remain on the server, while client-scoped components exist only on the client.
To conserve bandwidth, the server sends only what changed since the client’s last acknowledged packet. Position updates are rounded to 1/1024 stud, so movement finer than that generates no network traffic, and character rotations travel as 32-bit packed quaternions.
Communicating with the server#
Signals - instance:GetSignal(name), instance:Emit(...), and signal:Connect(fn) - never cross the network boundary: a client emit is never seen by the server. When a client communicates with the server, or the server pushes targeted data to specific clients, use an EventTable:
-- shared declaration, same in both scriptslocal ET = game.EventTablelocal ToolEquip = ET("ToolEquip", ET.ToServer, {{"slot", ET.U8}})-- Hotbar.client.luaslot.Activated:Connect(function() ToolEquip:Push(slotIdx)end)-- Tools.server.luafunction Update(dt) for i = 1, ToolEquip.count do applyEquip(ToolEquip.sender[i], ToolEquip.slot[i]) endendConfigure an EventTable using one of two directions:
ToServer: Clients push rows to this table, and the server consumes them.ToClients: The server pushes rows using eitherPush(broadcasting to everyone) orPushTo(userId, ...)(targeting a specific player), and clients read them.
Field types support F32, I32, U8, Bool, Vec3, Color, Entity, or String. Only the server can inspect the sender column. Event rows persist for exactly one tick: read them inside Update during the tick they arrive.
To keep bandwidth bounded and predictable, the engine enforces strict networking limits:
- Up to 16 fields per table
- A maximum of 256 bytes per row
- At most 64 rows per table per tick
- A maximum of 64 distinct tables per place
- If a client exceeds 16 KiB of row data within a single tick or transmits a malformed payload, the server immediately drops the connection.
Players and characters#
The Session API manages player lifecycles and character binding:
local function spawnFor(userId) if game.Session.CharacterOf(userId) ~= nil then return end local character = game.Prefabs.Spawn("Cadet") character.Position = pickSpawnPosition() game.Session.BindCharacter(userId, character)endgame.Session.PlayerJoined:Connect(spawnFor)Calling BindCharacter links an active entity to a user session. This call assigns Identity, DisplayName, and the locomotion intent that translates the player’s incoming input into movement. At the same time, the server informs the client which entity it should predict locally. A user can control only one character at a time; binding a new character requires explicitly destroying the old one first.
The session system provides several lifecycle hooks:
Session.PlayerLeftfires with(character, userId).Session.CharacterDiedfires with(character, userId, attacker).Session.Kick(userId, reason?)disconnects a target player.- On the client side, invoking
game.Session.LocalCharacter()retrieves the locally predicted character entity.
Platform-owned entities#
Certain entities belong to the platform itself, such as the core system menu, developer output screens, and its camera rigs. Scripts see these entities as nil unless the platform explicitly exposes them as view rows. Attempting to invoke Destroy or reassign Parent on a platform entity raises an error: the entity belongs to the platform. Similarly, scene queries and place saves automatically bypass them.
World streaming#
When Streaming.Enabled remains false, the server replicates every networked entity in the world to every connected client.
When world streaming is enabled, the engine divides the world into 64-stud cells. Each client receives only the entities within the World component’s active ViewRadius, configurable from 64 to 1536 studs and capped at 512 studs by that grid. As a player moves through the world, entities stream in as they come into range and stream out as they leave it. The engine keeps a one-cell margin past the radius, so objects at the horizon do not flicker in and out. Finally, a character’s complete skeletal rig always streams in tandem with its root entity.