Add persistent per-install player identity

Introduce PersistentPlayerId: a stable id that survives an app restart and is
unique per install (built on the UGS install-path-namespaced anonymous
PlayerId, with a local PlayerPrefs GUID fallback for loopback). Clones on the
same machine stay distinct.

Each client reports its id to the server on connect via a new Match_Identity
named message (mirroring Match_ReportHit, so no ConnectionApproval timing
risk); the host records its own directly. The server keeps an ephemeral
clientId -> persistent id map, exposed via PersistentIdOf(clientId) as the
foundation for later reconnect recognition. Map entries are cleared on
disconnect and net stop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 4.8 committed 2026-07-02 20:09:19 +02:00
1 parent 17bb37cb92
commit a59a5e53ab
3 files changed
+123

No files matched your search

+63
View File
@@ -183,11 +183,18 @@ public class MatchManager : MonoBehaviour
// reports a hit on a hider (ulong target client id); the server validates and marks the hider found.
private const string MsgPhase = "Match_Phase";
private const string MsgReportHit = "Match_ReportHit";
// Client -> server, right after the client connects: the sender's persistent per-install id (survives its
// app restarts). Lets the server map the ephemeral NGO clientId to a stable identity - the reconnect
// foundation. See PersistentPlayerId.
private const string MsgIdentity = "Match_Identity";
// Server-only sub-phase machine. _pendingHiders holds the hiders not yet found; seek ends the instant it
// empties. _phaseRoutine is the running Hide->Seek->Resolution coroutine (stopped if the match is ended
// early via the authority's Back-to-Lobby).
private readonly HashSet<ulong> _pendingHiders = new HashSet<ulong>();
// Server-only. Live NGO clientId -> persistent per-install id, reported by each client on connect. The
// clientId is ephemeral (reused after a disconnect); the value is stable across that client's restarts.
private readonly Dictionary<ulong, string> _persistentIds = new Dictionary<ulong, string>();
private Coroutine _phaseRoutine;
private readonly List<ulong> _phaseRecipients = new List<ulong>(); // reused scratch for the phase broadcast
@@ -237,6 +244,7 @@ public class MatchManager : MonoBehaviour
private void UnhookSessionCallbacks()
{
_net.OnClientConnectedCallback -= OnClientConnected;
_net.OnClientConnectedCallback -= OnLocalClientConnected;
_net.OnClientDisconnectCallback -= OnClientDisconnected;
if (_net.SceneManager != null) _net.SceneManager.OnSceneEvent -= OnSceneEvent;
if (_net.CustomMessagingManager != null)
@@ -245,6 +253,7 @@ public class MatchManager : MonoBehaviour
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgRequestEndMatch);
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgPhase);
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgReportHit);
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgIdentity);
}
}
@@ -269,10 +278,12 @@ public class MatchManager : MonoBehaviour
{
_net.CustomMessagingManager.RegisterNamedMessageHandler(MsgRequestEndMatch, OnRequestEndMatchMessage);
_net.CustomMessagingManager.RegisterNamedMessageHandler(MsgReportHit, OnReportHitMessage);
_net.CustomMessagingManager.RegisterNamedMessageHandler(MsgIdentity, OnIdentityMessage);
}
_subPhase = MatchSubPhase.None;
_pendingHiders.Clear();
_persistentIds.Clear();
// Spawn late joiners on connect (server-side callback). Re-hook defensively in case of re-host.
_net.OnClientConnectedCallback -= OnClientConnected;
@@ -295,6 +306,11 @@ public class MatchManager : MonoBehaviour
_net.CustomMessagingManager.RegisterNamedMessageHandler(MsgAuthorityGranted, OnAuthorityGrantedMessage);
_net.CustomMessagingManager.RegisterNamedMessageHandler(MsgPhase, OnPhaseMessage);
}
// Report our persistent id to the server the moment our own connection completes (host records its own
// directly). Re-hook defensively in case of re-connect.
_net.OnClientConnectedCallback -= OnLocalClientConnected;
_net.OnClientConnectedCallback += OnLocalClientConnected;
}
// Fired on shutdown / disconnect for both the server and the client (a host gets both, hence the
@@ -314,6 +330,7 @@ public class MatchManager : MonoBehaviour
Phase = MatchPhase.Lobby;
ApplyPhaseLocal(MatchSubPhase.None, 0f, _localMode);
_pendingHiders.Clear();
_persistentIds.Clear();
if (_net) UnhookSessionCallbacks();
ReturnToMainOnly();
@@ -384,6 +401,8 @@ public class MatchManager : MonoBehaviour
// A hider that leaves mid-match can never be found - drop it so "all hiders found" can still trigger.
_pendingHiders.Remove(clientId);
// Free the ephemeral clientId -> identity mapping (the clientId will be reused by a later joiner).
_persistentIds.Remove(clientId);
if (!_hasAuthorityClient || clientId != _authorityClientId) return;
if (_net.IsHost && clientId == _net.LocalClientId) return; // host shutting down; teardown handles it
@@ -400,6 +419,50 @@ public class MatchManager : MonoBehaviour
}
}
// Fired locally when OUR own client connection completes (host and pure client alike). The host is already
// the server, so it records its own identity directly; a pure client reports it to the server by message.
private void OnLocalClientConnected(ulong clientId)
{
if (_net == null || clientId != _net.LocalClientId) return;
if (_net.IsServer) RegisterIdentity(_net.LocalClientId, PersistentPlayerId.Value);
else SendIdentityToServer(PersistentPlayerId.Value);
}
// Client -> server: report this install's persistent id so the server can map it to our ephemeral clientId.
private void SendIdentityToServer(string persistentId)
{
var cmm = _net != null ? _net.CustomMessagingManager : null;
if (cmm == null || string.IsNullOrEmpty(persistentId)) return;
// Grows up to the cap if a future id source is longer than the initial buffer (persistent ids are short).
using (var writer = new FastBufferWriter(256, Allocator.Temp, 1024))
{
writer.WriteValueSafe(persistentId);
cmm.SendNamedMessage(MsgIdentity, NetworkManager.ServerClientId, writer);
}
}
// Server handler: a client reported its persistent per-install id right after connecting.
private void OnIdentityMessage(ulong senderClientId, FastBufferReader reader)
{
reader.ReadValueSafe(out string persistentId);
if (_net == null || !_net.IsServer) return;
RegisterIdentity(senderClientId, persistentId);
}
// Server-only. Records the ephemeral clientId -> persistent id mapping (idempotent; ignores empty ids).
private void RegisterIdentity(ulong clientId, string persistentId)
{
if (string.IsNullOrEmpty(persistentId)) return;
_persistentIds[clientId] = persistentId;
Debug.Log($"[MatchManager] Client {clientId} persistent id: {persistentId}", this);
}
/// <summary>Server-only. The stable per-install id a client reported on connect (survives that client's
/// app restarts), or null if unknown / not yet received. Foundation for reconnect recognition.</summary>
public string PersistentIdOf(ulong clientId) =>
_persistentIds.TryGetValue(clientId, out var id) ? id : null;
// Spawns the given prefab as a client's player object into the currently active gameplay scene. No-op
// if the client already owns a player object (double-spawn guard) or has since disconnected.
private void SpawnFor(ulong clientId, GameObject prefab, Vector3 at)