OnlineSubsystemSteam behaviour with Sessions and Join in Progress - UE5.5

Intro

Hi Everyone! This post is actually a 2-in-1, because it’s partially a question and also a discovery about OnlineSubsystemSteam that I wanted to share. This is my first real post on the forum, just keep it in mind.

So while I was working on my game project, and started adding steam support with the OnlineSubsystemSteam plugin (will refer to it as OSS from this point), I had a few bumps along the way. Luckily I managed to solve most of them, except for one that I’m about to share with you. Here is a quick breakdown of the setup, so you guys can relate.

  • Using Steamworks SDk v162, Unreal Engine 5.5.4
  • I’m hosting listen servers (player-hosted game) with Sessions/Lobbies configured like this:
  • bUsePresence = true
  • bUseLobbiesIfAvailable = true
  • The two mentioned above go hand-in-hand for steam, so I had to set both to true or false
  • And the rest:
SessionSettings.bIsDedicated = false;
SessionSettings.bShouldAdvertise = true;
SessionSettings.bAllowInvites = true;
SessionSettings.bAllowJoinInProgress = false;
SessionSettings.bAllowJoinViaPresence = true;
SessionSettings.bAllowJoinViaPresenceFriendsOnly = false;
SessionSettings.bIsLANMatch = false;

Engine.ini

[/Script/Engine.GameEngine]
+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver")

[OnlineSubsystem]
DefaultPlatformService=Steam

[OnlineSubsystemSteam]
bEnabled=true
SteamDevAppId=<APPID>
bInitServerOnClient=true

[/Script/OnlineSubsystemSteam.SteamNetDriver]
NetConnectionClassName="OnlineSubsystemSteam.SteamNetConnection"

[PacketHandlerComponents]
+Components=OnlineSubsystemSteam.SteamAuthComponentModuleInterface

The Issue

Everything worked fine, we were able to host a session, find it (search settings with USE_LOBBIES) was added, no SEARCH_PRESENCE, but it worked.)

Note: I had to do a workaround with FindSessions, because for some reason Steam did not return the correct settings from the query for a session, making it impossible to find. Source - Thanks to @VALERE91

The issue occurred when I tried to Find/Join the Session with a second client (The third player). Before you ask, all tests were done on separate networks, separate regions, accounts and computers. After one player joined the lobby the other were unable to Find the session, nor join it if they searched it up before the other player joined. Now this is the point I want to mention that I don’t 100% understand how steam does its backend, but I don’t think it is an intended behaviour, that only one player can join the Session (in this case I should call it Lobby, because the setup parameters) and after that it becomes impossible to join. So I did some digging.

OSS code:

I migrated the plugin’s code into my project and started searching for a possible answer. It took me almost 2 weeks to find something but I think I’ve found what I’ve been looking for.

You see, if you take a look at OnlineSubsystemNull or even OnlineSubsystemEOS, you’re going to find something similar to this:

NULL:

bool FOnlineSessionNull::IsSessionJoinable(const FNamedOnlineSession& Session) const
{
	const FOnlineSessionSettings& Settings = Session.SessionSettings;

	// LAN beacons are implicitly advertised.
	const bool bIsAdvertised = Settings.bShouldAdvertise || Settings.bIsLANMatch;
	const bool bIsMatchInProgress = Session.SessionState == EOnlineSessionState::InProgress;

	const bool bJoinableFromProgress = (!bIsMatchInProgress || Settings.bAllowJoinInProgress);

	const bool bAreSpacesAvailable = Session.NumOpenPublicConnections > 0;

	// LAN matches don't care about private connections / invites.
	// LAN matches don't care about presence information.
	return bIsAdvertised && bJoinableFromProgress && bAreSpacesAvailable;
}

Take a closer look at this line:

const bool bJoinableFromProgress = (!bIsMatchInProgress || Settings.bAllowJoinInProgress);

Source: Engine/Plugins/Online/OnlineSubsystemNull/Source/Private/OnlineSessionInterfaceNull.cpp

EOS:

For EOS, I could not find it at the time of writing, but it is somewhere in the EOS SDK itself for non-LAN session.

The point is, both subsystems determine the joinability of a session based on if the session is not in progress, SessionState != EOnlineSessionState::InProgress OR bAllowJoinInProgress = true

Now for steam, it does something different. The OSS code does not handle joinability questions, but Steamworks does. If you take a look at the steamworks sdk docs, the ISteamMatchmaking interface has a method called SetLobbyJoinable(). The docs say, that it determines whether a lobby is joinable by any means, but more importantly Lobbies with joining disabled will not be returned from a lobby search.

Now the docs say it defaults to true, and that kind of explains why the game is joinable for the first player, but it also implies that something runs after the first join, disabling the joinability. I’ve found 2 calls to this method in the OSS plugin, and after modifying it to log, I confirmed the first call only happens after the first player joined. But the interesting part is, what happens when it gets called.

The source for both of the calls: Engine/Plugins/Online/OnlineSubsystemSteam/Source/Private/OnlineSessionAsyncLobbySteam.cpp

1: FOnlineAsyncTaskSteamUpdateLobby::Tick(), its the async tick function for UpdateLobby, called after manually updating a session with UpdateSession()

int32 MaxLobbyMembers = SteamMatchmakingPtr->GetLobbyMemberLimit(*SessionInfo->SessionId);
bool bLobbyJoinable = Session->SessionSettings.bAllowJoinInProgress && (LobbyMemberCount < MaxLobbyMembers) && (MaxLobbyMembers != 0);
if (SteamMatchmakingPtr->SetLobbyJoinable(*SessionInfo->SessionId, bLobbyJoinable))
{
	//Removed the rest of the code from here.
}

2: FillMembersFromLobbyData() - I don’t exactly know when this gets called, but according to tests one of these options is definitely called after joining.

bool bLobbyJoinable = Session.SessionSettings.bAllowJoinInProgress && (LobbyMemberCount < MaxLobbyMembers);

UE_LOG_ONLINE_SESSION(Log, TEXT("Updating lobby joinability to %s."), bLobbyJoinable ? TEXT("true") : TEXT("false"));
if (!SteamMatchmakingPtr->SetLobbyJoinable(LobbyId, bLobbyJoinable))
{
	UE_LOG_ONLINE_SESSION(Warning, TEXT("Failed to update lobby joinability."));
	bSuccess = false;
}

The point is, if the session has bAllowJoinInProgress = false, (which is perfectly normal, I want to manually start the session later, once players joined, and prevent further joining.) only the first player can join the session, after join, this runs and disables joining functionality totally. I tested it out, I edited the mentioned calls so it takes into acount the session’s current state, and not just bAllowJoinInProgress, and it worked. Multiple players found the session, and everyone could join.

Example:

//Old:
bool bLobbyJoinable = Session.SessionSettings.bAllowJoinInProgress && (LobbyMemberCount < MaxLobbyMembers);
//New:
bool bLobbyJoinable = (Session.SessionSettings.bAllowJoinInProgress || Session.SessionState != EOnlineSessionState::InProgress)

Summary

Thank you for reading my post. As I mentioned, I this topic is open to discussion, because I’m not sure if the OnlineSubsystemSteam plugin was written that way for a reason (maybe something about steam’s session logic) but I feel like it is not the intended behaviour. If I’m wrong, feel free to comment and correct me. If I’m correct, I hope I was able to help someone with a solution. :smiley: