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:
@@ -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;
|
||||
|
||||
+26
-20
@@ -47,7 +47,7 @@ namespace Setlist
|
||||
return;
|
||||
}
|
||||
|
||||
var entries = new List<(string Title, bool HasSyncUrl, string BeatLeaderGuid)>();
|
||||
var entries = new List<(string Title, bool HasSyncUrl, string BeatLeaderGuid, string OwnerId)>();
|
||||
foreach (var playlist in playlists)
|
||||
{
|
||||
var hasSyncUrl = false;
|
||||
@@ -58,26 +58,22 @@ namespace Setlist
|
||||
hasSyncUrl = !string.IsNullOrWhiteSpace(url);
|
||||
}
|
||||
|
||||
string ownerId = null;
|
||||
if (playlist.TryGetCustomData("owner", out var ownerObj) && ownerObj is string o)
|
||||
{
|
||||
ownerId = string.IsNullOrWhiteSpace(o) ? null : o.Trim();
|
||||
}
|
||||
|
||||
string blGuid = null;
|
||||
if (hasSyncUrl && BeatLeaderPlaylistOwnership.TryExtractBeatLeaderPlaylistGuid(syncUrl, out var g))
|
||||
{
|
||||
blGuid = g;
|
||||
}
|
||||
|
||||
entries.Add((playlist.Title, hasSyncUrl, blGuid));
|
||||
entries.Add((playlist.Title, hasSyncUrl, blGuid, ownerId));
|
||||
}
|
||||
|
||||
if (entries.Any(e => e.BeatLeaderGuid != null))
|
||||
{
|
||||
BeatLeaderPlaylistOwnership.ScheduleVerifyAndLog(entries, Log);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var e in entries)
|
||||
{
|
||||
Log.Info(FormatPlaylistLogLine(e.Title, e.HasSyncUrl, e.BeatLeaderGuid, ownedGuids: null));
|
||||
}
|
||||
}
|
||||
BeatLeaderPlaylistOwnership.ScheduleVerifyAndLog(entries, Log);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -94,31 +90,41 @@ namespace Setlist
|
||||
string title,
|
||||
bool hasSyncUrl,
|
||||
string beatLeaderGuid,
|
||||
string ownerId,
|
||||
string platformUserId,
|
||||
HashSet<string> ownedGuids)
|
||||
{
|
||||
string ownerPart;
|
||||
var ownerToken = string.IsNullOrEmpty(ownerId) ? "owner=n/a" : $"owner={ownerId}";
|
||||
|
||||
string confirmPart;
|
||||
if (!hasSyncUrl)
|
||||
{
|
||||
ownerPart = "beatLeaderOwnerConfirmed=n/a (no sync URL)";
|
||||
confirmPart = "beatLeaderOwnerConfirmed=n/a (no sync URL)";
|
||||
}
|
||||
else if (string.IsNullOrEmpty(beatLeaderGuid))
|
||||
{
|
||||
ownerPart = "beatLeaderOwnerConfirmed=n/a (sync URL is not a BeatLeader playlist)";
|
||||
confirmPart = "beatLeaderOwnerConfirmed=n/a (sync URL is not a BeatLeader playlist)";
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(ownerId)
|
||||
&& !string.IsNullOrEmpty(platformUserId)
|
||||
&& string.Equals(ownerId, platformUserId, StringComparison.Ordinal))
|
||||
{
|
||||
confirmPart = "beatLeaderOwnerConfirmed=true (owner matches platform user id)";
|
||||
}
|
||||
else if (ownedGuids == null)
|
||||
{
|
||||
ownerPart = "beatLeaderOwnerConfirmed=unknown (BeatLeader /user/playlists did not succeed; see prior log line)";
|
||||
confirmPart = "beatLeaderOwnerConfirmed=unknown (BeatLeader /user/playlists did not succeed; see prior log line)";
|
||||
}
|
||||
else if (ownedGuids.Contains(beatLeaderGuid))
|
||||
{
|
||||
ownerPart = "beatLeaderOwnerConfirmed=true";
|
||||
confirmPart = "beatLeaderOwnerConfirmed=true";
|
||||
}
|
||||
else
|
||||
{
|
||||
ownerPart = "beatLeaderOwnerConfirmed=false";
|
||||
confirmPart = "beatLeaderOwnerConfirmed=false";
|
||||
}
|
||||
|
||||
return $"Playlist \"{title}\": hasSyncUrl={hasSyncUrl}, {ownerPart}";
|
||||
return $"Playlist \"{title}\": hasSyncUrl={hasSyncUrl}, {ownerToken}, {confirmPart}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,5 +11,5 @@ using System.Runtime.InteropServices;
|
||||
[assembly: AssemblyCulture("")]
|
||||
[assembly: ComVisible(false)]
|
||||
[assembly: Guid("50F53E6E-21D5-4780-8E67-273877DAA28C")]
|
||||
[assembly: AssemblyVersion("0.0.2.0")]
|
||||
[assembly: AssemblyFileVersion("0.0.2.0")]
|
||||
[assembly: AssemblyVersion("0.0.5.0")]
|
||||
[assembly: AssemblyFileVersion("0.0.5.0")]
|
||||
|
||||
@@ -50,6 +50,14 @@
|
||||
<HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\Main.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="DataModels">
|
||||
<HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\DataModels.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="PlatformUserModel">
|
||||
<HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\PlatformUserModel.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="HMLib">
|
||||
<HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\HMLib.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"id": "Setlist",
|
||||
"name": "Setlist",
|
||||
"author": "",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.5",
|
||||
"description": "Syncs playlists with external sources.",
|
||||
"gameVersion": "1.40.8",
|
||||
"dependsOn": {
|
||||
|
||||
Reference in New Issue
Block a user