State bags and server validated writes (tm.state)
Replicated player and entity state is written on the server. This import lets a client ask for a replicated value and lets the server decide. Local UI flags and non-networked preview entities keep using ordinary non-replicated state.
Strict mode (setr sv_stateBagStrictMode true, server artifact 12739 or newer) stops clients from publishing network state changes on their own. tm.state is the sanctioned path: the client sends a request, the server checks it against a rule your resource registered, and the server writes the value. Strict mode does not turn client reports into trusted gameplay evidence, so a rule should check what it can (distance, ownership, a sane range) and not assume the client is honest.
-- Server: register a narrowly scoped rule for a key this resource owns.
tm.state.registerEntity('blinker', function(source, entity, value)
local driver = GetPedInVehicleSeat(entity, -1) == GetPlayerPed(source)
return driver and tm.state.number(value, 0, 3) and value % 1 == 0, value
end, { entityType = 2, distance = 10 })
tm.state.registerPlayer('handsup', tm.state.boolean)
-- Client: request replication. The server checks the sender, bucket, distance and rule.
tm.state.entity(vehicle, 'blinker', 1)
tm.state.player('handsup', true)
-- Client: wait for permission before a gameplay action.
if tm.state.requestPlayer('inTrunk', vehicleNetId) then
-- attach the local ped only after the server reserved a trunk space
end
-- Client: acknowledge the exact snapshot that was applied.
tm.state.ack(vehicle, 'vehicleProperties', appliedProperties)
-- Local state only (an ordinary state:set(key, value, false) is also fine).
tm.state.player('editorActive', true, false)Where each call runs
| Call | Side | Description |
|---|---|---|
tm.state.registerPlayer(key, validate, options?) | Server | Lets clients ask for key on their own player state. |
tm.state.registerEntity(key, validate, options?) | Server | Lets clients ask for key on an entity near them. |
tm.state.setStaffGuard(fn) | Server | Replaces the staff check (see below). |
tm.state.isStaff(source) | Server | Whether the player passes the staff check. |
tm.state.player(key, value, replicated?) | Client | Asks the server to set a player key. Predicts the value locally. |
tm.state.requestPlayer(key, value) | Client | Asks and waits for the answer. Returns true when accepted. |
tm.state.entity(entity, key, value, replicated?, expected?) | Client | Asks the server to set an entity key. |
tm.state.ack(entity, key, expected) | Client | Clears an entity key if it still holds exactly expected. |
tm.state.number(value, lo, hi), tm.state.boolean, tm.state.vector(value, limit?) | Both | Validator helpers. |
Validators
validate(source, entity, value, expected) returns true, canonicalValue to accept. For player rules entity is nil and the target is always the sender. Returning false or nil rejects the update. Returning true, nil clears the key.
- The canonical value is what is stored, not the client's table. Build a fresh payload for compound data.
- Validators must not yield. An error inside a validator rejects the request and is contained.
tm.state.booleanis a ready validator for boolean keys.tm.state.number(value, lo, hi)is a finite-number range check.tm.state.vector(value, limit?)returns a fresh{ x, y, z }table (ornil), with each coordinate withinlimit(default 20000).
Options
| Option | Applies to | Description |
|---|---|---|
entityType | Entity | 1 ped, 2 vehicle, 3 object. |
owner | Entity | The sender must be the network owner of the entity. |
distance | Entity | Largest distance between the sender's ped and the entity, default 10 meters. |
maxBytes | Both | Largest packed payload, default 8 KiB. |
staffOnly | Both | Only senders that pass the staff check are accepted. |
Entity rules always check that the entity exists, shares the sender's routing bucket and is within distance.
What the dispatcher enforces
- Deny by default. A key without a registered rule is rejected.
- Requests per player are limited to 80 per second with a burst of 120.
- Values and the
expectedsnapshot are bounded before any validator sees them: nesting depth 6, 512 entries, strings up to 4096 characters, only finite numbers, no functions, and a packed size withinmaxBytes. - Entity network ids must be whole numbers from 1 to 65535.
- Rules belong to the resource that registered them. Each resource that imports
tm.statehas its own rule table and its own set of server events (named after the resource), so one resource can never accept or replace another resource's keys. Rules disappear when their resource stops.
Staff guard
A rule registered with staffOnly = true only accepts players that pass the staff check. By default that is tm.fw.isAdmin(source). Replace it with tm.state.setStaffGuard(function(source) return ... end) (for example to use your own permission system). A guard that errors counts as "not staff". tm.state.isStaff(source) runs the same check inside a validator.
Behavior of the client calls
playerandentityare asynchronous requests. They do not return approval.playerpredicts the value locally for immediate controls and UI, and skips the request when a plain value did not change.requestPlayerwaits up to 5 seconds, returnstruewhen the server accepted, and applies the server's canonical value locally. It yields, so call it from a thread.entitywrites ordinary local state for non-networked entities, or when its fourth argument isfalse. It returnsfalsefor an entity that does not exist.ackisentity(entity, key, nil, true, expected). The server comparesexpectedwith what the key holds now, so a late owner cannot clear newer work.
Notes
- The staff check is the injectable guard above (default
tm.fw.isAdmin). - The dispatcher is part of this import, not a separate resource, so there is no cross-resource policy table. The validator must be a plain function.
requestPlayeruses its own request and response events.initPed, used bytm.ped, is registered by the serverpedimport in the resource that uses it.