Files
chameleon/Assets/Scripts/Network/PersistentPlayerId.cs
T
adminandClaude Opus 4.8 a59a5e53ab 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>
2026-07-02 20:09:19 +02:00

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;
}
}