243 lines
9.0 KiB
C#
243 lines
9.0 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using BeatSaberPlaylistsLib.Types;
|
|
using UnityEngine;
|
|
using IPALogger = IPA.Logging.Logger;
|
|
|
|
namespace Setlist
|
|
{
|
|
/// <summary>
|
|
/// Parses BeatLeader-style sync URLs for logging. Ownership is determined in
|
|
/// <see cref="Plugin.FormatPlaylistLogLine"/> from playlist JSON only (no network).
|
|
/// </summary>
|
|
internal static class BeatLeaderPlaylistOwnership
|
|
{
|
|
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;
|
|
|
|
internal static bool TryExtractBeatLeaderPlaylistGuid(string syncUrl, out string guid)
|
|
{
|
|
guid = string.Empty;
|
|
if (string.IsNullOrWhiteSpace(syncUrl))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!Uri.TryCreate(syncUrl.Trim(), UriKind.Absolute, out var uri))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!string.Equals(uri.Host, "api.beatleader.com", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var segments = uri.AbsolutePath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
|
|
if (segments.Length != 3)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!string.Equals(segments[0], "playlist", StringComparison.OrdinalIgnoreCase)
|
|
|| !string.Equals(segments[1], "guid", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var id = segments[2];
|
|
if (id.Length != 32 || !id.All(Uri.IsHexDigit))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
guid = id;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Spawns a hidden coroutine runner that resolves the platform user id, then logs per-playlist
|
|
/// ownership from playlist JSON. Must be called from the Unity main thread (BSIPA <c>OnApplicationStart</c> is fine).
|
|
/// </summary>
|
|
internal static void ScheduleVerifyAndLog(
|
|
List<(IPlaylist Playlist, string Title, bool HasSyncUrl, string BeatLeaderGuid, string OwnerId)> entries,
|
|
IPALogger log)
|
|
{
|
|
var go = new GameObject("Setlist.OwnershipRunner");
|
|
UnityEngine.Object.DontDestroyOnLoad(go);
|
|
go.hideFlags = HideFlags.HideAndDontSave;
|
|
var runner = go.AddComponent<OwnershipRunner>();
|
|
runner.Initialize(entries, log);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Component that drives the logging coroutine; self-destroys when finished.
|
|
/// </summary>
|
|
private sealed class OwnershipRunner : MonoBehaviour
|
|
{
|
|
private List<(IPlaylist Playlist, string Title, bool HasSyncUrl, string BeatLeaderGuid, string OwnerId)> _entries;
|
|
private IPALogger _log;
|
|
|
|
public void Initialize(
|
|
List<(IPlaylist Playlist, string Title, bool HasSyncUrl, string BeatLeaderGuid, string OwnerId)> entries,
|
|
IPALogger log)
|
|
{
|
|
_entries = entries;
|
|
_log = log;
|
|
StartCoroutine(Run());
|
|
}
|
|
|
|
private IEnumerator Run()
|
|
{
|
|
string platformUserId = null;
|
|
yield return StartCoroutine(FetchPlatformUserId(id => { platformUserId = id; }));
|
|
_log.Info(string.IsNullOrEmpty(platformUserId)
|
|
? "platformUserId=(unknown)"
|
|
: $"platformUserId={platformUserId}");
|
|
|
|
foreach (var e in _entries)
|
|
{
|
|
_log.Info(Plugin.FormatPlaylistLogLine(
|
|
e.Title,
|
|
e.HasSyncUrl,
|
|
e.BeatLeaderGuid,
|
|
e.OwnerId,
|
|
platformUserId));
|
|
}
|
|
|
|
foreach (var e in _entries)
|
|
{
|
|
if (!string.Equals(e.Title, BeatLeaderPlaylistUpdateTest.TargetPlaylistTitle, StringComparison.Ordinal))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
yield return BeatLeaderPlaylistUpdateTest.RunIfTargetOwned(
|
|
e.Playlist,
|
|
e.HasSyncUrl,
|
|
e.BeatLeaderGuid,
|
|
e.OwnerId,
|
|
platformUserId,
|
|
_log);
|
|
break;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|