Download

How to resolve: Replicated functions can't have return values

I have a function that handles adding items to inventory. If the inventory is full it will fail and item will not be collected and user notified. However, when I add server replication I run into a problem as Replicated functions can’t have return values. So how should I handle this?


bool ACustomPawn::AddItem(AItem * pItem)
{
	if (Role == ROLE_Authority)
	{
		for (int i = 0; i < m_inventoryObjects.Num(); i++)
		{
			check(m_inventoryObjects*);
			if (m_inventoryObjects*->AddItemFirstAvailableSlot(pItem))
			{
				// successfully added item
				return true;
			}
		}

		// did not find any suitable inventory space
		return false;
	}
	else
	{
		// TODO: server is dealing with it, but how do we return success/failure since Replicated functions can't have return values...
		return ServerAddItem(pItem);
	}
}

// Generates: "error : In CustomPawn: Replicated functions can't have return values"
bool ACustomPawn::ServerAddItem_Implementation(AItem * pItem)
{
	AddItem(pItem);
}

Generally, you can emulate a return value by passing a bool as a reference or a pointer and check its state after the function has returned. However, I’m still pretty green when it comes to UE4 replication so I’m not certain if there are any issues with this approach.

You cannot have a return value in an RPC because you don’t know when the server will receive the request to call the function (it could take like 100ms to reach the server, for example). The way you can handle it is by getting the server to call another RPC on the client to acknowledge that it received the “AddItem” request, and telling them whether it succeeded or not.

If you gave all your items an ID of some sort then you could have your server function as ServerAddItem(itemID) and your client function as ClientAddItemAck(itemID, success), then when you receive the response from the server you’ll know which item it’s responding about (by comparing the ID) and you’ll know if it was added successfully or not. Obviously this won’t happen within your function you’ve written there, so you’ll have to wait for the response in your Tick function or something similar.

Passing a value by reference (or pointer) could be a workaround, but is very hacky and almost certainly not the correct way to do it. I’m not even sure if the calling client function waits round for the server version of the function to return and I would think that it doesn’t - otherwise they might as well have allowed return values, no?

Using a RPC call is not going to help either as AddItem is used in a loop, like so:


for (int i = 0; i < InventorySlots.Num(); i++)
{
	if (AddItem(i, Item))
	{
		// found slot space
		return true;
	}
}

I have a feeling the solution is to move all the inventory handling entirely server side and just replicate the inventory variables back to the clients, but that doesn’t seem very efficient for stuff like dragging an item from one slot to another in an inventory. I think some serious design is needed here and I’d appreciate help to get me going in the right direction from anyone who has done this sort of thing before.

As for a unique ID, I’ve seen that the suggested way of doing it is to use the pointer address to the actor, but obviously that won’t work when talking to a server - although I guess when the object is first created on the server it could use its pointer address and store that as a unique item ID. Obviously you would need to re-generate this unique item ID when re-spawning the same item (e.g. after a load), but that would not be an issue for RPC calls and only an issue if you do a lot of item tracking in your game that relies on being able to match up items between sessions. Perhaps there is a better method?

My understanding is that objects being synced over a network all have a FNetworkGUID associated with them. This is deterministically generated, i.e it is generated both client and server-side and used to synchronise objects between them. Perhaps you can leverage this alongside FNetGUIDCache::GetObjectFromNetGUID ?

If you kept track of the number of items and available slots (by making sure only the server handles any of that stuff) you should be able to decide if you can add an item to the inventory even on the client, right? If I understand your use-case and code right then as long as you reliably know that you can just call the RPC to add it and let the server update the inventory.

I think this is the direction I will have to take. So when an item is added the client first adds it locally and only if that succeeds it notifies the server so the server can update it too. I guess if the inventory and the pointers it has to item actors is replicated back to the client then if the client side gets out of sync for any reason the server will overwrite it and bring it into sync with the server - hopefully a very unlikely event.

The response will be asynchronous, so you can’t test the outcome of the function “call” right after making the call.
The way to solve this is to send another call/event/replication update in the other direction, that contains the outcome of your action.
The part of the client that makes the call will then have to go into a “pending” state after sending the action, and resolve that “pending” state with success or failure once it sees the response.
Meanwhile, the rest of the client will continue simulating/rendering as normal.

This sounds a lot like the proper way to do it and is essentially the same thing that Stanith suggested. It sounds like it could get rather complicated though as I would have to start doing some sort of state handling where a dragged item is pending insertion and may still fail, etc. Simply simulating the inventory on the client side and the overwriting any discrepencies from the server seems a lot simpler and less error prone. Does that make sense?

Also you shouldn’t need to handle the item not being added to the inventory on the client. This is assuming that the inventory itself is replicated(if not, it should be in order to prevent people form tampering with their inventory client side). This would mean that the client just sends input to the server, and the server will replicate back the new inventory for that player. If you need some sort of popup that happens when the inventory is full, then that logic should be handled client-side before asking the server to move the item as this will reduce 1 RPC to the server and the popup isn’t anything really game-playing altering, so it doesn’t need to be handled on the server, whereas the actual moving of the item should be done server side. I suggest keeping the networking like this: The clients send input to the server, the server handles the input and updates data, the data is then replicated back to the clients. This should keep you from needing a return value from RPC as the client doesn’t really care if the server couldn’t move the item, it only cares about the newly updated inventory.

Hope this help!
Connor

Is it possible to have some async callback to the RPC so you’re notified if it was successful?

The problem with this code is that since RPC’s aren’t instantaneous, that function isn’t going to return anything useful on the client anyway. It won’t compile because it can’t logically work. RPC’s take time.

IMO, you need to change the way the system works so that only the Server can add items to the inventory - and the client can only ‘request’ that the server do it. The Server then tells the Client WHEN something arbitrary has happened, not whether a specific request has worked or not.

Example: Inventory should be a replicated array or whatever it is that stores the items - and can only be modified on the Server. You can use a Rep Notify function to make the client do something when the inventory changes replicates from the Server and has changed.

Alternatively, you could make the Server run a client function such as ‘Client_OnInventoryItemAdded(AMyItem* AddedItem)’, so that the client can do whatever it needs to do (i.e. spawn an inventory widget or whatevers). Personally however, I prefer the Rep Notify approach.