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
{
///
/// Parses BeatLeader-style sync URLs for logging. Ownership is determined in
/// from playlist JSON only (no network).
///
internal static class BeatLeaderPlaylistOwnership
{
private const float PlatformUserPollStepSeconds = 0.5f;
/// How long to wait for to appear and populate playerId (plugin runs before menu init).
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;
}
///
/// 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 OnApplicationStart is fine).
///
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();
runner.Initialize(entries, log);
}
///
/// Component that drives the logging coroutine; self-destroys when finished.
///
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);
}
///
/// BSIPA runs us very early; may not exist yet and its
/// async init may not have set . Poll until ready.
///
private static IEnumerator FetchPlatformUserId(Action 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 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);
}
///
/// can miss very early; sometimes finds the scene instance sooner.
///
private static IEnumerable EnumerateLeaderboardModels()
{
var seen = new HashSet();
foreach (var plm in Resources.FindObjectsOfTypeAll())
{
if (plm != null && seen.Add(plm))
{
yield return plm;
}
}
var active = UnityEngine.Object.FindObjectOfType();
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;
}
}
}
}