Jump to content

New Modding RPCs API


zarklord_klei
 Share

Recommended Posts

  • Developer

With the forgotten knowledge beta released, we have added a couple new modding API features, namely the ability to send RPCs from servers to clients, and to send RPCs from servers to other servers.

All three RPC systems use a near identical API, so let's go over the existing API first, and then I'll explain the changes for the two new systems.
Creating RPCs:

AddModRPCHandler(namespace, name, fn)

This is the function to add RPCs to be called, lets go over the arguments

  • namespace - this argument is some unique identifier that only your mod uses, generally just using your modname is fine, so if your mod was called "The Hunt", you could do "TheHunt", "The Hunt", or even "The_Hunt", whatever namespace you use, just keep it consistent, your gonna end up using that same name to call your RPCs later.
  • name - this argument is the name of the RPC, it should be descriptive of what the RPC is doing, but it can really be whatever you want.
  • fn - this is the function that will get executed on the server, it will get called with its first argument being the player who sent the rpc, and then all other arguments passed from when you called it.

Sending RPCs:

SendModRPCToServer(GetModRPC(namespace, name), ...)

This is the function to send RPCs, while it looks scary, it's really simple, here is the arguments:

  • GetModRPC(namespace, name) - This should look slightly familiar, just use the exact same namespace and name arguments as you used in AddModRPCHandler.
  • ... - every other argument in this function call will get forwarded to the fn you added in AddModRPCHandler, so in the end it would call fn(player, ...).

Putting it all together:
So how does this all look put together, lets see:

--This is the function we'll call remotely to do it's thing, in this case make you giant!
local function GrowGiant(player, size)
    local valid_grow_state = true -- todo: make sure the player is allowed to grow right now
    if valid_grow_state then
    	player.Transform:SetScale(size,size,size)
    end
end
--This adds the handler, which means that if the server gets told "GrowGiant",
-- it will call our function, GrowGiant, above
AddModRPCHandler("GrowGiantRPC", "GrowGiant", GrowGiant)

--This has it send the RPC to the server when you press "v"
local size = 1
local function SendGrowGiantRPC()
  	size = size + 1
  	if size == 6 then size = 1 end
	SendModRPCToServer(GetModRPC("GrowGiantRPC", "GrowGiant"), size)
end
GLOBAL.TheInput:AddKeyDownHandler(GLOBAL.KEY_V, SendGrowGiantRPC)

So what does this mod do? This would make the player grow in size by 1 every time the player pressed the V key, at a size of 5, the players size will reset back to 1 when the V key is pressed.

In your mod, you should make sure to add appropriate checks so that a client cannot abuse your RPC, whether this is checking the prefab via player.prefab to limit it to a specific character, or check if they are a server admin with player.Network:IsServerAdmin(), purely depending on what its supposed to do.

So what about the new RPC systems? let's take a look at those now.

Shard RPCs:
Shard RPCs work largely the same as normal RPCs with a couple small differences:

  • Instead of AddModRPCHandler, we use AddShardModRPCHandler, all the arguments are the same.
  • Instead of SendModRPCToServer, we use SendModRPCToShard, this has one difference I'll explain in the following paragraph.
  • Instead of GetModRPC, we call GetShardModRPC, all the arguments are the same.

SendModRPCToShard has one key difference, which is that it takes a list of Shards to send to, so its arguments look like this:

SendModRPCToShard(GetShardModRPC(namespace, name), sender_list, ...)

sender_list can be a number of different values:

  • if sender_list is nil, all connected shards(including the one this is called on) will have this RPC executed.
  • if sender_list is a table, it will be iterated upon and send it to every shard that's ID is in the table.
  • if sender_list is either a string or a number, it will send that RPC to the specific shard with that ID.

In a lot of cases, you can just pass nil, but if you need to get a list of shard IDs(and view information about them), you can get them by calling:

local connected_shards = Shard_GetConnectedShards()

