Jump to content

net_variable types and sending data from server/host to clients


Recommended Posts

  • Developer

There are several data types available to your mod that handle sending data from the host to the clients. Here is a list of all the types and their formats.

net_float - maps to a 32-bit float
net_byte - maps to an unsigned 8-bit integer
net_shortint - maps to a signed 16-bit integer
net_ushortint - maps to an unsigned 16-bit integer
net_int - maps to a signed 32-bit integer
net_uint - maps to an unsigned 32-bit integer
net_bool - maps to a single bit boolean
net_hash - maps to a unsigned 32-bit integer hash of the string assigned
net_string - maps to a string of variable length
net_entity - maps to an unsigned 64-bit integer containing the network id of the entity instance that is assigned
net_tinybyte - maps to an unsigned 3-bit integer
net_smallbyte - maps to an unsigned 6-bit integer
net_bytearray - maps to an array of unsigned 8-bit integers
net_smallbytearray - maps to an array of unsigned 6-bit integers

The recent game mode mod, “The Hunt”, we make use of a few of these variables to send data to the clients from the server.

 

The first step in using these is to declare a member variable and a net_variable on the host and clients. This is done in the component’s constructor in The Hunt. If you don’t have the net_variable declared on the host and all the clients then you will get an error in your log file specifying “Error deserializing lua state for entity … - Failed to read net var data”. The member variable, “hunt_kills”, is what the mod’s UI code reads from, and the net_variable, “net_hunt_kills”, is how we assign data from the server to send to the clients. While the member variable isn’t strictly necessary, I believe it makes things clearer.

self.hunt_kills = 0
self.net_hunt_kills = net_ushortint(self.inst.GUID, "hunt_kills", "hunt_killsdirty" )

The first parameter to net_ushortint is the GUID of the entity instance you want to attach this variable to. The second parameter is a name for the net_variable that must be unique on the entity. The third parameter is the event that will be fired on the client when the data changes.

 

The next step is to listen for the network data dirty event. This is only needed on the client, and is also setup in the component’s constructor. In the callback function, we read out the value of the changed net_variable and assign it to our member variable.

local function OnHuntKillsDirty(inst)
	inst.components.HuntGameLogic.hunt_kills = inst.components.HuntGameLogic.net_hunt_kills:value()
end
--in the component's constructor
if not TheWorld.ismastersim then
	self.inst:ListenForEvent("hunt_killsdirty", OnHuntKillsDirty)
end

The third step, is on the host to actually set the value to your net_variable. Instead of just assigning values to self.hunt_kills directly, we wrap this in a function that also sets the value on the net_variable.

function HuntGameLogic:SetHuntKills( hunt_kills )
	self.hunt_kills = hunt_kills
    self.net_hunt_kills:set(hunt_kills)
end

With these three steps your mod will be able to send any custom data you need from the server to the client. Please let me know if something isn't clear, or you need more information.

 

Good luck with your mods!

 

  • Like 11
  • Thanks 1
Link to comment
Share on other sites

How did I not discover net_entity! :o

That would solve the intractable FrustumCheck problem I was running into...

 

A couple of questions:

  1. Are there any restrictions on what entities you can attach netvars to? It seems all of them have the CLASSIFIED tag, for example (and what does that tag actually do?)
  2. Since the GUID doesn't seem to be conserved between the client and host, attaching it by GUID seems a bit confusing to me-- or is that actually there to make sure the different GUIDs get paired?
  3. Are there any special rules for hashes? I'm guessing you can't just give it arbitrarily generated strings, because it'd need to compare to a hash on the client, but what determines the pool of strings it checks? The game only seems to be using it for inventory image texes and atlases at the moment, and it looks like it's just giving them arbitrary strings...
Link to comment
Share on other sites

  • Developer

1. You can attach netvars to any entity.  Entities with the CLASSIFIED tag generally contains extra detailed data (related to another entity) that we only want one player to receive at a time.  These are not actually physical entities in the world, so they will be excluded from input logic and FindEntities for example.

 

