Steam Provider and Authentication System
Provider is the central singleton managing Unturned's connection to Steam — it handles the Steamworks client and server initialization, authentication ticket exchange, server browser advertisement, workshop synchronization, player connection lifecycle, and network channel management. It is a Unity MonoBehaviour that lives across scene loads and coordinates the entire networking stack. It is a partial class split across multiple files to organize the large amount of static state.
This article covers the Provider singleton architecture, the Steam connection lifecycle (server init, client join, authentication, workshop sync, spawn), the server browser listing system, the player add/remove flow, the rate limiting system, and the third-party anti-cheat integration points.
Source code location: Unturned/Provider/Provider.cs, Unturned/Provider/NetTransportFactory.cs, SteamworksProvider/
Provider Singleton Architecture
Provider is a partial class MonoBehaviour with static fields for every networking and game state property. Key static state includes:
csharp
public partial class Provider : MonoBehaviour
{
public static readonly string STEAM_IC = "Steam";
public static readonly string STEAM_DC = "<color=#2784c6>Steam</color>";
public static readonly AppId_t APP_ID = new AppId_t(304930); // 407660 experimental
public static readonly AppId_t PRO_ID = new AppId_t(306460);
public static string APP_VERSION { get; protected set; }
public static uint APP_VERSION_PACKED { get; protected set; }
public static readonly string APP_NAME = "Unturned";
public static readonly string APP_AUTHOR = "Nelson Sexton";
public static readonly int CLIENT_TIMEOUT = 30;
internal static readonly float PING_REQUEST_INTERVAL = 1f;
// ...
}Key state:
APP_ID—304930(production) or407660(experimental).PRO_IDis306460.APP_VERSIONandAPP_VERSION_PACKED— the current game version as a string and packed 32-bit integer for network replication.CLIENT_TIMEOUT— 30 seconds._server(CSteamID) — the current server's Steam ID._client(CSteamID) — the local client's Steam ID._user(CSteamID) — the logged-in Steam user's ID._isServer,_isClient,_isConnected— connection state booleans._clients—List<SteamPlayer>of fully connected players.pending—List<SteamPending>of players in the connection queue._transportConnectionToPlayerMap— mapsITransportConnectiontoSteamPlayerfor fast lookup._transportConnectionToPendingPlayerMap— same for pending players.modeConfigData— the current game mode's config data, including event timings for arena, airdrop, and compactor.
The Provider also manages:
- Screenshot capture pipeline (Steam overlay integration, supersampling support).
- Rich presence updates (Steam
SetRichPresence, community service status updates). - Localization root path and language management.
- Workshop file ID registration and server-workshop file synchronization.
- Anti-cheat initialization (VAC and third-party via partial methods).
Localization System
csharp
private static string privateLanguage;
public static string language
{
get => privateLanguage;
private set
{
privateLanguage = value;
languageIsEnglish = value == "English";
}
}
public static string localizationRoot { get; private set; }
public static Local localization;
public static List<string> streamerNames { get; private set; }localizationRoot points to the directory containing language-specific .dat files. streamerNames is loaded from a configuration file and used by the chat filter system.
Rich Presence
updateRichPresence() updates Steam rich presence based on the current game state:
csharp
private static void updateSteamRichPresence()
{
if (OptionsSettings.ShouldHideRichPresence)
{
SteamFriends.ClearRichPresence();
return;
}
if (Level.info != null)
{
if (Level.isEditor)
SteamFriends.SetRichPresence("steam_display", "#Status_EditingLevel");
else if (isConnected && !isServer && server.m_SteamID > 0)
SteamFriends.SetRichPresence("steam_display", "#Status_PlayingMultiplayer");
else
SteamFriends.SetRichPresence("steam_display", "#Status_PlayingSingleplayer");
SteamFriends.SetRichPresence("level_name", Level.info.getLocalizedName());
}
else
{
if (Lobbies.inLobby)
SteamFriends.SetRichPresence("steam_display", "#Status_WaitingInLobby");
else
SteamFriends.SetRichPresence("steam_display", "#Status_AtMainMenu");
}
}The rich presence tokens %level_name% are substituted by Steam from the level_name key. The community service also receives a formatted status string.
Screenshot Capture
Provider implements a coroutine-based screenshot pipeline:
csharp
private IEnumerator CaptureScreenshot()
{
bool useSupersampling = OptionsSettings.enableScreenshotSupersampling;
int maxSizeMultiplier = useSupersampling ? 4 : 16;
int sizeMultiplier = Mathf.Clamp(OptionsSettings.screenshotSizeMultiplier, 1, maxSizeMultiplier);
int finalWidth = Screen.width * sizeMultiplier;
int finalHeight = Screen.height * sizeMultiplier;
if (finalWidth > SystemInfo.maxTextureSize || finalHeight > SystemInfo.maxTextureSize)
{
UnturnedLog.warn("Screenshot exceeds max supported texture size");
yield break;
}
if (useSupersampling)
{
// Supersample at 2x resolution, then bilinear downsample
int superSize = sizeMultiplier * 2;
Texture2D supersampledTexture = ScreenCapture.CaptureScreenshotAsTexture(superSize);
RenderTexture downsampleRT = RenderTexture.GetTemporary(finalWidth, finalHeight,
0, supersampledTexture.graphicsFormat);
Graphics.Blit(supersampledTexture, downsampleRT, screenshotBlitMaterial);
// Read back from render texture, encode to PNG, write to disk
}
else
{
ScreenCapture.CaptureScreenshot(filePath, sizeMultiplier);
}
// Tag screenshot with Steam location and player info
ScreenshotHandle handle = SteamScreenshots.AddScreenshotToLibrary(filePath, null, finalWidth, finalHeight);
if (Level.info != null) SteamScreenshots.SetLocation(handle, Level.info.getLocalizedName());
foreach (SteamPlayer client in clients)
{
// Tag visible players within 64m range
Vector3 worldPosition = client.player.transform.position + Vector3.up;
if ((worldPosition - cameraPosition).sqrMagnitude > 64 * 64) continue;
SteamScreenshots.TagUser(handle, client.playerID.steamID);
}
}The screenshot pipeline supports supersampling (captures at 2x target resolution, then bilinear downsamples for quality), and tags both the game location and visible players in the Steam screenshot library.
Server Initialization Flow
Provider.Start()triggers server startup on dedicated servers via SteamGameServer initialization.SteamGameServer.Init()is called with the app ID (304930), listening port (27015), Game Server Steam API port (27016), and query port (27017).SteamGameServer.LogOn(gslt)authenticates the server with its Game Server Login Token.SteamGameServer.SetGameTags()andSteamGameServer.SetGameDescription()advertise server metadata including transport type, map name, game mode, PvP status, and mod counts.NetTransportFactory.CreateServerTransport()instantiates the transport implementation — default isServerTransport_SteamNetworkingSockets, overridable via-NetTransportcommand-line argument.serverTransport.startListening()opens the transport listener on the configured port.- The server enters the game loop, processing incoming connections through
receiveTransportConnection().
Client Connection Flow
Connect Parameters
csharp
internal class ServerConnectParameters
{
public string address;
public ushort port;
public string password;
public CSteamID steamID; // For Steam ID-based joins
public ulong lobbyID; // For lobby joins
}The client can connect by IP:port, Steam ID (friend join), or lobby ID.
Connection Sequence
connect(ServerConnectParameters parameters)initiates the connection. If connecting by IP,SteamMatchmakingServers.PingInternet()retrievesSteamServerAdvertisement(A2S info).- The client sends an initial transport connection request to the server.
- On acceptance,
receiveTransportConnection()on the server creates aSteamPendingentry and adds it to the pending queue. - The pending player receives
ServerMessageHandler_ReadyToConnect— the server asks for their authentication data. - The client responds with
EServerMessage.Authenticate, sending their Steam auth ticket, character data, and equipped item IDs. - The server validates the auth ticket via
SteamGameServer.BeginAuthSession(). - The client waits for workshop file list via
ServerMessageHandler_GetWorkshopFilesandClientMessageHandler_DownloadWorkshopFiles. - Once workshop files are confirmed (either present or downloaded), the client sends
EServerMessage.ReadyToConnect. - The server sends
EClientMessage.Acceptedwith the player's assignedNetIdblock, spawn position, equipment state, and initial player data. addPlayer()constructs the player'sSteamPlayerandPlayerobjects, sets up the player's RPC channel, and broadcastsonServerConnected.
Workshop Synchronization
csharp
private static List<ulong> _serverWorkshopFileIDs = new List<ulong>();
public static void registerServerUsingWorkshopFileId(ulong id)
{
if (_serverWorkshopFileIDs.Contains(id)) return;
_serverWorkshopFileIDs.Add(id);
ServerRequiredWorkshopFile requiredFile = new ServerRequiredWorkshopFile()
{
fileId = id,
timestamp = DateTimeEx.FromUtcUnixTimeSeconds(timestamp)
};
serverRequiredWorkshopFiles.Add(requiredFile);
}The server maintains _serverWorkshopFileIDs — a list of all Workshop file IDs used by mods on the server. When a client connects:
- The server compiles
serverRequiredWorkshopFilesfromregisterServerUsingWorkshopFileId()calls. - The server sends the list to the client via
ServerMessageHandler_GetWorkshopFiles. - The client compares against locally cached files and downloads any missing via
ClientMessageHandler_DownloadWorkshopFiles. - After download,
receiveWorkshopResponse()validates the server's workshop response against the advertised data.
Workshop Response Validation
csharp
internal static void receiveWorkshopResponse(CachedWorkshopResponse response)
{
// Cache server info
serverName = response.serverName;
map = response.levelName;
isPvP = response.isPvP;
mode = response.gameMode;
cameraMode = response.cameraMode;
maxPlayers = response.maxPlayers;
isVacActive = response.isVACSecure;
// Validate against advertisement
if (CurrentServerAdvertisement != null)
{
if (!string.Equals(CurrentServerAdvertisement.map, response.levelName, ...))
{
_connectionFailureInfo = ESteamConnectionFailureInfo.SERVER_MAP_ADVERTISEMENT_MISMATCH;
RequestDisconnect("server map advertisement mismatch");
return;
}
if (CurrentServerAdvertisement.maxPlayers != response.maxPlayers)
{
_connectionFailureInfo = ESteamConnectionFailureInfo.SERVER_MAXPLAYERS_ADVERTISEMENT_MISMATCH;
RequestDisconnect();
return;
}
// ... camera mode, PvP, VAC checks
}
if (queryIDs.Count < 1)
{
launch(); // No workshop items needed
}
else
{
// Download and verify workshop items
provider.workshopService.queryServerWorkshopItems(queryIDs, response.ip);
}
}The client validates that the server's workshop response matches the advertised data to prevent the server from advertising a different configuration than it actually runs. Workshop file IDs must match the advertised set (cannot add hidden items).
Authentication and Anti-Cheat
Steam Auth
SteamGameServer.BeginAuthSession() validates the client's auth ticket. The server also checks:
SteamGameServer.RequestUserGroupStatus()for group membership checks.- Ban list via
SteamBlacklistchecks against Steam ID, IP, and hardware IDs.
VAC (Valve Anti-Cheat)
VAC status is obtained from the server's GameServerItem.m_bSecure field (advertised) and the workshop response isVACSecure field. Note: the VAC advertisement comparison is currently commented out due to m_bSecure variability issues.
Third-Party Anti-Cheat
Defined as partial methods in Provider:
csharp
#if WITH_THIRDPARTYAC
static partial void AddClientToThirdpartyAntiCheat(ITransportConnection clientId, SteamPlayerID playerID, SteamPlayer newClient);
static partial void RemoveClientFromThirdpartyAntiCheat(SteamPlayer clientToRemove);
static partial void ShutdownThirdpartyAntiCheatServer();
static partial void ShutdownThirdpartyAntiCheatClient();
private static partial bool InitThirdpartyAntiCheatServer();
static partial void RunThirdpartyAntiCheatFrame();
private static partial bool CheckThirdpartyAntiCheatWantsRestart();
#endifThe third-party anti-cheat system uses a separate message channel (EClientMessage.ThirdpartyAntiCheat, EServerMessage.ThirdPartyAntiCheat) for encrypted or opaque data exchange. Player IDs are allocated sequentially via AllocThirdpartyAntiCheatPlayerId().
Server Browser Listing
The server advertises itself in the Steam game server browser via:
SteamGameServer.SetGameTags()— serializes key-value pairs: transport tag ("sns","sys","def"), map name, game mode, PvP status, VAC status, anti-cheat status, Workshop status.SteamGameServer.SetGameDescription()— human-readable description.SteamGameServer.SetMaxPlayerCount()— from server config.
On the client side, SteamServerAdvertisement is populated from gameserveritem_t during a ping response:
csharp
public static SteamServerAdvertisement CurrentServerAdvertisement => _currentServerAdvertisement;It stores:
- Server name, map, game description.
- Ping, player count, max players.
- Steam ID, IP, port (
GetQueryPort(),GetConnectionAddress()). - VAC secure flag, third-party anti-cheat flag.
- Bot count, server version, OS.
- Is Workshop-enabled flag.
steamIDfor friend-join routing.
Player Connection Lifecycle
Adding a Player
csharp
internal static SteamPlayer addPlayer(ITransportConnection transportConnection, NetId netId,
SteamPlayerID playerID, Vector3 point, byte angle, bool isPro, bool isAdmin, int channel,
byte face, byte hair, byte beard, Color skin, Color color, Color markerColor, Color beardColor,
bool hand, int shirtItem, int pantsItem, int hatItem, int backpackItem, int vestItem,
int maskItem, int glassesItem, int[] skinItems, string[] skinTags, string[] skinDynamicProps,
EPlayerSkillset skillset, string language, CSteamID lobbyID, EClientPlatform clientPlatform)
{
// Destroy placeholder audio listener for local player
if (playerID.steamID == client && Level.placeholderAudioListener != null)
{
Destroy(Level.placeholderAudioListener);
Level.placeholderAudioListener = null;
}
// Instantiate player game object from game mode
Transform model = gameMode.getPlayerGameObject(playerID).transform;
model.position = point;
// Create SteamPlayer wrapper
SteamPlayer newClient = new SteamPlayer(transportConnection, netId, playerID, model,
isPro, isAdmin, channel, face, hair, beard, skin, color, markerColor, beardColor, hand,
shirtItem, pantsItem, hatItem, backpackItem, vestItem, maskItem, glassesItem, skinItems,
skinTags, skinDynamicProps, skillset, language, lobbyID, clientPlatform);
clients.Add(newClient);
_transportConnectionToPlayerMap.Add(transportConnection, newClient);
updateRichPresence();
broadcastEnemyConnected(newClient);
return newClient;
}addPlayer() is called with 34 parameters covering identity, cosmetics, economy items, skills, and network state. The player's NetId block (17 IDs) is pre-allocated via ClaimNetIdBlockForNewPlayer().
Removing a Player
csharp
internal static void RemoveClient(SteamPlayer clientToRemove)
{
// Clean up third-party anti-cheat
#if WITH_THIRDPARTYAC
RemoveClientFromThirdpartyAntiCheat(clientToRemove);
#endif
// Close transport connection
if (Dedicator.IsDedicatedServer)
clientToRemove.transportConnection.CloseConnection();
// Notify other clients
broadcastEnemyDisconnected(clientToRemove);
// Release NetId block
clientToRemove.player.ReleaseNetIdBlock();
// Destroy player game object
if (clientToRemove.model != null)
{
EffectManager.ClearAttachments(clientToRemove.model);
clientToRemove.player.isExpectingDestroy = true;
Destroy(clientToRemove.model.gameObject);
}
NetIdRegistry.Release(clientToRemove.GetNetId());
// Remove from lookup maps
_transportConnectionToPlayerMap.Remove(clientToRemove.transportConnection);
CSteamID steamId = clientToRemove.playerID.steamID;
clients.Remove(clientToRemove);
// Clean up culled player references
foreach (SteamPlayer otherClient in clients)
otherClient.culledPlayers.Remove(steamId);
// Let next queued player in
verifyNextPlayerInQueue();
updateRichPresence();
}The removal flow ensures proper cleanup of NetIds, visual attachments, queued players, and per-player culling state.
Connection Queue
csharp
internal static void verifyNextPlayerInQueue()
{
if (pending.Count < 1) return;
if (clients.Count >= maxPlayers) return;
SteamPending pendingPlayer = pending[0];
if (pendingPlayer.hasSentVerifyPacket) return;
pendingPlayer.sendVerifyPacket();
}When the server is full, new connections enter a pending queue. hasRoomForNewConnection returns true if clients.Count < maxPlayers or pending.Count < queueSize. The dequeue logic checks pending queue non-empty, free slot, and hasn't already sent a verify packet.
IP Rate Limiting
csharp
public static bool IsBlockedByMaxClientsWithSameIpAddressRule(ITransportConnection transportConnection,
bool includeQueuedPlayers)
{
if (configData.Server.Use_FakeIP) return false;
if (!transportConnection.TryGetIPv4Address(out uint address)) return false;
int max = configData.Server.Max_Clients_With_Same_IP_Address;
int otherClientCount = 0;
// Count pending and connected players with same IP
// ...
if (otherClientCount + 1 > max)
{
if (configData.Server.Max_Clients_With_Same_IP_Address_Log_Warnings)
CommandWindow.LogWarning("Connection {transportConnection} hit IP limit");
return true;
}
return false;
}Prevents more than Max_Clients_With_Same_IP_Address clients from the same IPv4 address. Use_FakeIP bypasses this.
The badMessageRateLimiter (TransportConnectionRateLimiter) tracks per-connection "bad" packet counts — packets that may be legitimate but excessive. If a connection exceeds the threshold, it's likely an attacker trying to waste server CPU time.
Spawn Position Loading
csharp
private static void loadPlayerSpawn(SteamPlayerID playerID, out Vector3 point, out byte angle,
out EPlayerStance initialStance)
{
point = Vector3.zero;
angle = 0;
initialStance = EPlayerStance.STAND;
bool needsSpawn = false;
if (PlayerSavedata.fileExists(playerID, "/Player/Player.dat") && Level.info.type == ELevelType.SURVIVAL)
{
Block block = PlayerSavedata.readBlock(playerID, "/Player/Player.dat", 1);
point = block.readSingleVector3() + new Vector3(0, 0.01f, 0);
angle = block.readByte();
if (!point.IsFinite()) { needsSpawn = true; }
else if (point.y > Level.HEIGHT) { point.y = Level.HEIGHT - 10.0f; }
else if (!PlayerStance.getStanceForPosition(point, ref initialStance)) { needsSpawn = true; }
}
else
{
needsSpawn = true;
}
// Fire plugin hook
onLoginSpawning?.Invoke(playerID, ref point, ref yaw, ref initialStance, ref needsSpawn);
if (needsSpawn)
{
PlayerSpawnpoint spawn = LevelPlayers.getSpawn(false);
point = spawn.point + new Vector3(0, 0.5f, 0);
angle = (byte)(spawn.angle / 2);
}
}Survival mode loads saved spawn positions from Player.dat. Non-survival modes and first-spawn always use a fresh spawnpoint. The onLoginSpawning delegate allows plugins to override spawn position.
Channel System
csharp
private static int nextPlayerChannelId = 2;
private static int allocPlayerChannelId()
{
const int maxID = byte.MaxValue;
for (int attempt = 0; attempt < maxID; ++attempt)
{
int pendingId = nextPlayerChannelId;
++nextPlayerChannelId;
if (nextPlayerChannelId > maxID) nextPlayerChannelId = 2;
SteamChannel existingComponent = findChannelComponent(pendingId);
if (existingComponent == null) return pendingId;
}
CommandWindow.LogError("Ran out of player RPC channel IDs");
shutdown(1, "Ran out of player RPC channel IDs");
return 2;
}Provider maintains a list of SteamChannel receivers. Channel 1 is reserved for manager components (LevelManager, EffectManager). Channels 2+ are allocated per-player. Channel IDs are 8-bit (max 255). The allocator wraps around at max and skips existing channels.
openChannel(receiver)— adds a receiver and logs it.closeChannel(receiver)— removes the receiver.findChannelComponent(id)— finds a channel by ID with null-safety cleanup.
The channel system routes ClientInstanceMethod and ServerInstanceMethod invocations to the correct component instance.
Provider Network Stats
csharp
private static uint _bytesSent;
public static uint bytesSent => _bytesSent;
private static uint _bytesReceived;
public static uint bytesReceived => _bytesReceived;
private static uint _packetsSent;
public static uint packetsSent => _packetsSent;
private static uint _packetsReceived;
public static uint packetsReceived => _packetsReceived;Provider tracks bandwidth and packet counters since last reset. resetChannels() clears all connection state, NetId registry, invocation deferral registry, asset integrity cache, and physics material net table. It is called when disconnecting or resetting the networking layer.
Transport Connection Utilities
Provider includes helper methods for gathering connection lists:
csharp
public static PooledTransportConnectionList GatherClientConnections()
public static PooledTransportConnectionList GatherRemoteClientConnections()
public static PooledTransportConnectionList GatherClientConnectionsWithinSphere(Vector3 position, float radius)
public static PooledTransportConnectionList GatherClientConnectionsMatchingPredicate(Predicate<SteamPlayer> predicate)
public static PooledTransportConnectionList GatherRemoteClientConnectionsWithinSphere(Vector3 position, float radius)All use TransportConnectionListPool to minimize allocation. "Remote" variants exclude the local loopback host player. "WithinSphere" variants check sqrMagnitude against the player's transform position.
Player lookup methods:
findPlayer(ITransportConnection)— resolve connection toSteamPlayer.findPendingPlayer(ITransportConnection)— resolve toSteamPending.findTransportConnection(CSteamID)— resolve Steam ID to connection (searches both pending and connected players).findTransportConnectionSteamId(ITransportConnection)— reverse resolve.
Connection Failure Handling
csharp
private static bool canCurrentlyHandleClientTransportFailure;
private static bool hasPendingClientTransportFailure;Some loading operations (workshop download, level loading) can't handle transport failures mid-process. canCurrentlyHandleClientTransportFailure gates whether transport errors trigger immediate disconnect or queue the failure for after the loading completes.
Server List Integration
The Provider integrates with Steam's server browser system for server discovery:
csharp
// Server advertisement via game tags
SteamGameServer.SetGameTags($"Transport:{transportTag}"
+ $" Map:{mapName}"
+ $" Mode:{gameMode}"
+ $" PvP:{(isPvP ? 1 : 0)}"
+ $" VAC:{(isVacSecure ? 1 : 0)}"
+ $" AC:{(antiCheatEnabled ? 1 : 0)}"
+ $" Workshop:{(hasWorkshopItems ? 1 : 0)}"
+ $" Mods:{modCount}");The client parses these tags to display server details in the server browser UI. ServerBookmarksManager allows players to bookmark favorite servers for quick reconnect, storing the server's Steam ID, last-known name, and connection history.
Lobby Integration
While the Provider manages direct server connections, Lobbies manages Steam lobby-based connections:
Lobbies.inLobby— whether the local client is currently in a Steam lobby.Lobbies.currentLobby— the currentCSteamIDof the lobby.- Lobbies use Steam's matchmaking system for player discovery and invitation.
When joining a lobby, the lobby's metadata includes the game server's Steam ID or connection string, which is passed to Provider.connect() via ServerConnectParameters.lobbyID.
Config Data Flow
The Provider holds the authoritative config that all gameplay systems reference:
csharp
public static ModeConfigData modeConfigData;This is populated from:
- Server-side:
LiveConfigreads theConfig.jsonfile on startup. - Client-side:
ClientMessageHandler_ReplicateConfigreceives the config from the server. - Per-map overrides:
LevelInfoConfigData.GetPerDifficultyConfigOverrides()applies overrides.
The config data includes:
Eventssection: airdrop frequency/speed, arena timings/speeds, compactor settings.Barricadessection: decay rates, damage multipliers.Inventorysection: stacking limits, drop settings.Playersection: health/food/water/virus settings, skills, experience.Vehiclessection: spawn rates, fuel consumption, damage.
