Is OnlineSubsystem required for replication in Unreal?

So I keep finding mixed information and the documentation isn’t helping here.

Do you have to use an OnlineSubsystem to do simple multiplayer in Unreal?

I have created my own NetDriver and Connection and have no need for Session in that I have no need for a system to abstract out the concept of discovery, etc. e.g. I don’t feel I need the added complexity of OnlineSubsystem and its Sessions I would really rather as I have in the past with other cases work with Steam APIs directly.

What I am doing is using my own Steam integration using Steam’s lobby, etc. to handle matchmaking and discovery that is all fine and well done this for years no issue there.

From what I can see Open ?listen will start a listen server and Open will connect to it as a client

The issue is something goes off when the PlayerController spawns I get an error “Received invalid swap message with child index -1”

I see nothing about a NetDriver that should cause this to be an issue, my NetDriver is very similar to Unreal’s SteamSocket driver only I directly use Steam as opposed to getting at via OnlineSubsystemSteam and of course, I use the latest Steamworks SDK

So is it that Unreal expects there to be a Session and or OnlineSubsystem that is causing this issue or do I have some other problem getting in the way

For note I am testing at current using the out of the box 3rd person level the only custom bits being my Steamworks integration and NetDriver

No! The server/replication system in Unreal works on plain sockets, and if you have the IP address (and port) of a server, a client can be told to connect straight to that address/port.

Online Subsystem is all about all the other things you’ll want, like finding matches, keeping friends, storing highscores, and so on.

This sounds like maybe a bug in your particular game, or NetDriver?
But why are you making a custom NetDriver? It should be possible to just wire up the replication / server / client stuff with all the default components (no particular online subsystem) and it should Just Work.

Steam Sockets based NetDriver that works with our Steamworks SDK integration as opposed to Online Subsystem Steam.

The bug must be with the NetDriver though as noted it is very nearly a copy of Unreal’s Steam Socket NetDriver the only notable difference are that we removed the dependency on OnlineSubsystem by simply working with ISteamGameServer and ISteamAPI directly as opposed to grabbing it from the OnlineSubsystemSteam

Our Steamworks Integration I know is all working as expected and as noted we are able to make the connection the issue comes when the connecting client’s PlayerController is spawned

The Server then throws that swap message error and the PlayerController gets destroyed and respawned and that process loops till we close the client

Just not sure what about the NetDriver that could cause that behaviour.

As to the test project, it is just the 5.3 Thirdperson sample with our Steamworks plugin added and our Steam Sockets based NetDriver configured. This is why I am assuming it must be something our NetDriver is doing to cause this issue or some dependency I am missing

What I would do:
Put a breakpoint in that location in a vanilla project using Epic’s OnlineSubsystemSteam.
Examine what that array and those arguments look like.
Then put a breakpoint in that location in your own app, and examine the arguments.
Follow that empty array back to where it’s supposed to be initialized to non-empty, and see why your code is different from EPIC.

In this case its that the swap message is being sent with an index of -1

FNetControlMessage<NMT_PCSwap>::Receive(Bunch, ChildIndex)

ChildIndex comes back as -1 e.g. the param on the message is -1
Sorting out what calls this is what I am having the issue find

Thing is I would need to debug this across the client and server, the server is strait up being told to swap to a child whose index is -1 … so its being told wrong

The client tells it this … or I think it does but I don’t know where/when on the client side the NMT_PCSwap control message is sent or triggered

The log message comes in on UWorld::NotifyControlMessage which was called from (I think) UControlChannel::ReceivedBunch which I understand is ultimately data received from a remote system in this case the client connecting to this server.

So sorting out who or rather what bit of code actually forms the message whose type is NMT_PCSwap is not so simple. the token NMT_PCSwap only occurs in 2 places both switch cases not sends but rather reads

So its the value that is being packed (15) but I am not yet sure how that gets packed up still digging

Thanks for the feedback though I will run the same test using the OSS Steam and see if I can reproduce and maybe learn more about how its structured

[Edit]
So a bit more digging finally found where the call is being made and its being made in 2 places UChildConnection::HandleClientPlayer and UNetConnection::HandleClientPlayer and -1 is intentionally passed here for the UNetConnection

So my issue isn’t that -1 is passed it must be that the real issue must be the line after it

if (SwapConnection != nullptr)
{
    bSuccess = DestroySwappedPC(SwapConnection);
}

So if SwapConnection is null or DestroySwappedPC fails then bSuccess is an error

The Log message after that is not very helpful

if (!bSuccess)
{
    UE_LOG(LogNet, Log, TEXT("Received invalid swap message with child index %i"), ChildIndex);
}

That message suggests the issue is because the index is invalid … I mean an index of -1 is invalid however it is intentionally passed

A better message would have been

DestroySwappedPC Failed for connection and child index %i

Setting that aside my next task is to sort out if its SwapConnection being null or failure to destroy that is the cause … my guess is failure to destroy but we will see

This is what I get for assuming the message’s information was relevant to the issue :slight_smile: I should have dropped a breakpoint there earlier and checked the state of values just before the log but you know what they say about assuming

For anyone following along

To trace this further I need to step through the logic of UWorld::NotifyControlMessage and UWorld::DestroySwappedPC to understand why DestroySwappedPC is returning false.

Here is my reasoning case someone has a better idea
In this code here

UNetConnection* SwapConnection = Connection;
int32 ChildIndex;

if (FNetControlMessage<NMT_PCSwap>::Receive(Bunch, ChildIndex))
{
      if (ChildIndex >= 0)
      {
            SwapConnection = Connection->Children.IsValidIndex(ChildIndex) ? ToRawPtr(Connection->Children[ChildIndex]) : nullptr;
      }

      bool bSuccess = false;

      if (SwapConnection != nullptr)
      {
            bSuccess = DestroySwappedPC(SwapConnection);
      }

      if (!bSuccess)
      {
            UE_LOG(LogNet, Log, TEXT("Received invalid swap message with child index %i"), ChildIndex);
      }
}

I know the only way I could see the message Received invalid swap message with child index -1

Is if ChildIndex >= 0 is false … so we did not set SwapConnection to nullptr

There for If(SwapConnection != nullptr is true and we called DestroySwappedPC

That must have returned false for us to get that log message. That can only have returned false if none of the player controllers (which I know there are 2 on the server at this time) had a matching Connection listed in the PendingSwapConnection

So is it that PendingSwapConnection is nullptr … or is it that the connection we are testing isn’t the same e.g. we have an orphaned connection or something? … not sure

I’ll also want to debug AGameModeBase::SwapPlayerControllers as far as I can tell this is where PendingSwapConnection is set, there would be a SwapPlayerControllers warning message if this was somehow invalid and I don’t see that so I am guessing this doesn’t fail … so why isn’t PendingSwapConnection that is being set here the same as the one being tested for in DestroySwappedPC hoping to see the values that are set there and to confirm they are set the way I think they are will shed some light on that

I am assuming it is something I am doing wrong in my NetDriver or rather something else I need aside from the NetDriver and Connection since my custom NetDriver and Connection are really close to identical to the builtin ones for SteamSocket.

I am also working on setting up the same test but using the built in OSS Steam and its driver … really not a fan of OSS so procrastinating on that :slight_smile: hoping I find my issue without bothering

Ran my traces and ya PendingSwapConnection is never being set
From what I can see that is only ever set when GameMode’s SwapPlayerControllers is called which is only called in Seemless Travel?

Yet NMT_PCSwap is sent on any UNetConnection::HandleClientPlayer when the APlayerController::OnActorChannelOpen runs which happens every time the player pawn is spawned

So I am confused by that, wouldn’t that mean you would always get the Received invalid swap message with child index -1 if it is not a Seemless travel?

Are all connections “seamless” if so why is that a filter as to whether or not to set PendingSwapConnection.

Setting that aside for a minute, the OnActorChannelOpen gets called when the pawn is spawned from the replication system … but nothing about that calls PendingSwapConnection so that would mean every call from that would always result in Received invalid swap message with child index -1 being logged which I guess is fine its info not an error explicitly I think.

So now I am assuming the issue isn’t related to the message so much as it is that on the client the pawn gets destroyed and thus recreated over and over, the message simply being a symptom of that.

Now I am looking for why the pawn could be destroyed and that isn’t as clear

More updates on this topic

Had no issue debugging the engine the first few days

Now none of the breakpoints hit, I know the lines are being called, and the IDE shows the breakpoint as valid … never the less NO breakpoint is hit the same breakpoints that were working

Debugging Unreal is a horrible experience

As to information, I can see that the problem is entirely client side, from the server side all is well

the symptom has been further refined down to

On connection the replication system causes all the required objects to spawn including the Listen server pawn, the local player’s pawn, controllers, etc. the next frame all those newly spawned objects are destroyed, and in the next frame the network connection causes them to all spawn again

So I need to understand why they are all getting destroyed but since I can’t seem to get Unreal to debug engine code for me again I am at a bit of a loss. 20 years in software engineering and I can say debugging Unreal networking logic is by far the least present thing I have done … :slight_smile: and I worked in enterprise software engineering for a solid while (famously sucks)

Given debugging is not reliable at the moment I am going back to manually reading over the code to understand the process since documentation doesn’t really go this deep. I will update again if I have something useful in hopes this journey might help another developer

I believe (but am not 100% sure) that the idea is that you either configure the game for seamless travel, OR you re-load the entire level (with new player controllers, new connection) for each new level.

It sounds like you’ll want to either re-configure your game to follow the “load everything from scratch for a new connection” path, or turn on Seamless Travel.

You answered the original question so I marked that and created a secondary question

https://forums.unrealengine.com/t/what-might-prevent-a-client-from-properly-acknowledging-the-pawn-it-has-possessed/1665704

From my trading and debugging, I think I have narrowed the issue down to the “acknowledgement” process that happens after the objects are spawned on the client side.

There is a step where the client checks if the GetPawn == the Acknowedged Pawn in the Player Controller … if that doesn’t match it retries basically and that is what I am seeing

So sorting out why NewPawn for that method is not set/null

Haven’t figured that out yet having to poke at the dark with it but slowly crawling deeper into the rabbit hole

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.