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<T> : IEnumerable, ICollection, IList, IEnumerable<T>, IReadOnlyCollection<T>, IReadOnlyList<T>, ICollection<T>, IList<T>
{
private sealed class Enumerator : IDisposable, IEnumerator, IEnumerator<T>
{
object IEnumerator.Current => _item;
T IEnumerator<T>.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<T>.Count => 1;
T IReadOnlyList<T>.this[int index]
{
get
{
if (index != 0)
{
throw new IndexOutOfRangeException();
}
return _item;
}
}
int ICollection<T>.Count => 1;
bool ICollection<T>.IsReadOnly => true;
T IList<T>.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<T>.Default.Equals(_item, (T)value);
}
int IList.IndexOf(object value)
{
if (!EqualityComparer<T>.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<T> IEnumerable<T>.GetEnumerator()
{
return new Enumerator(_item);
}
void ICollection<T>.Add(T item)
{
throw new NotSupportedException();
}
void ICollection<T>.Clear()
{
throw new NotSupportedException();
}
bool ICollection<T>.Contains(T item)
{
return EqualityComparer<T>.Default.Equals(_item, item);
}
void ICollection<T>.CopyTo(T[] array, int arrayIndex)
{
array[arrayIndex] = _item;
}
bool ICollection<T>.Remove(T item)
{
throw new NotSupportedException();
}
int IList<T>.IndexOf(T item)
{
if (!EqualityComparer<T>.Default.Equals(_item, item))
{
return -1;
}
return 0;
}
void IList<T>.Insert(int index, T item)
{
throw new NotSupportedException();
}
void IList<T>.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<bool> AllowRemoteRequests;
internal Configuration(ConfigFile config)
{
Plugin.Logger.LogDebug((object)"[Configuration] Binding configuration");
AllowRemoteRequests = config.Bind<bool>("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<CustomLevelData> <LevelsData>k__BackingField;
internal static BusyStatus Status => (BusyStatus)_status;
private static List<CustomLevelData> LevelsData
{
get
{
scnBase instance = scnBase.instance;
scnCLS val = (scnCLS)(object)((instance is scnCLS) ? instance : null);
if (val != null)
{
<LevelsData>k__BackingField = new List<CustomLevelData>(val.levelsData);
}
return <LevelsData>k__BackingField;
}
[CompilerGenerated]
set
{
<LevelsData>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<StubbedScnCLS>();
((scnCLS)stubbedScnCLS).levelsData = new List<CustomLevelData>();
scnBase instance = scnBase._instance;
scnBase._instance = (scnBase)(object)stubbedScnCLS;
DesktopLevelLoader.InitializeCacheLevelsDict();
DesktopLevelLoader.LoadLevelsPreviewFromPath(LevelValidation.CustomLevelsPath);
LevelsData = new List<CustomLevelData>(((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<CustomLevelData>? 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<string, IHandler> _handlers = new Dictionary<string, IHandler>();
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<Task?>)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<byte> 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<Task?>)HandleRequest);
}
}
}
namespace RhythmDoctor.CafeLink.Server.Responses
{
internal class ErrorResponse : IResponse
{
public HttpStatusCode StatusCode { get; } = HttpStatusCode.BadRequest;
public IReadOnlyCollection<byte> Data { get; }
internal ErrorResponse()
{
Data = (IReadOnlyCollection<byte>)(object)"{\"status\":\"error\",\"reason\":\"null\"}"u8.ToArray();
}
internal ErrorResponse(HttpStatusCode statusCode, string reason)
{
StatusCode = statusCode;
Data = (IReadOnlyCollection<byte>)(object)Encoding.UTF8.GetBytes("{\"status\":\"error\",\"reason\":\"" + reason + "\"}");
}
internal ErrorResponse(string reason)
{
Data = (IReadOnlyCollection<byte>)(object)Encoding.UTF8.GetBytes("{\"status\":\"error\",\"reason\":\"" + reason + "\"}");
}
}
public class ImportResponse : IResponse
{
private readonly IList<IResponse> _results = new List<IResponse>();
private int _badCount;
public HttpStatusCode StatusCode
{
get
{
if (_results.Count != _badCount)
{
return HttpStatusCode.Accepted;
}
return HttpStatusCode.BadRequest;
}
}
public IReadOnlyCollection<byte> Data
{
get
{
ReadOnlySpan<byte> readOnlySpan = "["u8;
List<byte> list = new List<byte>(readOnlySpan.Length);
ReadOnlySpan<byte>.Enumerator enumerator = readOnlySpan.GetEnumerator();
while (enumerator.MoveNext())
{
byte current = enumerator.Current;
list.Add(current);
}
List<byte> 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<byte> Data { get; init; }
}
internal class SuccessResponse : IResponse
{
public HttpStatusCode StatusCode => HttpStatusCode.OK;
public IReadOnlyCollection<byte> Data => (IReadOnlyCollection<byte>)(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<byte> 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<Uri>(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<string>("transient");
string text = parameters["uri"];
bool twoPlayer = parameters.AllKeys.Contains<string>("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<byte>)(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<string> _urisToImport = new List<string>();
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<string> 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<Uri> uris)
{
AddUrisToImport(uris.Select((Uri uri) => uri.OriginalString));
}
internal static void AddUrisToImportImmediate([ParamCollection] IEnumerable<string> uris)
{
AddUrisToImport(uris);
Plugin.Logger.LogDebug((object)"[DirectImportPatch] Import immediate - going to custom level select");
scnBase.GoToCustomLevelSelect();
}
internal static void AddUrisToImportImmediate([ParamCollection] IEnumerable<Uri> 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<string>(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<LevelData?> getLevelDataTask = Cafe.GetLevelData(id);
yield return (object)new WaitUntil((Func<bool>)(() => 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
{
}
}