Self-Hosted Game Servers and Listen Hosts
List a Unity game server you run yourself, or a game a player hosts, on a Discovery app with HeartbeatReporter and UdpEchoResponder, admit reservations and delist on shutdown.
Use this when your game server does not run on a PingCore fleet: a self-hosted dedicated game server on machines you run, or a listen host, which is a player's own game hosting while they play. Either one lists itself on a Discovery app's public server list with heartbeats, and players find it with the client SDK as usual. A LAN-only listen host does not contact PingCore. A game server on a fleet never heartbeats; it uses the game server SDK. Your assembly definition references PingCore.Discovery.Host and PingCore.Core, plus PingCore.Netcode.NGO and Unity.Netcode.Runtime to admit players, and PingCore.Discovery.Client to read PingCoreClientSettings.
Get a heartbeat token
using System;
string token = Environment.GetEnvironmentVariable("PINGCORE_DISCOVERY_TOKEN");
if (string.IsNullOrEmpty(token))
{
// Tell the operator the variable is not set. Never print the token itself.
}On your Discovery app's page in the panel, issue a token under Heartbeat Tokens with the Heartbeat scope. Start your game server with it in the PINGCORE_DISCOVERY_TOKEN environment variable, and read it as above. Never pass it as a command-line argument (other users on the machine can read those), never put it in an asset or a scene, and never log it. The SDK does not read the variable itself, so where the token comes from is up to your game.
The app's Self-hosted allowance caps how many self-hosted game servers it lists. At 0, self-hosting is off for the app; contact support to turn it on.
Answer the reachability probe
using PingCore.Discovery.Host;
using UnityEngine;
if (!UdpEchoResponder.TryBind(gamePort + 1, out UdpEchoResponder echo, out string error))
{
Debug.LogWarning("The reachability echo could not bind: " + error);
}When the app checks reachability (an open app always does), Discovery checks that a listed game server really answers at its address before it shows it to players. Bind the responder on the game port plus one, before the first heartbeat, and dispose it when the game server stops. It answers only Discovery's probe and limits how often it replies. Forward both ports (for example UDP 7777 for the game and UDP 7778 for the echo). The probe modes are on Reachability Verification.
TryBindreturns false when the port is in use. The game server still heartbeats, but it stays off the list until the probe gets an answer.
Start heartbeating
using Newtonsoft.Json.Linq;
using PingCore.Discovery.Host;
using UnityEngine;
HeartbeatReporter heartbeat = HeartbeatReporter.Create(new HeartbeatReporterOptions
{
BaseUrl = "https://discovery.pingcore.io",
Token = token,
Name = "Alice's arena",
GamePort = 7777,
MaxPlayers = 8,
Version = Application.version,
Meta = new JObject { ["proto"] = 2 }, // fields players filter on
});
HeartbeatStartResult start = await heartbeat.StartAsync(ct);
if (!start.IsStarted)
{
Debug.LogWarning("Not listed: " + start);
}Keep the reporter for the game server's lifetime. Once started it heartbeats every 30 seconds and retries a missed beat by itself. When the app checks reachability, heartbeat.Status.Verified reads pending until the probe succeeds (unverified if it fails), and the game server is hidden from the list until it reads verified. The heartbeat is described on Sending Heartbeats.
start.OutcomeisRefused: Discovery turned this game server away, and nothing more is sent.DiscoveryReason.SelfHostedCapmeans the app already lists its Self-hosted allowance, andstart.Limitsays how many. Tell the host why.Failed: the options are invalid (the token must start withdsc_), the reporter was already started, stopped or disposed, or the first heartbeat failed. After a failed heartbeat, callStartAsyncagain later. Beacon Rush retries after 5, 10 and 20 seconds, then every 30 seconds.LocalSdkEndpointPresent: this process runs on a PingCore fleet; use the game server SDK instead.
Keep the listing current
using Newtonsoft.Json.Linq;
heartbeat.SetPlayers(connectedPlayers);
heartbeat.SetMeta(new JObject { ["proto"] = 2, ["map"] = "coastline" });Call SetPlayers on every join and leave, and SetMeta when a field players filter on changes. A change goes out early, at most once every 5 seconds.
A game server whose listing lapsed during an outage can be refused when it comes back. The reporter then stops, and its
Beatevent reportsStopped. Start a new reporter.
Admit players
using PingCore.Core.Handshake;
using PingCore.Netcode.NGO;
using Unity.Netcode;
var approval = new PingCoreConnectionApproval(
NetworkManager.Singleton,
new ApprovalOptions { ProtocolVersion = 2, Mode = HostingMode.SelfHosted },
new HeartbeatAdmissionEvidence(heartbeat),
gate);
approval.Install();Players reach a heartbeat game server through quick join or a reservation, never a match, so each one arrives with a reservation join ticket. HeartbeatAdmissionEvidence asks Discovery whether the hold admits that player on this game server, and the approval admits only on a valid answer. Use HostingMode.Listen for an online listen host. Installing the approval and writing the gate are on The Game Server SDK.
reservation_invalid: the hold expired, is on another game server or names other players. The player reserves again.reservation_unverifiable: Discovery did not answer in time, or no heartbeat was accepted yet. The player can retry.
Delist on shutdown
using System.Threading;
using UnityEngine;
Application.quitting += () =>
{
_ = heartbeat.StopAsync(CancellationToken.None);
echo?.Dispose();
};StopAsync removes the listing at once, so players stop seeing the game server. Without it, the listing lapses 90 seconds after the last heartbeat.
The process can exit before Discovery answers. The listing then lapses on its own after 90 seconds.
Host online from a player's game
using PingCore.Discovery.Host;
// settings is your PingCoreClientSettings asset.
HeartbeatReporter heartbeat = HeartbeatReporter.Create(new HeartbeatReporterOptions
{
BaseUrl = settings.DiscoveryBaseUrl,
Token = settings.OpenRegistrationHeartbeatToken,
TokenShipsInGame = true,
Name = "Alice's game",
GamePort = 7777,
MaxPlayers = 8,
});An online listen host heartbeats its own Discovery app, not your fleet's, with Registration set to open (client-hosted). Its heartbeat token is the one token a player build may carry. Give the host its token in one of two ways:
From
PINGCORE_DISCOVERY_TOKEN, exactly like a self-hosted game server. Beacon Rush's Host a game, Online does this, and offers Online only when the variable is set.Shipped in the build, as above. Pick the open app and set its token under Player hosting in the PingCore window, then press Confirm for build (see The PingCore Window and Keeping Secrets Out of Your Build).
Everything else works as for a self-hosted game server: bind the echo, start the reporter, admit with HostingMode.Listen and delist on stop. An open app always checks reachability with the echo, so tell the player which ports to forward. Players browse an open app's list with a DiscoveryClient for settings.CommunityAppPublicId.
Until the token is confirmed, every build that carries it fails the build guard.
RefusedwithDiscoveryReason.IpCap: the player's network already has as many listed game servers as the app allows per address.
Host on a LAN only
using PingCore.Core.Handshake;
var options = new ApprovalOptions { ProtocolVersion = 2, Mode = HostingMode.Listen, LanOnly = true };
IAdmissionEvidence evidence = new LanAdmissionEvidence();
// On the joining client, with its own LAN player id:
JoinTicket joinTicket = JoinTicket.ForLan(playerId, 2, "Bob");A LAN-only listen host creates no reporter and no echo, is never listed, and admits only lan join tickets. Players connect to its address directly. To try it in Beacon Rush, click Host a game, LAN only, Start hosting, then Play. In a second copy of the game, click Direct connect, keep the Address 127.0.0.1:7777 and click Connect.
You should see "Joining the game server at 127.0.0.1:7777 (lan join)...", then both players in the arena.
Any other kind of join ticket, a reservation included, is refused
kind_not_accepted, because a LAN host cannot check one.