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:
1 parent
17bb37cb92
commit
a59a5e53ab
3 files changed
+123
No files matched your search
@@ -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)
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using Unity.Services.Authentication;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// A stable, per-install unique player id that survives application restarts, so the server can recognise a
|
||||
/// returning client across sessions (the NGO clientId is ephemeral and reused).
|
||||
///
|
||||
/// Primary source is the UGS anonymous <see cref="AuthenticationService"/> PlayerId, which is already cached
|
||||
/// per machine AND namespaced per install via the auth profile (see OnlineSessionManager.ProfileForThisInstall),
|
||||
/// so it is restart-stable and stays distinct even for same-machine clone tests. When there was no UGS sign-in
|
||||
/// (pure loopback / offline play) it falls back to a locally generated GUID persisted in PlayerPrefs.
|
||||
/// </summary>
|
||||
public static class PersistentPlayerId
|
||||
{
|
||||
private const string PrefKey = "chameleon.persistentPlayerId";
|
||||
|
||||
private static string _ugsId; // authoritative once signed in; cached permanently
|
||||
private static string _localFallback; // loopback/offline only
|
||||
|
||||
/// <summary>The persistent id for THIS install. Stable across restarts, never null/empty. Prefers the
|
||||
/// signed-in UGS id (the send path runs after sign-in, so online clients always report that); only pure
|
||||
/// loopback play resolves the local fallback.</summary>
|
||||
public static string Value
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_ugsId)) return _ugsId;
|
||||
|
||||
// AuthenticationService.Instance throws if UGS was never initialised (loopback) - guard it.
|
||||
try
|
||||
{
|
||||
var auth = AuthenticationService.Instance;
|
||||
if (auth != null && auth.IsSignedIn && !string.IsNullOrEmpty(auth.PlayerId))
|
||||
return _ugsId = auth.PlayerId;
|
||||
}
|
||||
catch (Exception) { /* services not initialised - use the local fallback */ }
|
||||
|
||||
return LocalFallback();
|
||||
}
|
||||
}
|
||||
|
||||
// NOT cached into _ugsId: a later signed-in read must be able to upgrade to the authoritative UGS id.
|
||||
// The fallback itself is restart-stable (PlayerPrefs), which is all offline play needs.
|
||||
private static string LocalFallback()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_localFallback)) return _localFallback;
|
||||
|
||||
_localFallback = PlayerPrefs.GetString(PrefKey, "");
|
||||
if (string.IsNullOrEmpty(_localFallback))
|
||||
{
|
||||
_localFallback = Guid.NewGuid().ToString("N");
|
||||
PlayerPrefs.SetString(PrefKey, _localFallback);
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
return _localFallback;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ff7e4aa09ded2724e9f12b92cd6c6f95
|
||||
Reference in new issue
Block a user