using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.IO.Compression; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using PulseLib.Cafe; using PulseLib.Cafe.Models; using PulseLib.Extensions; using RDLevelEditor; using RhythmDoctor.CafeLink.Patches; using RhythmDoctor.CafeLink.Server; using RhythmDoctor.CafeLink.Server.Handlers; using RhythmDoctor.CafeLink.Server.Interfaces; using RhythmDoctor.CafeLink.Server.Responses; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: IgnoresAccessChecksTo("Assembly-CSharp")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("io.github.nonperforming.cafelink")] [assembly: AssemblyConfiguration("Release-BepInEx5")] [assembly: AssemblyDescription("Additional support for rhythm.cafe")] [assembly: AssemblyFileVersion("1.0.4.0")] [assembly: AssemblyInformationalVersion("1.0.4+f0284d4a57a7f447916c333e19424954a18e6870")] [assembly: AssemblyProduct("RhythmDoctor.CafeLink")] [assembly: AssemblyTitle("io.github.nonperforming.cafelink")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.4.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] [CompilerGenerated] internal sealed class <>z__ReadOnlySingleElementList : IEnumerable, ICollection, IList, IEnumerable, IReadOnlyCollection, IReadOnlyList, ICollection, IList { private sealed class Enumerator : IDisposable, IEnumerator, IEnumerator { object IEnumerator.Current => _item; T IEnumerator.Current => _item; public Enumerator(T item) { _item = item; } bool IEnumerator.MoveNext() { if (!_moveNextCalled) { return _moveNextCalled = true; } return false; } void IEnumerator.Reset() { _moveNextCalled = false; } void IDisposable.Dispose() { } } int ICollection.Count => 1; bool ICollection.IsSynchronized => false; object ICollection.SyncRoot => this; object IList.this[int index] { get { if (index != 0) { throw new IndexOutOfRangeException(); } return _item; } set { throw new NotSupportedException(); } } bool IList.IsFixedSize => true; bool IList.IsReadOnly => true; int IReadOnlyCollection.Count => 1; T IReadOnlyList.this[int index] { get { if (index != 0) { throw new IndexOutOfRangeException(); } return _item; } } int ICollection.Count => 1; bool ICollection.IsReadOnly => true; T IList.this[int index] { get { if (index != 0) { throw new IndexOutOfRangeException(); } return _item; } set { throw new NotSupportedException(); } } public <>z__ReadOnlySingleElementList(T item) { _item = item; } IEnumerator IEnumerable.GetEnumerator() { return new Enumerator(_item); } void ICollection.CopyTo(Array array, int index) { array.SetValue(_item, index); } int IList.Add(object value) { throw new NotSupportedException(); } void IList.Clear() { throw new NotSupportedException(); } bool IList.Contains(object value) { return EqualityComparer.Default.Equals(_item, (T)value); } int IList.IndexOf(object value) { if (!EqualityComparer.Default.Equals(_item, (T)value)) { return -1; } return 0; } void IList.Insert(int index, object value) { throw new NotSupportedException(); } void IList.Remove(object value) { throw new NotSupportedException(); } void IList.RemoveAt(int index) { throw new NotSupportedException(); } IEnumerator IEnumerable.GetEnumerator() { return new Enumerator(_item); } void ICollection.Add(T item) { throw new NotSupportedException(); } void ICollection.Clear() { throw new NotSupportedException(); } bool ICollection.Contains(T item) { return EqualityComparer.Default.Equals(_item, item); } void ICollection.CopyTo(T[] array, int arrayIndex) { array[arrayIndex] = _item; } bool ICollection.Remove(T item) { throw new NotSupportedException(); } int IList.IndexOf(T item) { if (!EqualityComparer.Default.Equals(_item, item)) { return -1; } return 0; } void IList.Insert(int index, T item) { throw new NotSupportedException(); } void IList.RemoveAt(int index) { throw new NotSupportedException(); } } namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace RhythmDoctor.CafeLink { [Flags] public enum BusyStatus { None = 0, CleaningUpTransientPlay = 1, Importing = 2, ProcessingTransient = 4 } internal class Configuration { internal ConfigEntry AllowRemoteRequests; internal Configuration(ConfigFile config) { Plugin.Logger.LogDebug((object)"[Configuration] Binding configuration"); AllowRemoteRequests = config.Bind("Security", "AllowRemoteRequests", false, "Allows other devices on the network to import/play songs."); Plugin.Logger.LogDebug((object)"[Configuration] Binding done"); } } [BepInPlugin("io.github.nonperforming.cafelink", "RhythmDoctor.CafeLink", "1.0.4")] [BepInDependency("io.github.nonperforming.pulse", "0.0.6")] [BepInProcess("Rhythm Doctor.exe")] public class Plugin : BaseUnityPlugin { internal static ManualLogSource Logger; internal static Configuration Configuration; private static volatile int _status; private readonly Harmony _harmony = new Harmony("io.github.nonperforming.cafelink"); private RhythmDoctor.CafeLink.Server.Server _server; [CompilerGenerated] private static List k__BackingField; internal static BusyStatus Status => (BusyStatus)_status; private static List LevelsData { get { scnBase instance = scnBase.instance; scnCLS val = (scnCLS)(object)((instance is scnCLS) ? instance : null); if (val != null) { k__BackingField = new List(val.levelsData); } return k__BackingField; } [CompilerGenerated] set { k__BackingField = value; } } private void Awake() { Logger = ((BaseUnityPlugin)this).Logger; Logger.LogInfo((object)"[Plugin] Loading RhythmDoctor.CafeLink version 1.0.4"); Configuration = new Configuration(((BaseUnityPlugin)this).Config); Logger.LogInfo((object)"[Plugin] Cleaning up any prior transient plays"); Task.Run((Action)CleanupTransientPlay); Logger.LogInfo((object)"[Plugin] Creating Server"); _server = new RhythmDoctor.CafeLink.Server.Server(2615); Logger.LogInfo((object)"[Plugin] Applying DirectImportPatch"); _harmony.PatchAll(typeof(DirectImportPatch)); Logger.LogInfo((object)"[Plugin] Applying HandleCafeSchemePatch"); _harmony.PatchAll(typeof(HandleCafeSchemePatch)); Logger.LogInfo((object)"[Plugin] Applying HandleStatusPatch"); _harmony.PatchAll(typeof(HandleStatusPatch)); Logger.LogInfo((object)"[Plugin] RhythmDoctor.CafeLink loaded!"); } private void Start() { Logger.LogInfo((object)"[Plugin] Building LevelsData"); StubbedScnCLS stubbedScnCLS = ((Component)this).gameObject.AddComponent(); ((scnCLS)stubbedScnCLS).levelsData = new List(); scnBase instance = scnBase._instance; scnBase._instance = (scnBase)(object)stubbedScnCLS; DesktopLevelLoader.InitializeCacheLevelsDict(); DesktopLevelLoader.LoadLevelsPreviewFromPath(LevelValidation.CustomLevelsPath); LevelsData = new List(((scnCLS)stubbedScnCLS).levelsData); scnBase._instance = instance; Object.DestroyImmediate((Object)(object)stubbedScnCLS); Logger.LogInfo((object)"[Plugin] Built LevelsData"); } internal static async Task PlayTransient(string uri, bool twoPlayer) { Logger.LogInfo((object)string.Format("[{0}] Playing {1} transiently (2P: {2})...", "Plugin", uri, twoPlayer)); using HttpClient client = new HttpClient(); client.DefaultRequestHeaders.UserAgent.Add(ProductInfoHeaderValue.Parse("RhythmDoctor.CafeLink/1.0.4")); if (uri.StartsWith("cafe://")) { try { string text = uri.Split('/').Last(); Logger.LogInfo((object)("[Plugin] Getting level data of cafe level " + text + "...")); LevelData? val = await Cafe.GetLevelData(text); if (!val.HasValue) { Logger.LogError((object)"[Plugin] Could not get level data (is level ID valid?)"); throw new ArgumentException("Could not get level data"); } LevelData value = val.Value; if (TryFindLevelWithHash(((LevelData)(ref value)).RDMD5, out CustomLevelData customLevelData)) { Logger.LogWarning((object)"[Plugin] Found already imported level, going to that instead"); Task.Run((Action)CleanupTransientPlay); scnBaseExtensions.GoToLevelWithCustomLevelData(customLevelData, twoPlayer); scnBase.currentLevelSelect = "scnCLS"; return; } value = val.Value; uri = ((LevelData)(ref value)).RDZipUrl.AbsoluteUri; } catch (Exception ex) { Logger.LogError((object)ex); throw; } } if (!TrySetStatusFlag(BusyStatus.ProcessingTransient)) { Logger.LogWarning((object)("[Plugin] Ignoring request to play transient " + uri + " because we are processing one already")); return; } Logger.LogWarning((object)"[Plugin] Waiting for prior transient play to be cleaned up..."); Task.Run((Action)CleanupTransientPlay); await Task.Run(async delegate { while (Status.HasFlag(BusyStatus.CleaningUpTransientPlay)) { await Task.Delay(500); } }); Logger.LogWarning((object)"[Plugin] Prior play cleaned up"); DirectoryInfo transientDir = null; try { Stream stream = null; try { Logger.LogInfo((object)"[Plugin] Trying to get stream"); stream = await client.GetStreamAsync(uri); } catch { Logger.LogError((object)"[Plugin] Failed to get stream, abandoning"); if (stream != null) { await stream.DisposeAsync(); } throw; } Logger.LogInfo((object)"[Plugin] Got stream"); string text2 = Path.Combine(LevelValidation.TempLevelsFolder, "cafelink-transient"); transientDir = Directory.CreateDirectory(text2); Logger.LogInfo((object)("[Plugin] Extracting to " + text2)); using (ZipArchive source = new ZipArchive(stream, ZipArchiveMode.Read)) { source.ExtractToDirectory(text2); } Logger.LogInfo((object)("[Plugin] Extracted to " + text2)); Logger.LogDebug((object)"[Plugin] Attempting to get RDLevelData"); RDLevelData val2 = LevelBase.DecodeLevelData(RDFile.ReadAllText(Path.Join(text2.AsSpan(), "main.rdlevel".AsSpan()), (Encoding)null)); if (val2 == null) { Logger.LogError((object)"[Plugin] Could not get RDLevelData"); Task.Run((Action)CleanupTransientPlay); return; } if (TryGoToLevelWithHash(RDUtils.GetHash(new string[3] { val2.settings.author, val2.settings.artist, val2.settings.song }), twoPlayer)) { Logger.LogWarning((object)"[Plugin] Found already imported level, going to that instead"); Task.Run((Action)CleanupTransientPlay); scnBase.currentLevelSelect = "scnCLS"; } string text3 = ((twoPlayer && (int)val2.settings.canBePlayedOn == 3 && !string.IsNullOrEmpty(val2.settings.separate2PLevelFilename)) ? Path.Join(text2.AsSpan(), val2.settings.separate2PLevelFilename.AsSpan()) : Path.Join(text2.AsSpan(), "main.rdlevel".AsSpan())); Logger.LogInfo((object)("[Plugin] Going to " + text3)); scnGame.goToScnMenuAfterHelpingHands = true; scnBase.GoToLevelWithExternalPath(text3); } catch (Exception ex2) { Logger.LogError((object)ex2); transientDir?.Delete(recursive: true); throw; } finally { ClearStatusFlag(BusyStatus.ProcessingTransient); } } internal static void CleanupTransientPlay() { if (!TrySetStatusFlag(BusyStatus.CleaningUpTransientPlay)) { Logger.LogWarning((object)"[Plugin] Transient play is already being cleaned up"); return; } try { DirectoryInfo directoryInfo = new DirectoryInfo(Path.Combine(LevelValidation.TempLevelsFolder, "cafelink-transient")); try { Logger.LogInfo((object)"[Plugin] Cleaning up cafelink-transient folder"); directoryInfo.Delete(recursive: true); Logger.LogInfo((object)"[Plugin] Cleaned up cafelink-transient folder"); } catch (DirectoryNotFoundException) { } } catch (Exception ex2) { Logger.LogError((object)ex2); throw; } finally { ClearStatusFlag(BusyStatus.CleaningUpTransientPlay); } } internal static bool TrySetStatusFlag(BusyStatus flag) { Logger.LogDebug((object)string.Format("[{0}] Setting status flag {1}", "Plugin", flag)); BusyStatus status; BusyStatus value; do { status = (BusyStatus)_status; if ((status & flag) != BusyStatus.None) { return false; } value = status | flag; } while (Interlocked.CompareExchange(ref _status, (int)value, (int)status) != (int)status); return true; } internal static void ClearStatusFlag(BusyStatus flag) { Logger.LogDebug((object)string.Format("[{0}] Clearing status flag {1}", "Plugin", flag)); BusyStatus status; BusyStatus value; do { status = (BusyStatus)_status; value = status & ~flag; } while (Interlocked.CompareExchange(ref _status, (int)value, (int)status) != (int)status); } internal static bool TryFindLevelWithHash(string hash, [NotNullWhen(true)] out CustomLevelData? customLevelData, List? levelsDataOverride = null) { customLevelData = ((levelsDataOverride == null) ? LevelsData.Find((CustomLevelData levelData) => levelData.Hash == hash) : levelsDataOverride.Find((CustomLevelData levelData) => levelData.Hash == hash)); return customLevelData != null; } internal static bool TryGoToLevelWithHash(string hash, bool twoPlayer = false) { if (!TryFindLevelWithHash(hash, out CustomLevelData customLevelData)) { return false; } scnBaseExtensions.GoToLevelWithCustomLevelData(customLevelData, twoPlayer); return true; } } internal class StubbedScnCLS : scnCLS { private void Awake() { } private void Start() { } private void Update() { } private void LateUpdate() { } } public static class MyPluginInfo { public const string PLUGIN_GUID = "io.github.nonperforming.cafelink"; public const string PLUGIN_NAME = "RhythmDoctor.CafeLink"; public const string PLUGIN_VERSION = "1.0.4"; } } namespace RhythmDoctor.CafeLink.Server { internal class Server { internal const uint PROTOCOL_VERSION = 0u; private static readonly string[] ValidHosts = new string[1] { "127.0.0.1" }; private readonly HttpListener _listener; private ushort _port; private readonly Dictionary _handlers = new Dictionary(); internal Server(ushort port) { Plugin.Logger.LogInfo((object)string.Format("[{0}] Creating server for port {1}", "Server", port)); _port = port; if (!HttpListener.IsSupported) { Plugin.Logger.LogError((object)"[Server] HttpListener is not supported"); throw new NotSupportedException("HttpListener is not supported."); } _listener = new HttpListener(); string[] validHosts = ValidHosts; foreach (string text in validHosts) { Plugin.Logger.LogInfo((object)string.Format("[{0}] Binding to http:{1}:{2}/", "Server", text, port)); _listener.Prefixes.Add($"http://{text}:{port}/"); } IHandler[] array = new IHandler[3] { new ImportHandler(), new PlayHandler(), new StatusHandler() }; foreach (IHandler handler in array) { Plugin.Logger.LogInfo((object)("[Server] Adding handler for " + handler.Endpoint)); _handlers.Add(handler.Endpoint, handler); } Plugin.Logger.LogInfo((object)"[Server] Started"); Task.Run((Func)HandleRequest); } private async Task HandleRequest() { Plugin.Logger.LogInfo((object)"[Server] Waiting for request..."); _listener.Start(); HttpListenerContext httpListenerContext = await _listener.GetContextAsync(); HttpListenerRequest request = httpListenerContext.Request; HttpListenerResponse httpResponse = httpListenerContext.Response; httpResponse.KeepAlive = false; httpResponse.ContentEncoding = Encoding.UTF8; Plugin.Logger.LogInfo((object)"[Server] Handling request..."); if (!request.IsLocal && !Plugin.Configuration.AllowRemoteRequests.Value) { Plugin.Logger.LogWarning((object)string.Format("[{0}] Blocked request from remote {1}", "Server", request.RemoteEndPoint)); httpResponse.StatusCode = 403; } else if (request.AcceptTypes == null || (!request.AcceptTypes.Contains("*/*") && !request.AcceptTypes.Contains("application/json"))) { Plugin.Logger.LogWarning((object)("[Server] Unacceptable Accept types: " + string.Join(",", request.AcceptTypes ?? new string[1] { "(null)" }))); httpResponse.StatusCode = 406; } else { string text = null; string[] segments = request.Url.Segments; foreach (string text2 in segments) { if (!(text2 == "/")) { text = text2.Replace("/", ""); break; } } IResponse response; if (string.IsNullOrEmpty(text)) { Plugin.Logger.LogWarning((object)"[Server] No endpoint specified in request."); response = new ErrorResponse(HttpStatusCode.BadRequest, "No endpoint specified."); } else { Plugin.Logger.LogInfo((object)("[Server] Handling '" + text + "' request...")); if (_handlers.TryGetValue(text, out IHandler value)) { IResponse response2; if (!(value.AcceptedMethod.Method != request.HttpMethod)) { response2 = value.HandleRequest(httpListenerContext.Request.QueryString); } else { IResponse response3 = new ErrorResponse(HttpStatusCode.MethodNotAllowed, "Expected " + value.AcceptedMethod.Method + " method."); response2 = response3; } response = response2; } else { Plugin.Logger.LogWarning((object)("[Server] Unknown endpoint '" + text + "'.")); response = new ErrorResponse(HttpStatusCode.NotFound, "Unknown endpoint."); } } IReadOnlyCollection data = response.Data; httpResponse.AddHeader("Access-Control-Allow-Origin", "https://rhythm.cafe"); httpResponse.StatusCode = (int)response.StatusCode; httpResponse.ContentType = response.ContentType; httpResponse.ContentLength64 = data.Count; await httpResponse.OutputStream.WriteAsync(data.ToArray(), 0, data.Count); } httpResponse.Close(); _listener.Stop(); Task.Run((Func)HandleRequest); } } } namespace RhythmDoctor.CafeLink.Server.Responses { internal class ErrorResponse : IResponse { public HttpStatusCode StatusCode { get; } = HttpStatusCode.BadRequest; public IReadOnlyCollection Data { get; } internal ErrorResponse() { Data = (IReadOnlyCollection)(object)"{\"status\":\"error\",\"reason\":\"null\"}"u8.ToArray(); } internal ErrorResponse(HttpStatusCode statusCode, string reason) { StatusCode = statusCode; Data = (IReadOnlyCollection)(object)Encoding.UTF8.GetBytes("{\"status\":\"error\",\"reason\":\"" + reason + "\"}"); } internal ErrorResponse(string reason) { Data = (IReadOnlyCollection)(object)Encoding.UTF8.GetBytes("{\"status\":\"error\",\"reason\":\"" + reason + "\"}"); } } public class ImportResponse : IResponse { private readonly IList _results = new List(); private int _badCount; public HttpStatusCode StatusCode { get { if (_results.Count != _badCount) { return HttpStatusCode.Accepted; } return HttpStatusCode.BadRequest; } } public IReadOnlyCollection Data { get { ReadOnlySpan readOnlySpan = "["u8; List list = new List(readOnlySpan.Length); ReadOnlySpan.Enumerator enumerator = readOnlySpan.GetEnumerator(); while (enumerator.MoveNext()) { byte current = enumerator.Current; list.Add(current); } List list2 = list; foreach (IResponse result in _results) { list2.AddRange(result.Data); list2.AddRange(","u8.ToArray()); } list2[list2.Count - 1] = 93; return list2; } } internal void AddResult(IResponse result) { _results.Add(result); if (result is ErrorResponse) { _badCount++; } } } internal class StatusResponse : IResponse { public HttpStatusCode StatusCode => HttpStatusCode.OK; public required IReadOnlyCollection Data { get; init; } } internal class SuccessResponse : IResponse { public HttpStatusCode StatusCode => HttpStatusCode.OK; public IReadOnlyCollection Data => (IReadOnlyCollection)(object)"{\"status\":\"ok\"}"u8.ToArray(); } } namespace RhythmDoctor.CafeLink.Server.Interfaces { internal interface IHandler { internal string Endpoint { get; } HttpMethod AcceptedMethod { get; } internal IResponse HandleRequest(NameValueCollection parameters); } public interface IResponse { HttpStatusCode StatusCode { get; } string ContentType => "application/json"; IReadOnlyCollection Data { get; } } } namespace RhythmDoctor.CafeLink.Server.Handlers { internal class ImportHandler : IHandler { public string Endpoint => "import"; public HttpMethod AcceptedMethod => HttpMethod.Post; public IResponse HandleRequest(NameValueCollection parameters) { if (!RDStartup.hasInitialized) { return new ErrorResponse(HttpStatusCode.ServiceUnavailable, "Not initialized."); } if (Plugin.Status.HasFlag(BusyStatus.Importing)) { return new ErrorResponse(HttpStatusCode.ServiceUnavailable, "Already importing something."); } if (Plugin.Status.HasFlag(BusyStatus.ProcessingTransient)) { return new ErrorResponse(HttpStatusCode.ServiceUnavailable, "Currently processing prior request to play level transiently."); } string[] array = parameters["uri"].Split(','); ImportResponse importResponse = new ImportResponse(); string[] array2 = array; foreach (string text in array2) { Plugin.Logger.LogDebug((object)("[Server] Checking URI " + text)); if (string.IsNullOrEmpty(text)) { Plugin.Logger.LogError((object)("[Server] URI " + text + " is empty")); importResponse.AddResult(new ErrorResponse(HttpStatusCode.BadRequest, "URI is empty.")); continue; } if (!Uri.TryCreate(text, UriKind.Absolute, out Uri result)) { Plugin.Logger.LogError((object)("[Server] URI " + text + " is bad")); importResponse.AddResult(new ErrorResponse(HttpStatusCode.BadRequest, "Bad URI given.")); continue; } bool flag; switch (result.Scheme) { case "http": case "https": case "cafe": flag = true; break; default: flag = false; break; } if (!flag) { Plugin.Logger.LogError((object)("[Server] URI " + text + "'s scheme is not http[s]/cafe")); importResponse.AddResult(new ErrorResponse(HttpStatusCode.BadRequest, "Bad URI scheme.")); } else { Plugin.Logger.LogDebug((object)("[Server] URI " + text + " is OK")); DirectImportPatch.AddUrisToImport(new <>z__ReadOnlySingleElementList(result)); importResponse.AddResult(new SuccessResponse()); } } if (importResponse.StatusCode == HttpStatusCode.Accepted) { Plugin.TrySetStatusFlag(BusyStatus.Importing); scnBase.GoToCustomLevelSelect(); } return importResponse; } } internal class PlayHandler : IHandler { public string Endpoint => "play"; public HttpMethod AcceptedMethod => HttpMethod.Post; public IResponse HandleRequest(NameValueCollection parameters) { if (!RDStartup.hasInitialized) { return new ErrorResponse(HttpStatusCode.ServiceUnavailable, "Not initialized."); } if (Plugin.Status.HasFlag(BusyStatus.Importing)) { return new ErrorResponse(HttpStatusCode.ServiceUnavailable, "Currently importing something."); } bool transient = parameters.AllKeys.Contains("transient"); string text = parameters["uri"]; bool twoPlayer = parameters.AllKeys.Contains("2p"); if (Plugin.Status.HasFlag(BusyStatus.ProcessingTransient)) { return new ErrorResponse(HttpStatusCode.ServiceUnavailable, "Currently processing prior request to play level transiently."); } if (string.IsNullOrWhiteSpace(text)) { return new ErrorResponse(HttpStatusCode.BadRequest, "No URI given."); } if (!Uri.TryCreate(text, UriKind.Absolute, out Uri result)) { return new ErrorResponse(HttpStatusCode.BadRequest, "Bad URI given."); } bool flag; switch (result.Scheme) { case "http": case "https": case "cafe": flag = true; break; default: flag = false; break; } if (!flag) { return new ErrorResponse(HttpStatusCode.BadRequest, "Bad URI scheme."); } DirectImportPatch.SetUriToPlay(result, transient, twoPlayer); return new SuccessResponse(); } } internal class StatusHandler : IHandler { public string Endpoint => "status"; public HttpMethod AcceptedMethod => HttpMethod.Get; public IResponse HandleRequest(NameValueCollection parameters) { string text = "ok"; string text2 = ""; if (!RDStartup.hasInitialized) { text = "starting"; } else if (Plugin.Status != BusyStatus.None) { text = "busy"; foreach (BusyStatus value in Enum.GetValues(typeof(BusyStatus))) { text2 = text2 + "\"" + Enum.GetName(typeof(BusyStatus), value).ToLowerInvariant() + "\","; } } text2 = text2.TrimEnd(','); StatusResponse statusResponse = new StatusResponse(); statusResponse.Data = (IReadOnlyCollection)(object)Encoding.UTF8.GetBytes(string.Format("{{\"status\":\"{0}\",\"work\":[{1}],\"protocol\":{2},\"version\":\"{3}\"}}", text, text2, 0u, "1.0.4")); return statusResponse; } } } namespace RhythmDoctor.CafeLink.Patches { [HarmonyPatch] internal static class DirectImportPatch { private enum DirectPlayStatus { None, ImportAndPlay } private static List _urisToImport = new List(); private static DirectPlayStatus _directPlayStatus = DirectPlayStatus.None; private static bool _directPlayTwoPlayer = false; [HarmonyPatch(typeof(scnCLS), "LateUpdate")] [HarmonyPostfix] private static void HandleDirectImportAndPlayPatch(scnCLS __instance) { if (_urisToImport.Count != 0) { Plugin.Logger.LogInfo((object)"[DirectImportPatch] Handling direct import and play"); __instance.ChangeToImportOption(); __instance.WardOptionSelectedFeedback(true); __instance.levelImporter.Showing = true; } } [HarmonyPatch(typeof(LevelImporter), "LateUpdate")] [HarmonyPostfix] private static void AutoImportLevelsPatch(LevelImporter __instance) { if (_urisToImport.Count != 0) { __instance.urlInput.text = string.Join('\n', _urisToImport); __instance.ValidateUrl(); __instance.Install_Public(); _urisToImport.Clear(); } } [HarmonyPatch(typeof(LevelImporter), "Install")] [HarmonyPostfix] private static IEnumerator DirectPlayAfterInstallPatch(IEnumerator __result, LevelImporter __instance) { while (__result.MoveNext()) { yield return __result.Current; } if (_directPlayStatus != DirectPlayStatus.ImportAndPlay) { yield break; } _directPlayStatus = DirectPlayStatus.None; Plugin.Logger.LogInfo((object)"[DirectImportPatch] Handling direct play - after install"); if (__instance.errorsIS.levels.Count == 1 && __instance.installedIS.levels.Count == 0) { if (__instance.errorsIS.levels[0].errorToken == RDString.Get("customLevelSelect.import.sameHashError")) { Plugin.Logger.LogWarning((object)"[DirectImportPatch] Going to level that had same hash..."); Plugin.TryGoToLevelWithHash(__instance.errorsIS.levels[0].customLevel.Hash, _directPlayTwoPlayer); } } else if (__instance.errorsIS.levels.Count == 0 && __instance.installedIS.levels.Count == 1) { Plugin.Logger.LogInfo((object)"[DirectImportPatch] Going to just imported level..."); scnBaseExtensions.GoToLevelWithImportLevel(__instance.installedIS.levels[0], _directPlayTwoPlayer); } else { Plugin.Logger.LogError((object)string.Format("[{0}] More than one imported/error: {1}, {2}", "DirectImportPatch", __instance.errorsIS.levels.Count, __instance.installedIS.levels.Count)); } } [HarmonyPatch(typeof(scnBase), "GoToScene")] [HarmonyPostfix] internal static void CleanupAfterTransientPlayPatch(string name) { if (!(name == "scnGame")) { Task.Run((Action)Plugin.CleanupTransientPlay); } } internal static void AddUrisToImport([ParamCollection] IEnumerable uris) { foreach (string uri in uris) { Plugin.Logger.LogInfo((object)("[DirectImportPatch] Adding " + uri + " to import list")); _urisToImport.Add(uri); } } internal static void AddUrisToImport([ParamCollection] IEnumerable uris) { AddUrisToImport(uris.Select((Uri uri) => uri.OriginalString)); } internal static void AddUrisToImportImmediate([ParamCollection] IEnumerable uris) { AddUrisToImport(uris); Plugin.Logger.LogDebug((object)"[DirectImportPatch] Import immediate - going to custom level select"); scnBase.GoToCustomLevelSelect(); } internal static void AddUrisToImportImmediate([ParamCollection] IEnumerable uris) { AddUrisToImportImmediate(uris.Select((Uri uri) => uri.OriginalString)); } internal static void SetUriToPlay(string uri, bool transient = false, bool twoPlayer = false) { Plugin.Logger.LogInfo((object)string.Format("[{0}] Setting to play {1} (t: {2}, 2p: {3})", "DirectImportPatch", uri, transient, twoPlayer)); if (transient) { Task.Run(() => Plugin.PlayTransient(uri, twoPlayer)); return; } if (_urisToImport.Count != 0 && _urisToImport.Count > 1) { Plugin.Logger.LogWarning((object)("[DirectImportPatch] _urisToPlay already exists - overwriting with " + uri)); _urisToImport.Clear(); } AddUrisToImport(new <>z__ReadOnlySingleElementList(uri)); _directPlayStatus = DirectPlayStatus.ImportAndPlay; _directPlayTwoPlayer = twoPlayer; scnBase.GoToCustomLevelSelect(); } internal static void SetUriToPlay(Uri uri, bool transient = false, bool twoPlayer = false) { SetUriToPlay(uri.OriginalString, transient, twoPlayer); } } [HarmonyPatch(typeof(LevelImporter))] internal static class HandleCafeSchemePatch { private static bool _hasPlayedInstallSound; [HarmonyPatch("Install")] [HarmonyPostfix] private static IEnumerator FixupCafeSchemePatch(IEnumerator __result, LevelImporter __instance) { ((RDBase)__instance).cls.CLSPlaySound("sndImportInstallButtonClick", 1f, "CustomLevelSelect", 1f, 0f); _hasPlayedInstallSound = true; __instance.ToggleInsertUrlContainer(false, true, false); __instance.CurrentContentName = (ContentName)2; __instance.stoppedInstallCoroutine = false; __instance.CanToggleClearButton = false; foreach (ImportLevel level in __instance.toInstallIS.levels) { level.canToggleRemoveButton = false; } Plugin.Logger.LogDebug((object)"[HandleCafeSchemePatch] Fixing up cafe schemes"); for (int i = 0; i < __instance.toInstallIS.levels.Count; i++) { ImportLevel importLevel = __instance.toInstallIS.levels[i]; if (!importLevel.isUrl || !importLevel.path.StartsWith("cafe://")) { continue; } string id = importLevel.path.Split('/')[2]; if (!id.All(char.IsLetterOrDigit)) { Plugin.Logger.LogError((object)("[HandleCafeSchemePatch] Invalid ID " + id)); continue; } Plugin.Logger.LogInfo((object)("[HandleCafeSchemePatch] Getting level data for ID " + id)); Task getLevelDataTask = Cafe.GetLevelData(id); yield return (object)new WaitUntil((Func)(() => getLevelDataTask.IsCompleted)); if (getLevelDataTask.IsFaulted || getLevelDataTask.IsCanceled || !getLevelDataTask.Result.HasValue) { Plugin.Logger.LogError((object)string.Format("[{0}] Failed to get level data for ID {1} (faulted: {2} - {3}, cancelled: {4}, value: {5})", "HandleCafeSchemePatch", id, getLevelDataTask.IsFaulted, getLevelDataTask.Exception, getLevelDataTask.IsCanceled, getLevelDataTask.Result.HasValue)); continue; } LevelData value = getLevelDataTask.Result.Value; if (Plugin.TryFindLevelWithHash(((LevelData)(ref value)).RDMD5, out CustomLevelData _)) { Plugin.Logger.LogWarning((object)("[HandleCafeSchemePatch] Found duplicated hash for ID " + id)); CustomLevelData customLevel = importLevel.customLevel; value = getLevelDataTask.Result.Value; customLevel._hash = ((LevelData)(ref value)).RDMD5; __instance.AddLevelToErrorSection(importLevel, RDString.Get("customLevelSelect.import.sameHashError")); __instance.toInstallIS.Remove(importLevel); i--; } else { value = getLevelDataTask.Result.Value; string text = ((LevelData)(ref value)).RDZipUrl.ToString(); Plugin.Logger.LogInfo((object)("[HandleCafeSchemePatch] Resolved RDZip url to " + text + " for ID " + id)); importLevel.path = text; } } Plugin.Logger.LogDebug((object)"[HandleCafeSchemePatch] Running original Install coroutine"); while (__result.MoveNext()) { yield return __result.Current; } } [HarmonyPatch(typeof(scnCLS), "CLSPlaySound")] [HarmonyPrefix] private static void DontDuplicateInstallButtonClickSoundPatch(string sound, ref bool __runOriginal) { if (sound == "sndImportInstallButtonClick" && _hasPlayedInstallSound) { __runOriginal = false; _hasPlayedInstallSound = false; } } } [HarmonyPatch] internal static class HandleStatusPatch { [HarmonyPatch(typeof(LevelImporter), "Install")] [HarmonyPostfix] [HarmonyPriority(700)] private static void SetImportingStatusPatch() { Plugin.TrySetStatusFlag(BusyStatus.Importing); } [HarmonyPatch(typeof(LevelImporter), "Install")] [HarmonyPostfix] [HarmonyPriority(100)] private static void ClearImportingStatusPatch() { Plugin.ClearStatusFlag(BusyStatus.Importing); } } } namespace System.Diagnostics.CodeAnalysis { [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] [ExcludeFromCodeCoverage] internal sealed class ConstantExpectedAttribute : Attribute { public object? Min { get; set; } public object? Max { get; set; } } [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Module | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Interface | AttributeTargets.Delegate, Inherited = false)] [ExcludeFromCodeCoverage] internal sealed class ExperimentalAttribute : Attribute { public string DiagnosticId { get; } public string? UrlFormat { get; set; } public ExperimentalAttribute(string diagnosticId) { DiagnosticId = diagnosticId; } } [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] [ExcludeFromCodeCoverage] internal sealed class MemberNotNullAttribute : Attribute { public string[] Members { get; } public MemberNotNullAttribute(string member) { Members = new string[1] { member }; } public MemberNotNullAttribute(params string[] members) { Members = members; } } [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] [ExcludeFromCodeCoverage] internal sealed class MemberNotNullWhenAttribute : Attribute { public bool ReturnValue { get; } public string[] Members { get; } public MemberNotNullWhenAttribute(bool returnValue, string member) { ReturnValue = returnValue; Members = new string[1] { member }; } public MemberNotNullWhenAttribute(bool returnValue, params string[] members) { ReturnValue = returnValue; Members = members; } } [AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)] [ExcludeFromCodeCoverage] internal sealed class SetsRequiredMembersAttribute : Attribute { } [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] [ExcludeFromCodeCoverage] internal sealed class StringSyntaxAttribute : Attribute { public const string CompositeFormat = "CompositeFormat"; public const string DateOnlyFormat = "DateOnlyFormat"; public const string DateTimeFormat = "DateTimeFormat"; public const string EnumFormat = "EnumFormat"; public const string GuidFormat = "GuidFormat"; public const string Json = "Json"; public const string NumericFormat = "NumericFormat"; public const string Regex = "Regex"; public const string TimeOnlyFormat = "TimeOnlyFormat"; public const string TimeSpanFormat = "TimeSpanFormat"; public const string Uri = "Uri"; public const string Xml = "Xml"; public string Syntax { get; } public object?[] Arguments { get; } public StringSyntaxAttribute(string syntax) { Syntax = syntax; Arguments = new object[0]; } public StringSyntaxAttribute(string syntax, params object?[] arguments) { Syntax = syntax; Arguments = arguments; } } [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] [ExcludeFromCodeCoverage] internal sealed class UnscopedRefAttribute : Attribute { } } namespace System.Runtime.Versioning { [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Module | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Interface | AttributeTargets.Delegate, Inherited = false)] [ExcludeFromCodeCoverage] internal sealed class RequiresPreviewFeaturesAttribute : Attribute { public string? Message { get; } public string? Url { get; set; } public RequiresPreviewFeaturesAttribute() { } public RequiresPreviewFeaturesAttribute(string? message) { Message = message; } } } namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] internal sealed class IgnoresAccessChecksToAttribute : Attribute { public IgnoresAccessChecksToAttribute(string assemblyName) { } } [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] [ExcludeFromCodeCoverage] internal sealed class CallerArgumentExpressionAttribute : Attribute { public string ParameterName { get; } public CallerArgumentExpressionAttribute(string parameterName) { ParameterName = parameterName; } } [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface, Inherited = false)] [ExcludeFromCodeCoverage] internal sealed class CollectionBuilderAttribute : Attribute { public Type BuilderType { get; } public string MethodName { get; } public CollectionBuilderAttribute(Type builderType, string methodName) { BuilderType = builderType; MethodName = methodName; } } [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] [ExcludeFromCodeCoverage] internal sealed class CompilerFeatureRequiredAttribute : Attribute { public const string RefStructs = "RefStructs"; public const string RequiredMembers = "RequiredMembers"; public string FeatureName { get; } public bool IsOptional { get; set; } public CompilerFeatureRequiredAttribute(string featureName) { FeatureName = featureName; } } [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] [ExcludeFromCodeCoverage] internal sealed class InterpolatedStringHandlerArgumentAttribute : Attribute { public string[] Arguments { get; } public InterpolatedStringHandlerArgumentAttribute(string argument) { Arguments = new string[1] { argument }; } public InterpolatedStringHandlerArgumentAttribute(params string[] arguments) { Arguments = arguments; } } [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)] [ExcludeFromCodeCoverage] internal sealed class InterpolatedStringHandlerAttribute : Attribute { } [AttributeUsage(AttributeTargets.Method, Inherited = false)] [ExcludeFromCodeCoverage] internal sealed class ModuleInitializerAttribute : Attribute { } [AttributeUsage(AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] [ExcludeFromCodeCoverage] internal sealed class OverloadResolutionPriorityAttribute : Attribute { public int Priority { get; } public OverloadResolutionPriorityAttribute(int priority) { Priority = priority; } } [AttributeUsage(AttributeTargets.Parameter, Inherited = true, AllowMultiple = false)] [ExcludeFromCodeCoverage] internal sealed class ParamCollectionAttribute : Attribute { } [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = false)] [ExcludeFromCodeCoverage] internal sealed class RequiredMemberAttribute : Attribute { } [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] [EditorBrowsable(EditorBrowsableState.Never)] [ExcludeFromCodeCoverage] internal sealed class RequiresLocationAttribute : Attribute { } [AttributeUsage(AttributeTargets.Module | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Event | AttributeTargets.Interface, Inherited = false)] [ExcludeFromCodeCoverage] internal sealed class SkipLocalsInitAttribute : Attribute { } }