2. The GUID parameter is just used locally to map back to C-side entity.  The value of the GUID itself is not meaningful in this case, and it was simply chosen to match most of the legacy lua bindings.

 

3. You can assume that passing in any arbitrary string will generate the same hash on any client.  These do not need to be pre-generated.  There should be a lua call to our hash function somewhere, I'll edit it into here if I find it =)

 

Hope that helps!

  • Like 3
Link to comment
Share on other sites

Ah, okay, then there must've been something else going wrong with the way I was attaching netvars to the campfire before.

 

3. You can assume that passing in any arbitrary string will generate the same hash on any client.  These do not need to be pre-generated.  There should be a lua call to our hash function somewhere, I'll edit it into here if I find it =)
I assumed it would generate the same hash, but is the handling of the hash on the client side manual or what? I can't find anywhere that it's comparing the hash to hashes of local strings to find the correct one in the image/atlas fields, so I'm still a bit confused. If you can give it any arbitrary string without having to worry about how the client figures out what string to translate the hash back into, then what is the net_string for?
Link to comment
Share on other sites

@PeterA, could we please get a net variable that allows clients to communicate with the server through events.

 

Example:

inst.event = net_event(self.inst.GUID, "event_name", FunctionToCall)inst.event:Push()

This would allow modders to create unique hot keys which does different things. If this is already possible, have the client communicate with the server through events, could you please post a short example it would be much appreciated.

Link to comment
Share on other sites

@Kzisor, The way simplex put it was that RPCs are client -> server, and netvars are server -> client. But in either case we kind of have to wait, as there isn't a nice way to do custom RPCs yet... 

 

ugh I keep typing "RPC call". Failing to correct it would make me as bad a person as people who say "PIN number"...

 

Edit: Although, actually, net_event would still be convenient for server->client communication. I have some things that I've come across where all I need is the dirty event being pushed on the client, because the rest of the data to update is available elsewhere (in my case, the client table). I've been doing that by using a boolean and setting it to the opposite of its value, but a net_event would be nicer to use (I was testing and it doesn't seem to push the event if you just send the same value, but something else might've been wrong with my code at that point).

Edited by rezecib
Link to comment
Share on other sites

  • Developer

We use net_hash mainly to optimize data that is only going to be used as parameters for calls into C-side functions where we know that any strings will be converted to hash anyway.

 

Using net_bools to trigger events is also how we do it, since it is the cheapest.

  • Like 2
Link to comment
Share on other sites

ugh I keep typing "RPC call". Failing to correct it would make me as bad a person as people who say "PIN number"...

Don't worry, I do it all the time myself :razz:.

Edit: Although, actually, net_event would still be convenient for server->client communication. I have some things that I've come across where all I need is the dirty event being pushed on the client, because the rest of the data to update is available elsewhere (in my case, the client table). I've been doing that by using a boolean and setting it to the opposite of its value, but a net_event would be nicer to use (I was testing and it doesn't seem to push the event if you just send the same value, but something else might've been wrong with my code at that point).

As V2C said, net_bool's work fine for that. And you don't need to keep switching its value, you can just do

local netvar = net_bool(inst.GUID, "somename", "someeventname")--[[ stuff ]]--netvar:set_local(true)netvar:set(true)
when you set a local value, the next non-local value set forces a sync raising the ondirty event across the network, even if the value never changed.

I personally wrapped all netvar types in Lua classes to (a) make them singleplayer compatible by keeping the interface with an alternate implementation in the singleplayer case, and (b) automate bounds checking and otherwise extend their interface. One additional class I implemented is NetSignal, on top of NetBool (itself a wrapper of net_bool). It's used as such:

local function foo()    print "Hello, world!"endlocal netvar = NetSignal(inst, "somename") -- ondirty event name is automatic.netvar:Connect(foo)--[[ stuff ]]--if TheNet:GetIsServer() then    -- This will run foo on the host and all clients    netvar()end
Internally it's just a net_bool variable with a perpetual value of 'true', being fully reliant on the set_local/set trick.

