Decompiled source of CustomSong v1.1.2
BepInEx/plugins/CustomSong/CustomSong.dll
Decompiled a day ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Compression; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Nodes; using BepInEx; using BepInEx.Logging; using BepInEx.Unity.IL2CPP; using CustomSong.Auto; using HarmonyLib; using Il2CppInterop.Common; using Il2CppInterop.Runtime; using Il2CppInterop.Runtime.InteropTypes; using Il2CppInterop.Runtime.InteropTypes.Arrays; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.1.2.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [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 CustomSong { internal static class BackgroundCatalog { internal struct Entry { internal IntPtr Material; internal string Video; internal bool Said { get { if (!(Material != IntPtr.Zero)) { return Video != null; } return true; } } } private static readonly Dictionary<string, Entry[]> ByBase = new Dictionary<string, Entry[]>(StringComparer.Ordinal); internal static int Count => ByBase.Count; internal static void Reset() { ByBase.Clear(); } internal static bool Add(ChartInfo info, out string reason) { reason = null; if (info.BackgroundPaths == null) { return true; } Entry[] array = new Entry[4]; bool result = true; for (int i = 0; i < array.Length; i++) { string text = info.BackgroundPaths[i]; if (text == null) { continue; } if (IsVideo(text)) { array[i].Video = text; continue; } string reason2; IntPtr intPtr = JacketFactory.Build(text, "background", out reason2); if (intPtr == IntPtr.Zero) { result = false; } else { array[i].Material = intPtr; } } ByBase[info.BaseName] = array; return result; } internal static bool Find(IntPtr songInfo, int difficulty, out Entry entry, out string why) { entry = default(Entry); why = null; string text = JacketCatalog.NameOf(songInfo); if (text == null) { return false; } if (!ByBase.TryGetValue(text, out var value)) { return false; } if (difficulty >= 0 && difficulty < value.Length) { entry = value[difficulty]; } else { for (int i = 0; i < value.Length; i++) { if (i == 0) { entry = value[0]; } else if (value[i].Material != entry.Material || value[i].Video != entry.Video) { return false; } } if (!entry.Said) { return false; } } if (!entry.Said) { return false; } if (entry.Video == null) { return true; } IntPtr intPtr = VideoBackground.Material(entry.Video, out why); if (intPtr == IntPtr.Zero) { return false; } entry.Material = intPtr; return true; } private static bool IsVideo(string path) { return string.Equals(Path.GetExtension(path), ".mp4", StringComparison.OrdinalIgnoreCase); } } internal static class BackgroundClone { internal static IntPtr Build(IntPtr gameMaterial, IntPtr ours, out string reason) { reason = null; if (VideoBackground.Owns(ours)) { return VideoBackground.Rebase(gameMaterial, out reason); } IntPtr intPtr = JacketFactory.MainTexture(ours, out reason); if (intPtr == IntPtr.Zero) { return IntPtr.Zero; } IntPtr intPtr2 = JacketFactory.CloneFrom(gameMaterial, out reason); if (intPtr2 == IntPtr.Zero) { return IntPtr.Zero; } if (!JacketFactory.SetMainTexture(intPtr2, intPtr, out reason)) { return IntPtr.Zero; } return intPtr2; } } internal static class VideoBackground { private const int TextureWidth = 1920; private const int TextureHeight = 1080; private const string CoreAssembly = "UnityEngine.CoreModule.dll"; private const string VideoAssembly = "UnityEngine.VideoModule.dll"; private const string CoreNamespace = "UnityEngine"; private const string VideoNamespace = "UnityEngine.Video"; private static IntPtr _object; private static IntPtr _player; private static IntPtr _texture; private static IntPtr _material; private static string _playing; private static bool _armed; private static bool _running; private static bool _held; private static bool _ended; private static double _heldClock = double.NaN; private static bool _rewound; private static IntPtr _gameSceneClass; private static int _engineField = -1; private static int _clockField = -1; private static bool _clockMissing; private static IntPtr _objectHandle; private static IntPtr _playerHandle; private static IntPtr _textureHandle; private static IntPtr _materialHandle; private static bool _unavailable; private const double ClockSlack = 0.3; private const double Chase = 6.0; private const long PlaceCooldownMs = 250L; private const string PlaceFailed = "the background video could not be placed on the chart clock; it will run out of step with the chart"; private static bool _placeWarned; private static double _lastClock = double.NaN; private static long _lastPlace = long.MinValue; private static double _length = -1.0; private const string StartFailed = "the background video could not be started with the song; the background will not move with it"; internal static long Starts; internal static long Holds; internal static long Resumes; private static IntPtr LastResult; internal static string State { get { if (_unavailable) { return "fail"; } if (_player == IntPtr.Zero) { return "none"; } if (_ended) { return "end"; } if (Flag("get_isPlaying")) { return "play"; } if (Flag("get_isPrepared")) { return "ready"; } return "load"; } } internal static IntPtr Material(string path, out string reason) { reason = null; if (_unavailable) { return IntPtr.Zero; } if (_playing == path && _material != IntPtr.Zero) { string reason2 = null; Park(out reason2); if (!Show(_texture, out reason)) { return IntPtr.Zero; } _armed = false; _running = false; _held = false; _ended = false; return _material; } try { if (!Ensure(out reason)) { _unavailable = true; return IntPtr.Zero; } if (_playing != null) { Stop(); } if (!Set("set_source", 1, 1, out reason)) { return IntPtr.Zero; } if (!Set("set_url", 1, FileUrl(path), out reason)) { return IntPtr.Zero; } if (!Set("set_renderMode", 1, 2, out reason)) { return IntPtr.Zero; } if (!Set("set_targetTexture", 1, _texture, out reason)) { return IntPtr.Zero; } if (!Set("set_isLooping", 1, value: false, out reason)) { return IntPtr.Zero; } if (!Set("set_playOnAwake", 1, value: false, out reason)) { return IntPtr.Zero; } if (!Set("set_audioOutputMode", 1, 0, out reason)) { return IntPtr.Zero; } if (!Call("Prepare", 0, _player, out reason)) { return IntPtr.Zero; } if (!Show(_texture, out reason)) { return IntPtr.Zero; } _playing = path; _length = -1.0; _armed = false; _running = false; _held = false; _ended = false; return _material; } catch (Exception) { return IntPtr.Zero; } } internal static bool Owns(IntPtr material) { if (material != IntPtr.Zero) { return material == _material; } return false; } internal static IntPtr Rebase(IntPtr gameMaterial, out string reason) { reason = null; if (_material == IntPtr.Zero) { reason = "no clip material is loaded"; return IntPtr.Zero; } IntPtr intPtr = JacketFactory.CloneFrom(gameMaterial, out reason); if (intPtr == IntPtr.Zero) { return IntPtr.Zero; } if (!Show(intPtr, _texture, out reason)) { return IntPtr.Zero; } _material = intPtr; return intPtr; } internal static void Restart() { if (!(_player == IntPtr.Zero) && _playing != null) { string reason = null; Park(out reason); _armed = true; _running = false; _held = false; _ended = false; } } private static void Park(out string reason) { if (Flag("get_isPrepared")) { Set("set_time", 1, 0.0, out reason); } Call("Pause", 0, _player, out reason); } internal static void Follow() { if (_player == IntPtr.Zero || _playing == null) { return; } double num = ChartClock(); if (double.IsNaN(num)) { if (_held) { _heldClock = double.NaN; } return; } bool moved = num != _lastClock; _lastClock = num; if (!_running && !_held && num < 0.0 && Flag("get_isPlaying")) { string reason = null; Park(out reason); } else { if (!_armed) { return; } if (_held) { if (double.IsNaN(_heldClock)) { return; } if (!_rewound) { if (num < _heldClock - 0.3) { _rewound = true; } return; } double num2 = Time(); if (!(num2 < 0.0) && !(num < num2)) { Resume(); } } else { if (_ended) { return; } if (!_running) { if (num >= 0.0) { Start(num); } } else if (!FilmOver()) { Place(num, moved); } } } } private static bool FilmOver() { if (_ended) { return true; } if (_running && !Flag("get_isPlaying")) { double num = Length(); double num2 = ChartClock(); if (num > 0.0 && !double.IsNaN(num2) && num2 >= num) { _ended = true; _running = false; return true; } } return false; } private unsafe static double Length() { if (_length >= 0.0) { return _length; } IntPtr intPtr = Entry(_player, "get_length", 0); if (intPtr == IntPtr.Zero) { return 0.0; } IntPtr zero = IntPtr.Zero; IntPtr intPtr2 = IL2CPP.il2cpp_runtime_invoke(intPtr, _player, (void**)null, ref zero); if (zero != IntPtr.Zero || intPtr2 == IntPtr.Zero) { return 0.0; } return _length = *(double*)(void*)(intPtr2 + 16); } private static void Place(double clock, bool moved = true) { double num = Time(); if (num < 0.0) { return; } double num2 = num - clock; if ((num2 <= 0.3 && num2 >= -0.3) || num2 > 6.0 || num2 < -6.0 || !moved) { return; } long tickCount = Environment.TickCount64; if (tickCount - _lastPlace < 250) { return; } string reason = null; if (!Set("set_time", 1, clock, out reason)) { _lastPlace = tickCount; if (!_placeWarned) { _placeWarned = true; ModLog.WarnRelease("the background video could not be placed on the chart clock; it will run out of step with the chart"); } } else { _lastPlace = tickCount; } } private static bool Start(double clock) { string reason = null; Park(out reason); if (!Show(_texture, out reason) || !Call("Play", 0, _player, out reason)) { ModLog.WarnRelease("the background video could not be started with the song; the background will not move with it"); _armed = false; return false; } _running = true; Starts++; return true; } private static double ChartClock() { if (_clockMissing) { return double.NaN; } if (_gameSceneClass == IntPtr.Zero) { _gameSceneClass = FieldResolver.ClassPointer("GameScene"); if (_gameSceneClass == IntPtr.Zero) { return double.NaN; } } if (_engineField == -1) { _engineField = FieldResolver.Field("GameScene", "_DN"); _clockField = FieldResolver.Field("_VD", "_WEA"); if (_engineField < 0 || _clockField < 0) { _clockMissing = true; ModLog.Error("the chart's clock could not be read by name in this build; the in-play background video will not play"); return double.NaN; } } IntPtr intPtr = FieldResolver.Statics(_gameSceneClass); if (intPtr == IntPtr.Zero) { return double.NaN; } IntPtr intPtr2 = Memory.Ptr(intPtr + _engineField); if (!Memory.LooksLikeObject(intPtr2)) { return double.NaN; } return Memory.F64(intPtr2 + _clockField); } internal static void Hold(double at) { if (!(_player == IntPtr.Zero) && _running && !_held) { string reason = null; if (Call("Pause", 0, _player, out reason)) { _held = true; _heldClock = ChartClock(); _rewound = false; Holds++; } } } internal static void Resume(bool atMusicReturn = false) { if (!(_player == IntPtr.Zero) && _running && _held && !_ended && (!atMusicReturn || double.IsNaN(_heldClock))) { _held = false; double num = ChartClock(); if (!double.IsNaN(num)) { Place(num); } string reason = null; if (Call("Play", 0, _player, out reason)) { Resumes++; } } } private unsafe static bool Flag(string name) { IntPtr intPtr = Entry(_player, name, 0); if (intPtr == IntPtr.Zero) { return false; } IntPtr zero = IntPtr.Zero; IntPtr intPtr2 = IL2CPP.il2cpp_runtime_invoke(intPtr, _player, (void**)null, ref zero); if (zero == IntPtr.Zero && intPtr2 != IntPtr.Zero) { return *(byte*)(void*)(intPtr2 + 16) != 0; } return false; } private unsafe static double Time() { IntPtr intPtr = Entry(_player, "get_time", 0); if (intPtr == IntPtr.Zero) { return -1.0; } IntPtr zero = IntPtr.Zero; IntPtr intPtr2 = IL2CPP.il2cpp_runtime_invoke(intPtr, _player, (void**)null, ref zero); if (zero != IntPtr.Zero || intPtr2 == IntPtr.Zero) { return -1.0; } return *(double*)(void*)(intPtr2 + 16); } internal static void Stop() { if (!(_player == IntPtr.Zero)) { string reason = null; Call("Stop", 0, _player, out reason); _playing = null; _armed = false; _running = false; _held = false; _ended = false; } } private unsafe static bool Ensure(out string reason) { reason = null; if (_player != IntPtr.Zero) { return true; } IntPtr intPtr = Require("UnityEngine.CoreModule.dll", "UnityEngine", "GameObject", out reason); if (intPtr == IntPtr.Zero) { return false; } IntPtr intPtr2 = Require("UnityEngine.VideoModule.dll", "UnityEngine.Video", "VideoPlayer", out reason); if (intPtr2 == IntPtr.Zero) { return false; } IntPtr intPtr3 = Require("UnityEngine.CoreModule.dll", "UnityEngine", "RenderTexture", out reason); if (intPtr3 == IntPtr.Zero) { return false; } IntPtr intPtr4 = Require("UnityEngine.CoreModule.dll", "UnityEngine", "Object", out reason); if (intPtr4 == IntPtr.Zero) { return false; } IntPtr intPtr5 = IL2CPP.il2cpp_object_new(intPtr); if (intPtr5 == IntPtr.Zero) { return false; } IntPtr* ptr = stackalloc IntPtr[1]; IntPtr intPtr6 = IL2CPP.ManagedStringToIl2Cpp("CustomSong background video"); *ptr = intPtr6; if (!Invoke(Entry(intPtr5, ".ctor", 1), intPtr5, ptr, 1, out reason)) { return false; } *ptr = intPtr5; if (!Invoke(EntryOn(intPtr4, "DontDestroyOnLoad", 1), IntPtr.Zero, ptr, 1, out reason)) { return false; } IntPtr intPtr7 = IL2CPP.il2cpp_class_get_type(intPtr2); if (intPtr7 == IntPtr.Zero) { return false; } *ptr = IL2CPP.il2cpp_type_get_object(intPtr7); if (!Invoke(Entry(intPtr5, "AddComponent", 1), intPtr5, ptr, 1, out reason)) { return false; } IntPtr lastResult = LastResult; if (!Memory.LooksLikeObject(lastResult)) { return false; } if (!IsVideoPlayer(IL2CPP.il2cpp_object_get_class(lastResult), intPtr2)) { return false; } IntPtr intPtr8 = IL2CPP.il2cpp_object_new(intPtr3); if (intPtr8 == IntPtr.Zero) { return false; } int num = 1920; int num2 = 1080; int num3 = 0; IntPtr* ptr2 = stackalloc IntPtr[3]; *ptr2 = (IntPtr)(&num); ptr2[1] = (IntPtr)(&num2); ptr2[2] = (IntPtr)(&num3); if (!Invoke(Entry(intPtr8, ".ctor", 3), intPtr8, ptr2, 3, out reason)) { return false; } if (!Call("Create", 0, intPtr8, out reason)) { return false; } if (WidthOf(intPtr8) != 1920) { return false; } IntPtr intPtr9 = JacketFactory.BuildFromTexture(intPtr8, "background", out reason); if (intPtr9 == IntPtr.Zero) { return false; } _object = intPtr5; _player = lastResult; _texture = intPtr8; _material = intPtr9; Root(ref _objectHandle, intPtr5); Root(ref _playerHandle, lastResult); Root(ref _textureHandle, intPtr8); Root(ref _materialHandle, intPtr9); return true; } private unsafe static int WidthOf(IntPtr texture) { IntPtr intPtr = Entry(texture, "get_width", 0); if (intPtr == IntPtr.Zero) { return -1; } IntPtr zero = IntPtr.Zero; IntPtr intPtr2 = IL2CPP.il2cpp_runtime_invoke(intPtr, texture, (void**)null, ref zero); if (zero != IntPtr.Zero || intPtr2 == IntPtr.Zero) { return -1; } return *(int*)(void*)(intPtr2 + 16); } private static bool IsVideoPlayer(IntPtr klass, IntPtr wanted) { IntPtr intPtr = klass; while (intPtr != IntPtr.Zero) { if (intPtr == wanted) { return true; } intPtr = IL2CPP.il2cpp_class_get_parent(intPtr); } return false; } private static void Root(ref IntPtr handle, IntPtr obj) { if (!(handle != IntPtr.Zero) && !(obj == IntPtr.Zero)) { handle = IL2CPP.il2cpp_gchandle_new(obj, false); } } private unsafe static bool Call(string name, int parameters, IntPtr instance, out string reason) { reason = null; IntPtr intPtr = Entry(instance, name, parameters); if (intPtr == IntPtr.Zero) { return false; } IntPtr zero = IntPtr.Zero; LastResult = IL2CPP.il2cpp_runtime_invoke(intPtr, instance, (void**)null, ref zero); if (zero == IntPtr.Zero) { return true; } return false; } private unsafe static bool Set(string name, int parameters, int value, out string reason) { int num = value; return SetArgument(name, (IntPtr)(&num), out reason); } private unsafe static bool Set(string name, int parameters, double value, out string reason) { double num = value; return SetArgument(name, (IntPtr)(&num), out reason); } private unsafe static bool Set(string name, int parameters, bool value, out string reason) { byte b = (value ? ((byte)1) : ((byte)0)); return SetArgument(name, (IntPtr)(&b), out reason); } private static bool Set(string name, int parameters, IntPtr reference, out string reason) { return SetArgument(name, reference, out reason); } private static bool Set(string name, int parameters, string value, out string reason) { return SetArgument(name, IL2CPP.ManagedStringToIl2Cpp(value), out reason); } private unsafe static bool SetArgument(string name, IntPtr argument, out string reason) { reason = null; if (_player == IntPtr.Zero) { return false; } IntPtr intPtr = Entry(_player, name, 1); if (intPtr == IntPtr.Zero) { return false; } IntPtr* ptr = stackalloc IntPtr[1]; *ptr = argument; IntPtr zero = IntPtr.Zero; IL2CPP.il2cpp_runtime_invoke(intPtr, _player, (void**)ptr, ref zero); if (zero == IntPtr.Zero) { return true; } return false; } private unsafe static bool Show(IntPtr material, IntPtr texture, out string reason) { reason = null; if (material == IntPtr.Zero) { return false; } IntPtr intPtr = Entry(material, "set_mainTexture", 1); if (intPtr == IntPtr.Zero) { reason = "Material.set_mainTexture could not be found"; return false; } IntPtr* ptr = stackalloc IntPtr[1]; *ptr = texture; IntPtr zero = IntPtr.Zero; IL2CPP.il2cpp_runtime_invoke(intPtr, material, (void**)ptr, ref zero); if (zero == IntPtr.Zero) { return true; } reason = "Material.set_mainTexture raised: " + Raised.Text(zero); return false; } private static bool Show(IntPtr texture, out string reason) { return Show(_material, texture, out reason); } private static string FileUrl(string path) { return "file:///" + path.Replace('\\', '/'); } private static IntPtr Entry(IntPtr instance, string name, int parameters) { if (!Memory.LooksLikeObject(instance)) { return IntPtr.Zero; } return IL2CPP.il2cpp_class_get_method_from_name(IL2CPP.il2cpp_object_get_class(instance), name, parameters); } private static IntPtr EntryOn(IntPtr klass, string name, int parameters) { if (klass == IntPtr.Zero) { return IntPtr.Zero; } return IL2CPP.il2cpp_class_get_method_from_name(klass, name, parameters); } private static IntPtr Require(string assembly, string namespaze, string typeName, out string reason) { reason = null; IntPtr il2CppClass = IL2CPP.GetIl2CppClass(assembly, namespaze, typeName); if (il2CppClass == IntPtr.Zero) { return IntPtr.Zero; } IL2CPP.il2cpp_runtime_class_init(il2CppClass); return il2CppClass; } private unsafe static bool Invoke(IntPtr method, IntPtr instance, IntPtr* args, int count, out string reason) { reason = null; if (method == IntPtr.Zero) { return false; } for (int i = 0; i < count; i++) { if (!(args[i] != IntPtr.Zero)) { return false; } } IntPtr zero = IntPtr.Zero; LastResult = IL2CPP.il2cpp_runtime_invoke(method, instance, (void**)args, ref zero); if (zero == IntPtr.Zero) { return true; } return false; } } internal static class AudioCatalog { private sealed class Entry { internal int Index; internal string Path; internal int Length; private AudioCodec.Prepared _audio; internal byte[] Bytes { get { if (_audio != null) { return _audio.Bytes; } _audio = AudioCodec.Load(Path, out var reason); if (_audio != null) { return _audio.Bytes; } ModLog.Error("audio '" + Path + "' could not be prepared: " + reason); return Array.Empty<byte>(); } } } private const string Extension = ".wav"; private static readonly Dictionary<string, Entry> ByBase = new Dictionary<string, Entry>(StringComparer.Ordinal); private static readonly Dictionary<string, Entry> ByDifficulty = new Dictionary<string, Entry>(StringComparer.Ordinal); private static readonly Dictionary<int, Entry> ByIndex = new Dictionary<int, Entry>(); internal static long Served; internal static long ServedBytes; internal static long Resolved; internal static void Reset() { ByBase.Clear(); ByDifficulty.Clear(); ByIndex.Clear(); } internal static int IndexOf(string baseName) { if (!ByBase.TryGetValue(baseName, out var value)) { return -1; } return value.Index; } internal static int BytesOf(string baseName) { if (!ByBase.TryGetValue(baseName, out var value)) { return -1; } return value.Length; } internal static bool Add(string baseName, string path, int difficulty, out string reason) { reason = null; int num = AudioCodec.Measure(path, out reason); if (num <= 0) { return false; } int num2 = StreamingAssets.Add(num); if (num2 < 0) { reason = "the game's asset manager would not take another file"; return false; } Entry value = new Entry { Index = num2, Path = path, Length = num }; if (difficulty < 0) { ByBase[baseName] = value; } else { ByDifficulty[baseName + "\n" + difficulty] = value; } ByIndex[num2] = value; return true; } internal static bool TryResolve(IntPtr namePtr, IntPtr indexPtr) { if (!CouldBeAudio(namePtr)) { return false; } string text = Memory.Text(namePtr, 128); if (text == null) { return false; } if (!text.EndsWith(".wav", StringComparison.Ordinal)) { return false; } string text2 = text.Substring(0, text.Length - ".wav".Length); if (text2.Length == 0) { return false; } int appliedDifficulty = CurrentSong.AppliedDifficulty; if ((appliedDifficulty < 0 || !ByDifficulty.TryGetValue(text2 + "\n" + appliedDifficulty, out var value)) && !ByBase.TryGetValue(text2, out value)) { return false; } Memory.WriteI32(indexPtr, value.Index); Resolved++; return true; } private unsafe static bool CouldBeAudio(IntPtr s) { if (!Memory.LooksLikeObject(s)) { return false; } try { int num = IL2CPP.il2cpp_string_length(s); if (num < ".wav".Length + 1 || num > 128) { return false; } char* ptr = IL2CPP.il2cpp_string_chars(s); if (ptr == null) { return false; } return ptr[num - 1] == 'v'; } catch (Exception) { return false; } } internal unsafe static bool TryServe(IntPtr record) { if (Hooks.CompleteRead == (delegate* unmanaged[Cdecl]<IntPtr, uint, void>)null) { return false; } int key = (int)(Memory.I64(record + 8) & 0xFFFFFF); if (!ByIndex.TryGetValue(key, out var value)) { return false; } IntPtr intPtr = Memory.Ptr(record + 0); if (intPtr == IntPtr.Zero) { return false; } int num = Memory.I32(intPtr + 8); int num2 = Memory.I32(intPtr + 12); IntPtr intPtr2 = Memory.Ptr(intPtr + 32); if (intPtr2 == IntPtr.Zero || num2 <= 0) { return false; } byte[] bytes = value.Bytes; int val = ((num >= 0 && num < bytes.Length) ? (bytes.Length - num) : 0); int num3 = Math.Min(num2, val); if (num3 > 0) { Marshal.Copy(bytes, num, intPtr2, num3); } Served++; ServedBytes += num3; Memory.WriteI32(intPtr + 40, num3); ReadBookkeeping.Finished(record); byte* ptr = stackalloc byte[40]; Buffer.MemoryCopy((void*)record, ptr, 40L, 40L); Hooks.CompleteRead((IntPtr)ptr, 0u); return true; } } internal static class ChartCatalog { private const string Extension = ".spc"; private static readonly Dictionary<string, ChartInfo> ByBase = new Dictionary<string, ChartInfo>(StringComparer.Ordinal); private static int _sourceTextField = int.MinValue; internal static void Adopt(ChartInfo info) { ByBase[info.BaseName] = info; } internal static void Reset() { ByBase.Clear(); } internal unsafe static bool TryServe(IntPtr namePtr, IntPtr result) { string text = Memory.Text(namePtr, 128); if (text == null) { return false; } if (!Split(text, out var baseName, out var difficulty)) { return false; } if (!ByBase.TryGetValue(baseName, out var value)) { return false; } string path = value.ChartPaths[difficulty]; string seed = value.ChartNames[difficulty]; bool flag = false; try { flag = ChartCodec.TryDecode(File.ReadAllBytes(path), seed, result, out var reason); if (!flag) { ModLog.Error($"'{value.Name}' difficulty {difficulty}: {reason}"); } } catch (Exception e) { ModLog.Error($"'{value.Name}' difficulty {difficulty} could not be read: " + ModLog.Describe(e)); } if (!flag && !ChartCodec.Empty(result)) { return false; } int num = SourceTextField(); if (num >= 0) { *(IntPtr*)(void*)(result + num) = IntPtr.Zero; } return true; } private static int SourceTextField() { if (_sourceTextField == int.MinValue) { _sourceTextField = FieldResolver.Field("_R", "_RC"); } return _sourceTextField; } private static bool Split(string name, out string baseName, out int difficulty) { baseName = null; difficulty = 0; if (!name.EndsWith(".spc", StringComparison.Ordinal)) { return false; } int num = name.Length - ".spc".Length; if (num < 2) { return false; } char c = name[num - 1]; if (c < '0' || c > '3') { return false; } difficulty = c - 48; baseName = name.Substring(0, num - 1); return true; } } internal static class ChartFolderLibrary { private const string FolderName = "Charts"; private const string DefaultPack = "Custom Song"; private static string _located; internal static string ModFolder() { try { string location = Assembly.GetExecutingAssembly().Location; if (!string.IsNullOrEmpty(location)) { return Path.GetDirectoryName(location); } } catch (Exception) { } return null; } private static string GameFolder() { try { string gameRootPath = Paths.GameRootPath; if (!string.IsNullOrEmpty(gameRootPath)) { return gameRootPath; } } catch (Exception) { } try { string baseDirectory = AppDomain.CurrentDomain.BaseDirectory; if (!string.IsNullOrEmpty(baseDirectory)) { return baseDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); } } catch (Exception) { } return null; } internal static string Locate() { if (_located != null) { return _located; } string text = ModFolder(); if (text == null) { return null; } DirectoryInfo parent = Directory.GetParent(text); string text2 = GameFolder(); string[] array = new string[4] { (text2 == null) ? null : Path.GetFullPath(Path.Combine(text2, "Charts")), (parent?.Parent == null) ? null : Path.GetFullPath(Path.Combine(parent.Parent.FullName, "Charts")), (parent == null) ? null : Path.GetFullPath(Path.Combine(parent.FullName, "Charts")), Path.GetFullPath(Path.Combine(text, "Charts")) }; string[] array2 = array; foreach (string text3 in array2) { if (text3 != null && Directory.Exists(text3)) { _located = text3; ModLog.Load("Charts folder: " + text3); return text3; } } string text4 = array[0] ?? array[1]; if (text4 == null) { return null; } try { Directory.CreateDirectory(text4); Directory.CreateDirectory(Path.Combine(text4, "Custom Song")); _located = text4; ModLog.Load("Charts folder: " + text4 + " (created)"); return text4; } catch (Exception) { return text4; } } internal static List<ChartPack> ImportPacks(string root, Func<ChartInfo, string> validateCharts) { List<ChartPack> list = new List<ChartPack>(); if (string.IsNullOrEmpty(root) || !Directory.Exists(root)) { ModLog.WarnRelease($"no {"Charts"} folder at {root}; nothing to load"); return list; } string[] directories; try { directories = Directory.GetDirectories(root); Array.Sort(directories, (IComparer<string>?)StringComparer.OrdinalIgnoreCase); } catch (Exception e) { ModLog.WarnRelease(root + " could not be listed: " + ModLog.Describe(e)); return list; } if (directories.Length == 0) { ModLog.WarnRelease("'" + root + "' has no pack folder inside it"); } string[] files; try { files = Directory.GetFiles(root, "*.falsum"); foreach (string path in files) { ModLog.WarnRelease("'" + Path.GetFileName(path) + "' is directly under Charts; put it inside a pack folder instead"); } } catch (Exception) { } files = directories; foreach (string text in files) { string fileName = Path.GetFileName(text); if (File.Exists(Path.Combine(text, "if"))) { continue; } ChartPack chartPack = ReadPack(text); string[] directories2; string[] files2; try { files2 = Directory.GetFiles(text, "*.falsum"); for (int j = 0; j < files2.Length; j++) { FalsumSong.Import(files2[j]); } directories2 = Directory.GetDirectories(text); Array.Sort(directories2, (IComparer<string>?)StringComparer.OrdinalIgnoreCase); } catch (Exception) { continue; } files2 = directories2; foreach (string text2 in files2) { if (!File.Exists(Path.Combine(text2, "if"))) { StudioProject.EnsureIf(text2); } if (!File.Exists(Path.Combine(text2, "if"))) { string text3 = Nested(text2); ModLog.WarnRelease((text3 == null) ? ("chart folder '" + Path.GetFileName(text2) + "' has no if; skipped") : ($"chart folder '{Path.GetFileName(text2)}' has no if, but its '{text3}' " + "does; the song is one folder too deep")); } else { AddSong(chartPack, text2, validateCharts); } } if (chartPack.Charts.Count > 0) { list.Add(chartPack); } else { ModLog.WarnRelease("pack folder '" + fileName + "' has no chart folder; skipped"); } } int num = 0; foreach (ChartPack item in list) { num += item.Charts.Count; } ModLog.Load($"packs: {list.Count}, charts: {num} imported"); return list; } private static ChartPack ReadPack(string folder) { string fileName = Path.GetFileName(folder); ChartPack chartPack = new ChartPack { Folder = folder, Name = fileName, Slug = fileName }; string path = Path.Combine(folder, "pack.json"); if (File.Exists(path)) { try { using JsonDocument jsonDocument = JsonDocument.Parse(File.ReadAllText(path), new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true }); JsonElement rootElement = jsonDocument.RootElement; if (rootElement.ValueKind == JsonValueKind.Object) { if (rootElement.TryGetProperty("name", out var value)) { if (value.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(value.GetString())) { chartPack.Name = value.GetString().Trim(); } else if (value.ValueKind == JsonValueKind.Array || value.ValueKind == JsonValueKind.Object) { string[] array = Titles(value); if (array != null) { chartPack.Names = array; chartPack.Name = array[0]; } } } if (rootElement.TryGetProperty("visuals", out var value2) && value2.ValueKind == JsonValueKind.Object) { foreach (JsonProperty item in value2.EnumerateObject()) { if (Array.IndexOf(ChartPack.PictureKeys, item.Name) >= 0 && (item.Value.ValueKind == JsonValueKind.String || item.Value.ValueKind == JsonValueKind.Array || item.Value.ValueKind == JsonValueKind.Object)) { string[] array2 = Pictures(folder, fileName, item.Name, item.Value); if (array2 != null) { chartPack.Visuals[item.Name] = array2; } } } } } } catch (Exception) { } } return chartPack; } private static string[] Pictures(string folder, string packName, string key, JsonElement value) { string[] array = new string[5]; if (value.ValueKind == JsonValueKind.String) { string text = LocalPicture(folder, value.GetString()); if (text == null) { return null; } for (int i = 0; i < array.Length; i++) { array[i] = text; } return array; } if (value.ValueKind == JsonValueKind.Object) { return NamedPictures(folder, packName, key, value); } if (value.GetArrayLength() != 5) { return null; } bool flag = false; for (int j = 0; j < array.Length; j++) { JsonElement jsonElement = value[j]; if (jsonElement.ValueKind != JsonValueKind.Null) { if (jsonElement.ValueKind != JsonValueKind.String) { return null; } array[j] = LocalPicture(folder, jsonElement.GetString()); if (array[j] != null) { flag = true; } } } if (!flag) { return null; } return array; } private static string[] NamedPictures(string folder, string packName, string key, JsonElement value) { string[] array = new string[5]; bool flag = false; foreach (JsonProperty item in value.EnumerateObject()) { int num = ChartPack.LanguageIndex(item.Name); if (num >= 0 && item.Value.ValueKind == JsonValueKind.String) { array[num] = LocalPicture(folder, item.Value.GetString()); if (array[num] != null) { flag = true; } } } if (!flag) { return null; } return array; } private static string[] Titles(JsonElement value) { string[] array = new string[5]; if (value.ValueKind == JsonValueKind.Array) { if (value.GetArrayLength() != 5) { return null; } for (int i = 0; i < array.Length; i++) { JsonElement jsonElement = value[i]; if (jsonElement.ValueKind != JsonValueKind.String || string.IsNullOrWhiteSpace(jsonElement.GetString())) { return null; } array[i] = jsonElement.GetString(); } return array; } foreach (JsonProperty item in value.EnumerateObject()) { int num = ChartPack.LanguageIndex(item.Name); if (num >= 0) { if (item.Value.ValueKind != JsonValueKind.String || string.IsNullOrWhiteSpace(item.Value.GetString())) { return null; } array[num] = item.Value.GetString(); } } string text = null; string[] array2 = array; foreach (string text2 in array2) { if (text2 != null) { text = text2; break; } } if (text == null) { return null; } for (int k = 0; k < array.Length; k++) { if (array[k] == null) { array[k] = text; } } return array; } private static string LocalPicture(string folder, string name) { if (string.IsNullOrWhiteSpace(name)) { return null; } if (Path.GetFileName(name) != name) { return null; } string text = Path.GetExtension(name).ToLowerInvariant(); if (text != ".png" && text != ".jpg" && text != ".jpeg") { return null; } string text2 = Path.Combine(folder, name); if (!File.Exists(text2)) { return null; } return text2; } private static string Nested(string folder) { try { string[] directories = Directory.GetDirectories(folder); foreach (string text in directories) { if (File.Exists(Path.Combine(text, "if"))) { return Path.GetFileName(text); } } } catch (Exception) { } return null; } private static void AddSong(ChartPack pack, string folder, Func<ChartInfo, string> validateCharts) { string fileName = Path.GetFileName(folder); if (!IfReader.TryRead(folder, out var info, out var reason)) { ModLog.WarnRelease("chart folder '" + fileName + "' skipped: " + (reason ?? IfReader.WhyNot(folder))); return; } reason = validateCharts(info); if (reason != null) { ModLog.WarnRelease("chart folder '" + fileName + "' skipped: " + reason); } else { pack.Charts.Add(info); } } private static string Levels(ChartInfo info) { string[] array = new string[4]; for (int i = 0; i < array.Length; i++) { array[i] = info.Level[i]?.ToString() ?? "-"; } return string.Join("/", array); } } internal sealed class ChartInfo { internal const int Difficulties = 4; internal string Folder; internal string Name; internal string[] Titles; internal string[][] TitleSets; internal string Composer; internal string[] ComposerLanguages; internal string[][] ComposerSets; internal string[] IllustratorLanguages; internal string[][] IllustratorSets; internal string SongPath; internal string[] Songs; internal string[] JacketPaths; internal string[] BackgroundPaths; internal float PreviewStart; internal float PreviewEnd; internal float[][] Previews; internal string[] ChartPaths; internal string[] ChartNames; internal bool[] NoChart; internal string[] Charters; internal int?[] Level; internal string FolderName; internal string BaseName; } internal sealed class ChartPack { internal string Folder; internal string Name; internal string Slug; internal string[] Names; internal readonly Dictionary<string, string[]> Visuals = new Dictionary<string, string[]>(StringComparer.Ordinal); internal const int Languages = 5; internal const int FallbackLanguage = 2; internal readonly List<ChartInfo> Charts = new List<ChartInfo>(); internal int PackId = -1; internal static readonly string[] PictureKeys = new string[7] { "songSelect", "songSelectHover", "selected", "selectedHover", "deselected", "deselectedHover", "outline" }; internal string Visual(string key, int language) { if (!Visuals.TryGetValue(key, out var value)) { return null; } if (language >= 0 && language < value.Length && value[language] != null) { return value[language]; } if (value[2] != null) { return value[2]; } string[] array = value; foreach (string text in array) { if (text != null) { return text; } } return null; } internal static int LanguageIndex(string key) { if (key == null) { return -1; } return key.Trim().ToLowerInvariant() switch { "sc" => 0, "tc" => 1, "en" => 2, "ja" => 3, "ko" => 4, _ => -1, }; } } internal static class FalsumSong { internal const string Extension = ".falsum"; internal const string ProjectFileName = "song.json"; internal static void Import(string archive) { string text = Path.Combine(Path.GetDirectoryName(archive) ?? string.Empty, Path.GetFileNameWithoutExtension(archive)); string path = Path.Combine(text, "if"); if (File.Exists(path)) { return; } try { if (!File.Exists(Path.Combine(text, "song.json"))) { ZipFile.ExtractToDirectory(archive, text, overwriteFiles: true); ModLog.Load($"'{Path.GetFileName(archive)}': unpacked into '{Path.GetFileName(text)}'"); } string reason; JsonNode jsonNode = Build(text, out reason); if (jsonNode == null) { ModLog.WarnRelease($"'{Path.GetFileName(text)}': its {"song.json"} could not be used: {reason}"); } else { File.WriteAllText(path, StudioProject.Serialize(jsonNode), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); ModLog.Load("'" + Path.GetFileName(text) + "': wrote an if from its song.json"); } } catch (Exception e) { ModLog.WarnRelease("'" + Path.GetFileName(archive) + "' could not be unpacked: " + ModLog.Describe(e)); } } private static JsonNode Build(string folder, out string reason) { reason = null; using JsonDocument jsonDocument = JsonDocument.Parse((ReadOnlyMemory<byte>)File.ReadAllBytes(Path.Combine(folder, "song.json")), default(JsonDocumentOptions)); JsonElement rootElement = jsonDocument.RootElement; if (rootElement.ValueKind != JsonValueKind.Object || Text(rootElement, "format") != "falsum") { reason = "it is not a falsum song"; return null; } string text = Named(rootElement, "audio"); if (string.IsNullOrEmpty(text) || Path.GetFileName(text) != text || !File.Exists(Path.Combine(folder, text))) { reason = "the audio it names is not in the archive"; return null; } JsonArray jsonArray = new JsonArray(); JsonArray jsonArray2 = new JsonArray(); JsonArray jsonArray3 = new JsonArray(); int num = 0; if (rootElement.TryGetProperty("difficulties", out var value) && value.ValueKind == JsonValueKind.Array) { int num2 = 0; foreach (JsonElement item in value.EnumerateArray()) { if (num2 >= 4) { break; } JsonElement value2; string text2 = ((item.ValueKind == JsonValueKind.Object && item.TryGetProperty("available", out value2) && value2.ValueKind == JsonValueKind.True) ? ChartAt(folder, Text(item, "chart")) : null); if (text2 == null) { jsonArray.Add(null); jsonArray2.Add(null); jsonArray3.Add(null); } else { string text3 = Text(item, "chartDesigner"); jsonArray.Add(text2); jsonArray2.Add(string.IsNullOrWhiteSpace(text3) ? "Unknown" : text3); JsonElement value4; int value5; int value3 = ((!item.TryGetProperty("rating", out value4) || !value4.TryGetInt32(out value5)) ? 1 : value5); jsonArray3.Add(value3); num++; } num2++; } } while (jsonArray.Count < 4) { jsonArray.Add(null); jsonArray2.Add(null); jsonArray3.Add(null); } if (num == 0) { reason = "no difficulty is marked available with a chart file in the archive"; return null; } StudioProject.DefaultPreview(AudioCodec.Measure(Path.Combine(folder, text), out var _), out var start, out var end); JsonObject jsonObject = new JsonObject(); jsonObject["id"] = (string.IsNullOrWhiteSpace(Text(rootElement, "songId")) ? Path.GetFileName(folder) : Text(rootElement, "songId")); jsonObject["name"] = (string.IsNullOrWhiteSpace(Text(rootElement, "title")) ? Path.GetFileName(folder) : Text(rootElement, "title")); jsonObject["composer"] = (string.IsNullOrWhiteSpace(Text(rootElement, "artist")) ? "Unknown" : Text(rootElement, "artist")); jsonObject["song"] = text; jsonObject["preview_seconds"] = new JsonArray(Rounded(start), Rounded(end)); jsonObject["charter"] = jsonArray2; jsonObject["chart"] = jsonArray; jsonObject["lv"] = jsonArray3; JsonObject jsonObject2 = jsonObject; string text4 = Picture(folder, Named(rootElement, "cover"), IfReader.ImageExtensions); if (text4 != null) { jsonObject2["jacket"] = text4; } string text5 = Picture(folder, Text(rootElement, "background"), IfReader.BackgroundExtensions); if (text5 != null) { jsonObject2["background"] = text5; } string text6 = Text(rootElement, "jacketDesigner"); if (!string.IsNullOrWhiteSpace(text6)) { jsonObject2["illust"] = text6; } return jsonObject2; } private static string ChartAt(string folder, string relative) { if (string.IsNullOrEmpty(relative)) { return null; } string fullPath = Path.GetFullPath(folder); string fullPath2 = Path.GetFullPath(Path.Combine(fullPath, relative.Replace('\\', '/').Replace('/', Path.DirectorySeparatorChar))); if (!fullPath2.StartsWith(fullPath + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) || !File.Exists(fullPath2)) { return null; } string fileName = Path.GetFileName(fullPath2); if (fileName.Length == 0) { return null; } string text = Path.Combine(fullPath, fileName); if (!string.Equals(fullPath2, text, StringComparison.OrdinalIgnoreCase) && !File.Exists(text)) { File.Move(fullPath2, text); ModLog.Load($"'{Path.GetFileName(folder)}': moved '{fileName}' out of {Path.GetDirectoryName(relative)}"); } return fileName; } private static string Picture(string folder, string name, string[] allowed) { if (string.IsNullOrEmpty(name) || Path.GetFileName(name) != name) { return null; } string extension = Path.GetExtension(name); bool flag = false; foreach (string b in allowed) { if (string.Equals(extension, b, StringComparison.OrdinalIgnoreCase)) { flag = true; } } if (!flag) { return null; } if (!File.Exists(Path.Combine(folder, name))) { return null; } return name; } private static string Named(JsonElement item, string key) { if (item.ValueKind != JsonValueKind.Object || !item.TryGetProperty(key, out var value)) { return null; } if (value.ValueKind == JsonValueKind.String) { return value.GetString(); } if (value.ValueKind != JsonValueKind.Object) { return null; } return Text(value, "file"); } private static string Text(JsonElement item, string key) { if (item.ValueKind != JsonValueKind.Object || !item.TryGetProperty(key, out var value)) { return null; } if (value.ValueKind != JsonValueKind.String) { return null; } return value.GetString(); } private static JsonNode Rounded(float seconds) { return JsonValue.Create(Math.Round(seconds, 3)); } } internal static class Icp1Writer { private struct State { private ulong _a; private ulong _b; private ulong _c; private ulong _d; private uint _count; internal static State Seed(string key, int noteCount) { State result = new State { _a = (ulong)(-4265267296055464877L + noteCount), _b = (((ulong)(uint)noteCount << 32) ^ 0x9E3779B185EBCA87uL), _c = 15111065706836454660uL, _d = 10723151780598845930uL, _count = 0u }; for (int i = 0; i < key.Length; i++) { ulong num = ((ulong)(uint)i << 32) | key[i]; result.Finalize(num, num + (ulong)noteCount, 1 ^ num); } result.Finalize((uint)noteCount, 1uL, (ulong)key.Length); return result; } internal ulong Field(int index, int field) { ulong num = ((ulong)(uint)index << 32) | (uint)field; return (field & 3) switch { 0 => _d ^ (_a + Rol(_c ^ num, 11)), 1 => _a ^ (_b - Rol(num + _d, 17)), 2 => _b ^ (_c + Rol(_a ^ num, 29)), _ => _d ^ Rol(num + _b + _c, 37), }; } internal void Advance(int[] fields, int[] plains, int index) { Finalize((ulong)fields[0], (ulong)fields[1], ((ulong)(uint)fields[3] << 32) | (uint)fields[2]); Finalize((ulong)fields[4], (ulong)fields[5], Pack(plains[1], plains[0])); Finalize(Pack(plains[3], plains[2]), Pack(plains[5], plains[4]), Pack(plains[7], plains[6])); Record(index, fields[0], Gcd(plains[0], plains[1]), Gcd(plains[2], plains[3]), Gcd(plains[4], plains[5]), Gcd(plains[6], plains[7])); } private static ulong Pack(int high, int low) { return (uint)low | ((ulong)(uint)high << 32); } private void Finalize(ulong first, ulong second, ulong third) { switch (_count & 3) { case 0u: { _a = _a + Rol(_d, 7) + first; ulong num4 = Rol(_a + second, 13); _b ^= num4; third ^= _b; _c -= third; _d = Rol(_c + _d, 27) ^ _a; break; } case 1u: { _b = _b + Rol(_a, 9) + third; ulong num3 = Rol(_b + first, 19); _c ^= num3; second ^= _c; _d -= second; _a = Rol(_d + _a, 31) ^ _b; break; } case 2u: { _c = _c + Rol(_b, 15) + second; ulong num2 = Rol(_c + third, 23); _d ^= num2; first ^= _d; _a -= first; _b = Rol(_b + _a, 39) ^ _c; break; } default: { ulong c = _c; _d = _d + Rol(_c, 21) + first; ulong num = Rol(_d + second, 29); _a ^= num; num = _a ^ third; _b -= num; _c = Rol(c + _b, 43) ^ _d; break; } } _count++; } private void Record(int index, int field0, int g0, int g1, int g2, int g3) { ulong num = ((uint)index | ((ulong)(uint)index << 32)) + ((_a + Rol(_b, 9)) ^ (_d + Rol(_c, 23))); ulong num2 = (num ^ (num >> 32)) & 0xFFFFFFFFu; ulong value = (ulong)(g3 - 1) ^ (num2 >> 24); ulong num3 = Rol((ulong)(g2 - 1) ^ ((num2 >> 16) & 0xFF), 29) ^ Rol(value, 47); ulong num4 = (ulong)(g0 - 1) ^ (num2 & 0xFF) ^ Rol((ulong)(g1 - 1) ^ ((num2 >> 8) & 0xFF), 13) ^ num3; long num5 = (uint)((field0 ^ index ^ (int)num4 ^ (int)(num4 >> 32) ^ (int)((ulong)field0 >> 32)) & -1); ulong num6 = (ulong)(num5 | (num5 << 32)); _a ^= num6; _b += Rol(num6, 17); _c -= Rol(num6, 31); _d ^= Rol(num6, 47); } private static ulong Rol(ulong value, int bits) { return (value << bits) | (value >> 64 - bits); } } private const int HeaderSize = 28; private const int NoteSize = 80; private const int EventSize = 32; private const ulong M64 = ulong.MaxValue; private const uint M32 = uint.MaxValue; internal static byte[] Build(TextChart chart, string key) { int count = chart.Notes.Count; int count2 = chart.Events.Count; byte[] array = new byte[28 + count * 80 + count2 * 32]; array[0] = 73; array[1] = 67; array[2] = 80; array[3] = 49; Put16(array, 4, 1); Put16(array, 6, 28); Put16(array, 8, 80); Put16(array, 10, 32); Put32(array, 12, (uint)count); Put32(array, 16, (uint)count2); PutF32(array, 20, chart.Bpm); PutF32(array, 24, chart.Meter); State state = State.Seed(key, count); for (int i = 0; i < count; i++) { int num = 28 + i * 80; TextChart.Note note = chart.Notes[i]; int[] plains = note.Plains; int[] array2 = new int[10] { i, note.Line, note.Start, note.End, (note.Type << 8) | note.Track, Field5(in note), Gcd(plains[0], plains[1]), Gcd(plains[2], plains[3]), Gcd(plains[4], plains[5]), Gcd(plains[6], plains[7]) }; Put64(array, num, Masked(in state, i, 0, (ulong)array2[0], 8)); Put64(array, num + 8, Masked(in state, i, 1, (ulong)array2[1], 8)); Put32(array, num + 16, (uint)Masked(in state, i, 2, (ulong)array2[2], 4)); Put32(array, num + 20, (uint)Masked(in state, i, 3, (ulong)array2[3], 4)); Put32(array, num + 24, (uint)Masked(in state, i, 4, (ulong)array2[4], 4)); Put32(array, num + 28, (uint)Masked(in state, i, 5, (ulong)array2[5], 4)); Put32(array, num + 32, (uint)Masked(in state, i, 6, (ulong)array2[6], 4)); Put32(array, num + 36, (uint)Masked(in state, i, 7, (ulong)array2[7], 4)); Put32(array, num + 40, (uint)Masked(in state, i, 8, (ulong)array2[8], 4)); Put32(array, num + 44, (uint)Masked(in state, i, 9, (ulong)array2[9], 4)); for (int j = 0; j < 8; j++) { Put32(array, num + 48 + j * 4, (uint)plains[j]); } state.Advance(array2, plains, i); } int num2 = 28 + count * 80; for (int k = 0; k < count2; k++) { int num3 = num2 + k * 32; TextChart.Event obj = chart.Events[k]; ulong value = (ulong)((obj.Type == 3) ? ((uint)obj.Flags) : BitConverter.DoubleToInt64Bits(obj.Number)); ulong value2 = (ulong)(obj.HasSpeed ? BitConverter.SingleToInt32Bits(obj.Speed) : 0); Put64(array, num3, (ulong)k); Put32(array, num3 + 8, (uint)obj.TimeMs); Put32(array, num3 + 12, (uint)obj.Type); Put64(array, num3 + 16, value); Put64(array, num3 + 24, value2); } return array; } private static int Field5(in TextChart.Note note) { if (note.Flags != 0) { return note.Flags; } if (note.Type == 4) { return note.Flick << 8; } return note.LeftEase | note.RightEase; } private static ulong Masked(in State state, int index, int field, ulong value, int size) { ulong num = state.Field(index, field); if (size == 4) { num &= 0xFFFFFFFFu; } return value + num; } private static int Gcd(int a, int b) { long num = Math.Abs((long)a); long num2 = Math.Abs((long)b); while (num2 != 0L) { long num3 = num % num2; num = num2; num2 = num3; } return (int)((num == 0L) ? 1 : num); } private static void Put16(byte[] file, int at, int value) { file[at] = (byte)value; file[at + 1] = (byte)(value >> 8); } private static void Put32(byte[] file, int at, uint value) { for (int i = 0; i < 4; i++) { file[at + i] = (byte)(value >> 8 * i); } } private static void PutF32(byte[] file, int at, float value) { Put32(file, at, (uint)BitConverter.SingleToInt32Bits(value)); } private static void Put64(byte[] file, int at, ulong value) { for (int i = 0; i < 8; i++) { file[at + i] = (byte)(value >> 8 * i); } } } internal static class IfReader { private static readonly string[] AudioExtensions = new string[3] { ".wav", ".mp3", ".ogg" }; internal static readonly string[] ImageExtensions = new string[3] { ".png", ".jpg", ".jpeg" }; internal static readonly string[] BackgroundExtensions = new string[2] { ".png", ".mp4" }; internal const string InfoFileName = "if"; private static readonly JsonDocumentOptions JsonOptions = new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true }; private static readonly JsonElement Null = NullElement(); private static JsonElement NullElement() { using JsonDocument jsonDocument = JsonDocument.Parse("null"); return jsonDocument.RootElement.Clone(); } internal static bool TryRead(string folder, out ChartInfo info, out string reason) { info = null; reason = null; string path = Path.Combine(folder, "if"); if (!File.Exists(path)) { return false; } string json; try { json = File.ReadAllText(path); } catch (Exception) { return false; } JsonDocument jsonDocument; try { jsonDocument = JsonDocument.Parse(json, JsonOptions); } catch (Exception) { return false; } using JsonDocument jsonDocument2 = jsonDocument; JsonElement rootElement = jsonDocument2.RootElement; if (rootElement.ValueKind != JsonValueKind.Object) { return false; } string fileName = Path.GetFileName(folder); ChartInfo chartInfo = new ChartInfo { Folder = folder, FolderName = fileName }; if (!Text(rootElement, "id", out var value, out reason)) { return false; } chartInfo.BaseName = value; if (!Faces(rootElement, "name", required: true, out chartInfo.Titles, out chartInfo.TitleSets, out reason)) { return false; } chartInfo.Name = chartInfo.Titles[0]; if (!Faces(rootElement, "composer", required: true, out chartInfo.ComposerLanguages, out chartInfo.ComposerSets, out reason)) { return false; } chartInfo.Composer = chartInfo.ComposerLanguages[0]; if (!Faces(rootElement, "illust", required: false, out chartInfo.IllustratorLanguages, out chartInfo.IllustratorSets, out reason)) { return false; } if (!Songs(rootElement, folder, out chartInfo.Songs, out reason)) { return false; } chartInfo.SongPath = chartInfo.Songs[0]; if (!Jackets(rootElement, folder, out chartInfo.JacketPaths, out reason)) { return false; } if (!Backgrounds(rootElement, folder, out chartInfo.BackgroundPaths, out reason)) { return false; } if (!Previews(rootElement, out chartInfo.Previews, out reason)) { return false; } chartInfo.PreviewStart = chartInfo.Previews[0][0]; chartInfo.PreviewEnd = chartInfo.Previews[0][1]; if (!DifficultyValue(rootElement, "chart", out var token, out reason)) { return false; } if (!DifficultyValue(rootElement, "charter", out var token2, out reason)) { return false; } if (!DifficultyValue(rootElement, "lv", out var token3, out reason)) { return false; } JsonElement[] array = Slots(token); JsonElement[] array2 = Slots(token2); JsonElement[] array3 = Slots(token3); if (!Aligned(array, array2, array3, out reason)) { return false; } if (!Texts(array2, "charter", out chartInfo.Charters, out reason)) { return false; } if (!Levels(array3, "lv", out chartInfo.Level, out reason)) { return false; } if (!ChartFiles(array, folder, out chartInfo.ChartPaths, out chartInfo.ChartNames, out chartInfo.NoChart, out reason)) { return false; } info = chartInfo; return true; } private static bool Faces(JsonElement root, string key, bool required, out string[] languages, out string[][] sets, out string reason) { languages = null; sets = null; reason = null; if (!root.TryGetProperty(key, out var value) || value.ValueKind == JsonValueKind.Null) { if (!required) { return true; } return false; } if (value.ValueKind == JsonValueKind.String) { if (!One(value, key, out var text, out reason)) { return false; } languages = SameFive(text); return true; } if (value.ValueKind == JsonValueKind.Object) { return Dictionary(value, key, required, out languages, out sets, out reason); } if (value.ValueKind != JsonValueKind.Array) { return false; } switch (value.GetArrayLength()) { case 5: if (!Nested(value, key, out sets, out reason)) { return false; } if (sets == null) { return Five(value, key, required, out languages, out reason); } languages = Base(null, sets, required, out reason); if (languages == null) { return !required; } Fill(sets, languages); return true; default: return false; case 4: { string[][] array = new string[4][]; bool flag = false; for (int i = 0; i < 4; i++) { JsonElement item = value[i]; if (item.ValueKind != JsonValueKind.Null) { if (item.ValueKind == JsonValueKind.Array) { return false; } if (!One(item, key, out var text2, out reason)) { return false; } array[i] = SameFive(text2); flag = true; } } if (!flag) { return !required; } languages = Base(null, array, required, out reason); if (languages == null) { return false; } sets = array; return true; } } } private static bool Dictionary(JsonElement token, string key, bool required, out string[] languages, out string[][] sets, out string reason) { languages = null; sets = null; reason = null; string[] array = new string[5]; string[][] array2 = new string[4][]; bool flag = false; bool flag2 = false; foreach (JsonProperty item2 in token.EnumerateObject()) { int num = ChartPack.LanguageIndex(item2.Name); if (num < 0) { ModLog.WarnRelease($"\"{key}\": '{item2.Name}' is not a language " + "(sc/tc/en/ja/ko); it is ignored"); continue; } JsonElement value = item2.Value; if (value.ValueKind == JsonValueKind.String) { if (!One(value, key, out var text, out reason)) { return false; } array[num] = text; flag = true; continue; } if (value.ValueKind != JsonValueKind.Array || value.GetArrayLength() != 4) { return false; } for (int i = 0; i < 4; i++) { JsonElement item = value[i]; if (item.ValueKind != JsonValueKind.Null) { if (!One(item, key, out var text2, out reason)) { return false; } string[][] array3 = array2; int num2 = i; if (array3[num2] == null) { array3[num2] = new string[5]; } array2[i][num] = text2; flag2 = true; } } } if (!flag && !flag2) { return !required; } string[] slots = (flag ? array : null); languages = Base(slots, flag2 ? array2 : null, required, out reason); if (languages == null) { return !required; } if (flag2) { Fill(array2, languages); sets = array2; } return true; } private static bool Nested(JsonElement token, string key, out string[][] sets, out string reason) { sets = null; reason = null; for (int i = 0; i < 5; i++) { JsonElement jsonElement = token[i]; if (jsonElement.ValueKind != JsonValueKind.Array) { return true; } if (jsonElement.GetArrayLength() != 4) { return false; } } string[][] array = new string[4][]; for (int j = 0; j < 4; j++) { array[j] = new string[5]; } for (int k = 0; k < 5; k++) { JsonElement jsonElement2 = token[k]; for (int l = 0; l < 4; l++) { JsonElement item = jsonElement2[l]; if (item.ValueKind != JsonValueKind.Null) { if (!One(item, key, out var text, out reason)) { return false; } array[l][k] = text; } } } sets = array; return true; } private static string[] Base(string[] slots, string[][] sets, bool required, out string reason) { reason = null; string[] array = new string[5]; bool flag = false; for (int i = 0; i < array.Length; i++) { if (slots != null && slots[i] != null) { array[i] = slots[i]; flag = true; } else { if (sets == null) { continue; } foreach (string[] array2 in sets) { if (array2 != null && array2[i] != null) { array[i] = array2[i]; flag = true; break; } } } } if (!flag) { return null; } string text = null; string[] array3 = array; foreach (string text2 in array3) { if (text2 != null) { text = text2; break; } } for (int k = 0; k < array.Length; k++) { if (array[k] == null) { array[k] = text; } } return array; } private static void Fill(string[][] sets, string[] languages) { foreach (string[] array in sets) { if (array == null) { continue; } for (int j = 0; j < array.Length; j++) { if (array[j] == null) { array[j] = languages[j]; } } } } private static bool Five(JsonElement token, string key, bool required, out string[] values, out string reason) { values = null; reason = null; string[] array = new string[5]; for (int i = 0; i < array.Length; i++) { JsonElement item = token[i]; if ((item.ValueKind != JsonValueKind.Null || required) && !One(item, key, out array[i], out reason)) { return false; } } values = array; return true; } private static bool One(JsonElement item, string key, out string text, out string reason) { text = null; reason = null; if (item.ValueKind != JsonValueKind.String || string.IsNullOrWhiteSpace(item.GetString())) { return false; } text = item.GetString(); return true; } internal static string[] SameFive(string text) { return new string[5] { text, text, text, text, text }; } internal static string WhyNot(string folder) { string path = Path.Combine(folder, "if"); if (!File.Exists(path)) { return "no if file"; } try { using (JsonDocument.Parse(File.ReadAllText(path), JsonOptions)) { } } catch (Exception ex) { return "if is not valid JSON: " + ex.Message; } return "its if was read but turned down; install the Debug build for the reason"; } private static bool Text(JsonElement root, string key, out string value, out string reason) { value = null; reason = null; if (!root.TryGetProperty(key, out var value2) || value2.ValueKind == JsonValueKind.Null) { return false; } if (value2.ValueKind != JsonValueKind.String) { return false; } value = value2.GetString(); if (string.IsNullOrWhiteSpace(value)) { return false; } return true; } private static bool LocalFile(JsonElement root, string folder, string key, string[] allowed, out string path, out string reason) { path = null; if (!Text(root, key, out var value, out reason)) { return false; } return LocalFileAt(folder, key, value, allowed, out path, out reason); } private static bool LocalFileAt(string folder, string key, string name, string[] allowed, out string path, out string reason) { path = null; reason = null; if (Path.GetFileName(name) != name) { return false; } string value = Path.GetExtension(name).ToLowerInvariant(); if (Array.IndexOf(allowed, value) < 0) { return false; } path = Path.Combine(folder, name); if (!File.Exists(path)) { path = null; return false; } return true; } private static bool Jackets(JsonElement root, string folder, out string[] paths, out string reason) { return OneOrFour(root, folder, "jacket", ImageExtensions, entriesMayBeNull: false, out paths, out reason); } private static bool Backgrounds(JsonElement root, string folder, out string[] paths, out string reason) { paths = null; reason = null; if (!root.TryGetProperty("background", out var value) || value.ValueKind == JsonValueKind.Null) { return true; } return OneOrFour(root, folder, "background", BackgroundExtensions, entriesMayBeNull: true, out paths, out reason); } private static bool OneOrFour(JsonElement root, string folder, string key, string[] allowed, bool entriesMayBeNull, out string[] paths, out string reason) { paths = null; reason = null; if (!root.TryGetProperty(key, out var value) || value.ValueKind == JsonValueKind.Null) { return false; } if (value.ValueKind == JsonValueKind.String) { if (!LocalFile(root, folder, key, allowed, out var path, out reason)) { return false; } string[] array = new string[4]; for (int i = 0; i < array.Length; i++) { array[i] = path; } paths = array; return true; } if (value.ValueKind != JsonValueKind.Array) { return false; } if (value.GetArrayLength() != 4) { return false; } string[] array2 = new string[4]; for (int j = 0; j < array2.Length; j++) { JsonElement jsonElement = value[j]; if (!(jsonElement.ValueKind == JsonValueKind.Null && entriesMayBeNull)) { if (jsonElement.ValueKind != JsonValueKind.String || string.IsNullOrWhiteSpace(jsonElement.GetString())) { return false; } if (!LocalFileAt(folder, key, jsonElement.GetString(), allowed, out array2[j], out reason)) { return false; } } } paths = array2; return true; } private static bool Preview(JsonElement root, out float start, out float end, out string reason) { start = 0f; end = 0f; reason = null; if (!root.TryGetProperty("preview_seconds", out var value) || value.ValueKind == JsonValueKind.Null) { return false; } if (value.ValueKind != JsonValueKind.Array) { return false; } if (value.GetArrayLength() != 2) { return false; } if (!Seconds(value[0], 0, out start, out reason)) { return false; } if (!Seconds(value[1], 1, out end, out reason)) { return false; } return true; } private static bool Seconds(JsonElement item, int index, out float value, out string reason) { value = 0f; reason = null; if (item.ValueKind == JsonValueKind.Number && item.TryGetDouble(out var value2)) { value = (float)value2; return true; } if (item.ValueKind == JsonValueKind.String && float.TryParse(item.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture, out value)) { return true; } return false; } private static bool DifficultyValue(JsonElement root, string key, out JsonElement token, out string reason) { token = default(JsonElement); reason = null; if (!root.TryGetProperty(key, out var value)) { return false; } if (value.ValueKind == JsonValueKind.Array && value.GetArrayLength() != 4) { return false; } token = value; return true; } private static JsonElement[] Slots(JsonElement token) { JsonElement[] array = new JsonElement[4]; if (token.ValueKind != JsonValueKind.Array) { for (int i = 0; i < array.Length - 1; i++) { array[i] = Null; } array[^1] = token; return array; } for (int j = 0; j < array.Length; j++) { array[j] = token[j]; } return array; } private static bool Aligned(JsonElement[] charts, JsonElement[] charters, JsonElement[] levels, out string reason) { reason = null; for (int i = 0; i < 4; i++) { if (charts[i].ValueKind != JsonValueKind.Null && (charters[i].ValueKind == JsonValueKind.Null || levels[i].ValueKind == JsonValueKind.Null)) { return false; } } return true; } private static bool Texts(JsonElement[] slots, string key, out string[] values, out string reason) { values = null; reason = null; string[] array = new string[4]; for (int i = 0; i < array.Length; i++) { JsonElement jsonElement = slots[i]; if (jsonElement.ValueKind != JsonValueKind.Null) { if (jsonElement.ValueKind != JsonValueKind.String || string.IsNullOrWhiteSpace(jsonElement.GetString())) { return false; } array[i] = jsonElement.GetString(); } } values = array; return true; } private static bool Levels(JsonElement[] slots, string key, out int?[] values, out string reason) { values = null; reason = null; int?[] array = new int?[4]; for (int i = 0; i < array.Length; i++) { JsonElement item = slots[i]; if (item.ValueKind != JsonValueKind.Null) { if (!Whole(item, out var value)) { return false; } if (value <= 0) { return false; } array[i] = value; } } values = array; return true; } private static bool Whole(JsonElement item, out int value) { value = 0; if (item.ValueKind != JsonValueKind.Number) { return false; } string rawText = item.GetRawText(); if (rawText.IndexOf('.') >= 0 || rawText.IndexOf('e') >= 0 || rawText.IndexOf('E') >= 0) { return false; } return item.TryGetInt32(out value); } private static bool ChartFiles(JsonElement[] slots, string folder, out string[] paths, out string[] chartNames, out bool[] noChart, out string reason) { paths = null; chartNames = null; noChart = null; reason = null; string[] array = new string[4]; string[] array2 = new string[4]; bool[] array3 = new bool[4]; for (int i = 0; i < 4; i++) { JsonElement jsonElement = slots[i]; if (jsonElement.ValueKind == JsonValueKind.Null) { array3[i] = true; continue; } if (jsonElement.ValueKind != JsonValueKind.String || string.IsNullOrWhiteSpace(jsonElement.GetString())) { return false; } string text = jsonElement.GetString(); if (Path.GetFileName(text) != text) { return false; } string text2 = Path.Combine(folder, text); if (!File.Exists(text2)) { return false; } array[i] = text2; array2[i] = text; } paths = array; chartNames = array2; noChart = array3; return true; } private static bool Names(JsonElement root, out string[] names, out string reason) { names = null; reason = null; if (!root.TryGetProperty("name", out var value) || value.ValueKind == JsonValueKind.Null) { return false; } if (value.ValueKind == JsonValueKind.String) { string value2 = value.GetString(); if (string.IsNullOrWhiteSpace(value2)) { return false; } names = Same(value2); return true; } return Texts(root, "name", out names, out reason); } private static bool Songs(JsonElement root, string folder, out string[] songs, out string reason) { songs = null; reason = null; if (!root.TryGetProperty("song", out var value) || value.ValueKind == JsonValueKind.Null) { return false; } if (value.ValueKind == JsonValueKind.String) { if (!LocalFileAt(folder, "song", value.GetString(), AudioExtensions, out var path, out reason)) { return false; } songs = Same(path); return true; } if (value.ValueKind != JsonValueKind.Array) { return false; } string[] slots = new string[4]; if (!FilesAcross(value, folder, "song", AudioExtensions, slots, out reason)) { return false; } songs = Carry(slots, "song", out reason); return songs != null; } private static bool Previews(JsonElement root, out float[][] previews, out string reason) { previews = null; if (!List(root, "preview_seconds", out var token, out reason)) { return false; } if (token.GetArrayLength() == 0 || token[0].ValueKind != JsonValueKind.Array) { if (!Preview(root, out var start, out var end, out reason)) { return false; } float[][] array = new float[4][]; for (int i = 0; i < array.Length; i++) { array[i] = new float[2] { start, end }; } previews = array; return true; } if (token.GetArrayLength() > 4) { return false; } float[][] array2 = new float[4][]; for (int j = 0; j < token.GetArrayLength(); j++) { JsonElement jsonElement = token[j]; if (jsonElement.ValueKind != JsonValueKind.Null) { if (jsonElement.GetArrayLength() != 2 || !Seconds(jsonElement[0], 0, out var value, out reason) || !Seconds(jsonElement[1], 1, out var value2, out reason)) { return false; } array2[j] = new float[2] { value, value2 }; } } if (array2[0] == null) { return false; } for (int k = 1; k < array2.Length; k++) { float[][] array3 = array2; int num = k; if (array3[num] == null) { array3[num] = array2[0]; } } previews = array2; return true; } private static bool List(JsonElement root, string key, out JsonElement token, out string reason) { token = default(JsonElement); reason = null; if (!root.TryGetProperty(key, out token) || token.ValueKind == JsonValueKind.Null) { return false; } if (token.ValueKind != JsonValueKind.Array) { return false; } return true; } private static bool Texts(JsonElement root, string key, out string[] texts, out string reason) { texts = null; if (!List(root, key, out var token, out reason)) { return false; } int arrayLength = token.GetArrayLength(); if (arrayLength == 0 || arrayLength > 4) { return false; } string[] array = new string[4]; for (int i = 0; i < arrayLength; i++) { JsonElement jsonElement = token[i]; if (jsonElement.ValueKind != JsonValueKind.Null) { if (jsonElement.ValueKind != JsonValueKind.String || string.IsNullOrWhiteSpace(jsonElement.GetString())) { return false; } array[i] = jsonElement.GetString(); } } texts = Carry(array, key, out reason); return texts != null; } private static bool FilesAcross(JsonElement token, string folder, string key, string[] allowed, string[] slots, out string reason) { reason = null; int arrayLength = token.GetArrayLength(); if (arrayLength == 0 || arrayLength > 4) { return false; } for (int i = 0; i < arrayLength; i++) { JsonElement jsonElement = token[i]; if (jsonElement.ValueKind != JsonValueKind.Null) { if (jsonElement.ValueKind != JsonValueKind.String) { return false; } if (!LocalFileAt(folder, key, jsonElement.GetString(), allowed, out var path, out reason)) { return false; } slots[i] = path; } } return true; } private static string[] Same(string value) { string[] array = new string[4]; for (int i = 0; i < array.Length; i++) { array[i] = value; } return array; } private static string[] Carry(string[] slots, string key, out string reason) { reason = null; if (slots[0] == null) { return null; } for (int i = 1; i < slots.Length; i++) { ref string reference = ref slots[i]; if (reference == null) { reference = slots[0]; } } return slots; } } internal static class Json { internal static object Parse(string text, out string reason) { reason = null; int at = 0; try { object result = Value(text, ref at); Space(text, ref at); if (at != text.Length) { throw new FormatException("trailing text"); } return result; } catch (Exception ex) { reason = $"at {at}: {ex.Message}"; return null; } } internal static Dictionary<string, object> Object(object value) { return value as Dictionary<string, object>; } internal static List<object> Array(object value) { return value as List<object>; } internal static double Number(Dictionary<string, object> at, string key, double fallback = 0.0) { if (at != null && at.TryGetValue(key, out var value) && value is double) { return (double)value; } return fallback; } internal static bool Has(Dictionary<string, object> at, string key) { return at?.ContainsKey(key) ?? false; } internal static object Field(Dictionary<string, object> at, string key) { if (at == null || !at.TryGetValue(key, out var value)) { return null; } return value; } internal static double Scalar(object value) { if (!(value is double result)) { if (value is string s) { double result2; return double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out result2) ? result2 : 0.0; } return 0.0; } return result; } private static object Value(string text, ref int at) { Space(text, ref at); if (at >= text.Length) { throw new FormatException("unexpected end"); } switch (text[at]) { case '{': return Map(text, ref at); case '[': return List(text, ref at); case '"': return String(text, ref at); case 't': Word(text, ref at, "true"); return true; case 'f': Word(text, ref at, "false"); return false; case 'n': Word(text, ref at, "null"); return null; default: return Number(text, ref at); } } private static Dictionary<string, object> Map(string text, ref int at) { Dictionary<string, object> dictionary = new Dictionary<string, object>(StringComparer.Ordinal); at++; Space(text, ref at); if (at < text.Length && text[at] == '}') { at++; return dictionary; } while (true) { Space(text, ref at); string key = String(text, ref at); Space(text, ref at); if (at >= text.Length || text[at] != ':') { throw new FormatException("expected ':'"); } at++; dictionary[key] = Value(text, ref at); Space(text, ref at); if (at >= text.Length) { throw new FormatException("unterminated object"); } if (text[at] != ',') { break; } at++; } if (text[at] == '}') { at++; return dictionary; } throw new FormatException("expected ',' or '}'"); } private static List<object> List(string text, ref int at) { List<object> list = new List<object>(); at++; Space(text, ref at); if (at < text.Length && text[at] == ']') { at++; return list; } while (true) { list.Add(Value(text, ref at)); Space(text, ref at); if (at >= text.Length) { throw new FormatException("unterminated array"); } if (text[at] != ',') { break; } at++; } if (text[at] == ']') { at++; return list; } throw new FormatException("expected ',' or ']'"); } private static string String(string text, ref int at) { if (at >= text.Length || text[at] != '"') { throw new FormatException("expected a string"); } at++; StringBuilder stringBuilder = new StringBuilder(); while (at < text.Length) { char c = text[at++]; switch (c) { case '"': return stringBuilder.ToString(); default: stringBuilder.Append(c); break; case '\\': { if (at >= text.Length) { throw new FormatException("unterminated escape"); } char c2 = text[at++]; switch (c2) { case 'n': stringBuilder.Append('\n'); break; case 't': stringBuilder.Append('\t'); break; case 'r': stringBuilder.Append('\r'); break; case 'b': stringBuilder.Append('\b'); break; case 'f': stringBuilder.Append('\f'); break; case 'u': if (at + 4 > text.Length) { throw new FormatException("bad \\u escape"); } stringBuilder.Append((char)int.Parse(text.Substring(at, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture)); at += 4; break; default: stringBuilder.Append(c2); break; } break; } } } throw new FormatException("unterminated string"); } private static double Number(string text, ref int at) { int num = at; while (at < text.Length && (char.IsDigit(text[at]) || "+-.eE".IndexOf(text[at]) >= 0)) { at++; } if (at == num) { throw new FormatException("expected a number"); } return double.Parse(text.Substring(num, at - num), NumberStyles.Float, CultureInfo.InvariantCulture); } private static void Word(string text, ref int at, string word) { if (string.CompareOrdinal(text, at, word, 0, word.Length) != 0) { throw new FormatException("expected " + word); } at += word.Length; } private static void Space(string text, ref int at) { while (at < text.Length && char.IsWhiteSpace(text[at])) { at++; } } } internal static class PackText { private enum TextWhat { PackName, SongText } internal enum SongText { Title, Artist, Illustrator } private static int PackAssetsTable = -1; private static int PackAssetsField = -1; private static int StringMappingField = -1; private const int TemplateRow = 2; private static readonly IntPtr[] DefaultArt = new IntPtr[14]; private static bool _defaultArt; private static readonly (string Field, string Key, string File)[] VisualSet = new(string, string, string)[14] { ("SongSelectLargeNonCompleted", "songSelect", "packvisual-songselect-default.png"), ("SongSelectLargeNonCompletedHover", "songSelectHover", "packvisual-songselect-hover.png"), ("SongSelectLargeCompleted", "songSelect", "packvisual-songselect-default.png"), ("SongSelectLargeCompletedHover", "songSelectHover", "packvisual-songselect-hover.png"), ("PackSelectLargeNonCompleted", "selected", "packvisual-selected-default.png"), ("PackSelectLargeNonCompletedHover", "selectedHover", "packvisual-selected-hover.png"), ("PackSelectLargeCompleted", "selected", "packvisual-selected-default.png"), ("PackSelectLargeCompletedHover", "selectedHover", "packvisual-selected-hover.png"), ("PackSelectNonCompleted", "deselected", "packvisual-deselected-default.png"), ("PackSelectNonCompletedHover", "deselectedHover", "packvisual-deselected-hover.png"), ("PackSelectCompleted", "deselected", "packvisual-deselected-default.png"), ("PackSelectCompletedHover", "deselectedHover", "packvisual-deselected-hover.png"), ("OutlineNonCompleted", "outline", "outline-packvisua.png"), ("OutlineCompleted", "outline", "outline-packvisua.png") }; private static readonly Dictionary<string, IntPtr> Built = new Dictionary<string, IntPtr>(StringComparer.Ordinal); private static int _language = int.MinValue; private const int FallbackLanguage = 2; private static int PackIdTypeMapping = -1; private static int SongTitleTypeMapping = -1; private static int SongArtistTypeMapping = -1; private static int SongIllustratorTypeMapping = -1; private static bool _fieldsResolved; private static bool _fieldsMissing; private static readonly int TextMappingValuesSize = 5 * IntPtr.Size; private static readonly int ValueEnglish; private static readonly int ValueJapanese; private static readonly int ValueKorean; private static readonly int ValueTraditionalChinese; private static readonly int ValueSimplifiedChinese; private static int _mappingOffset; internal static bool TryGetVisuals(out IntPtr packAssets) { packAssets = IntPtr.Zero; if (!ResolveFields()) { return false; } IntPtr intPtr = FieldResolver.ClassPointer("DataAccess"); if (intPtr == IntPtr.Zero) { return false; } IntPtr intPtr2 = FieldResolver.Statics(intPtr); if (intPtr2 == IntPtr.Zero) { return false; } packAssets = Memory.Ptr(intPtr2 + PackAssetsField); return Memory.LooksLikeObject(packAssets); } internal unsafe static bool PrepareVisualRows(IntPtr packAssets, int highestId) { IntPtr intPtr = Memory.Ptr(packAssets + PackAssetsTable); if (!Memory.LooksLikeObject(intPtr)) { return false; } int num = Memory.I32(intPtr + 24); if (num <= 0) { return false; } int num2 = highestId + 1; if (num == num2) { return true; } int num3 = (int)(IL2CPP.il2cpp_array_get_byte_length(intPtr) / num); if (num3 <= 0) { return false; } IntPtr intPtr2 = SongCatalog.NewArray(intPtr, num2); if (intPtr2 == IntPtr.Zero) { return false; } IntPtr intPtr3 = intPtr + 32; IntPtr intPtr4 = intPtr2 + 32; long num4 = (long)Math.Min(num, num2) * (long)num3; Buffer.MemoryCopy((void*)intPtr3, (void*)intPtr4, num4, num4); StoreDefaultArt(intPtr3, num3); IntPtr intPtr5 = intPtr3 + Math.Min(2, num - 1) * num3; for (int i = num; i < num2; i++) { Buffer.MemoryCopy((void*)intPtr5, (void*)(intPtr4 + i * num3), num3, num3); } Memory.WritePtr(packAssets + PackAssetsTable, intPtr2); ModLog.Load($"pack visual rows: {num} -> {num2} (shape=row {Math.Min(2, num - 1)})"); return true; } private static void StoreDefaultArt(IntPtr rows, int stride) { if (_defaultArt) { return; } for (int i = 0; i < VisualSet.Length; i++) { int num = FieldResolver.Lookup("SongSelectPackAssets", VisualSet[i].Field); if (num < 0) { return; } DefaultArt[i] = Memory.Ptr(rows + num); } _defaultArt = true; } internal static IntPtr RowMaterial(IntPtr packAssets, int packId, string field) { IntPtr intPtr = Memory.Ptr(packAssets + PackAssetsTable); if (!Memory.LooksLikeObject(intPtr)) { return IntPtr.Zero; } int num = Memory.I32(intPtr + 24); if (packId < 0 || packId >= num) { return IntPtr.Zero; } long num2 = IL2CPP.il2cpp_array_get_byte_length(intPtr); int num3 = (int)((num > 0) ? (num2 / num) : 0); if (num3 <= 0) { return IntPtr.Zero; } int num4 = FieldResolver.Lookup("SongSelectPackAssets", field); if (num4 < 0) { return IntPtr.Zero; } return Memory.Ptr(intPtr + 32 + packId * num3 + num4); } internal static int SetPackVisuals(IntPtr packAssets, int packId, ChartPack pack) { IntPtr intPtr = Memory.Ptr(packAssets + PackAssetsTable); if (!Memory.LooksLikeObject(intPtr)) { return 0; } int num = Memory.I32(intPtr + 24); if (packId < 0 || packId >= num) { return 0; } long num2 = IL2CPP.il2cpp_array_get_byte_length(intPtr); int num3 = (int)((num > 0) ? (num2 / num) : 0); if (num3 <= 0) { return 0; } IntPtr intPtr2 = intPtr + 32 + packId * num3; int language = Language(); int num4 = 0; int i = 0; for (int j = 0; j < VisualSet.Length; j++, i++) { (string Field, string Key, string File) tuple = VisualSet[j]; string item = tuple.Field; string item2 = tuple.Key; string item3 = tuple.File; string text = Picture(pack, item2, item3, language); int num5 = FieldResolver.Lookup("SongSelectPackAssets", item); if (num5 < 0) { continue; } if (string.IsNullOrEmpty(text) || !File.Exists(text)) { IntPtr intPtr3 = DefaultArt[i]; if (Memory.LooksLikeObject(intPtr3)) { Memory.WritePtr(intPtr2 + num5, intPtr3); } continue; } IntPtr intPtr4 = Memory.Ptr(intPtr2 + num5); if (!Memory.LooksLikeObject(intPtr4)) { continue; } string key = packId + "|" + text; if (!Built.TryGetValue(key, out var value)) { value = JacketFactory.Cover(text, intPtr4, out var _); if (value == IntPtr.Zero) { continue; } Built[key] = value; ModLog.Load($"pack {packId} visual {item} off=0x{num5:X} src=0x{intPtr4.ToInt64():X} mat=0x{value.ToInt64():X}"); } Memory.WritePtr(intPtr2 + num5, value); num4++; } return num4; } internal static void ResetVisualCache() { Built.Clear(); } internal static int Language() { if (_language != int.MinValue) { return _language; } _language = ReadLanguage(); return _language; } private static int ReadLanguage() { try { MethodInfo methodInfo = MethodResolver.ManagedMethod("Strings", "GetCurrentLocalization", "Str"); if (methodInfo == null) { ModLog.WarnRelease("the game's current language could not be found; pack pictures use their English slot"); return 2; } int num = Convert.ToInt32(methodInfo.Invoke(null, null), CultureInfo.InvariantCulture); string text = (methodInfo.ReturnType.IsEnum ? Enum.GetName(methodInfo.ReturnType, num) : null); int num2 = SlotOf(text); ModLog.Load($"pack pictures: the game is showing {text ?? num.ToString()} (language slot {num2})"); return num2; } catch (Exception e) { ModLog.WarnRelease("the game's current language could not be read: " + ModLog.Describe(e)); return 2; } } private static int SlotOf(string name) { if (name == null) { return 2; } if (name.IndexOf("Simplified", StringComparison.OrdinalIgnoreCase) >= 0 || name.EndsWith("SC", StringComparison.Ordinal)) { return 0; } if (name.IndexOf("Traditional", StringComparison.OrdinalIgnoreCase) >= 0 || name.EndsWith("TC", StringComparison.Ordinal)) { return 1; } if (name.IndexOf("Japanese", StringComparison.OrdinalIgnoreCase) >= 0) { return 3; } if (name.IndexOf("Korean", StringComparison.OrdinalIgnoreCase) >= 0) { return 4; } return 2; } internal static bool Install() { MethodInfo methodInfo = HarmonyHooks.Target("Strings", "SetDefaultLocalization", "Str"); if (methodInfo == null) { ModLog.WarnRelease("the game's language setting could not be watched; changing it needs a restart for the pack pictures"); return false; } bool num = HarmonyHooks.Patch(methodInfo, null, HarmonyHooks.Handler(typeof(PackText), "AfterLanguage")); if (num) { ModLog.Load("pack pictures follow the game's language"); } return num; } private static void AfterLanguage() { _language = int.MinValue; CustomSongCore.InvalidateVisualTables(); } private static string Picture(ChartPack pack, string key, string file, int language) { string text = pack.Visual(key, language); if (text != null && File.Exists(text)) { return text; } string text2 = Path.Combine(pack.Folder, file); if (File.Exists(text2)) { return text2; } string path = (file.Contains("-hover") ? file.Replace("-hover", "-default") : file.Replace("-default", "-hover")); text2 = Path.Combine(pack.Folder, path); if (!File.Exists(text2)) { return null; } return text2; } internal unsafe static void RefreshLookup(IntPtr packData) { IntPtr intPtr = FieldResolver.ClassPointer("PackData"); if (!(intPtr == IntPtr.Zero)) { IntPtr intPtr2 = IL2CPP.il2cpp_class_get_method_from_name(intPtr, "_SkA", 0); if (!(intPtr2 == IntPtr.Zero)) { IntPtr zero = IntPtr.Zero; IL2CPP.il2cpp_runtime_invoke(intPtr2, packData, (void**)null, ref zero); _ = zero != IntPtr.Zero; } } } private static bool ResolveFields() { if (_fieldsResolved) { return true; } if (_fieldsMissing) { return false; } if (FieldResolver.ClassPointer("DataAccess") == IntPtr.Zero) { return false; } PackAssetsTable = FieldResolver.Field("PackSelectSceneAssets", "packToAssets"); PackIdTypeMapping = FieldResolver.Field("DynamicStringMapping", "packIdTypeMapping"); SongTitleTypeMapping = FieldResolver.Field("DynamicStringMapping", "songIdTitleTypeMapping"); SongArtistTypeMapping = FieldResolver.Field("DynamicStringMapping", "songIdArtistTypeMapping"); SongIllustratorTypeMapping = FieldResolver.Field("DynamicStringMapping", "jacketIllustratorNameTypeMapping"); PackAssetsField = FieldResolver.Field("DataAccess", "_NAb"); StringMappingField = FieldResolver.Field("DataAccess", "_mAb"); if (PackAssetsTable < 0 || PackIdTypeMapping < 0 || SongTitleTypeMapping < 0 || SongArtistTypeMapping < 0 || SongIllustratorTypeMapping < 0 || PackAssetsField < 0 || StringMappingField < 0) { _fieldsMissing = true; ModLog.Error("the pack's fields could not be read by name in this build; the custom pack keeps the game's own visuals and text"); return false; } _fieldsResolved = true; return true; } private static string[] SameFive(string text) { return new string[5] { text, text, text, text, text }; } internal static bool SetPackName(int packId, string name) { return SetPackName(packId, SameFive(name)); } internal static bool SetPackName(int packId, string[] names) { if (!ResolveFields()) { return false; } return SetText(PackIdTypeMapping, packId, names, TextWhat.PackName); } internal static bool SetSongText(SongText which, int songId, string text) { return SetSongText(which, songId, SameFive(text)); } internal static bool SetSongText(SongText which, int songId, string[] names) { if (!ResolveFields()) { return false; } return SetText(which switch { SongText.Artist => SongArtistTypeMapping, SongText.Title => SongTitleTypeMapping, _ => SongIllustratorTypeMapping, }, songId, names, TextWhat.SongText); } private static bool SetText(int mappingField, int id, string[] names, TextWhat what) { if (!ResolveFields()) { return false; } if (names == null || names.Length != 5) { return false; } IntPtr intPtr = FieldResolver.ClassPointer("DataAccess"); if (intPtr == IntPtr.Zero) { return false; } IntPtr intPtr2 = FieldResolver.Statics(intPtr); if (intPtr2 == IntPtr.Zero) { return false; } IntPtr intPtr3 = Memory.Ptr(intPtr2 + StringMappingField); if (!Memory.LooksLikeObject(intPtr3)) { return false; } IntPtr intPtr4 = Memory.Ptr(intPtr3 + mappingField); if (!Memory.LooksLikeObject(intPtr4)) { return false; } int num = MappingOffset(intPtr4); if (num < 0) { return false; } IntPtr intPtr5 = Memory.Ptr(intPtr4 + num); if (!Memory.LooksLikeObject(intPtr5)) { return false; } if (!Insert(intPtr5, id, names)) { return false; } return true; } [Conditional("DEBUG")] private static void NotWritten(TextWhat what, string where) { } private static string Name(TextWhat what) { return null; } private static int MappingOffset(IntPtr typeMapping) { if (_mappingOffset != int.MinValue) { return _mappingOffset; } IntPtr intPtr = IL2CPP.il2cpp_object_get_class(typeMapping); if (intPtr != IntPtr.Zero) { IntPtr intPtr2 = IL2CPP.il2cpp_class_get_field_from_name(intPtr, "Mapping"); if (intPtr2 != IntPtr.Zero) { int num = (int)IL2CPP.il2cpp_field_get_offset(intPtr2); if (num > 0) { return _mappingOffset = num; } } } foreach (KeyValuePair<string, List<Type>> item in InteropTypeIndex.ByName()) { if (item.Key.Contains("StringTypeMapping") && item.Key.IndexOf('`') < 0) { int num2 = FieldResolver.Lookup(item.Key, "Mapping"); if (num2 > 0) { return _mappingOffset = num2; } } } ModLog.Error("StringTypeMapping.Mapping could not be found on its own class in this build; the custom pack's texts will not be written"); return _mappingOffset = -1; } private unsafe static bool Insert(IntPtr table, int packId, string[] names) { ushort num = (ushort)packId; fixed (byte* ptr = new byte[TextMappingValuesSize]) { *(IntPtr*)(ptr + ValueSimplifiedChinese) = IL2CPP.ManagedStringToIl2Cpp(names[0]); *(IntPtr*)(ptr + ValueTraditionalChinese) = IL2CPP.ManagedStringToIl2Cpp(names[1]); *(IntPtr*)(ptr + ValueEnglish) = IL2CPP.ManagedStringToIl2Cpp(names[2]); *(IntPtr*)(ptr + ValueJapanese) = IL2CPP.ManagedStringToIl2Cpp(names[3]); *(IntPtr*)(ptr + ValueKorean) = IL2CPP.ManagedStringToIl2Cpp(names[4]); IntPtr* ptr2 = stackalloc IntPtr[2]; *ptr2 = (IntPtr)(&num); ptr2[1] = (IntPtr)ptr; IntPtr intPtr = IL2CPP.il2cpp_class_get_method_from_name(IL2CPP.il2cpp_object_get_class(table), "set_Item", 2); if (intPtr == IntPtr.Zero) { return false; } IntPtr zero = IntPtr.Zero; IL2CPP.il2cpp_runtime_invoke(intPtr, table, (void**)ptr2, ref zero); if (zero != IntPtr.Zero) { return false; } } return true; } static PackText() { _ = IntPtr.Size; ValueEnglish = 0; ValueJapanese = IntPtr.Size; ValueKorean = 2 * IntPtr.Size; ValueTraditionalChinese = 3 * IntPtr.Size; ValueSimplifiedChinese = 4 * IntPtr.Size; _mappingOffset = int.MinValue; } } internal static class ScoreKeys { internal static long Refiled; private const string Records = "GameResultsV4.highscores"; private const string KeyCollection = "Dictionary.KeyCollection"; private const string ValueCollection = "Dictionary.ValueCollection"; internal unsafe static void Migrate(IntPtr table) { if (table == IntPtr.Zero) { return; } try { IntPtr intPtr = FieldResolver.ClassPointer("GameResultsV4"); int num = FieldResolver.Field("GameResultsV4", "highscores"); if (intPtr == IntPtr.Zero || num < 0) { MigrateFailed("no table"); return; } IntPtr intPtr2 = Memory.Ptr(table + num); if (!Memory.LooksLikeObject(intPtr2)) { return; } Dictionary<string, ushort> dictionary = new Dictionary<string, ushort>(); foreach (KeyValuePair<ushort, ChartInfo> item in SongCatalog.ById) { dictionary[item.Value.BaseName] = item.Key; } if (dictionary.Count == 0) { return;