That returns a table where each key is the shard ID, and the value contains a bit of info about the connected world, for information about what data is in the table, you can view scripts/shardnetworking.lua.

The only other change, is that instead of getting a player as the first argument of the RPC Handler fn, you get the shard ID of the shard that sent the RPC, so it would look like this: fn(sending_shard_id, ...).

Client RPCs:
Client RPCs also work largely the same as normal RPCs with a couple small differences:

  • Instead of AddModRPCHandler, we use AddClientModRPCHandler, all the arguments are the same.
  • Instead of SendModRPCToServer, we use SendModRPCToClient, this has one difference I'll explain in the following paragraph.
  • Instead of GetModRPC, we call GetClientModRPC, all the arguments are the same.

SendModRPCToClient has one key difference, which is that it takes a list of Clients to send to, so its arguments look like this:

SendModRPCToClient(GetClientModRPC(namespace, name), sender_list, ...)

sender_list can be a number of different values:

  • if sender_list is nil, all clients connected to the current shard will execute the RPC.
  • if sender_list is a table, it will be iterated upon and send it to every client that is connected to the current shard thats UserID is in the table.
  • if sender_list is a string, it will send the RPC to the specific client with that UserID, as long as they are connected to that shard.

To get a list of players connected to the current shard, you can iterate over AllPlayers and get UserIDs like this:

local userids = {}
for i, player in ipairs(AllPlayers) do
	table.insert(userids, player.userid)
end

Then userids would contain the userids of all players connected to that shard, if you wanted to send that to a subset of players, say for example only wilsons, you could do this:

local userids = {}
for i, player in ipairs(AllPlayers) do
	if player.prefab == "wilson" then
		table.insert(userids, player.userid)
	end
end

And now you would only have the userids of anybody playing wilson.
Just make sure you do this every time right before sending your RPC, otherwise the information could be out of date!

The only other change, is that Client RPCs have no first argument like player or sending_shard_id, since the sending shard would always just be the current world the player is connected to, so there is no first argument, making it look like this: fn(...).

If you have any questions, please ask them, and I'll do my best to answer them.

  • Like 4
  • Thanks 10
Link to comment
Share on other sites

6 minutes ago, zarklord_klei said:

as long as you can get their userid, it should!

Why Klei haven't hired you sooner?? I made a while system that dynamically spawns classified prefabs only to send data to a certain player in lobby. This would make it SO MUCH easier

Edited by Cunning fox
  • Like 2
  • Thanks 1
Link to comment
Share on other sites

For some reason, ShardRPC fails if sender_list is anything but nil (Invalid RPC sender list). And when it is nil, the rpc won't fire on the shard it's called from.

Also I wonder why tables aren't supported as valid arguments (Invalid RPC data type)?

Link to comment
Share on other sites

  • Developer
2 hours ago, Tykvesh said:

For some reason, ShardRPC fails if sender_list is anything but nil (Invalid RPC sender list). And when it is nil, the rpc won't fire on the shard it's called from.

Also I wonder why tables aren't supported as valid arguments (Invalid RPC data type)?

tables have always been invalid data types, would you mind sharing the relevant code snippet?

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

This code sends a RPC with player's position each time they locomote. After that the RPC prints the received position:

Spoiler

AddShardModRPCHandler("test_namespace", "test_name", function(shardid, x, y, z)
	print("(Received from a ShardRPC)", x, y, z)
end)

local function OnLocomote(inst)
	local x, y, z = inst.Transform:GetWorldPosition()
	print("(Sending a ShardRPC)", x, y, z)	
	SendModRPCToShard(GetShardModRPC("test_namespace", "test_name"), nil, x, y, z)
end

AddComponentPostInit("playercontroller", function(self, inst)
	inst:ListenForEvent("locomote", OnLocomote)
end)

And here's how it appears in the logs:


From the shard from which the rpc was sent:
[00:10:14]: (Sending a ShardRPC)	355.60037231445	0	349.40600585938	
[00:10:14]: (Sending a ShardRPC)	355.72143554688	0	349.40570068359	
[00:10:15]: (Sending a ShardRPC)	355.72143554688	0	349.40570068359

