How to use State Bags

Please read the docs before reading this guide.

Disclaimer: Entity state bags are a OneSync feature, if you do not have OneSync they will not work.

With the addition of Entity State Bags back in v2843, there’s now a better way to sync data across multiple players. Previously the way to do this was to use Decors, but it wasn’t able to be accessed/modified on the server.


Replication

State bags will only replicate to every player when set to replicate.

Only the owning player of an entity can replicate an entity’s state bag, a prime example of this would be the Player state bag, if you’re Player(1) you would only be able to replicate the state on Player(1), but not any other players state, such as Player(2). Attempting to do the code below with Player(1) substituted as Player(2) will get rejected by the server as we’re not Player(2).

By default if you’re on the server, Player(1).state.someState = true will automatically replicate to every client, while if you’re on the client Player(1).state.someState = true will only change the state bag for your client.

If you want to replicate some value on the client you would instead have to use

Player(1).state:set( --[[keyName]] 'alive', --[[value]] false, --[[replicate to server/client]] true)

You should also not put any large-scale data into state bags if you plan on replicating the data, if you need to sync large-scale data you should use Latent Events, which doesn’t block the clients’ network thread (sadly there’s no available documentation on them currently).

Its also useful to note not every entity is networked, in that case you can still use state bags on that entity, but when the entity leaves the player scope it will be removed alongside the state bag tied to it.


How to use GlobalStates:

GlobalStates are a global variable that is shared across every resource, server, and client-side. Any variable stored in a GlobalState will be replicated to the client, so don’t put any sensitive data them.

GlobalStates are also immutable to the client.

Server:

GlobalState.curState = 'yes!'
print(GlobalState.curState) -- returns 'yes!'

Client:

print(GlobalState.curState) -- returns 'yes!'
GlobalState.curState = 'no!' -- doesn't work, client can't change global state.

How to use state bags:

There are two different types of state bags, one for the player Player(plySrc), and one for the entity Entity(entityId).

State bags can be initialized on the client, but they cannot replicate data to the server unless it's created on the server.

In the example below ‘source’ is assumed to be the player source.

Server:

local ply = Player(source).state
ply.alive = true
print(ply.alive) -- returns true

Client:

print(LocalPlayer.state.alive) -- returns true as we set it to true in the previous example
LocalPlayer.state:set('alive', false, true)
print(LocalPlayer.state.alive)

This works exactly the same for Entities

In the example below ‘vehNet’ is assumed to be the entity’s network id.

local ent = Entity(NetworkGetEntityFromNetworkId(vehNet))
ent.state.exploded = false
print(ent.state.exploded) -- returns false

Client:

local ent = Entity(NetToVeh(vehNet))
print(ent.state.exploded) -- returns false
ent.state:set('exploded',  true, true)
print(ent.state.exploded) -- returns true

State Bag Change Handlers

State Bag Change handlers are a way to listen to when a specific state bag value gets changed.

This means you can do specific logic on a state bag change, like cause a explosion on the specified entity

-- setting bag filter to 'entity:5' instead of nil would only listen to the event for the entity with the network id of 5.
AddStateBagChangeHandler('exploded' --[[key filter]], nil --[[bag filter]], function(bagName, key, value, _unused, replicated)
    -- we only want to cause an explosion when the value is set to true!
    if not value then return end
    local ent = GetEntityFromStateBagName(bagName)
    -- the entity didn't exist
    if ent == 0 then return end
    local entCoords = GetEntityCoords(ent)
    AddExplosion(entCoords, 0xFFFFFFFF, 1.0, true, false, 1.0)
end)
48 Likes

thank you for the guide! im so the community will love it!

1 Like

Hello, Avarian

Me again :smiley:

I am currently experimenting with state bags and again, I encountered something that I don’t understand.

I am using simple commands:

-- server side
RegisterCommand("testset", function()
    GlobalState.mode = 'open'
    print(GlobalState.mode) -- prints open just fine
end)

--client side
RegisterCommand("test", function()
    print( GlobalState.mode )
end)

But once client side command is entered, it prints nil
am I doing something wrong ?

Thanks!

Thank you,

This will surely be a big help to everyone here that is developing scripts with Massive OneSync Servers in mind!

Best regards,
Sebastian

?

