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>
59 lines
2.5 KiB
C#
59 lines
2.5 KiB
C#
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;
|
|
}
|
|
}
|