But in any case, network events per se would be very handy. I implemented two kinds of them (all of them built on my custom RPC implementation). Server events, which are sent by any client (or the server itself) and pushed on the server, and broadcast events, which are sent by any client (or the server itself) and pushed on the server and all clients. Both internalise the mapping of event names to numerical ids to save bandwidth. This kind of thing is fully reliant on being able to add custom RPCs; once we can do that (without having to jump through hoops like I did when coming up with my implementation), fancier functionality follows easily by wrapping code.

  • Like 3
Link to comment
Share on other sites

  • Developer

In our latest hotfix, we've added a net_event wrapper.

net_event = Class(function(self, guid, event)    self._bool = net_bool(guid, event, event)end)function net_event:push()    self._bool:set_local(true)    self._bool:set(true)end--Example:inst.screenshakeevent = net_event(inst.GUID, "screenshake")inst:ListenForEvent("screenshake", OnScreenShake)--Servers can push this and it will trigger on clientsinst.screenshakeevent:push()
  • Like 3
Link to comment
Share on other sites

1) How to send array of strings?

 

2) How to send whole table? And subtables?

 

3) How to send variable only for player with specific nickname? I mean inst.name == "Maris" (or inst.userid == "....").

 

P.S. I want to improve my mod "Killer's Trap". Traps must be visible only for owner and his friends. So the trap must "know" their nicknames and send this info to clients (or send "visible" event only for owner and his friends).

 

I could make nickname1, nickname2, nickname3, nickname4 and nickname5 net strings. But may be there is another way? :)

Edited by Maris
Link to comment
Share on other sites

@Maris, Use the userids for the players.

 

For arrays of tables and strings... preferably don't xD

You can kind of get around tables with net_entity. Strings... you could just concatenate into one string and split it based on newlines.

Edited by rezecib
Link to comment
Share on other sites

I've been struggling to implement a net_bool. I'm trying to push an event so that when a use unequips an item then they have a HUD status element become visible. I have it working wonderfully if the player is the host but I can't figure out how to push the event to the client. Here are my questions.

 

First of all where do I initialize the client setup?

 

Here is my function

 