Decorator data is part of CEntityGameStateDataNode or such, should be replicated no matter what. If you have a repro for this please report it.

1 Like

:+1: This is another case of my ignorance thinking that something was already known/reported, sorry.

1 Like

Appreciated!

Hey @AvarianKnight

Nice tutorial, one thing im wondering, whats the overhead, and is there use cases where using State Bags are not recommended.

The reason im asking if the overhead is more or less 0 my thought was that State Bags could be used for generic data like PlayerPedId() to avoid having to call the native in multiple scripts, and just have one script update the state bag with data every lets say 1000 ms.

After some testing my self, i noticed if using a state bag in a script that runs every frame it tends to give a overhead of 1 ms, but guess thats expected behaviour that it’s heavyer then actually running the PlayerPedId() when it comes down to loops that runs every sec?

They can be ran every frame with minimal impact (0.1ms overhead) but they’re no replacement for regular natives, which will always be faster.

State bags have serialization and deserialization overhead which is what you’re seeing.

It should be said if there is a native to use for something it should be used, but there’s no general answer for state bag use cases as they can be used for anything, we even use it for our car lock system with no issues.

@AvarianKnight, okay it’s nice to get a bit of inside, so i can decide where i want to use state bags and where i dont want to use them, it has to make sence, and not create unwanted overhead in the end.

For things like status checks if a person got handsup, vehicle locks what not, i can see something like statebags makes alot more sence with out doubt.

Has anyone tried AddStateBagChangeHandler on the server, where if the client changes a bag value, it is handled on the server? As I’ve only experienced server crashes and would like to know if its just something I’m doing wrong before I write up a report. Change handler works fine if the data is from the same side.

Is there currently no way to spawn an entity with a statebag already applied? It’s very annoying to have to have the server setup the statebag before it can be changed.

No, the Entity must exist for the State Bag to be applied. Just ping back over a setup event with the NetworkID and then maybe the type of State Bag(s) that need to be appiled in a single method?

1 Like

You can also just use entityCreating but I would assume that this isn’t done by default because of memory usage

Personally I use entityCreating to add some generic state bag information, but in most cases you don’t know what the entity is for at that point, so its best suited to be something thats called after its created in your script.

1 Like

How would I go about removing a statebag? I have tried the approach of setting the value to nil to try to clear it out of the table but its giving me an error.

Setting works fine

SetStateBagValue('player:'..GetPlayerServerId(PlayerId()), 'instance', instanceId, string.len(instanceId), true)

I don’t see a remove function so I was trying to set the value to nil but it doesn’t like nil values.
SetStateBagValue('player:'..GetPlayerServerId(PlayerId()), 'instance', nil, 0, false)

1 Like

Why are you using internal functions?

Cause I’m pleb. I figured it out, thanks.

1 Like

when trying to gsub the bagName im getting a weird space and an extra number somehow.

RegisterCommand("blowherup", function(source, args, rawCommand)
	local veh = IRPCore.Game.GetClosestVehicle()
	local id = NetworkGetNetworkIdFromEntity(veh)
	local ent = Entity(NetToVeh(id))
	print(ent.state.exploded) -- returns false
	ent.state:set(--[[keyName]] 'exploded', --[[value]] true, --[[replicate to server]] true)
	print(ent.state.exploded) -- returns true
end)


-- setting bag filter to 'entity:5' instead of nil would only listen to the event for the entity with the network id of 5.
AddStateBagChangeHandler('exploded' --[[key filter]], nil --[[bag filter]], function(bagName, key, value, _unused, replicated)
    -- we only want to cause an explosion when the value is set to true!
	print("called")
    if not value then return end
    -- this can also be player: or localEntity:
    local entNet = tonumber(string.gsub(bagName, "entity:", ""))
    local ent = NetToEnt(entNet)
    local entCoords = GetEntityCoords(ent)
    AddExplosion(entCoords, 0xFFFFFFFF, 1.0, true, false, 1.0)
end)

the “32 0” is me printing the gsub bagName in the console picture

tried also with the gsub:bagName way, same thing

string.gsub returns string and count see lua docs at the very bottom, and tonumber takes two arguments (so it’ll try to use the gsub count as the base), so you’ll have to use tonumber(string.sub(...), 10) so it only tries to try to do base 10

I edited the guide to reflect this

1 Like