Get the user id from the platform, so now we can identify the user in the playlists customData.owner field

This commit is contained in:
pleb
2026-04-18 21:17:08 -07:00
parent abc658a4f9
commit f1a853691c
6 changed files with 207 additions and 63 deletions
+159 -32
View File
@@ -3,6 +3,8 @@ using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using BeatLeader.Utils;
using Newtonsoft.Json;
using UnityEngine;
@@ -24,6 +26,10 @@ namespace Setlist
private const string AuthenticationTypeName = "BeatLeader.API.Authentication";
private const string SignedInFieldName = "_signedIn";
private const float LoginWaitTimeoutSeconds = 90f;
private const float PlatformUserPollStepSeconds = 0.5f;
/// <summary>How long to wait for <see cref="PlatformLeaderboardsModel"/> to appear and populate <c>playerId</c> (plugin runs before menu init).</summary>
private const float PlatformUserWaitTimeoutSeconds = 30f;
private const float PlatformUserGetUserInfoRetrySeconds = 3f;
private const int RequestTimeoutSeconds = 30;
private sealed class UserPlaylistSummary
@@ -78,7 +84,7 @@ namespace Setlist
/// Must be called from the Unity main thread (BSIPA's <c>OnApplicationStart</c> is fine).
/// </summary>
internal static void ScheduleVerifyAndLog(
List<(string Title, bool HasSyncUrl, string BeatLeaderGuid)> entries,
List<(string Title, bool HasSyncUrl, string BeatLeaderGuid, string OwnerId)> entries,
IPALogger log)
{
var go = new GameObject("Setlist.OwnershipRunner");
@@ -93,11 +99,11 @@ namespace Setlist
/// </summary>
private sealed class OwnershipRunner : MonoBehaviour
{
private List<(string Title, bool HasSyncUrl, string BeatLeaderGuid)> _entries;
private List<(string Title, bool HasSyncUrl, string BeatLeaderGuid, string OwnerId)> _entries;
private IPALogger _log;
public void Initialize(
List<(string Title, bool HasSyncUrl, string BeatLeaderGuid)> entries,
List<(string Title, bool HasSyncUrl, string BeatLeaderGuid, string OwnerId)> entries,
IPALogger log)
{
_entries = entries;
@@ -107,43 +113,52 @@ namespace Setlist
private IEnumerator Run()
{
string platformUserId = null;
yield return StartCoroutine(FetchPlatformUserId(id => { platformUserId = id; }));
_log.Info(string.IsNullOrEmpty(platformUserId)
? "platformUserId=(unknown)"
: $"platformUserId={platformUserId}");
HashSet<string> owned = null;
string failure = null;
FieldInfo signedInField;
try
if (_entries.Any(e => e.BeatLeaderGuid != null))
{
signedInField = ResolveSignedInField(_log);
}
catch (Exception ex)
{
signedInField = null;
failure = "reflecting BeatLeader Authentication failed: " + ex.Message;
}
if (signedInField != null)
{
var waitedSeconds = 0f;
while (!IsSignedIn(signedInField))
FieldInfo signedInField;
try
{
if (waitedSeconds >= LoginWaitTimeoutSeconds)
{
failure = $"BeatLeader login did not complete within {LoginWaitTimeoutSeconds:F0}s; "
+ "is the BeatLeader mod actually signing in (check BeatLeader log lines)?";
break;
}
yield return new WaitForSeconds(1f);
waitedSeconds += 1f;
signedInField = ResolveSignedInField(_log);
}
catch (Exception ex)
{
signedInField = null;
failure = "reflecting BeatLeader Authentication failed: " + ex.Message;
}
if (failure == null)
if (signedInField != null)
{
var fetchEnumerator = FetchOwnedGuids(result =>
var waitedSeconds = 0f;
while (!IsSignedIn(signedInField))
{
owned = result.OwnedGuids;
failure = result.Failure;
});
yield return StartCoroutine(fetchEnumerator);
if (waitedSeconds >= LoginWaitTimeoutSeconds)
{
failure = $"BeatLeader login did not complete within {LoginWaitTimeoutSeconds:F0}s; "
+ "is the BeatLeader mod actually signing in (check BeatLeader log lines)?";
break;
}
yield return new WaitForSeconds(1f);
waitedSeconds += 1f;
}
if (failure == null)
{
var fetchEnumerator = FetchOwnedGuids(result =>
{
owned = result.OwnedGuids;
failure = result.Failure;
});
yield return StartCoroutine(fetchEnumerator);
}
}
}
@@ -154,12 +169,124 @@ namespace Setlist
foreach (var e in _entries)
{
_log.Info(Plugin.FormatPlaylistLogLine(e.Title, e.HasSyncUrl, e.BeatLeaderGuid, owned));
_log.Info(Plugin.FormatPlaylistLogLine(
e.Title,
e.HasSyncUrl,
e.BeatLeaderGuid,
e.OwnerId,
platformUserId,
owned));
}
Destroy(gameObject);
}
/// <summary>
/// BSIPA runs us very early; <see cref="PlatformLeaderboardsModel"/> may not exist yet and its
/// async init may not have set <see cref="PlatformLeaderboardsModel.playerId"/>. Poll until ready.
/// </summary>
private static IEnumerator FetchPlatformUserId(Action<string> onDone)
{
var waited = 0f;
var lastGetUserInfoAttempt = -PlatformUserGetUserInfoRetrySeconds;
while (waited < PlatformUserWaitTimeoutSeconds)
{
foreach (var plm in EnumerateLeaderboardModels())
{
var pid = plm.playerId;
if (!string.IsNullOrEmpty(pid))
{
onDone(pid.Trim());
yield break;
}
}
if (waited - lastGetUserInfoAttempt >= PlatformUserGetUserInfoRetrySeconds)
{
lastGetUserInfoAttempt = waited;
var model = ResolvePlatformUserModel();
if (model != null)
{
Task<UserInfo> task;
try
{
task = model.GetUserInfo(CancellationToken.None);
}
catch (Exception)
{
task = null;
}
if (task != null)
{
while (!task.IsCompleted)
{
yield return null;
}
if (!task.IsFaulted)
{
var uid = task.Result?.platformUserId;
if (!string.IsNullOrEmpty(uid))
{
onDone(uid.Trim());
yield break;
}
}
}
}
}
waited += PlatformUserPollStepSeconds;
yield return new WaitForSeconds(PlatformUserPollStepSeconds);
}
onDone(null);
}
/// <summary>
/// <see cref="Resources.FindObjectsOfTypeAll{T}"/> can miss very early; <see cref="Object.FindObjectOfType{T}()"/> sometimes finds the scene instance sooner.
/// </summary>
private static IEnumerable<PlatformLeaderboardsModel> EnumerateLeaderboardModels()
{
var seen = new HashSet<PlatformLeaderboardsModel>();
foreach (var plm in Resources.FindObjectsOfTypeAll<PlatformLeaderboardsModel>())
{
if (plm != null && seen.Add(plm))
{
yield return plm;
}
}
var active = UnityEngine.Object.FindObjectOfType<PlatformLeaderboardsModel>();
if (active != null && seen.Add(active))
{
yield return active;
}
}
private static IPlatformUserModel ResolvePlatformUserModel()
{
var field = typeof(PlatformLeaderboardsModel).GetField(
"_platformUserModel",
BindingFlags.Instance | BindingFlags.NonPublic);
if (field == null)
{
return null;
}
IPlatformUserModel last = null;
foreach (var plm in EnumerateLeaderboardModels())
{
if (field.GetValue(plm) is IPlatformUserModel m)
{
last = m;
}
}
return last;
}
private struct FetchResult
{
public HashSet<string> OwnedGuids;