local function fn()

       inst.AddComponent(
        inst.Tired = net_bool(inst.GUID, "triggertired", "triggertireddirty")  
        inst.Tired:set(true)
        inst:ListenForEvent("triggertireddirty", OnPlayerTiredDirty)
    end
end

 

which I call in common_postinit in my character lua

 

The listener function is then given by

 

local function OnPlayerTiredDirty(inst)
   if  inst.TiredStatus ~= nil then
        if inst.Tired:value() then
            inst.TiredStatus:Show()
        else
            inst.TiredStatus:Hide()
        end
    end
end

 

where TiredStatus is my HUD element set up in mod main by

 

function StatusPostConstruct(self)
    self.TiredStatus = self:AddChild(Text(GLOBAL.NUMBERFONT, 28), self.owner)
    self.TiredStatus:SetScale(1,.78,1)
    self.TiredStatus:SetPosition(40,-55.5,0)
    self.TiredStatus:SetString("Tired")
    if self.inst:HasTag("tired") then
            self.TiredStatus:Show()
        else
            self.TiredStatus:Hide()
    end
end
AddClassPostConstruct("widgets/statusdisplays", StatusPostConstruct)

 

I then use  "owner.Tired:set(true)" in the OnUnequip function in my custom item lua.

 

It is also changed in the onwake function of the sleepingbag component in modmain using

self.sleeper.Tired:set(false)

 

Again, this works fine in singleplayer using PushEvent and event listeners but I have been unable to get it to work with a client. Am I calling the statusdisplay wrong by using inst.TiredStatus:Show() in OnPlayerTiredDirty? I know that TireStatus initializes for the client becuase if I set it as visible when I declare it in modmain it shows up on the client. I just can't get it to switch with the net variable.

 

In the net_bool declaration

 

        inst.Tired = net_bool(inst.GUID, "triggertired", "triggertireddirty") 
 

what exactly do "triggertired" and "triggertireddirty" do. I thought they were like event markers which you can then listen to.

 

Any help would be appreciated

 

 

Link to comment
Share on other sites

After a bit of work I finally figured it out! You can see it in my mod "Shadow Dragons"

 

I first added my UI widget in modmain given by

function StatusPostConstruct(self)    self.TiredStatus = self:AddChild(Text(GLOBAL.NUMBERFONT, 28), self.owner)    self.TiredStatus:SetScale(1,.78,1)    self.TiredStatus:SetPosition(40,-55.5,0)    self.TiredStatus:SetString("Tired")    self.TiredStatus:Hide()endAddClassPostConstruct("widgets/statusdisplays", StatusPostConstruct)

I then added the following in my character lua file

function TriggerTired(inst)--Calls the net_bool Tired which triggers the function OnPlayerTiredDirty   if inst:HasTag("tired") then       inst.Tired:set(true)    else       inst.Tired:set(false)    end--This is if you are playing as the Host    if inst.HUD ~= nil then       if inst.Tired:value() then          inst.HUD.controls.status.TiredStatus:Show()       else          inst.HUD.controls.status.TiredStatus:Hide()       end    end        end    --The client side function to trigger Tired labellocal function OnPlayerTiredDirty(self)    if self.HUD ~= nil then        if self.Tired:value() then            self.HUD.controls.status.TiredStatus:Show()        else            self.HUD.controls.status.TiredStatus:Hide()        end    endend--Initialize the net_bool and set up the listenerslocal function fn(inst)        inst.Tired = net_bool(inst.GUID, "status.TiredStatus", "triggertireddirty")          if not TheWorld.ismastersim then            inst:ListenForEvent("triggertireddirty", OnPlayerTiredDirty)        else            inst:ListenForEvent("triggertired", TriggerTired)        endend

and told it to run by starting the ball rolling in common_postinit using

fn(inst)inst:PushEvent("triggertired")

Then whenever I want to show or hide the Tired HUD label I change my tired tag and initiate my server event.

inst:AddTag("tired")inst:PushEvent("triggertired")

 

The big problem I was having is understanding how variables were passed and the difference between inst and self etc. I'm still a little fuzzy on those. If there are any good explanations on that I would love to see the post.

 

 

Edited by gregdwilson
  • Thanks 1
Link to comment
Share on other sites

inst.secret_var = "All my valuable stuff are in a treasure chest, which to the North from here."
inst.net_secret_var = net_string(inst.GUID, "secret_var", "secret_var_dirty" )

"inst" is a character prefab.

Variable inst.secret_var must be shown (as client widget) only for player which nickname is inst.name

But other players are able to get value of this variable by accessing AllPlayers table on client side. This is a kind of cheat.

Is there any way to send value only for one specific player?

Edited by Maris
Link to comment
Share on other sites

@DarkXero, player_classified actually doesn't always work correctly on the client. I used to have netvars added on player_classified via AddPrefabPostInit for Wortox, Demon King. I had to change them to his prefab file in order for the netvars to actually work correctly.

 

I don't know if this is an issue with loading order or if it's a bug I found, but I had trouble getting the data from player_classified.

  • Like 1
Link to comment
Share on other sites

@DarkXero, you'd be wrong if that is what you imagine. As stated before I put the netvars on the player_classified through AddPrefabPostInit exactly as you suggested to do exactly what Maris wanted to do with it, alter or manipulate a widget on the client. This did not work 100% of the time because the classified information could be nil on the clients end. It required me actually doing all the work on the prefab compared to in the player_classified which is where the work should have been done.

Link to comment
Share on other sites

@Kzisor, nevermind then. I never had issues with player_classified, though.

 

The player_classified is nil right when entering the server, given how OnSetOwner is reached.

You would need to use inst:DoTaskInTime(0, function() // Here we access the classified // end).

 

But then again I don't fully know when and why the player_classified is nil, considering stuff like:

                    (inst.player_classified ~= nil and inst.player_classified.isghostmode:value()) or                    (inst.player_classified == nil and inst:HasTag("playerghost"))

That is in player_common. But pretty much all replicas always return a value in their get functions, valid classified or nil, as failsafes. Or because stuff like birds get a replica but don't have a classified.

 

So next thing I imagine are widgets trying to get the info right off the bat, accesing nothing.

Well, if the widgets update, then just a check for nil would suffice, right?

It would be nil when entering the server, then when everything is set up, we are good to go.

 

After all, if a client got their player_classified nil apart from the beginning, then the game would be broken, all the other variables wouldn't be there.

 

 

I wouldn't be so insistent on this, but player_classified is my go to for secret variables.

    if TheWorld.ismastersim then        TheNet:SetIsClientInWorld(inst.userid, true)        inst.player_classified.Network:SetClassifiedTarget(inst)        inst.components.inspectable.getspecialdescription = GetDescription    end

You get the C side security, I think.

 

Although I'm not too sure how all of this works. For example, if you host a dedicated server, join it, and c_spawn a Wilson, it will have a player_classified that you, as a client, will be able to see, reach and get info. Only "true" players get their classified removed.

 

 

If your mod had something like:

AddPlayerPostInit(function(inst)	inst:DoTaskInTime(0, function(inst)		if GLOBAL.TheWorld.ismastersim or inst == GLOBAL.ThePlayer then			inst.secret = GLOBAL.net_bool(inst.GUID, "player.secret")		end	end)end)

Then somebody could change the inst == GLOBAL.ThePlayer and initialize the netvar and get the data.

Edited by DarkXero
  • Like 1
Link to comment
Share on other sites

@DarkXero, I just wanted to say, I did try the DoTaskInTime before changing the placement just to see if it worked and it did not. I'm not sure about all the clauses as to why it's set to nil, but I can safely say that you are correct that it's nil when you join the server and is later set. I was baffled because I spent a full week trying to get it to work properly only to have it all completely blow up in my face, so I decided the next best route, simply put it on the prefab.

Link to comment
Share on other sites

@Kzisor, alright.

 

Next time I suggest doing inst:DoTaskInTime(1, to see if maybe more time is needed. Or:

local function Look(inst)	if inst.player_classified == nil then		inst:DoTaskInTime(0.5, Look)	else		-- Push event or do stuff	endendinst:DoTaskInTime(0.5, Look)

Next time you have trouble you know where to post.

Link to comment
Share on other sites

Please, correct all blocks of the code, especially in the first message.

net_float - maps to a 32-bit float
net_byte - maps to an unsigned 8-bit integer
net_shortint - maps to a signed 16-bit integer
net_ushortint - maps to an unsigned 16-bit integer
net_int - maps to a signed 32-bit integer
net_uint - maps to an unsigned 32-bit integer
net_bool - maps to a single bit boolean
net_hash - maps to a unsigned 32-bit integer hash of the string assigned
net_string - maps to a string of variable length
net_entity - maps to an unsigned 64-bit integer containing the network id of the entity instance that is assigned
net_tinybyte - maps to an unsigned 3-bit integer
net_smallbyte - maps to an unsigned 6-bit integer
net_bytearray - maps to an array of unsigned 8-bit integers
net_smallbytearray - maps to an array of unsigned 6-bit integers

More fixed code:

self.hunt_kills = 0
self.net_hunt_kills = net_ushortint(self.inst.GUID, "hunt_kills", "hunt_killsdirty" )

local function OnHuntKillsDirty(inst)
	inst.components.HuntGameLogic.hunt_kills = inst.components.HuntGameLogic.net_hunt_kills:value()
end

--in the component's constructor
if not TheWorld.ismastersim then
	self.inst:ListenForEvent("hunt_killsdirty", OnHuntKillsDirty)
end

function HuntGameLogic:SetHuntKills( hunt_kills )
	self.hunt_kills = hunt_kills
	self.net_hunt_kills:set(hunt_kills)
end

 

Edited by Maris
  • Like 3
  • Thanks 3
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
  • Create New...