Replicated World Settings
A world’s rules are the server’s, and they ride the snapshot so every client runs the same ones. Each is a preference a host can set from the console, an autoexec or the server settings panel.
World time scale
Section titled “World time scale”world.timeScale is a server-owned multiplier on the rate of fixed ticks, following Source’s host_timescale: 1.0 is normal, 0.5 half speed, 2.0 double. It multiplies the real seconds fed into the fixed-step accumulator, not the per-tick dt — every tick is still exactly SecondsPerTick, so the world simply advances more or fewer identical ticks per real second. Per-tick physics is unchanged and prediction stays deterministic, because the client and server run bit-identical fixed ticks; only the cadence moves.
The server scales its sim-loop accumulation by the value, and the client scales its prediction tick accumulator by the replicated value (defaulting to 1.0 until its first snapshot), so predicted movement stays in lockstep with the authoritative sim. A change produces a brief predicted/authoritative transient that reconciliation smooths out; steady state converges.
It is a cheat-gated, transient world.* preference: set it from the console with world.timeScale <value> (rejected unless world.cheats 1, exactly like noclip), clamped to a sane positive range, and deliberately never persisted so a slow-mo session never survives a restart.
Coyote time
Section titled “Coyote time”world.coyoteTime is the per-world coyote-time grace window in seconds — the brief period after walking off a ledge during which a jump input still fires as if the pawn were grounded. It is on by default (0.1 s, i.e. 6 ticks at 60 Hz); 0 disables it. Like every other world.* movement tunable it lives in the server-owned world preference store (so it survives gameplay-assembly hot reload and two worlds can disagree), and the mover converts it to whole sim ticks against the shared Conventions.TickRate, never a hardcoded 60.
The window is why it must be replicated. Jump logic runs inside the shared CharacterMover stepped on both the server and the client predictor; if the two used different coyote values, every ledge jump would mispredict and rubber-band. So the value rides the WorldSettings section (the same channel as world.timeScale) and the predictor applies the replicated value to its mover config before each predicted step — and re-applies it whenever a mid-session change arrives — so prediction and the authoritative sim step the ledge jump identically. The mover tracks ticks-since-grounded per pawn (replicated in the owner-detail section for reconciliation) and consumes the window the instant any jump launches, so a single airborne period yields at most one coyote jump and a normal jump cannot be chased by a bonus one.
Jump budget
Section titled “Jump budget”world.maxJumps is how many jumps a pawn may make before it touches the ground again: 1 (the default) is the ordinary single jump, 2 a double jump, and so on up to MovePreferences.MaxMaxJumps. Every jump past the first fires in the air, at the same impulse as a ground jump, and replaces the vertical speed rather than adding to it — so a jump at the apex and a jump mid-fall reach the same height. A ground touch refills the budget.
Being airborne at all spends the first slot: walking off a ledge with world.maxJumps 2 leaves exactly one air jump, never a free one the ground would not have given. A coyote jump (world.coyoteTime) counts as that first jump, so a ledge jump still has its double jump after it. It is a persisted, per-player-overridable world.* movement tunable like the rest (players.<target>.maxJumps 3), replicated through WorldSettings so the client predictor meters the same count the server does; the pawn’s spent count rides in the owner detail for the same reason.
Noclip allowance
Section titled “Noclip allowance”world.noclipAllowed decides whether a player without world authority may use noclip. The host, the server console and admins (server.operators) may always fly; a guest needs either world.cheats 1 or world.noclipAllowed 1 — the allowance opens flight to everyone on the server without opening every cheat with it. Default 0, persisted, replicated read-only, and a row of the launch presets so a server can ship it on.
Noclip momentum
Section titled “Noclip momentum”Noclip flight ramps up from rest and bleeds off on release through the same shared accelerate/friction math ground movement uses, rather than snapping straight to speed — a tap nudges, a held key ramps up. It has its own values instead of borrowing the ground ones: world.noclipSpeed (12 m/s, unchanged) is the fly speed accelerate ramps toward, world.noclipAccelerate (5, Source’s noclip default) is a flatter ramp than the ground’s 10, and world.noclipFriction (4, equal to the ground default) is what bleeds it back to rest. Friction is capped at the accelerate value rather than the 4x ground friction Source is sometimes credited with: under the shared accelerate-then-friction tick order, a held key can only climb to accelerate * noclipSpeed / friction when that ratio undershoots the target speed, so a friction of 16 against an accelerate of 5 would cap flight at 3.75 m/s and never reach noclipSpeed at all. All three are persisted, per-player-overridable world.* preferences (players.<target>.noclipAccelerate 8) replicated through WorldSettings so the client predictor ramps and bleeds identically to the server.
Slope limit
Section titled “Slope limit”world.slopeLimit is the steepest surface a pawn can walk up, in degrees; steeper surfaces cause the player to slide. The mover never compares angles — it compares the ground contact’s normal-Y against a cosine threshold — so the degree value is converted to that ground-normal-Y minimum (cos(slopeLimit)) once, where the MoveVarValues snapshot is assembled or overlaid, never per tick. The default is derived the other way round from a fixed 0.7 minimum walkable normal-Y (so the degree default, ≈ 45.57°, and the 0.7 threshold can never drift apart through rounding). A lower degree limit demands a flatter surface, which is a higher minimum normal-Y, so fewer surfaces count as ground. Like the other movement tunables it is a persisted world.* preference, part of the world autoexec, and replicated through WorldSettings so prediction agrees with the authoritative sim.
Step height
Section titled “Step height”world.stepHeight is the tallest riser a pawn walks up without jumping, in meters, and how far a pawn walking off a ledge or down a stair is held to the ground below it. The default is 0.5: a Minecraft stair step and slab are half a block, so a voxel staircase walks up and down, and every Source stair (8 u to 16 u) is unchanged. Source’s own value is 18 u, 0.4572; a world that wants exactly Half-Life’s stairs sets world.stepHeight 0.4572. A riser is climbed when it is at most the step height and never when it is taller, at full walking speed, and walking down a staircase keeps the pawn on every step rather than skipping off them.
The climb is Source’s StepMove: try the move; if something stops it, lift by the step height, make the same move, settle back down, and keep whichever went further, as long as it stands on walkable ground no more than a step above the ground it left. The pawn’s hull is a capsule, not Source’s box, so its round bottom meets a step’s edge with a tilted contact however flat the step’s top is. The mover reads through that edge: a short ray straight down just inside the contact finds the face it belongs to, and a walkable face counts as ground. A steep slope answers with its own steep normal, so it still slides. Standing with the capsule’s rim on a lip the step carried it onto is ground too, exactly as a Source box stands on a ledge by its corner.
A lip whose top is more than the ground reach above the feet is a raised lip, and the box meets it with its side, not its bottom. So the mover treats it the way Source does: a pawn moving into it meets a vertical wall beside the tilted contact, so the round bottom never rides up the edge or launches off it, and a pawn in the air does not land on it. A thin bar at hip height, such as a railing’s top rail, stops a walk, a sprint and a jump that cannot reach its top, at every player size. A real step is still climbed, by the step attempt above. A pawn in water is the exception: its round bottom still lands on and rides over the lip it is pulling itself out over, which is how a swimmer climbs out where Source’s water jump glides its box up and over.
Like the other movement tunables it is a persisted, per-player-overridable world.* preference, scaled with the pawn when world.scaleMovement is on, and replicated through WorldSettings so the client predictor climbs the same steps the server does.
Step view smoothing
Section titled “Step view smoothing”The pawn climbs a step in a single tick, so without help the first-person camera pops up by the whole riser at once. client.view.stepSmooth is Source’s stair smoothing: the pawn still moves at once, but the eye is held where it was and slides to the new height.
- What it smooths. A tick that starts and ends grounded and changes height by more than the ground’s own slope explains: a step up or down. The rise a walkable slope accounts for (the tick’s horizontal travel times the steepest walkable slope) is subtracted first, so a ramp is followed exactly and only a stair is smoothed. A jump, a fall, a landing, a swim, noclip, a frozen pawn, a teleport, a spawn and a map load are never smoothed, and an offset a step left is dropped the moment the pawn leaves the ground. A pawn riding a mover is not smoothed either: the platform’s travel is the platform’s.
- How it decays. Like Source’s
CalcView(which walks the remembered height toward the real one at a fixed 150 u/s), the offset closes on zero at a constant speed, not exponentially. A stair is a series of steps, and an exponential tail never lands before the next riser; a linear ramp does. The speed isclient.view.stepSmoothMax / client.view.stepSmooth, so the preference is the time the largest smoothed step takes and a smaller step is proportionally quicker. The defaults (0.15 s over 0.6 m, 4 m/s) are Source’s feel. - The cap. One rise taller than
client.view.stepSmoothMaxin a single tick is a teleport or a mover, not a step, and snaps. Consecutive steps accumulate up to the same cap. It sits just above the defaultworld.stepHeightso every step the pawn can climb is smoothed. - What it never touches. Movement, prediction, the server, the aim, every trace and interaction, the audio listener, other players’ rendered positions and the editor free camera. It is added to the drawn view only (the same split as the first-person neck pivot: the aim is the authority and the picture is derived from it).
- Reconciliation. A correction from the server is eased by the predictor’s own visual offset, which already keeps the rendered pawn continuous. Step smoothing is fed only by real predicted ticks, never by a replay, so a correction is never counted twice and a correction snap is never read as a step.
| Preference | Default | Meaning |
|---|---|---|
client.view.stepSmooth | 0.15 | Seconds the eye takes to absorb the biggest smoothed step; 0 turns the smoothing off. |
client.view.stepSmoothMax | 0.6 | Tallest step in meters that is smoothed; a bigger rise in one tick snaps. |
client.view.stepSmoothMinimum | 0.02 | Smallest unexplained rise in meters that counts as a step, so a bumpy floor is not smeared. |
All three are client-side and apply on the very next frame from the console.
Surface friction
Section titled “Surface friction”world.surfaceFriction is a ground grip multiplier applied to ground acceleration, ground friction, ground-stick strength and slope grip together — the default is 1.0 (full grip, the baseline feel). It is resolved at the ground-categorization point: the mover reads the grip of the dh.material on the surface under the feet (surface.frictionMultiplier, keyed per-triangle for mesh maps), falling back to this world default where no per-surface material applies. Lower values are slidy — the pawn keeps more speed and gets less acceleration control — and, crucially, they let gravity slide the pawn down walkable slopes: the grip statically holds a slope while its angle is within the grip (tan(angle) ≤ grip, so at full grip the pawn holds every walkable slope up to the ~45.57° limit), and once the angle exceeds that the pawn accelerates downhill at g·(sin − grip·cos), exactly a block on an incline. A slime/ice material (e.g. frictionMultiplier 0.12) therefore slides you down even a gentle ramp, while a steeper-than-limit surface slides regardless. 0 is frictionless. Like the other movement tunables it is a persisted, per-player-overridable world.* preference, part of the world autoexec, and replicated through WorldSettings. (The same grip will drive dynamic rigid-body props once those exist; today it scopes to the player mover.)
Swimming and buoyancy
Section titled “Swimming and buoyancy”There is one water, and everything asks it. The map load walks every water surface it carries — a boxBrush under a water material and an imported GLB node under one alike — and mints a WaterBody per surface into the world’s single WaterField. Swimming, buoyancy, the collision the water brush subtracts from the solid world and the camera’s own under-water test are all that one field read four ways, so nothing the field does not hold is water and nothing it holds is water to only half the engine. The camera in particular asks the field rather than the geometry the renderer happens to draw: a pool authored as a thin surface slab over an empty basin is a whole column to the field, and an eye a hair under the surface fogs and flips the surface’s face on the very frame the pawn starts swimming.
A pawn’s water level is Quake’s and Source’s PM_CatagorizePosition/CheckWater rule: three point tests — feet, waist, eyes — against the water’s displaced surface height at the pawn’s own column, evaluated by the same Gerstner sum the shader draws, giving a level of 0 to 3. A band of hysteresis (world.water.hysteresis, in meters, default 0.08) holds the level the pawn already has, so a wave smaller than the band cannot flicker it in and out of swimming.
Waist deep and up the mover switches to Source’s WaterMove: ordinary gravity off and the water’s own vertical term in its place, a view-relative three-dimensional wish, its own world.water.accelerate (10) and a lower world.water.maxSpeed (4.064 m/s, Source’s 160 u/s), world.water.sprintSpeed (8.128 m/s) while sprint is held — the land sprint-over-walk ratio applied to the swim cap, so Shift buys exactly as much in water as it does on a floor — drag before acceleration, a fixed world.water.jumpSpeed (2.54 m/s) while jump is held, and the CheckWaterJump ledge climb-out that lifts a swimmer onto a bank within world.water.jumpReach. Below the waist the pawn walks normally and wades against world.water.drag.
The water a player swims in decides whether that player sinks, hangs or rises, and it decides it from the very density a floating crate’s law reads — the dh.material water preset’s own density, never a second knob. The pawn brings world.water.playerDensity (1000 kg/m³, the reference fresh water) and the water brings its preset’s, and the swimmer gets a net vertical acceleration of g · (1 − waterDensity / playerDensity): denser water lifts, thinner water sinks, equal water hangs. Plain fresh water is exactly equal, so lake, pool and river are the zero-gravity case a swimmer simply stops in; ocean at 1025, baltic at 1005, silt at 1010, mud at 1700 and stylized at 1060 rise, while swamp at 960 and oil at 900 pull you under. quicksand at 2000 is the far end of the same law and needs no special case for it: g · (1 − 2000/1000) is −g, so a person who walks into it accelerates upward at a full gravity and stands at waist height, unable to go under. The same density also decides how the body bends light — its index of refraction derives from it, and the optional ior, dispersion and causticStrength fields override that derivation without ever moving what an unauthored body does. See the preset table, Optics and Thin film — the oil slick’s iridescence is a spectral Airy summation in the shared iridescence.glsl, mixed against the ordinary Fresnel by a weight every other body carries at zero. The term is clamped to world.water.terminalSpeed (1.27 m/s, half the swim-jump speed) and only ever drives the pawn toward it, so a swimmer already moving faster under its own stroke or a ledge climb-out is never dragged back by the water it is in, and the swim wish always adds on top. The surface still belongs to the level rule: a lift applies only while the eyes are under, so it stops the frame they break out and nobody bobs above the waterline. world.water.playerDensity is per-player overridable like the caps (players.<target>.playerDensity) — a heavier player sinks in the water everyone else floats in.
The level is predicted state, and swimming is read off it rather than carried as a flag of its own, so the two can never disagree. It rides the snapshot’s trailing owner-detail section — additively, with no protocol bump — and the client predictor samples the same water field the server does, so ReplayFromServer reproduces a swim rather than fighting it. The buoyancy tuning (world.water.samples, footprint, heaveDamping, rollDamping, wakeThreshold, maxRollRate, centerOfMassDrop) travels the same channel, which is what keeps a floating crate at one depth on both machines. Both swim caps are per-player overridable under the ordinary move-var vocabulary — players.<target>.swimSpeed and players.<target>.swimSprintSpeed — and the server resolves a pawn’s overrides into the swim tuning it replicates, so the owning client predicts the caps it is actually held to.
A floating body is sampled at a ring of points on its own box; each wet sampler pushes up with the weight of the water it displaces at its own world point, so the righting couple falls out of where the points are. Damping is a fraction of each body’s own critical damping, not a fixed velocity-decay rate. world.water.heaveDamping (0.7) is resolved against the body’s waterplane stiffness ρ·g·4·hx·hz and its mass; world.water.rollDamping (0.7) against its righting stiffness — the sampler ring’s springs on their own arms plus the couple world.water.centerOfMassDrop (0.35) adds — and its roll inertia. A fixed rate cannot do this: 1.4 rad/s is ~4.5% of critical for a 0.7 m, 30 kg crate, so the pool’s own ripple re-excited an essentially undamped roll mode forever. On the physics-eval pool that crate now settles in 2.0 s to a bob of 0.013–0.026 m — the wave’s own amplitude — at 2.7° of heel, which is the wave slope itself, and a plunge throws it 0.79 m clear rather than 2.72 m.
Sleep is reachable. A sleeping float whose lift is within world.water.wakeThreshold (0.15, a fraction of its own weight) of balance is left untouched — no force, no damping write — so a settled crate on still water goes to sleep and stays there, while a wave tall enough to move it puts the imbalance past the band and wakes it.
A client-simulated prop floats by the same law. physics-eval’s blue crates are authored "sim": "client", so no solver on the server ever touches them: they live in the client predictor’s own physics world and are stepped by ClientPhysicsProps.Step, which runs the identical Buoyancy.Apply the server’s prop tick runs and drives that world’s solver gravity through PhysicsPropBodies.SyncGravity — the same call NetLogicHost makes, so both piles fall at one world.gravity instead of the client’s falling at whatever the solver ships with. The water is the WaterField the predictor already holds from the map’s fold, sampled at the same wave clock. The tuning is the replicated owner-detail world.water.* whenever a server has sent any, which is what holds the blue crate at the orange one’s exact draft; with no server behind it — the offscreen physics probe, a predictor a harness built — it falls back to the client’s own preference store, the way world.prop.push* is always read from the client’s store for the local shove.
prop.drop (see spawning an asset at runtime) mints a fresh server-simulated physics prop in front of the caller for exercising this same buoyancy path on demand, rather than only ever watching a map’s own authored pile.
A water brush is the same body in both worlds. DevMapColliders.Brush is the one rule that turns an authored brush into collision: a brush the map’s water fold claims is installed on CollisionLayers.Trigger and as a sensor, everything else is a solid CollisionLayers.World box. The server’s map install, the client predictor’s world rebuild and the offscreen physics probe all call it, so none of them can disagree about what water is. Both halves matter — the layer alone only steers masked queries, so a client-simulated crate dropped in a pool would still rest on the surface while the server’s identical crate sank through it.
Three behaviors fall out of that one rule rather than being written twice. The mover’s MoveMask is World | Debris, so a sensor is never ground: a pawn over water cannot be grounded, cannot be given the ground acceleration and ground friction that made a jump into a pool slide like ice, and can play no footstep, because the feel path takes its surface from a ground hit and there is no ground hit to take. The predictor’s world also carries the map’s MapSurfaceFold, so its water query is the server’s water query — the same Gerstner sample at the same tick, the same hysteresis band carried in CharacterMotion.WaterLevel — and a pawn walking off a bank into a pool steps identically on both sides.
Player scale
Section titled “Player scale”A pawn has a size: a replicated, server-authoritative multiplier, default 1, that the hull radius, the stand and crouch heights, both eye heights and the camera are all derived from — CharacterDimensions is the single place each of those is multiplied, so a player’s size can never mean one thing to the mover and another to the camera. A pawn at 1 carries no scale component at all, the same absent-means-identity convention the rest of the replication uses.
Players request a size and the server decides: scale 2, scale 0.5, bare scale to read. The request is clamped to server.playerScale.min (default 0.1, a mouse) and server.playerScale.max (default 100, a kaiju), and a request past either end lands at the end with a clientNotice saying so. The top end is wide for fun, not realism — an operator narrows it per world with these two prefs when a size needs to actually fit the map. The bottom end is where the movement code stops working, measured rather than guessed: 0.05 stands, walks, climbs and lands, 0.02 loses the ground, and at 0.01 it falls through the world. client.scale is the persisted default, sent as a request on join — what the player asked for, never necessarily what they got. The options slider is linear in the size itself, so its label reads the real multiplier, over a track of client.scaleTrackMin–client.scaleTrackMax (0.1–4); the field beside it takes any size the server allows, so a size past the track is typed rather than dragged.
Operators use the ordinary override scope, through the same clamp: players.<target>.scale 3, reset players.<target>.scale, and players prints it beside the name.
world.scaleMovement (bool, default on) decides whether a pawn’s size carries its movement with it: maxSpeed, sprintSpeed, crouchSpeed, stopSpeed, airCap, noclipSpeed, stepHeight and jumpHeight multiply by the scale when it is on, so the world feels the same size to a giant as ours does to us. Gravity, friction, the acceleration multipliers and every time window are left alone at any setting — rates and durations, not lengths, and leaving them is what keeps a scaled pawn’s jump arc the same shape rather than a slow-motion copy. It is a world move var like the rest, per-player overridable (players.<target>.scaleMovement 0) and replicated through WorldSettings so prediction agrees.
Every length the mover compares a distance against is a fraction of the pawn’s own capsule, so a small pawn is a small player rather than a broken one. Each is a world move var, replicated through WorldSettings like the rest:
| Preference | Default | Fraction of | At the reference hull |
|---|---|---|---|
world.groundCheckFraction | 0.05 / 1.83 | standing hull height | 0.05 m of ground reach below the feet |
world.groundStickFraction | 0.05 / 1.83 | standing hull height | 0.05 m of up-trace before the ground snap (Source’s StayOnGround 2 u) |
world.moveEpsilonFraction | 0.001 / 0.40 | capsule radius | 0.001 m under which a move counts as no progress |
world.uncrouchSkinFraction | 0.01 / 0.40 | capsule radius | 0.01 m the un-crouch clearance probe shrinks by |
world.contactSkin | 0.01 m | nothing — an absolute floor | never reached: the two ground reaches are five times longer |
Each fraction’s default is the mover’s absolute length divided by the reference hull dimension it belongs to, so an unscaled pawn resolves exactly those absolute numbers and nothing about its simulation moves. world.contactSkin is the one deliberate absolute: the solver lets a resting hull settle inside its own contact tolerance (about 5 mm), that tolerance belongs to the physics backend rather than to the pawn, and a ground reach shorter than it leaves a small pawn unable to see the floor it is standing on.
The hull always follows the pawn whatever the variable says; scaleMovement only decides whether it moves like one. server.forgivenessRadiusAt0 does not scale — it is a trust budget in world meters rather than a body measurement, and a size a player asks for themselves must not widen the margin they may lie in.
See avatars → sizing for how a worn model is fitted to the same number.
Per-player overrides
Section titled “Per-player overrides”Every movement variable is not only a per-world default but also per-player overridable, server-authoritative. The world’s effective MoveVarValues are resolved once per tick, then each pawn’s sparse override set (MoveVarOverrides — every field optional, absent fields inherit the world value) is overlaid before that pawn is stepped, so one player can run moon gravity or a higher jump without touching anyone else. Derived values follow the effective inputs: JumpImpulse is recomputed as sqrt(2·g·h) over the overridden gravity and jump height, and an overridden slopeLimit converts through the same degree→cosine path.
The same machinery carries the world’s look — see per-player look overrides below — so the console scope has one vocabulary spanning both.
Overrides are set from the console under the players.<target>.<var> scope, authority-gated (world.cheats-style, host/local server console only):
players.<target>.<var>— bare, prints the effective value and whether it is inherited or an override.players.<target>.<var> <value>— sets that one override.players.<target>.look <slug>— adopts a whole built-in look in one line (see below).reset players.<target>.<var>— clears one override (the field re-inherits; the component is removed when its last override is cleared, leaving the pawn byte-identical to a fresh joiner).reset players.<target>.look— clears that player’s whole look set, leaving their movement overrides alone.reset players.<target>— clears all of that player’s overrides, as if they had just joined.reset players— clears every connected player’s overrides, echoing the affected count.players/players.<target>— lists the connected sessions with their override counts, or one player’s active overrides across both vocabularies. Behind a server the listing is the session roster described above; in a world with no server behind it, it falls back to the live pawns, which is all such a world knows about the people in it.
<target> is a player’s display name or numeric id; an ambiguous name errors and lists the matching ids. <var> is a leaf name from either vocabulary — the movement values (gravity, maxSpeed, jumpHeight, slopeLimit, the swim set swimSpeed, swimSprintSpeed and playerDensity, …) or the look values below. The two sets of leaf names are disjoint, so a name in neither errors and lists both. Overrides live on the pawn entity, so they survive respawn and map change; they are cleared on that player’s disconnect and on session reset, and are runtime-only (never persisted).
Prediction parity is mandatory, so the receiving client’s own pawn’s sparse override rides the owner-detail section of the snapshot (alongside ticks-since-grounded); the predictor overlays the replicated world WorldSettings and then its own override before each predicted step. Remote pawns need nothing extra — the server is authoritative over their motion.
The world’s look
Section titled “The world’s look”WorldSettings also carries the world’s render block — the art direction a map authors and every client of that world renders with. It is fixed-size and stateless like the movement block: a float exposure, the tonemap curve as its enum ordinal in one byte, then the bloom block — a switch byte and five floats (bloomIntensity, bloomThreshold, bloomSoftKnee, bloomDiffusion, bloomAnamorphic). Adding the bloom fields bumped NetProtocol.Version 18 → 19, and nothing else in the pipeline changed — which is exactly the append-a-field, bump-the-version path this section is built for.
The seven coefficients of the hable curve trail the block as floats named hableShoulderStrength through hableWhitePoint. Since the wire became schema-matched they moved no protocol number: a peer that predates them steps over them by name, and a client reading a server that lacks them gets zeros, which it replaces with Hable’s published curve.
The block is look, never simulation, so a client that disagrees with it mispredicts nothing: a player’s client.render.* override is unset by default and simply follows the world, and pinning one changes only what that player sees. See the color pipeline for the full setting list and dh.map → The world’s look for how a map seeds it.
Per-player look overrides
Section titled “Per-player look overrides”The look is per-player overridable on exactly the terms the movement variables are: a sparse all-nullable LookOverrides set on the pawn, every field optional, absent fields inheriting the world’s resolved look. It is what lets an admin park one spectator on a flat curve, or hand a ported map’s author the source engine’s response chain while everyone else keeps the house look, without touching the shared world.render.* rules.
The set carries exactly the seventeen fields a look preset carries, addressed by the same leaf names the world preferences use:
| Variable | World preference |
|---|---|
players.<target>.exposure | world.render.exposure |
players.<target>.tonemap | world.render.tonemap |
players.<target>.bloom | world.render.bloom |
players.<target>.bloomIntensity | world.render.bloomIntensity |
players.<target>.bloomThreshold | world.render.bloomThreshold |
players.<target>.bloomSoftKnee | world.render.bloomSoftKnee |
players.<target>.bloomDiffusion | world.render.bloomDiffusion |
players.<target>.bloomAnamorphic | world.render.bloomAnamorphic |
players.<target>.hableShoulderStrength | world.render.hableShoulderStrength |
players.<target>.hableLinearStrength | world.render.hableLinearStrength |
players.<target>.hableLinearAngle | world.render.hableLinearAngle |
players.<target>.hableToeStrength | world.render.hableToeStrength |
players.<target>.hableToeNumerator | world.render.hableToeNumerator |
players.<target>.hableToeDenominator | world.render.hableToeDenominator |
players.<target>.hableWhitePoint | world.render.hableWhitePoint |
players.<target>.halfLambert | world.lighting.halfLambert |
players.<target>.falloff | world.lighting.falloff |
Fifteen are world.render.* and two are world.lighting.*, because a look is the shading model’s response and the response is split across the two blocks. Each variable parses, clamps and formats through its world preference, so the per-player value can never accept a range the world value would not.
players.<target>.look <slug> is the batch form: it expands a built-in look — neutral, unity, source, filmic, the same slugs a map adopts — into the override set in one line. It replaces the player’s look set rather than merging into it, because a look is a whole response chain and half of one is nobody’s intent; a field the preset does not speak to stays inherited, exactly as it does when a map adopts the same look. neutral therefore pins nothing at all, which removes the component and returns the player to following the world. An unknown slug errors and lists the valid ones.
Rendering is purely local, so the set is delivered to the owning client only — it rides the owner-detail section of the snapshot, in its own 16-bit presence mask appended after the movement overrides, followed by an 8-bit mask for the seven Hable coefficients (the first mask is full at sixteen). A player with no look override costs three bytes; one carrying every field costs sixty-two (fourteen floats, plus the bool and the two enums at one byte each, plus the two masks). Adding the block bumped NetProtocol.Version 24 → 25.
The client resolves in four layers, outermost winning: an explicit client.render.* stored preference, then the player’s replicated look override, then the world’s resolved look, then the engine default. A player’s own console edit still beats what the server hands them — the local override is the one thing that was always theirs.
Custom avatars
Section titled “Custom avatars”server.customAvatars decides whether players may wear an avatar this server does not already hold. Default true, persisted, and replicated read-only through WorldSettings so a client knows whether its picker has anything to offer before it asks. With it off, a barcode the server cannot resolve locally is refused where every other appearance refusal happens — the pawn keeps what it was wearing and the player is told with a clientNotice — so a server can pin everyone to the set it ships. With it on, that same miss starts an upload of the wearer’s pallet closure, which the server then serves out to every peer.
It is not HostOnly: nothing about it can unbind a listener or lock an operator out of the server it is configuring, so an operator may flip it live from a remote session like any other world rule.
server.avatarUploadMaxBytes (default 256 MiB) bounds one closure — not one pallet, and not a session’s lifetime total. An advert carries no sizes, so the running total is measured against each member’s first chunk, which declares its length; the closure is refused with a notice at the member that crosses the line and nothing past it is transferred. The test avatar’s whole closure is about 16 MB, which is the number the default was chosen against.
A transfer that fails partway, or one nobody ever finishes wearing, leaves its already-verified pallet bytes committed in the server’s content store with nothing left to reference them — the all-or-nothing closure publish that makes a half-landed avatar invisible to every consumer is exactly what makes its bytes invisible to everything else, too. A periodic sweep reclaims them: an entry is a candidate once no connected session wears it and no session has referenced it for longer than server.avatars.unreferencedTtl (default 3600 seconds, one hour), and the sweep itself checks in no more often than server.avatars.sweepInterval (default 600 seconds, ten minutes) — both persisted floats, both clamped to at least one second. The check rides the same tick-thread slot the editor-drag lease sweep already uses rather than a second timer, and an entry still mid-transfer is never a candidate regardless of how stale its TTL would otherwise read. Tracking is in memory only: a server restart forgets every last-referenced time, which is fine, since nothing is held across a restart until something references it again. A sweep that actually removes something logs one line naming the count and bytes reclaimed; a sweep that removes nothing logs nothing.