From the other shard that received the rpc:
[00:10:14]: (Received from a ShardRPC)	355.60037231445	0	349.40600585938	
[00:10:15]: (Received from a ShardRPC)	355.72143554688	0	349.40570068359	
[00:10:15]: (Received from a ShardRPC)	355.72143554688	0	349.40570068359

 

 

  • Like 1
Link to comment
Share on other sites

For some reason when I call ClientRPC, I always get 

[00:13:35]: Invalid RPC namespace: 	1	table: 0x40915618

My RPC:
 

AddClientModRPCHandler("HG", "TEAM_MSG", function(...)
    printwrap("", {...})
end)

How I call it
 

SendModRPCToClient(GetClientModRPC("HG", "TEAM_MSG"), nil)

Edit: Zarklord found a bug and this should be fixed in the next patch.

Edited by Cunning fox
Link to comment
Share on other sites

I made a mod named Craftable Wormholes, where I added wormhole icon support in the last update. I use SendModRPCToClient to make the client delete the icons. This works if I use newly created wormholes, but if I try to do it when an original wormhole that was created with the world is connected to a new wormhole, nothing happens. I get no error in the client or server log, just nothing. If I only do it with the new wormholes, it works without problems.

I added prints to check if the RPC is indeed sent and they appear in the log.

The code in question:

Spoiler

local function onhammered(inst, worker)
    GLOBAL.TheWorld:PushEvent("wormhole_destroyed",{wormhole = inst})
    if GetModConfigData("ENABLED") then
    	if inst.components.teleporter.targetTeleporter ~= nil then
    		local userids = {}
			for i, player in ipairs(GLOBAL.AllPlayers) do
				table.insert(userids, player.userid)
			end
			SendModRPCToClient(GetClientModRPC("Wormhole_Crafter", "RemoveWormhole"),userids,inst,inst.components.teleporter.targetTeleporter)
    	end
    end
    inst:DoTaskInTime(0.3, function() inst:Remove() end)
end

local function RemoveWormhole(wormhole_removed,wormhole_still_here)
	local hole_removed = {inst=wormhole_removed,pos = wormhole_removed:GetPosition()}
	local hole_still_here = {inst=wormhole_still_here,pos = wormhole_still_here:GetPosition()}
	RemoveWormholePair(hole_removed,hole_still_here)
end

AddClientModRPCHandler("Wormhole_Crafter", "RemoveWormhole", RemoveWormhole)

 

Does someone have an idea why this doesn't work?

  • Like 1
Link to comment
Share on other sites

On 10/18/2020 at 8:07 AM, Cunning fox said:

For some reason when I call ClientRPC, I always get 


[00:13:35]: Invalid RPC namespace: 	1	table: 0x40915618

My RPC:
 


AddClientModRPCHandler("HG", "TEAM_MSG", function(...)
    printwrap("", {...})
end)

How I call it
 


SendModRPCToClient(GetClientModRPC("HG", "TEAM_MSG"), nil)

Edit: Zarklord found a bug and this should be fixed in the next patch.

Do you have confirmation it's been fixed? I have a similar issue right now, though it's not quite the same,  but I'm wondering if they're related.

At any rate, I'm getting the invalid RPC namespace error message too, but mine prints out the namespace I used in the error message.

If anyone can help.

Here's some of my code. The following is in a mod-specific component's constructor, inside an if TheWorld.ismastersim.

local function ClientcideReorient(angle)
	TheCamera:SetHeadingTarget(angle)
end

AddClientModRPCHandler("CameraSinker", "reorient", ClientcideReorient)

self.inst.ListenForEvent("camsync_synchrotate", function(world, data)
	-- stuff, including making a list of clients, which I name "tracked"
	
	SendModRPCToClient(GetClientModRPC("CameraSinker", "reorient"), tracked, self.angle)

end)

(... How do you make monospaced text in these forum posts?)

And for completeness, I get an error message on the client saying

Invalid RPC namespace:	camera sinker	1

Any ideas, anyone? I'm about to resign to using net_events for now, since I got that mostly working before I found this post saying RPC's can go the other way now.

Link to comment
Share on other sites

I'm working on a part of my mod----an interior room which is totally dark.To do this, I need to push the "overrideambientlighting" event in player's client side(I just need a visual effect,so I have no need to do this in server),so I use the ClientModRPC, whenever a player enters the dark room, I will let server send the RPC to the player's client side, which make the player feels dark(visually). This method works fine when I start the game with cave. However, when I start the game without cave and I enter the dark room, it seems the whole server in affected by the "overrideambientlighting" (I find my character is attacked by Charile,which means the whole server becomes dark), but I just need to "overrideambientlighting" in my client side !!!!! What's going on ??? Shouldn't the ClientModRPC works only in client side ???

And Here is my code,would you like to help me to find the problem ? (or solution ?)

thank you very much XD.

-- Should be client side override ambient lighting
AddClientModRPCHandler("gale_rpc","check_gale_interior_ambientlighting",function()
    if ThePlayer then
        ThePlayer:CheckInteriorAmbientLightAndOcean()
    else
        print("check_gale_interior_ambientlighting FAILED !!!")
    end
end)
 
AddPlayerPostInit(function(inst)
    if not TheNet:IsDedicated() then
        -- overrideambientlighting functions
        inst.InteriorAmbientLightAndOceanEnabled = false
        inst.EnableInteriorAmbientLightAndOcean = function(inst,enable)
            local oceancolor =TheWorld.components.oceancolor
 
            if enable then
                TheWorld:PushEvent("overrideambientlighting",Point(0 / 255, 0 / 255, 0 / 255))
 
                if oceancolor ~= nil then
                    TheWorld:StopWallUpdatingComponent(oceancolor)
                    oceancolor:Initialize(not enable and TheWorld.has_ocean)
                end
            else
                TheWorld:PushEvent("overrideambientlighting",nil)
 
                if oceancolor ~= nil then
                    TheWorld:StartWallUpdatingComponent(oceancolor)
                    oceancolor:Initialize(not enable and TheWorld.has_ocean)
                end
            end
           
           
 
            inst.InteriorAmbientLightAndOceanEnabled = enable
        end
 
        -- Check functions
        inst.CheckInteriorAmbientLightAndOcean = function(inst)
            local room = TheWorld.components.gale_interior_room_manager:GetRoom(inst:GetPosition())
            if room and room:IsValid() then
                if not inst.InteriorAmbientLightAndOceanEnabled then
                    inst:EnableInteriorAmbientLightAndOcean(true)
                end
            else
                if inst.InteriorAmbientLightAndOceanEnabled then
                    inst:EnableInteriorAmbientLightAndOcean(false)
                end
            end
        end
 
        -- Used for initialize ambient lighting
        inst:DoTaskInTime(0.1,function()
            TheCamera:CheckGaleInteriorCamera()  -- camera or sth
            inst:CheckInteriorAmbientLightAndOcean()
        end)
    end
end)
 
-- Sample: Server send rpc command to client side
-- SendModRPCToClient(CLIENT_MOD_RPC["gale_rpc"]["check_gale_interior_ambientlighting"],inst.userid)
Edited by Collecter
Link to comment
Share on other sites

On 10/14/2022 at 1:36 AM, TheSkylarr said:

Forest and Caves both run on individual servers called "shards". Shard RPCs can be used to communicate data between Forest and Caves, since they aren't the same server.

Thank you. bro:wilson_wink:

Link to comment
Share on other sites

On 10/16/2020 at 1:22 AM, zarklord_klei said:

as long as you can get their userid, it should!

It looks like that if all players are in the lobby - the simulation is paused, the RPCs sent to the server are not handled until the simulation is resumed. This happens on dedicated servers because the RPC queue uses simulation ticks.

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...