diff --git a/BmsLoader.cs b/BmsLoader.cs index 65b875a..1fb07d8 100644 --- a/BmsLoader.cs +++ b/BmsLoader.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Nodes; +using System.Diagnostics; using CustomAlbums.Data; using CustomAlbums.Managers; using CustomAlbums.Utilities; @@ -6,7 +6,6 @@ using Il2CppAssets.Scripts.GameCore; using Il2CppAssets.Scripts.GameCore.Managers; using Il2CppAssets.Scripts.PeroTools.Commons; -using Il2CppAssets.Scripts.PeroTools.Managers; using Il2CppFormulaBase; using Il2CppGameLogic; using Il2CppPeroPeroGames.GlobalDefines; @@ -18,656 +17,762 @@ using Decimal = Il2CppSystem.Decimal; using Logger = CustomAlbums.Utilities.Logger; -namespace CustomAlbums +namespace CustomAlbums; + +internal static class BmsLoader { - internal static class BmsLoader + private static readonly Dictionary NoteData = new(); + private static readonly Dictionary> BossData = new(); + private static Decimal _delay; + private static readonly Logger Logger = new(nameof(BmsLoader)); + + private static readonly Dictionary StartDelayCache = new(); + private static readonly Dictionary<(string scene, string animation), float> AnimationDurationCache = new(); + + private static readonly HashSet SceneAgnosticPrefixes = new() { "00", "em", "bo" }; + + /// + /// Creates a Bms object from a BMS file. + /// + /// MemoryStream of BMS file. + /// Name of BMS score. + /// Loaded Bms object, or null if critical data is missing. + internal static Bms Load(Stream stream, string bmsName) { - private static readonly Dictionary NoteData = new(); - private static readonly Dictionary> BossData = new(); - private static Decimal _delay; - private static readonly Logger Logger = new(nameof(BmsLoader)); - - /// - /// Creates a Bms object from a BMS file. - /// - /// MemoryStream of BMS file. - /// Name of BMS score. - /// Loaded Bms object. - internal static Bms Load(Stream stream, string bmsName) - { - Logger.Msg($"Loading bms {bmsName}..."); + Logger.Msg($"Loading bms {bmsName}..."); - var bpmDict = new Dictionary(); - var notePercents = new Dictionary(); - var dataList = new List(); - var notesArray = new JsonArray(); - var info = new JsonObject(); + var bpmDict = new Dictionary(); + var timeSigEntries = new Dictionary(); + var bpmEntries = new List(); + var rawNotes = new List(); + var info = new BmsInfo(); - using var streamReader = new StreamReader(stream); - while (streamReader.ReadLine()?.Trim() is { } line) - { - if (string.IsNullOrEmpty(line) || !line.StartsWith("#")) continue; + using var streamReader = new StreamReader(stream); + while (streamReader.ReadLine()?.Trim() is { } line) + { + if (string.IsNullOrEmpty(line) || !line.StartsWith("#")) continue; - // Remove # from beginning of line - line = line[1..]; + // Remove # from beginning of line + line = line[1..]; - if (line.Contains(' ')) + if (line.Contains(' ')) + { + // Parse header + var spaceIndex = line.IndexOf(' '); + var key = line[..spaceIndex]; + var value = line[(spaceIndex + 1)..]; + + // Skip WAV definition + if (key.StartsWith("WAV")) continue; + + // Map known headers to typed properties + if (!ParseHeader(info, key, value, bpmDict, bpmEntries)) + // Store unrecognized headers in fallback dictionary + info.Extra[key] = value; + } + else if (line.Contains(':')) + { + // Parse data field + var colonIndex = line.IndexOf(':'); + if (colonIndex < 5) { - // Parse header - var split = line.Split(' '); - var key = split[0]; - var value = split[1]; + Logger.Warning($"Malformed data line (key too short): #{line}"); + continue; + } - info[key] = value; + var key = line[..colonIndex]; + var value = line[(colonIndex + 1)..]; - if (!key.Contains("BPM")) continue; + if (!key[..3].TryParseAsInt(out var beat)) + { + Logger.Warning($"Malformed measure number in: #{line}"); + continue; + } - var bpmKey = string.IsNullOrEmpty(key[3..]) ? "00" : key[3..]; - bpmDict.Add(bpmKey, value.ParseAsFloat()); + var typeCode = key.Substring(3, 2); - if (bpmKey != "00") continue; + if (!Bms.Channels.TryGetValue(typeCode, out var type)) continue; - var freq = 60f / value.ParseAsFloat() * 4f; - var obj = new JsonObject - { - { "tick", 0f }, - { "freq", freq } - }; - dataList.Add(obj); + if (type is Bms.ChannelType.SpTimesig) + { + if (value.TryParseAsFloat(out var percent)) + timeSigEntries[beat] = new TimeSigEntry(beat, percent); + else + Logger.Warning($"Invalid time signature value: {value}"); } - else if (line.Contains(':')) + else { - // Parse data field - var split = line.Split(':'); - var key = split[0]; - var value = split[1]; - - var beat = key[..3].ParseAsInt(); - var typeCode = key.Substring(3, 2); - - if (!Bms.Channels.TryGetValue(typeCode, out var type)) continue; - - if (type is Bms.ChannelType.SpTimesig) + if (value.Length % 2 != 0) { - var obj = new JsonObject - { - { "beat", beat }, - { "percent", value.ParseAsFloat() } - }; - notePercents.Add(beat, obj); + Logger.Warning($"Odd-length data field (must be pairs of 2 chars): #{line}"); + continue; } - else - { - var objLength = value.Length / 2; - for (var i = 0; i < objLength; i++) - { - var note = value.Substring(i * 2, 2); - if (note is "00") continue; - var tick = (float)i / objLength + beat; - - if (type is Bms.ChannelType.SpBpmDirect or Bms.ChannelType.SpBpmLookup) - { - // Handle BPM changes - var freqDivide = type == Bms.ChannelType.SpBpmLookup && - bpmDict.TryGetValue(note, out var bpm) - ? bpm - : Convert.ToInt32(note, 16); - var freq = 60f / freqDivide * 4f; + var objLength = value.Length / 2; + for (var i = 0; i < objLength; i++) + { + var note = value.Substring(i * 2, 2); + if (note is "00") continue; - var obj = new JsonObject - { - { "tick", tick }, - { "freq", freq } - }; - dataList.Add(obj); - dataList.Sort((l, r) => - { - var tickL = l["tick"].GetValue(); - var tickR = r["tick"].GetValue(); + var tick = (float)i / objLength + beat; - return tickR.CompareTo(tickL); - }); - } + if (type is Bms.ChannelType.SpBpmDirect or Bms.ChannelType.SpBpmLookup) + { + // Handle BPM changes + float freqDivide; + if (type == Bms.ChannelType.SpBpmLookup && bpmDict.TryGetValue(note, out var bpm)) + freqDivide = bpm; else - { - // Parse other note data - var time = 0f; // num3 - var totalOffset = 0f; // num4 - - var data = dataList.FindAll(d => d["tick"].GetValue() < tick); - for (var j = data.Count - 1; j >= 0; j--) + try { - var obj = data[j]; - var offset = 0f; // num5 - var freq = obj["freq"].GetValue(); // num6 - - if (j - 1 >= 0) - { - var prevObj = data[j - 1]; - offset = prevObj["tick"].GetValue() - obj["tick"].GetValue(); - } - - if (j == 0) offset = tick - obj["tick"].GetValue(); - - var localOffset = totalOffset; // num7 - totalOffset += offset; - var floorOffset = Mathf.FloorToInt(localOffset); // num8 - var ceilOffset = Mathf.CeilToInt(totalOffset); // num9 - - for (var k = floorOffset; k < ceilOffset; k++) - { - var off = 1f; // num10 - - if (k == floorOffset) - off = k + 1 - localOffset; - if (k == ceilOffset - 1) - off = totalOffset - (ceilOffset - 1); - if (ceilOffset == floorOffset + 1) - off = totalOffset - localOffset; - - notePercents.TryGetValue(k, out var node); - var percent = node?["percent"].GetValue() ?? 1f; - time += Mathf.RoundToInt(off * percent * freq / 1E-06f) * 1E-06F; - } + freqDivide = Convert.ToInt32(note, 16); } - - var noteObj = new JsonObject + catch (FormatException) { - { "time", time }, - { "value", note }, - { "tone", typeCode } - }; - notesArray.Add(noteObj); - } + Logger.Warning($"Invalid hex BPM value: {note}"); + continue; + } + + var freq = 60f / freqDivide * 4f; + bpmEntries.Add(new BpmEntry(tick, freq)); + } + else + { + var noteObj = new RawNote( + CalculateNoteTime(tick, bpmEntries, timeSigEntries), + note, + typeCode + ); + rawNotes.Add(noteObj); } } } } + } - var list = notesArray.ToList(); - list.Sort((l, r) => - { - var lTime = l["time"]!.GetValue(); - var rTime = r["time"]!.GetValue(); - var lTone = l["tone"]!.GetValue(); - var rTone = r["tone"]!.GetValue(); + // Validate required headers + if (string.IsNullOrEmpty(info.Genre)) + Logger.Warning($"BMS '{bmsName}' is missing required GENRE header."); + if (info.Bpm <= 0) + Logger.Warning($"BMS '{bmsName}' has no valid BPM."); + + // Sort notes by time, with events (channel "15") sorted before notes at the same timestamp + rawNotes.Sort((l, r) => + { + // Accurate for note sorting up to 6 decimal places + var lScore = (long)(l.Time * 1000000) * 10 + (l.Tone == "15" ? 0 : 1); + var rScore = (long)(r.Time * 1000000) * 10 + (r.Tone == "15" ? 0 : 1); + return lScore.CompareTo(rScore); + }); - // Accurate for note sorting up to 6 decimal places - var lScore = (long)(lTime * 1000000) * 10 + (lTone == "15" ? 0 : 1); - var rScore = (long)(rTime * 1000000) * 10 + (rTone == "15" ? 0 : 1); + var bms = new Bms + { + Info = info, + Notes = rawNotes, + NotesPercent = timeSigEntries.Values.ToList(), + Md5 = stream.GetHash() + }; + bms.Info.Name = bmsName; - return Math.Sign(lScore - rScore); - }); + if (!string.IsNullOrEmpty(info.Banner)) + bms.Info.Banner = "cover/" + info.Banner; - notesArray.Clear(); - list.ForEach(notesArray.Add); + Logger.Msg($"Loaded bms {bmsName}."); - var percentsArray = new JsonArray(); - notePercents.Values.ToList().ForEach(percentsArray.Add); - var bms = new Bms + return bms; + } + + /// + /// Parses a known BMS header into the object. + /// + /// true if the header was recognized and parsed, false otherwise. + private static bool ParseHeader(BmsInfo info, string key, string value, + Dictionary bpmDict, List bpmEntries) + { + // Handle BPMxx headers (BPM, BPM01, BPM02, etc.) + if (key.StartsWith("BPM")) + { + if (!value.TryParseAsFloat(out var bpmValue)) { - Info = info, - Notes = notesArray, - NotesPercent = percentsArray, - Md5 = stream.GetHash() - }; - bms.Info["NAME"] = bmsName; - bms.Info["NEW"] = true; - - if (bms.Info.TryGetPropertyValue("BANNER", out var banner)) - bms.Info["BANNER"] = "cover/" + banner; - else - bms.Info["BANNER"] = "cover/none_cover.png"; + Logger.Warning($"Invalid BPM value: {value}"); + return true; // Recognized but invalid + } - Logger.Msg($"Loaded bms {bmsName}."); + var bpmKey = key.Length > 3 ? key[3..] : "00"; + bpmDict[bpmKey] = bpmValue; - return bms; + if (bpmKey == "00") + { + info.Bpm = bpmValue; + var freq = 60f / bpmValue * 4f; + bpmEntries.Add(new BpmEntry(0f, freq)); + } + + return true; } - /// - /// Transmutes Bms data into StageInfo data. - /// - /// The Bms object to transmute. - /// The transmuted StageInfo object. - internal static StageInfo TransmuteData(Bms bms) + switch (key) { - if (NoteData.Count == 0) InitNoteData(); - MusicDataManager.Clear(); - _delay = 0; + case "PLAYER": + if (value.TryParseAsInt(out var player)) info.Player = player; + return true; + case "GENRE": + info.Genre = value; + return true; + case "TITLE": + info.Title = value; + return true; + case "ARTIST": + info.Artist = value; + return true; + case "LEVELDESIGN": + info.LevelDesign = value; + return true; + case "BANNER": + info.Banner = value; + return true; + case "RANK": + if (value.TryParseAsInt(out var rank)) info.Rank = rank; + return true; + case "LNTYPE": + if (value.TryParseAsInt(out var lnType)) info.LnType = lnType; + return true; + case "PLAYLEVEL": + info.PlayLevel = value; + return true; + default: + return false; + } + } + + /// + /// Calculates the absolute time (in seconds) for a note at a given tick position, + /// accounting for BPM changes and time signature changes. + /// Reverse-engineered from the official game's BMS-to-time conversion. + /// + /// The beat position of the note. + /// All BPM change entries parsed so far, in insertion order. + /// Time signature changes by measure number. + /// Absolute time in seconds. + private static float CalculateNoteTime(float tick, List bpmEntries, + Dictionary timeSigEntries) + { + // Build a sorted (ascending by tick) list of BPM entries that occur before this note + // We must sort because BPM entries may have been added in any order from the BMS data + var relevantBpms = new List(); + foreach (var entry in bpmEntries) + if (entry.Tick < tick) + relevantBpms.Add(entry); + + if (relevantBpms.Count == 0) return 0f; + relevantBpms.Sort(); - var noteData = bms.GetNoteData(); - Logger.Msg("Got note data"); + var accumulatedTime = 0f; + var totalBeatOffset = 0f; - LoadMusicData(noteData); - MusicDataManager.Sort(); + // Walk through BPM segments from earliest to latest + for (var j = 0; j < relevantBpms.Count; j++) + { + var entry = relevantBpms[j]; + var beatFrequency = entry.Freq; - ProcessBossData(bms); - ProcessDelay(bms); - MusicDataManager.Sort(); + // Calculate how many beats this segment covers + float segmentBeats; + if (j < relevantBpms.Count - 1) + segmentBeats = relevantBpms[j + 1].Tick - entry.Tick; + else + segmentBeats = tick - entry.Tick; - ProcessGeminis(); + var previousOffset = totalBeatOffset; + totalBeatOffset += segmentBeats; + var floorBeat = Mathf.FloorToInt(previousOffset); + var ceilBeat = Mathf.CeilToInt(totalBeatOffset); - // Process the delay for each MusicData - foreach (var mData in MusicDataManager.Data) + for (var k = floorBeat; k < ceilBeat; k++) { - mData.tick -= _delay; - mData.showTick = Decimal.Round(mData.tick - mData.dt, 2); - if (mData.isLongPressType) - mData.endIndex -= (int)(_delay / (Decimal)0.001f); + var beatFraction = 1f; + + if (k == floorBeat) + beatFraction = k + 1 - previousOffset; + if (k == ceilBeat - 1) + beatFraction = totalBeatOffset - (ceilBeat - 1); + if (ceilBeat == floorBeat + 1) + beatFraction = totalBeatOffset - previousOffset; + + var timeSigMultiplier = timeSigEntries.TryGetValue(k, out var timeSig) + ? timeSig.Percent + : 1f; + accumulatedTime += + Mathf.RoundToInt(beatFraction * timeSigMultiplier * beatFrequency / 1E-06f) * 1E-06F; } + } - // Transmute the MusicData to a new StageInfo object - var stageInfo = ScriptableObject.CreateInstance(); - stageInfo.musicDatas = new Il2CppSystem.Collections.Generic.List(); - foreach (var musicData in MusicDataManager.Data) - stageInfo.musicDatas.Add(musicData); - stageInfo.delay = _delay; + return accumulatedTime; + } - MusicDataManager.Clear(); - return stageInfo; + /// + /// Transmutes Bms data into StageInfo data. + /// + /// The Bms object to transmute. + /// The transmuted StageInfo object. + internal static StageInfo TransmuteData(Bms bms) + { + var stopwatch = Stopwatch.StartNew(); + + if (NoteData.Count == 0) InitNoteData(); + MusicDataManager.Clear(); + _delay = 0; + + var noteData = bms.GetNoteData(); + Logger.Msg("Got note data"); + + LoadMusicData(noteData); + MusicDataManager.Sort(); + + ProcessBossData(bms); + ProcessDelay(bms); + MusicDataManager.Sort(); + + ProcessGeminis(); + + // Process the delay for each MusicData + foreach (var mData in MusicDataManager.Data) + { + mData.tick -= _delay; + mData.showTick = Decimal.Round(mData.tick - mData.dt, 2); + if (mData.isLongPressType) + mData.endIndex -= (int)(_delay / (Decimal)0.001f); } - private static void InitNoteData() + // Transmute the MusicData to a new StageInfo object + var stageInfo = ScriptableObject.CreateInstance(); + stageInfo.musicDatas = new Il2CppSystem.Collections.Generic.List(); + foreach (var musicData in MusicDataManager.Data) + stageInfo.musicDatas.Add(musicData); + stageInfo.delay = _delay; + + stopwatch.Stop(); + Logger.Msg($"Transmuted BMS in {stopwatch.Elapsed}", false); + + MusicDataManager.Clear(); + return stageInfo; + } + + private static void InitNoteData() + { + foreach (var nData in SingletonScriptableObject.instance.noteDatas) { - foreach (var nData in SingletonScriptableObject.instance.noteDatas) - { - NoteData.TryAdd(nData.uid, nData); - NoteData.TryAdd(Bms.GetNoteDataKey(nData.ibms_id, nData.pathway, nData.speed, nData.scene), nData); + NoteData.TryAdd(nData.uid, nData); + NoteData.TryAdd(Bms.GetNoteDataKey(nData.ibms_id, nData.pathway, nData.speed, nData.scene), nData); - if (nData.GetNoteType() != NoteType.None || string.IsNullOrEmpty(nData.boss_action) || - nData.boss_action == "0") continue; + if (nData.GetNoteType() != NoteType.None || string.IsNullOrEmpty(nData.boss_action) || + nData.boss_action == "0") continue; - BossData.TryAdd(nData.scene, new Dictionary()); - BossData[nData.scene].TryAdd(nData.boss_action, nData); - } + BossData.TryAdd(nData.scene, new Dictionary()); + BossData[nData.scene].TryAdd(nData.boss_action, nData); } + } - private static void LoadMusicData(JsonArray noteData) + private static void LoadMusicData(List noteData) + { + short noteId = 1; + foreach (var note in noteData) { - short noteId = 1; - foreach (var node in noteData) + if (noteId == short.MaxValue) { - if (noteId == short.MaxValue) - { - Logger.Warning( - $"Cannot process full chart, there are too many objects. Max objects is {short.MaxValue}."); - break; - } + Logger.Warning( + $"Cannot process full chart, there are too many objects. Max objects is {short.MaxValue}."); + break; + } - var configData = node.ToMusicConfigData(); - if (configData.time < 0) continue; + var configData = note.ToMusicConfigData(); + if (configData.time < 0) continue; - // Create a new note for each configData - var newNote = Interop.CreateTypeValue(); - newNote.objId = noteId++; - newNote.tick = Decimal.Round(configData.time, 3); - newNote.configData = configData; - newNote.isLongPressEnd = false; - newNote.isLongPressing = false; + // Create a new note for each configData + var newNote = Interop.CreateTypeValue(); + newNote.objId = noteId++; + newNote.tick = Decimal.Round(configData.time, 3); + newNote.configData = configData; + newNote.isLongPressEnd = false; + newNote.isLongPressing = false; - if (NoteData.TryGetValue(newNote.configData.note_uid, out var newNoteData)) - newNote.noteData = newNoteData; + if (NoteData.TryGetValue(newNote.configData.note_uid, out var newNoteData)) + newNote.noteData = newNoteData; - MusicDataManager.Add(newNote); + MusicDataManager.Add(newNote); - // Create ticks for hold notes. If it isn't a hold note, there is no need to continue. - if (!newNote.isLongPressStart) continue; + // Create ticks for hold notes. If it isn't a hold note, there is no need to continue. + if (!newNote.isLongPressStart) continue; - // Calculate the index in which the hold note ends - var endIndex = (int)(Decimal.Round( - newNote.tick + newNote.configData.length - newNote.noteData.left_great_range - - newNote.noteData.left_perfect_range, - 3) / (Decimal)0.001f); + // Calculate the index in which the hold note ends + var endIndex = (int)(Decimal.Round( + newNote.tick + newNote.configData.length - newNote.noteData.left_great_range - + newNote.noteData.left_perfect_range, + 3) / (Decimal)0.001f); - for (var i = 1; i <= newNote.longPressCount; i++) - { - var holdTick = Interop.CreateTypeValue(); - holdTick.objId = noteId++; - holdTick.tick = i == newNote.longPressCount - ? newNote.tick + newNote.configData.length - : newNote.tick + (Decimal)0.1f * i; - holdTick.configData = newNote.configData; - - // ACTUALLY REQUIRED TO WORK - var dataCopy = holdTick.configData; - dataCopy.length = 0; - holdTick.configData = dataCopy; - - holdTick.isLongPressing = i != newNote.longPressCount; - holdTick.isLongPressEnd = i == newNote.longPressCount; - holdTick.noteData = newNote.noteData; - holdTick.longPressPTick = newNote.configData.time; - holdTick.endIndex = endIndex; - - MusicDataManager.Add(holdTick); - } + for (var i = 1; i <= newNote.longPressCount; i++) + { + var holdTick = Interop.CreateTypeValue(); + holdTick.objId = noteId++; + holdTick.tick = i == newNote.longPressCount + ? newNote.tick + newNote.configData.length + : newNote.tick + (Decimal)0.1f * i; + holdTick.configData = newNote.configData; + + // ACTUALLY REQUIRED TO WORK + var dataCopy = holdTick.configData; + dataCopy.length = 0; + holdTick.configData = dataCopy; + + holdTick.isLongPressing = i != newNote.longPressCount; + holdTick.isLongPressEnd = i == newNote.longPressCount; + holdTick.noteData = newNote.noteData; + holdTick.longPressPTick = newNote.configData.time; + holdTick.endIndex = endIndex; + + MusicDataManager.Add(holdTick); } - - Logger.Msg("Loaded music data!"); } - private static void ProcessBossData(Bms bms) - { - var scene = bms.Info["GENRE"]?.GetValue() ?? string.Empty; - var bossData = MusicDataManager.Data.Where(mData => mData.isBossNote).ToList(); + Logger.Msg("Loaded music data!"); + } - // If the boss is not used for some reason, no need to process animations. - if (bossData.Count == 0) return; + private static void ProcessBossData(Bms bms) + { + var scene = bms.Info.Genre; + var bossData = MusicDataManager.Data.Where(mData => mData.isBossNote).ToList(); - // Add a boss exit animation if it is missing. - var finalData = bossData[^1]; - if (AnimStatesRight[finalData.noteData.boss_action] != BossState.OffScreen) + // If the boss is not used for some reason, no need to process animations. + if (bossData.Count == 0) return; + + // Add a boss exit animation if it is missing. + var finalData = bossData[^1]; + if (AnimStatesRight[finalData.noteData.boss_action] != BossState.OffScreen) + { + var finalNote = MusicDataManager.Data[^1]; + var tick = finalNote.tick; + + if (finalNote.isBossNote) { - var finalNote = MusicDataManager.Data[^1]; - var tick = finalNote.tick; + var startDelay = Decimal.Round((Decimal)GetStartDelay(finalNote.noteData.prefab_name), 3); + var duration = (Decimal)GetAnimationDuration(scene, finalNote.noteData.boss_action); + tick += Decimal.Round(-startDelay + duration, 3); + } - if (finalNote.isBossNote) - { - var startDelay = Decimal.Round((Decimal)GetStartDelay(finalNote.noteData.prefab_name), 3); - var duration = (Decimal)GetAnimationDuration(scene, finalNote.noteData.boss_action); - tick += Decimal.Round(-startDelay + duration, 3); - } + tick = Decimal.Round(tick + (Decimal)0.1f, 3); - tick = Decimal.Round(tick + (Decimal)0.1f, 3); + var exitNoteData = BossData[scene]["out"]; - var exitNoteData = BossData[scene]["out"]; + var exitConfig = Interop.CreateTypeValue(); + exitConfig.note_uid = exitNoteData.uid; + exitConfig.time = tick; - var exitConfig = Interop.CreateTypeValue(); - exitConfig.note_uid = exitNoteData.uid; - exitConfig.time = tick; + var exitMusicData = Interop.CreateTypeValue(); + exitMusicData.objId = 0; + exitMusicData.tick = exitConfig.time; + exitMusicData.configData = exitConfig; + exitMusicData.noteData = exitNoteData; - var exitMusicData = Interop.CreateTypeValue(); - exitMusicData.objId = 0; - exitMusicData.tick = exitConfig.time; - exitMusicData.configData = exitConfig; - exitMusicData.noteData = exitNoteData; + MusicDataManager.Add(exitMusicData); + bossData.Add(exitMusicData); + Logger.Msg("Added missing boss exit at " + exitConfig.time); + } - MusicDataManager.Add(exitMusicData); - bossData.Add(exitMusicData); - Logger.Msg("Added missing boss exit at " + exitConfig.time); - } + // Fix incorrect phase gears + NoteConfigData cachedGearConfig = null; + var allNoteData = SingletonScriptableObject.instance.noteDatas; - // Fix incorrect phase gears - var phaseGearConfig = Interop.CreateTypeValue(); - phaseGearConfig.ibms_id = ""; + for (var i = 0; i < bossData.Count; i++) + { + var data = bossData[i]; + + if (data.noteData.GetNoteType() != NoteType.Block) continue; - for (var i = 0; i < bossData.Count; i++) + // Find the next boss animation that is not a gear + MusicData bossAnimAhead = null; + for (var j = i + 1; j < bossData.Count; j++) { - var data = bossData[i]; + var dataAhead = bossData[j]; + if (dataAhead.noteData.GetNoteType() == NoteType.Block) continue; - if (data.noteData.GetNoteType() != NoteType.Block) continue; + bossAnimAhead = dataAhead; + break; + } - // Find the next boss animation that is not a gear - var bossAnimAhead = Interop.CreateTypeValue(); - bossAnimAhead.configData.time = Decimal.MinValue; + var bossAnimBefore = i > 0 ? bossData[i - 1] : null; - for (var j = i + 1; j < bossData.Count; j++) - { - var dataAhead = bossData[j]; - if (dataAhead.noteData.GetNoteType() == NoteType.Block) continue; + var aheadTime = bossAnimAhead?.configData.time ?? Decimal.MinValue; + var beforeTime = bossAnimBefore?.configData.time ?? Decimal.MinValue; - bossAnimAhead = dataAhead; - break; - } + var diffToAhead = Math.Abs((float)data.configData.time - (float)aheadTime); + var diffToBefore = Math.Abs((float)data.configData.time - (float)beforeTime); + var ahead = diffToAhead < diffToBefore; - MusicData bossAnimBefore; - if (i > 0) - { - bossAnimBefore = bossData[i - 1]; - } - else - { - bossAnimBefore = Interop.CreateTypeValue(); - bossAnimBefore.configData.time = Decimal.MinValue; - } + var stateBehind = bossAnimBefore != null + ? AnimStatesRight[bossAnimBefore.noteData.boss_action] + : BossState.OffScreen; - var diffToAhead = Math.Abs((float)data.configData.time - (float)bossAnimAhead.configData.time); - var diffToBefore = Math.Abs((float)data.configData.time - (float)bossAnimBefore.configData.time); - var ahead = diffToAhead < diffToBefore; - - var stateBehind = i > 0 ? AnimStatesRight[bossAnimBefore.noteData.boss_action] : BossState.OffScreen; - var stateAhead = AnimStatesLeft.TryGetValue(bossAnimAhead.noteData.boss_action, out var state) - ? state - : BossState.OffScreen; - var usedState = ahead ? stateAhead : stateBehind; - var correctState = usedState is BossState.Phase1 or BossState.Phase2; - if (!correctState) - { - ahead = !ahead; - usedState = ahead ? stateAhead : stateBehind; - correctState = usedState is BossState.Phase1 or BossState.Phase2; - } + var stateAhead = BossState.OffScreen; + if (bossAnimAhead != null && + AnimStatesLeft.TryGetValue(bossAnimAhead.noteData.boss_action, out var parsedState)) + stateAhead = parsedState; - if (!correctState) continue; - if (usedState is not (BossState.Phase1 or BossState.Phase2)) continue; + var usedState = ahead ? stateAhead : stateBehind; + var correctState = usedState is BossState.Phase1 or BossState.Phase2; + if (!correctState) + { + ahead = !ahead; + usedState = ahead ? stateAhead : stateBehind; + correctState = usedState is BossState.Phase1 or BossState.Phase2; + } - if ((ahead && AnimStatesLeft[data.noteData.boss_action] == usedState) || - (!ahead && AnimStatesRight[data.noteData.boss_action] == usedState)) - continue; + if (!correctState) continue; + if (usedState is not (BossState.Phase1 or BossState.Phase2)) continue; - var phase = usedState == BossState.Phase1 ? 1 : 2; + if ((ahead && AnimStatesLeft[data.noteData.boss_action] == usedState) || + (!ahead && AnimStatesRight[data.noteData.boss_action] == usedState)) + continue; - var noteData = SingletonScriptableObject.instance.noteDatas; - if (phaseGearConfig.ibms_id != data.noteData.ibms_id - || phaseGearConfig.pathway != data.noteData.pathway - || phaseGearConfig.scene != data.noteData.scene - || phaseGearConfig.speed != data.noteData.speed - || !phaseGearConfig.boss_action.StartsWith($"boss_far_atk_{phase}")) - foreach (var d in noteData) - { - if (d.ibms_id != data.noteData.ibms_id - || d.pathway != data.noteData.pathway - || d.scene != data.noteData.scene - || d.speed != data.noteData.speed - || !d.boss_action.StartsWith($"boss_far_atk_{phase}")) continue; - - phaseGearConfig = d; - break; - } + var phase = usedState == BossState.Phase1 ? 1 : 2; - var fixedConfigData = Interop.CreateTypeValue(); - fixedConfigData.blood = data.configData.blood; - fixedConfigData.id = data.configData.id; - fixedConfigData.length = data.configData.length; - fixedConfigData.note_uid = phaseGearConfig.uid; - fixedConfigData.pathway = data.configData.pathway; - fixedConfigData.time = data.configData.time; - - var fixedGear = Interop.CreateTypeValue(); - fixedGear.objId = data.objId; - fixedGear.tick = data.tick; - fixedGear.configData = fixedConfigData; - fixedGear.isLongPressEnd = data.isLongPressEnd; - fixedGear.isLongPressing = data.isLongPressing; - fixedGear.noteData = phaseGearConfig; - - MusicDataManager.Set(fixedGear.objId, fixedGear); - bossData[i] = fixedGear; - Logger.Msg($"Fixed gear at tick {data.tick}."); - } + // Use cached gear config if it matches, otherwise search for a new one + if (cachedGearConfig == null + || cachedGearConfig.ibms_id != data.noteData.ibms_id + || cachedGearConfig.pathway != data.noteData.pathway + || cachedGearConfig.scene != data.noteData.scene + || cachedGearConfig.speed != data.noteData.speed + || !cachedGearConfig.boss_action.StartsWith($"boss_far_atk_{phase}")) + foreach (var d in allNoteData) + { + if (d.ibms_id != data.noteData.ibms_id + || d.pathway != data.noteData.pathway + || d.scene != data.noteData.scene + || d.speed != data.noteData.speed + || !d.boss_action.StartsWith($"boss_far_atk_{phase}")) continue; - // Resolve state changes in list - var bossState = BossState.OffScreen; - for (var i = 0; i < bossData.Count; i++) - { - var anim = bossData[i].noteData.boss_action; - var nextState = AnimStatesLeft[anim]; + cachedGearConfig = d; + break; + } - if (bossState != nextState) - { - var transfer = StateTransferAnims[bossState][nextState]; - var transferNoteData = BossData[scene][transfer]; - var alignment = TransferAlignment[transfer]; + if (cachedGearConfig == null) continue; + var phaseGearConfig = cachedGearConfig; + + var fixedConfigData = Interop.CreateTypeValue(); + fixedConfigData.blood = data.configData.blood; + fixedConfigData.id = data.configData.id; + fixedConfigData.length = data.configData.length; + fixedConfigData.note_uid = phaseGearConfig.uid; + fixedConfigData.pathway = data.configData.pathway; + fixedConfigData.time = data.configData.time; + + var fixedGear = Interop.CreateTypeValue(); + fixedGear.objId = data.objId; + fixedGear.tick = data.tick; + fixedGear.configData = fixedConfigData; + fixedGear.isLongPressEnd = data.isLongPressEnd; + fixedGear.isLongPressing = data.isLongPressing; + fixedGear.noteData = phaseGearConfig; + + MusicDataManager.Set(fixedGear.objId, fixedGear); + bossData[i] = fixedGear; + Logger.Msg($"Fixed gear at tick {data.tick}."); + } - var alignData = alignment == AnimAlignment.Right ? bossData[i] : bossData[i - 1]; + // Resolve state changes in list + var bossState = BossState.OffScreen; + for (var i = 0; i < bossData.Count; i++) + { + var anim = bossData[i].noteData.boss_action; + var nextState = AnimStatesLeft[anim]; - var rightDelay = (Decimal)GetStartDelay(bossData[i].noteData.prefab_name); - var leftDelay = i == 0 ? 0 : (Decimal)GetStartDelay(bossData[i - 1].noteData.prefab_name); - var alignDelay = alignment == AnimAlignment.Left ? leftDelay : rightDelay; + if (bossState != nextState) + { + var transfer = StateTransferAnims[bossState][nextState]; + var transferNoteData = BossData[scene][transfer]; + var alignment = TransferAlignment[transfer]; - var duration = (Decimal)GetAnimationDuration(scene, transfer); + var alignData = alignment == AnimAlignment.Right ? bossData[i] : bossData[i - 1]; - var mConfig = Interop.CreateTypeValue(); - mConfig.note_uid = transferNoteData.uid; - mConfig.time = Decimal.Round(alignData.tick - alignDelay - duration * (int)alignment, 3); + var rightDelay = (Decimal)GetStartDelay(bossData[i].noteData.prefab_name); + var leftDelay = i == 0 ? 0 : (Decimal)GetStartDelay(bossData[i - 1].noteData.prefab_name); + var alignDelay = alignment == AnimAlignment.Left ? leftDelay : rightDelay; - var mData = Interop.CreateTypeValue(); - mData.tick = mConfig.time; - mData.configData = mConfig; - mData.noteData = transferNoteData; + var duration = (Decimal)GetAnimationDuration(scene, transfer); - var tolerance = (Decimal)0.300f; - var fits = alignment switch - { - AnimAlignment.Left => mData.tick + duration < bossData[i].tick - rightDelay + tolerance, - _ => i == 0 || mData.tick > bossData[i - 1].tick - leftDelay - }; + var mConfig = Interop.CreateTypeValue(); + mConfig.note_uid = transferNoteData.uid; + mConfig.time = Decimal.Round(alignData.tick - alignDelay - duration * (int)alignment, 3); - if (!fits) - { - bossState = AnimStatesRight[bossData[i].noteData.boss_action]; - continue; - } + var mData = Interop.CreateTypeValue(); + mData.tick = mConfig.time; + mData.configData = mConfig; + mData.noteData = transferNoteData; - MusicDataManager.Add(mData); - bossData.Insert(i, mData); - i--; - } - else + var tolerance = (Decimal)0.300f; + var fits = alignment switch + { + AnimAlignment.Left => mData.tick + duration < bossData[i].tick - rightDelay + tolerance, + _ => i == 0 || mData.tick > bossData[i - 1].tick - leftDelay + }; + + if (!fits) { bossState = AnimStatesRight[bossData[i].noteData.boss_action]; + continue; } - } - Logger.Msg("Processed boss animations!"); + MusicDataManager.Add(mData); + bossData.Insert(i, mData); + i--; + } + else + { + bossState = AnimStatesRight[bossData[i].noteData.boss_action]; + } } - private static void ProcessDelay(Bms bms) - { - var scene = bms.Info["GENRE"]?.GetValue() ?? string.Empty; - var sceneIndex = scene.Split('_')[1].ParseAsInt(); - var sceneInfo = Singleton.instance.sceneInfo; - var delayCache = new Dictionary(); + Logger.Msg("Processed boss animations!"); + } + + private static void ProcessDelay(Bms bms) + { + var scene = bms.Info.Genre; + var sceneIndex = scene.Split('_')[1].ParseAsInt(); + var sceneInfo = Singleton.instance.sceneInfo; + var delayCache = new Dictionary(); - for (var i = 0; i < MusicDataManager.Data.Count; i++) + for (var i = 0; i < MusicDataManager.Data.Count; i++) + { + var mData = MusicDataManager.Data[i]; + if (!string.IsNullOrEmpty(mData.noteData.ibms_id)) { - var mData = MusicDataManager.Data[i]; - if (!string.IsNullOrEmpty(mData.noteData.ibms_id)) + var type = mData.noteData.GetNoteType(); + if (type == NoteType.SceneChange) sceneIndex = sceneInfo[mData.noteData.ibms_id]; + + var prefabName = mData.noteData.prefab_name; + if (!string.IsNullOrEmpty(prefabName)) { - var type = mData.noteData.GetNoteType(); - if (type == NoteType.SceneChange) sceneIndex = sceneInfo[mData.noteData.ibms_id]; + // If not a pickup type, convert to most recent scene + if (type != NoteType.Hp && type != NoteType.Music) + { + var prefix = prefabName[..2]; + if (prefix is not "00" and not "em" and not "bo") + prefabName = prefabName.Remove(0, 2).Insert(0, $"{sceneIndex:D2}"); + } - var prefabName = mData.noteData.prefab_name; - if (!string.IsNullOrEmpty(prefabName)) + if (!delayCache.ContainsKey(prefabName)) { - // If not a pickup type, convert to most recent scene - if (type != NoteType.Hp && type != NoteType.Music) - { - var prefix = prefabName[..2]; - if (!new[] { "00", "em", "bo" }.Contains(prefix)) - prefabName = prefabName.Remove(0, 2).Insert(0, $"{sceneIndex:D2}"); - } + var gameObject = ResourcesManager.instance.LoadFromName(prefabName); - if (!delayCache.ContainsKey(prefabName)) + if (gameObject != null) { - var gameObject = ResourcesManager.instance.LoadFromName(prefabName); - - if (gameObject != null) - { - var spineActionController = gameObject.GetComponent(); - delayCache[prefabName] = (Decimal)spineActionController.startDelay; - } + var spineActionController = gameObject.GetComponent(); + delayCache[prefabName] = (Decimal)spineActionController.startDelay; } + } - if (delayCache.TryGetValue(prefabName, out var delay)) - { - mData.dt = delay; - MusicDataManager.Set(i, mData); - } + if (delayCache.TryGetValue(prefabName, out var delay)) + { + mData.dt = delay; + MusicDataManager.Set(i, mData); } } - - var showTick = mData.tick - mData.dt; - _delay = showTick < _delay ? showTick : _delay; } - // Round delay - _delay = Decimal.Round(_delay, 3); - Logger.Msg("Processed delay!"); + var showTick = mData.tick - mData.dt; + _delay = showTick < _delay ? showTick : _delay; } - private static void ProcessGeminis() - { - var geminiCache = new Dictionary>(); + // Round delay + _delay = Decimal.Round(_delay, 3); + Logger.Msg("Processed delay!"); + } - for (var i = 1; i < MusicDataManager.Data.Count; i++) - { - var mData = MusicDataManager.Data[i]; - mData.doubleIdx = -1; - MusicDataManager.Set(i, mData); + private static void ProcessGeminis() + { + var geminiCache = new Dictionary>(); - if (mData.noteData.GetNoteType() != NoteType.Monster && mData.noteData.GetNoteType() != NoteType.Hide) - continue; + for (var i = 1; i < MusicDataManager.Data.Count; i++) + { + var mData = MusicDataManager.Data[i]; + mData.doubleIdx = -1; + MusicDataManager.Set(i, mData); - if (geminiCache.TryGetValue(mData.tick, out var geminiList)) - { - var isNoteGemini = Bms.BmsIds[mData.noteData.ibms_id ?? "00"] == Bms.BmsId.Gemini; - var isTargetGemini = false; - var target = Interop.CreateTypeValue(); + if (mData.noteData.GetNoteType() != NoteType.Monster && mData.noteData.GetNoteType() != NoteType.Hide) + continue; - foreach (var gemini in geminiList.Where(gemini => mData.isAir != gemini.isAir)) - { - target = gemini; - isTargetGemini = Bms.BmsIds[gemini.noteData.ibms_id ?? "00"] == Bms.BmsId.Gemini; - - if (isNoteGemini && isTargetGemini) break; - if (!isNoteGemini) break; - } + if (geminiCache.TryGetValue(mData.tick, out var geminiList)) + { + var isNoteGemini = Bms.BmsIds[mData.noteData.ibms_id] == Bms.BmsId.Gemini; + var isTargetGemini = false; + var target = Interop.CreateTypeValue(); - if (target.objId > 0) - { - mData.isDouble = isNoteGemini && isTargetGemini; - mData.doubleIdx = target.objId; - target.isDouble = isNoteGemini && isTargetGemini; - target.doubleIdx = mData.objId; + foreach (var gemini in geminiList.Where(gemini => mData.isAir != gemini.isAir)) + { + target = gemini; + isTargetGemini = Bms.BmsIds[gemini.noteData.ibms_id] == Bms.BmsId.Gemini; - MusicDataManager.Set(mData.objId, mData); - MusicDataManager.Set(target.objId, target); - } + if (isNoteGemini && isTargetGemini) break; + if (!isNoteGemini) break; } - else + + if (target.objId > 0) { - geminiCache[mData.tick] = new List(); - } + mData.isDouble = isNoteGemini && isTargetGemini; + mData.doubleIdx = target.objId; + target.isDouble = isNoteGemini && isTargetGemini; + target.doubleIdx = mData.objId; - geminiCache[mData.tick].Add(mData); + MusicDataManager.Set(mData.objId, mData); + MusicDataManager.Set(target.objId, target); + } + } + else + { + geminiCache[mData.tick] = new List(); } - Logger.Msg("Processed geminis!"); + geminiCache[mData.tick].Add(mData); } - private static float GetStartDelay(string prefab) - { - return ResourcesManager.instance.LoadFromName(prefab).GetComponent() - .startDelay; - } + Logger.Msg("Processed geminis!"); + } - private static float GetAnimationDuration(string scene, string animation) + private static float GetStartDelay(string prefab) + { + if (StartDelayCache.TryGetValue(prefab, out var cached)) + return cached; + + var delay = ResourcesManager.instance.LoadFromName(prefab) + .GetComponent().startDelay; + StartDelayCache[prefab] = delay; + return delay; + } + + private static float GetAnimationDuration(string scene, string animation) + { + var cacheKey = (scene, animation); + if (AnimationDurationCache.TryGetValue(cacheKey, out var cached)) + return cached; + + var controller = ResourcesManager.instance + .LoadFromName(Boss.Instance.BossFestival($"{scene.Split("_")[1]}01_boss")) + .GetComponent(); + var animations = controller.gameObject.GetComponent().skeletonDataAsset + .GetSkeletonData(true).Animations; + + var animName = animation; + foreach (var actionData in controller.actionData) { - var controller = ResourcesManager.instance.LoadFromName(Boss.Instance.BossFestival($"{scene.Split("_")[1]}01_boss")) - .GetComponent(); - var animations = controller.gameObject.GetComponent().skeletonDataAsset - .GetSkeletonData(true).Animations; - - var arr = new SkeletActionData[controller.actionData.Count]; - controller.actionData.CopyTo(arr, 0); - var actionData = new List(arr).Find(dd => dd.name == animation); - - var animName = animation; - if (actionData is { actionIdx: not null } && actionData.actionIdx.Length != 0) + if (actionData.name != animation) continue; + if (actionData.actionIdx is { Length: > 0 }) animName = actionData.actionIdx[0]; - return animations.Find((Il2CppSystem.Predicate)((Animation a) => a.Name == animName)).Duration; + break; } + + var duration = animations + .Find((Il2CppSystem.Predicate)((Animation a) => a.Name == animName)).Duration; + AnimationDurationCache[cacheKey] = duration; + return duration; } } \ No newline at end of file diff --git a/CustomAlbums.csproj b/CustomAlbums.csproj index d7f825e..5474414 100644 --- a/CustomAlbums.csproj +++ b/CustomAlbums.csproj @@ -1,4 +1,4 @@ - + net6.0 enable @@ -9,36 +9,39 @@ true - - - - - - - - - + + + + + + + + + + + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - contentFiles - - - - - + + contentFiles + + + + - - + + - + \ No newline at end of file diff --git a/Data/Album.cs b/Data/Album.cs index 0fae3f4..c135c15 100644 --- a/Data/Album.cs +++ b/Data/Album.cs @@ -1,175 +1,364 @@ -using System.IO.Compression; +using System.Collections; +using System.IO.Compression; +using System.Reflection; using CustomAlbums.Managers; +using CustomAlbums.Patches; using CustomAlbums.Utilities; +using MelonLoader; +using MelonLoader.Utils; using UnityEngine; +using UnityEngine.Networking; using Logger = CustomAlbums.Utilities.Logger; -namespace CustomAlbums.Data +namespace CustomAlbums.Data; + +public class Album { - public class Album + private static readonly Logger Logger = new(nameof(Album)); + private static Sprite _defaultCover; + + private Sprite _cover; + private bool _isCoverLoading; + + public Album(string directory, ZipArchiveEntry mdm, int index, string packName = null) + { + if (!string.IsNullOrEmpty(packName)) PackName = packName; + + using var mdmStream = mdm.Open(); + using var openedZip = new ZipArchive(mdmStream); + + var info = openedZip.GetEntry("info.json"); + if (info == null) + { + Logger.Error($"Could not find info.json in package: {mdm.Name}"); + throw new FileNotFoundException(); + } + + using var stream = info.Open(); + Info = Json.Deserialize(stream); + IsPackaged = true; + + // CurrentPack will always be null if album is not in a pack + IsPack = AlbumManager.CurrentPack != null; + + HasPng = openedZip.GetEntry("cover.png") != null; + + Index = index; + Path = directory; + PackAlbumName = System.IO.Path.GetFileNameWithoutExtension(mdm.Name); + GetSheets(); + } + + public Album(string path, int index, string packName = null) { - private static readonly Logger Logger = new(nameof(Album)); + // If packName is not null then it's a file path, not a folder chart + if (Directory.Exists(path) && packName == null) + { + // Load album from directory + if (!File.Exists($"{path}\\info.json")) + { + Logger.Error($"Could not find info.json at: {path}\\info.json"); + throw new FileNotFoundException(); + } + + using var fileStream = File.OpenRead($"{path}\\info.json"); + Info = Json.Deserialize(fileStream); + HasPng = File.Exists(System.IO.Path.Combine(path, "cover.png")); + } + else if (File.Exists(path)) + { + // Load album from package + PackName = packName; + using var zip = ZipFile.OpenRead(path); + var info = zip.GetEntry("info.json"); + if (info == null) + { + Logger.Error($"Could not find info.json in package: {path}"); + throw new FileNotFoundException(); + } + + using var stream = info.Open(); + Info = Json.Deserialize(stream); + IsPackaged = true; + + // CurrentPack will always be null if album is not in a pack + IsPack = AlbumManager.CurrentPack != null; + + HasPng = zip.GetEntry("cover.png") != null; + } + else + { + Logger.Error($"Could not find album at: {path}"); + throw new FileNotFoundException(); + } + + Index = index; + Path = path; + + GetSheets(); + } + + public string PackAlbumName { get; } = string.Empty; + public int Index { get; internal set; } + public string Path { get; } + public bool IsPackaged { get; } + public bool IsPack { get; } + public bool HasPng { get; } - public Album(string path, int index, string packName = null) + public bool HasCover + { + get { - // If packName is not null then it's a file path, not a folder chart - if (Directory.Exists(path) && packName == null) + if (HasPng) { - // Load album from directory - if (!File.Exists($"{path}\\info.json")) - { - Logger.Error($"Could not find info.json at: {path}\\info.json"); - throw new FileNotFoundException(); - } - - using var fileStream = File.OpenRead($"{path}\\info.json"); - Info = Json.Deserialize(fileStream); + HotReloadManager.PnlStageInstance?.RefreshStageUI(); + return true; } - else if (File.Exists(path)) + + return false; + } + } + + public string PackName { get; } + public AlbumInfo Info { get; } + + public static Sprite DefaultCover + { + get + { + if (_defaultCover != null) return _defaultCover; + + var assembly = Assembly.GetExecutingAssembly(); + using var stream = assembly.GetManifestResourceStream("CustomAlbums.Resources.default_cover.png"); + + Texture2D tex; + if (stream != null) { - // Load album from package - PackName = packName; - using var zip = ZipFile.OpenRead(path); - var info = zip.GetEntry("info.json"); - if (info == null) - { - Logger.Error($"Could not find info.json in package: {path}"); - throw new FileNotFoundException(); - } - - using var stream = info.Open(); - Info = Json.Deserialize(stream); - IsPackaged = true; - - // CurrentPack will always be null if album is not in a pack - IsPack = AlbumManager.CurrentPack != null; + using var ms = new MemoryStream(); + stream.CopyTo(ms); + tex = new Texture2D(2, 2, TextureFormat.ARGB32, false); + tex.LoadImage(ms.ToArray()); } else { - Logger.Error($"Could not find album at: {path}"); - throw new FileNotFoundException(); + tex = new Texture2D(2, 2); } - Index = index; - Path = path; + _defaultCover = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f)); + _defaultCover.hideFlags |= HideFlags.DontUnloadUnusedAsset; + return _defaultCover; + } + } - GetSheets(); + public Sprite Cover + { + get + { + if (_cover != null) return _cover; + if (!_isCoverLoading && HasPng) + { + _isCoverLoading = true; + MelonCoroutines.Start(LoadCoverAsync()); + } + + return DefaultCover; } + } + + public AudioClip Music => this.GetAudio(); + public AudioClip Demo => this.GetAudio("demo"); + public Dictionary Sheets { get; } = new(); + + public string AlbumName => + IsPackaged + ? $"album_{(string.IsNullOrEmpty(PackAlbumName) ? System.IO.Path.GetFileNameWithoutExtension(Path) : string.Empty)}{(PackName != null ? $"{PackAlbumName}_{PackName}" : string.Empty)}" + : $"album_{System.IO.Path.GetFileName(Path)}_folder"; - public int Index { get; } - public string Path { get; } - public bool IsPackaged { get; } - public bool IsPack { get; } - public string PackName { get; } - public AlbumInfo Info { get; } - public Sprite Cover => this.GetCover(); - public AnimatedCover AnimatedCover => this.GetAnimatedCover(); - public AudioClip Music => this.GetAudio(); - public AudioClip Demo => this.GetAudio("demo"); - public Dictionary Sheets { get; } = new(); - public string AlbumName => - IsPackaged ? - $"album_{System.IO.Path.GetFileNameWithoutExtension(Path)}{(PackName != null ? $"_{PackName}" : string.Empty)}" - : $"album_{System.IO.Path.GetFileName(Path)}_folder"; - public string Uid => $"{AlbumManager.Uid}-{Index}"; + public string Uid => $"{AlbumManager.Uid}-{Index}"; - public bool HasFile(string name) + private IEnumerator LoadCoverAsync() + { + string url; + var cacheDir = System.IO.Path.Combine(MelonEnvironment.UserDataDirectory, "CustomAlbums", "Cache"); + if (!Directory.Exists(cacheDir)) Directory.CreateDirectory(cacheDir); + + if (IsPack || IsPackaged) { - if (IsPackaged) + var cacheFile = System.IO.Path.Combine(cacheDir, $"{Uid}_cover.png"); + if (File.Exists(cacheFile)) File.Delete(cacheFile); + + using var stream = OpenNullableStream("cover.png"); + if (stream != null) { - if (!File.Exists(Path)) return false; - try - { - using var zip = ZipFile.OpenRead(Path); - return zip.GetEntry(name) != null; - } - catch (IOException) - { - // This is expected in the case of deleting an album - return false; - } + using var fs = File.OpenWrite(cacheFile); + stream.CopyTo(fs); } - var path = $"{Path}\\{name}"; - return File.Exists(path); + url = "file:///" + cacheFile.Replace('\\', '/'); + } + else + { + url = "file:///" + System.IO.Path.Combine(Path, "cover.png").Replace('\\', '/'); + } + + var request = UnityWebRequestTexture.GetTexture(url); + yield return request.SendWebRequest(); + + if (!request.isHttpError && !request.isNetworkError) + { + var texture = DownloadHandlerTexture.GetContent(request); + texture.wrapMode = TextureWrapMode.MirrorOnce; + _cover = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f)); + _cover.hideFlags |= HideFlags.DontUnloadUnusedAsset; + + AssetPatch.UpdateCache($"{AlbumName}_cover", _cover); + CoverRefreshPatch.Refresh($"{AlbumManager.Uid}-{Index}"); + } + + _isCoverLoading = false; + } + + public bool HasFile(string name) + { + if (IsPack && !string.IsNullOrEmpty(PackAlbumName)) + { + if (!File.Exists(Path)) return false; + try + { + using var mdp = ZipFile.OpenRead(Path); + using var openedMdm = mdp.GetNestedZip(PackAlbumName + ".mdm"); + return openedMdm.GetEntry(name) != null; + } + catch (IOException) + { + return false; + } } - public Stream OpenFileStreamIfPossible(string file) + if (IsPackaged) { - if (IsPackaged) + if (!File.Exists(Path)) return false; + try { using var zip = ZipFile.OpenRead(Path); - var entry = zip.GetEntry(file); + return zip.GetEntry(name) != null; + } + catch (IOException) + { + // This is expected in the case of deleting an album + return false; + } + } - if (entry != null) - { - return entry.Open().ToMemoryStream(); - } + var path = $"{Path}\\{name}"; + return File.Exists(path); + } - Logger.Error($"Could not find file in package: {file}"); - throw new FileNotFoundException(); - } + public Stream OpenFileStreamIfPossible(string file) + { + if (IsPack && !string.IsNullOrEmpty(PackAlbumName)) + { + using var mdp = ZipFile.OpenRead(Path); + using var openedMdm = mdp.GetNestedZip(PackAlbumName + ".mdm"); + var entry = openedMdm.GetEntry(file); - var path = $"{Path}\\{file}"; - if (File.Exists(path)) - return File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + if (entry != null) return entry.Open().ToMemoryStream(); - Logger.Error($"Could not find file: {path}"); + Logger.Error($"Could not find file in package: {file}"); throw new FileNotFoundException(); } - public Stream OpenNullableStream(string file) + if (IsPackaged) { - if (IsPackaged) - { - using var zip = ZipFile.OpenRead(Path); - var entry = zip.GetEntry(file); + using var zip = ZipFile.OpenRead(Path); + var entry = zip.GetEntry(file); - if (entry != null) - { - return entry.Open().ToMemoryStream(); - } + if (entry != null) return entry.Open().ToMemoryStream(); - return null; - } + Logger.Error($"Could not find file in package: {file}"); + throw new FileNotFoundException(); + } + + var path = $"{Path}\\{file}"; + if (File.Exists(path)) + return File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + + Logger.Error($"Could not find file: {path}"); + throw new FileNotFoundException(); + } - var path = $"{Path}\\{file}"; - if (File.Exists(path)) - return File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + public Stream OpenNullableStream(string file) + { + if (IsPack && !string.IsNullOrEmpty(PackAlbumName)) + { + using var mdp = ZipFile.OpenRead(Path); + using var openedMdm = mdp.GetNestedZip(PackAlbumName + ".mdm"); + var entry = openedMdm.GetEntry(file); + + if (entry != null) return entry.Open().ToMemoryStream(); return null; } - public MemoryStream OpenMemoryStream(string file) + if (IsPackaged) { - if (IsPackaged) - { - using var zip = ZipFile.OpenRead(Path); - var entry = zip.GetEntry(file); + using var zip = ZipFile.OpenRead(Path); + var entry = zip.GetEntry(file); - if (entry != null) - { - return entry.Open().ToMemoryStream(); - } + if (entry != null) return entry.Open().ToMemoryStream(); - Logger.Error($"Could not find file in package: {file}"); - throw new FileNotFoundException(); - } + return null; + } + + var path = $"{Path}\\{file}"; + if (File.Exists(path)) + return File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + + return null; + } + + public MemoryStream OpenMemoryStream(string file) + { + if (IsPack && !string.IsNullOrEmpty(PackAlbumName)) + { + using var mdp = ZipFile.OpenRead(Path); + using var openedMdm = mdp.GetNestedZip(PackAlbumName + ".mdm"); + var entry = openedMdm.GetEntry(file); - var path = $"{Path}\\{file}"; - if (File.Exists(path)) - return File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite).ToMemoryStream(); + if (entry != null) return entry.Open().ToMemoryStream(); - Logger.Error($"Could not find file: {path}"); + Logger.Error($"Could not find file in package: {file}"); throw new FileNotFoundException(); } - private void GetSheets() + + if (IsPackaged) { - // Adds to the Sheets dictionary - foreach (var difficulty in Info.Difficulties.Keys.Where(difficulty => HasFile($"map{difficulty}.bms"))) - Sheets.Add(difficulty, new Sheet(this, difficulty)); + using var zip = ZipFile.OpenRead(Path); + var entry = zip.GetEntry(file); + + if (entry != null) return entry.Open().ToMemoryStream(); + + Logger.Error($"Could not find file in package: {file}"); + throw new FileNotFoundException(); } - public bool HasDifficulty(int difficulty) => Sheets.ContainsKey(difficulty); + var path = $"{Path}\\{file}"; + if (File.Exists(path)) + return File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite).ToMemoryStream(); + + Logger.Error($"Could not find file: {path}"); + throw new FileNotFoundException(); + } + + private void GetSheets() + { + // Adds to the Sheets dictionary + foreach (var difficulty in Info.Difficulties.Keys.Where(difficulty => HasFile($"map{difficulty}.bms"))) + Sheets.Add(difficulty, new Sheet(this, difficulty)); + } + + public bool HasDifficulty(int difficulty) + { + return Sheets.ContainsKey(difficulty); } } \ No newline at end of file diff --git a/Data/AlbumInfo.cs b/Data/AlbumInfo.cs index 9fba157..2bf66ee 100644 --- a/Data/AlbumInfo.cs +++ b/Data/AlbumInfo.cs @@ -2,58 +2,58 @@ using CustomAlbums.Utilities; using static CustomAlbums.Data.SceneEgg; -namespace CustomAlbums.Data +namespace CustomAlbums.Data; + +public class AlbumInfo { - public class AlbumInfo + public enum HiddenMode { - public enum HiddenMode - { - Click, - Press, - Toggle - } + Click, + Press, + Toggle + } + + public string Name { get; set; } = string.Empty; - public string Name { get; set; } = string.Empty; - [JsonPropertyName("name_romanized")] - public string NameRomanized { get; set; } = string.Empty; - public string Author { get; set; } = string.Empty; - public string LevelDesigner { get; set; } = string.Empty; - public string LevelDesigner1 { get; set; } = string.Empty; - public string LevelDesigner2 { get; set; } = string.Empty; - public string LevelDesigner3 { get; set; } = string.Empty; - public string LevelDesigner4 { get; set; } = string.Empty; - public string LevelDesigner5 { get; set; } = string.Empty; - public string Bpm { get; set; } = "0"; - public string Scene { get; set; } = "scene_01"; - public SceneEggs SceneEgg { get; set; } = SceneEggs.None; - - public Dictionary Difficulties + [JsonPropertyName("name_romanized")] public string NameRomanized { get; set; } = string.Empty; + + public string Author { get; set; } = string.Empty; + public string LevelDesigner { get; set; } = string.Empty; + public string LevelDesigner1 { get; set; } = string.Empty; + public string LevelDesigner2 { get; set; } = string.Empty; + public string LevelDesigner3 { get; set; } = string.Empty; + public string LevelDesigner4 { get; set; } = string.Empty; + public string LevelDesigner5 { get; set; } = string.Empty; + public string Bpm { get; set; } = "0"; + public string Scene { get; set; } = "scene_01"; + public SceneEggs SceneEgg { get; set; } = SceneEggs.None; + + public Dictionary Difficulties + { + get { - get - { - var dict = new Dictionary(); - if (Difficulty1 != "0") dict.Add(1, Difficulty1); - if (Difficulty2 != "0") dict.Add(2, Difficulty2); - if (Difficulty3 != "0") dict.Add(3, Difficulty3); - if (Difficulty4 != "0") dict.Add(4, Difficulty4); - if (Difficulty5 != "0") dict.Add(5, Difficulty5); - return dict; - } + var dict = new Dictionary(); + if (Difficulty1 != "0") dict.Add(1, Difficulty1); + if (Difficulty2 != "0") dict.Add(2, Difficulty2); + if (Difficulty3 != "0") dict.Add(3, Difficulty3); + if (Difficulty4 != "0") dict.Add(4, Difficulty4); + if (Difficulty5 != "0") dict.Add(5, Difficulty5); + return dict; } + } - public string Difficulty1 { get; set; } = "0"; - public string Difficulty2 { get; set; } = "0"; - public string Difficulty3 { get; set; } = "0"; - public string Difficulty4 { get; set; } = "0"; - public string Difficulty5 { get; set; } = "0"; + public string Difficulty1 { get; set; } = "0"; + public string Difficulty2 { get; set; } = "0"; + public string Difficulty3 { get; set; } = "0"; + public string Difficulty4 { get; set; } = "0"; + public string Difficulty5 { get; set; } = "0"; - public string HideBmsMode { get; set; } = "CLICK"; + public string HideBmsMode { get; set; } = "CLICK"; - [JsonConverter(typeof(Converters.NumberConverter))] - public string HideBmsDifficulty { get; set; } = "0"; + [JsonConverter(typeof(Converters.NumberConverter))] + public string HideBmsDifficulty { get; set; } = "0"; - public string HideBmsMessage { get; set; } = string.Empty; + public string HideBmsMessage { get; set; } = string.Empty; - public string[] SearchTags { get; set; } = Array.Empty(); - } + public string[] SearchTags { get; set; } = Array.Empty(); } \ No newline at end of file diff --git a/Data/AnimatedCover.cs b/Data/AnimatedCover.cs deleted file mode 100644 index 1e92631..0000000 --- a/Data/AnimatedCover.cs +++ /dev/null @@ -1,19 +0,0 @@ -using UnityEngine; - -namespace CustomAlbums.Data -{ - public class AnimatedCover - { - public AnimatedCover(Sprite[] frames, int framesPerSecond) - { - Frames = frames; - FramesPerSecond = framesPerSecond; - } - - public Sprite[] Frames { get; } - public int FrameCount => Frames.Length; - public int Width => Frames[0].texture.width; - public int Height => Frames[0].texture.height; - public int FramesPerSecond { get; } - } -} \ No newline at end of file diff --git a/Data/AssetIdentifiers.cs b/Data/AssetIdentifiers.cs index 9ee879b..9ca56be 100644 --- a/Data/AssetIdentifiers.cs +++ b/Data/AssetIdentifiers.cs @@ -1,17 +1,16 @@ -namespace CustomAlbums.Data +namespace CustomAlbums.Data; + +internal class AssetIdentifiers { - internal class AssetIdentifiers + internal static readonly List AssetSuffixes = new() { - internal static readonly List AssetSuffixes = new() - { - "_demo", - "_music", - "_cover", - "_map1", - "_map2", - "_map3", - "_map4", - "_map5" - }; - } + "_demo", + "_music", + "_cover", + "_map1", + "_map2", + "_map3", + "_map4", + "_map5" + }; } \ No newline at end of file diff --git a/Data/Bms.cs b/Data/Bms.cs index 17fc36b..f7aee96 100644 --- a/Data/Bms.cs +++ b/Data/Bms.cs @@ -1,521 +1,517 @@ -using System.Text.Json.Nodes; using CustomAlbums.Utilities; using Il2CppAssets.Scripts.GameCore.Managers; -using Il2CppAssets.Scripts.PeroTools.Commons; using Il2CppGameLogic; using Il2CppPeroPeroGames.GlobalDefines; +using Decimal = Il2CppSystem.Decimal; -namespace CustomAlbums.Data +namespace CustomAlbums.Data; + +public class Bms { - public class Bms + public enum BmsId { - public enum BmsId - { - None, - Small, - SmallUp, - SmallDown, - Medium1, - Medium1Up, - Medium1Down, - Medium2, - Medium2Up, - Medium2Down, - Large1, - Large2, - Raider, - Hammer, - Gemini, - Hold, - Masher, - Gear, - UpsideDownRaider, - UpsideDownHammer, - Speed1Both, - Speed2Both, - Speed3Both, - Speed1Low, - Speed2Low, - Speed3Low, - Speed1High, - Speed2High, - Speed3High, - Music, - BossMelee1, - BossMelee2, - BossProjectile1, - BossProjectile2, - BossProjectile3, - BossMasher1, - BossMasher2, - BossGear, - BossEntrance, - BossExit, - BossReadyPhase1, - BossEndPhase1, - BossReadyPhase2, - BossEndPhase2, - BossSwapPhase12, - BossSwapPhase21, - HideNotes, - UnhideNotes, - HideBoss, - UnhideBoss, - SceneSwitchSpaceStation, - SceneSwitchRetroCity, - SceneSwitchCastle, - SceneSwitchRainyNight, - SceneSwitchOriental, - SceneSwitchGrooveCoaster, - SceneSwitchTouhou, - SceneSwitchDjmax, - SceneSwitchMiku, - PItem, - Ghost, - Heart, - Note, - HideBackground, - UnhideBackground, - ScreenScrollUp, - ScreenScrollDown, - ScreenScrollOff, - ScanlineRipplesOn, - ScanlineRipplesOff, - ChromaticAberrationOn, - ChromaticAberrationOff, - VignetteOn, - VignetteOff, - TvStaticOn, - TvStaticOff, - FlashbangStart, - FlashbangMid, - FlashbangEnd, - BgStopOn, - BgStopOff, - MosaicOn, - MosaicOff, - SepiaOn, - SepiaOff, - MediumBullet, - MediumBulletUp, - MediumBulletDown, - MediumBulletLaneShift, - SmallBullet, - SmallBulletUp, - SmallBulletDown, - SmallBulletLaneShift, - LargeBullet, - LargeBulletUp, - LargeBulletDown, - LargeBulletLaneShift, - BossBullet1, - BossBullet1LaneShift, - BossBullet2, - BossBullet2LaneShift - } + None, + Small, + SmallUp, + SmallDown, + Medium1, + Medium1Up, + Medium1Down, + Medium2, + Medium2Up, + Medium2Down, + Large1, + Large2, + Raider, + Hammer, + Gemini, + Hold, + Masher, + Gear, + UpsideDownRaider, + UpsideDownHammer, + Speed1Both, + Speed2Both, + Speed3Both, + Speed1Low, + Speed2Low, + Speed3Low, + Speed1High, + Speed2High, + Speed3High, + Music, + BossMelee1, + BossMelee2, + BossProjectile1, + BossProjectile2, + BossProjectile3, + BossMasher1, + BossMasher2, + BossGear, + BossEntrance, + BossExit, + BossReadyPhase1, + BossEndPhase1, + BossReadyPhase2, + BossEndPhase2, + BossSwapPhase12, + BossSwapPhase21, + HideNotes, + UnhideNotes, + HideBoss, + UnhideBoss, + SceneSwitchSpaceStation, + SceneSwitchRetroCity, + SceneSwitchCastle, + SceneSwitchRainyNight, + SceneSwitchOriental, + SceneSwitchGrooveCoaster, + SceneSwitchTouhou, + SceneSwitchDjmax, + SceneSwitchMiku, + PItem, + Ghost, + Heart, + Note, + HideBackground, + UnhideBackground, + ScreenScrollUp, + ScreenScrollDown, + ScreenScrollOff, + ScanlineRipplesOn, + ScanlineRipplesOff, + ChromaticAberrationOn, + ChromaticAberrationOff, + VignetteOn, + VignetteOff, + TvStaticOn, + TvStaticOff, + FlashbangStart, + FlashbangMid, + FlashbangEnd, + BgStopOn, + BgStopOff, + MosaicOn, + MosaicOff, + SepiaOn, + SepiaOff, + MediumBullet, + MediumBulletUp, + MediumBulletDown, + MediumBulletLaneShift, + SmallBullet, + SmallBulletUp, + SmallBulletDown, + SmallBulletLaneShift, + LargeBullet, + LargeBulletUp, + LargeBulletDown, + LargeBulletLaneShift, + BossBullet1, + BossBullet1LaneShift, + BossBullet2, + BossBullet2LaneShift + } - [Flags] - public enum ChannelType - { - /// - /// Channel does not support anything. - /// - None = 0, - - /// - /// Channel supports the Ground Lane. - /// - Ground = 1, - - /// - /// Channel supports the Air Lane. - /// - Air = 2, - - /// - /// Channel supports standard events. - /// - Event = 4, - - /// - /// Channel supports scene events. - /// - Scene = 8, - - /// - /// Notes in this channel become Heart Enemies if possible. - /// - SpBlood = 16, - - /// - /// Notes in this channel become Tap Holds if possible. - /// - SpTapHolds = 32, - - /// - /// This channel is used for BPM changes, with the value directly present. - /// - SpBpmDirect = 64, - - /// - /// This channel is used for BPM changes, with the value placed in a lookup table. - /// - SpBpmLookup = 128, - - /// - /// This channel is used for time signature changes. - /// - SpTimesig = 256, - - /// - /// This channel is used for scroll speed changes. - /// - SpScroll = 512 - } + [Flags] + public enum ChannelType + { + /// + /// Channel does not support anything. + /// + None = 0, + + /// + /// Channel supports the Ground Lane. + /// + Ground = 1, + + /// + /// Channel supports the Air Lane. + /// + Air = 2, + + /// + /// Channel supports standard events. + /// + Event = 4, + + /// + /// Channel supports scene events. + /// + Scene = 8, + + /// + /// Notes in this channel become Heart Enemies if possible. + /// + SpBlood = 16, + + /// + /// Notes in this channel become Tap Holds if possible. + /// + SpTapHolds = 32, + + /// + /// This channel is used for BPM changes, with the value directly present. + /// + SpBpmDirect = 64, + + /// + /// This channel is used for BPM changes, with the value placed in a lookup table. + /// + SpBpmLookup = 128, + + /// + /// This channel is used for time signature changes. + /// + SpTimesig = 256, + + /// + /// This channel is used for scroll speed changes. + /// + SpScroll = 512 + } - public static readonly Dictionary Channels = new() - { - ["01"] = ChannelType.Scene, - ["02"] = ChannelType.SpTimesig, - ["03"] = ChannelType.SpBpmDirect, - ["08"] = ChannelType.SpBpmLookup, - ["15"] = ChannelType.Event, - ["16"] = ChannelType.Event, - ["18"] = ChannelType.Event, - ["11"] = ChannelType.Air | ChannelType.Event, - ["13"] = ChannelType.Air | ChannelType.Event, - ["31"] = ChannelType.Air | ChannelType.Event | ChannelType.SpBlood, - ["33"] = ChannelType.Air | ChannelType.Event | ChannelType.SpBlood, - ["51"] = ChannelType.Air | ChannelType.Event, - ["53"] = ChannelType.Air | ChannelType.Event, - ["D1"] = ChannelType.Air | ChannelType.Event | ChannelType.SpTapHolds, - ["D3"] = ChannelType.Air | ChannelType.Event | ChannelType.SpTapHolds, - ["12"] = ChannelType.Ground | ChannelType.Event, - ["14"] = ChannelType.Ground | ChannelType.Event, - ["32"] = ChannelType.Ground | ChannelType.Event | ChannelType.SpBlood, - ["34"] = ChannelType.Ground | ChannelType.Event | ChannelType.SpBlood, - ["52"] = ChannelType.Ground | ChannelType.Event, - ["54"] = ChannelType.Ground | ChannelType.Event, - ["D2"] = ChannelType.Ground | ChannelType.Event | ChannelType.SpTapHolds, - ["D4"] = ChannelType.Ground | ChannelType.Event | ChannelType.SpTapHolds, - ["SC"] = ChannelType.SpScroll - }; - - public static readonly Dictionary BmsIds = new() - { - ["00"] = BmsId.None, - ["01"] = BmsId.Small, - ["02"] = BmsId.SmallUp, - ["03"] = BmsId.SmallDown, - ["04"] = BmsId.Medium1, - ["05"] = BmsId.Medium1Up, - ["06"] = BmsId.Medium1Down, - ["07"] = BmsId.Medium2, - ["08"] = BmsId.Medium2Up, - ["09"] = BmsId.Medium2Down, - ["0A"] = BmsId.Large1, - ["0B"] = BmsId.Large2, - ["0C"] = BmsId.Raider, - ["0D"] = BmsId.Hammer, - ["0E"] = BmsId.Gemini, - ["0F"] = BmsId.Hold, - ["0G"] = BmsId.Masher, - ["0H"] = BmsId.Gear, - ["0I"] = BmsId.UpsideDownRaider, - ["0J"] = BmsId.UpsideDownHammer, - ["0O"] = BmsId.Speed1Both, - ["0P"] = BmsId.Speed2Both, - ["0Q"] = BmsId.Speed3Both, - ["0R"] = BmsId.Speed1Low, - ["0S"] = BmsId.Speed2Low, - ["0T"] = BmsId.Speed3Low, - ["0U"] = BmsId.Speed1High, - ["0V"] = BmsId.Speed2High, - ["0W"] = BmsId.Speed3High, - ["10"] = BmsId.Music, - ["11"] = BmsId.BossMelee1, - ["12"] = BmsId.BossMelee2, - ["13"] = BmsId.BossProjectile1, - ["14"] = BmsId.BossProjectile2, - ["15"] = BmsId.BossProjectile3, - ["16"] = BmsId.BossMasher1, - ["17"] = BmsId.BossMasher2, - ["18"] = BmsId.BossGear, - ["1A"] = BmsId.BossEntrance, - ["1B"] = BmsId.BossExit, - ["1C"] = BmsId.BossReadyPhase1, - ["1D"] = BmsId.BossEndPhase1, - ["1E"] = BmsId.BossReadyPhase2, - ["1F"] = BmsId.BossEndPhase2, - ["1G"] = BmsId.BossSwapPhase12, - ["1H"] = BmsId.BossSwapPhase21, - ["1J"] = BmsId.HideNotes, - ["1K"] = BmsId.UnhideNotes, - ["1L"] = BmsId.HideBoss, - ["1M"] = BmsId.UnhideBoss, - ["1O"] = BmsId.SceneSwitchSpaceStation, - ["1P"] = BmsId.SceneSwitchRetroCity, - ["1Q"] = BmsId.SceneSwitchCastle, - ["1R"] = BmsId.SceneSwitchRainyNight, - ["1S"] = BmsId.SceneSwitchOriental, - ["1T"] = BmsId.SceneSwitchGrooveCoaster, - ["1U"] = BmsId.SceneSwitchTouhou, - ["1V"] = BmsId.SceneSwitchDjmax, - ["1X"] = BmsId.SceneSwitchMiku, - ["20"] = BmsId.PItem, - ["21"] = BmsId.Ghost, - ["22"] = BmsId.Heart, - ["23"] = BmsId.Note, - ["25"] = BmsId.HideBackground, - ["26"] = BmsId.UnhideBackground, - ["27"] = BmsId.ScreenScrollUp, - ["28"] = BmsId.ScreenScrollDown, - ["29"] = BmsId.ScreenScrollOff, - ["2A"] = BmsId.ScanlineRipplesOn, - ["2B"] = BmsId.ScanlineRipplesOff, - ["2C"] = BmsId.ChromaticAberrationOn, - ["2D"] = BmsId.ChromaticAberrationOff, - ["2E"] = BmsId.VignetteOn, - ["2F"] = BmsId.VignetteOff, - ["2G"] = BmsId.TvStaticOn, - ["2H"] = BmsId.TvStaticOff, - ["2I"] = BmsId.FlashbangStart, - ["2J"] = BmsId.FlashbangMid, - ["2K"] = BmsId.FlashbangEnd, - ["2N"] = BmsId.BgStopOn, - ["2O"] = BmsId.BgStopOff, - ["2P"] = BmsId.MosaicOn, - ["2Q"] = BmsId.MosaicOff, - ["2R"] = BmsId.SepiaOn, - ["2S"] = BmsId.SepiaOff, - ["30"] = BmsId.MediumBullet, - ["31"] = BmsId.MediumBulletUp, - ["32"] = BmsId.MediumBulletDown, - ["33"] = BmsId.MediumBulletLaneShift, - ["34"] = BmsId.SmallBullet, - ["35"] = BmsId.SmallBulletUp, - ["36"] = BmsId.SmallBulletDown, - ["37"] = BmsId.SmallBulletLaneShift, - ["38"] = BmsId.LargeBullet, - ["39"] = BmsId.LargeBulletUp, - ["3A"] = BmsId.LargeBulletDown, - ["3B"] = BmsId.LargeBulletLaneShift, - ["3C"] = BmsId.BossBullet1, - ["3D"] = BmsId.BossBullet1LaneShift, - ["3E"] = BmsId.BossBullet2, - ["3F"] = BmsId.BossBullet2LaneShift - }; - - public JsonObject Info { get; set; } - public JsonArray Notes { get; set; } - public JsonArray NotesPercent { get; set; } - public string Md5 { get; set; } - public Dictionary NoteData { get; set; } - - public float Bpm + public static readonly Dictionary Channels = new() + { + ["01"] = ChannelType.Scene, + ["02"] = ChannelType.SpTimesig, + ["03"] = ChannelType.SpBpmDirect, + ["08"] = ChannelType.SpBpmLookup, + ["15"] = ChannelType.Event, + ["16"] = ChannelType.Event, + ["18"] = ChannelType.Event, + ["11"] = ChannelType.Air | ChannelType.Event, + ["13"] = ChannelType.Air | ChannelType.Event, + ["31"] = ChannelType.Air | ChannelType.Event | ChannelType.SpBlood, + ["33"] = ChannelType.Air | ChannelType.Event | ChannelType.SpBlood, + ["51"] = ChannelType.Air | ChannelType.Event, + ["53"] = ChannelType.Air | ChannelType.Event, + ["D1"] = ChannelType.Air | ChannelType.Event | ChannelType.SpTapHolds, + ["D3"] = ChannelType.Air | ChannelType.Event | ChannelType.SpTapHolds, + ["12"] = ChannelType.Ground | ChannelType.Event, + ["14"] = ChannelType.Ground | ChannelType.Event, + ["32"] = ChannelType.Ground | ChannelType.Event | ChannelType.SpBlood, + ["34"] = ChannelType.Ground | ChannelType.Event | ChannelType.SpBlood, + ["52"] = ChannelType.Ground | ChannelType.Event, + ["54"] = ChannelType.Ground | ChannelType.Event, + ["D2"] = ChannelType.Ground | ChannelType.Event | ChannelType.SpTapHolds, + ["D4"] = ChannelType.Ground | ChannelType.Event | ChannelType.SpTapHolds, + ["SC"] = ChannelType.SpScroll + }; + + public static readonly Dictionary BmsIds = new() + { + ["00"] = BmsId.None, + ["01"] = BmsId.Small, + ["02"] = BmsId.SmallUp, + ["03"] = BmsId.SmallDown, + ["04"] = BmsId.Medium1, + ["05"] = BmsId.Medium1Up, + ["06"] = BmsId.Medium1Down, + ["07"] = BmsId.Medium2, + ["08"] = BmsId.Medium2Up, + ["09"] = BmsId.Medium2Down, + ["0A"] = BmsId.Large1, + ["0B"] = BmsId.Large2, + ["0C"] = BmsId.Raider, + ["0D"] = BmsId.Hammer, + ["0E"] = BmsId.Gemini, + ["0F"] = BmsId.Hold, + ["0G"] = BmsId.Masher, + ["0H"] = BmsId.Gear, + ["0I"] = BmsId.UpsideDownRaider, + ["0J"] = BmsId.UpsideDownHammer, + ["0O"] = BmsId.Speed1Both, + ["0P"] = BmsId.Speed2Both, + ["0Q"] = BmsId.Speed3Both, + ["0R"] = BmsId.Speed1Low, + ["0S"] = BmsId.Speed2Low, + ["0T"] = BmsId.Speed3Low, + ["0U"] = BmsId.Speed1High, + ["0V"] = BmsId.Speed2High, + ["0W"] = BmsId.Speed3High, + ["10"] = BmsId.Music, + ["11"] = BmsId.BossMelee1, + ["12"] = BmsId.BossMelee2, + ["13"] = BmsId.BossProjectile1, + ["14"] = BmsId.BossProjectile2, + ["15"] = BmsId.BossProjectile3, + ["16"] = BmsId.BossMasher1, + ["17"] = BmsId.BossMasher2, + ["18"] = BmsId.BossGear, + ["1A"] = BmsId.BossEntrance, + ["1B"] = BmsId.BossExit, + ["1C"] = BmsId.BossReadyPhase1, + ["1D"] = BmsId.BossEndPhase1, + ["1E"] = BmsId.BossReadyPhase2, + ["1F"] = BmsId.BossEndPhase2, + ["1G"] = BmsId.BossSwapPhase12, + ["1H"] = BmsId.BossSwapPhase21, + ["1J"] = BmsId.HideNotes, + ["1K"] = BmsId.UnhideNotes, + ["1L"] = BmsId.HideBoss, + ["1M"] = BmsId.UnhideBoss, + ["1O"] = BmsId.SceneSwitchSpaceStation, + ["1P"] = BmsId.SceneSwitchRetroCity, + ["1Q"] = BmsId.SceneSwitchCastle, + ["1R"] = BmsId.SceneSwitchRainyNight, + ["1S"] = BmsId.SceneSwitchOriental, + ["1T"] = BmsId.SceneSwitchGrooveCoaster, + ["1U"] = BmsId.SceneSwitchTouhou, + ["1V"] = BmsId.SceneSwitchDjmax, + ["1X"] = BmsId.SceneSwitchMiku, + ["20"] = BmsId.PItem, + ["21"] = BmsId.Ghost, + ["22"] = BmsId.Heart, + ["23"] = BmsId.Note, + ["25"] = BmsId.HideBackground, + ["26"] = BmsId.UnhideBackground, + ["27"] = BmsId.ScreenScrollUp, + ["28"] = BmsId.ScreenScrollDown, + ["29"] = BmsId.ScreenScrollOff, + ["2A"] = BmsId.ScanlineRipplesOn, + ["2B"] = BmsId.ScanlineRipplesOff, + ["2C"] = BmsId.ChromaticAberrationOn, + ["2D"] = BmsId.ChromaticAberrationOff, + ["2E"] = BmsId.VignetteOn, + ["2F"] = BmsId.VignetteOff, + ["2G"] = BmsId.TvStaticOn, + ["2H"] = BmsId.TvStaticOff, + ["2I"] = BmsId.FlashbangStart, + ["2J"] = BmsId.FlashbangMid, + ["2K"] = BmsId.FlashbangEnd, + ["2N"] = BmsId.BgStopOn, + ["2O"] = BmsId.BgStopOff, + ["2P"] = BmsId.MosaicOn, + ["2Q"] = BmsId.MosaicOff, + ["2R"] = BmsId.SepiaOn, + ["2S"] = BmsId.SepiaOff, + ["30"] = BmsId.MediumBullet, + ["31"] = BmsId.MediumBulletUp, + ["32"] = BmsId.MediumBulletDown, + ["33"] = BmsId.MediumBulletLaneShift, + ["34"] = BmsId.SmallBullet, + ["35"] = BmsId.SmallBulletUp, + ["36"] = BmsId.SmallBulletDown, + ["37"] = BmsId.SmallBulletLaneShift, + ["38"] = BmsId.LargeBullet, + ["39"] = BmsId.LargeBulletUp, + ["3A"] = BmsId.LargeBulletDown, + ["3B"] = BmsId.LargeBulletLaneShift, + ["3C"] = BmsId.BossBullet1, + ["3D"] = BmsId.BossBullet1LaneShift, + ["3E"] = BmsId.BossBullet2, + ["3F"] = BmsId.BossBullet2LaneShift + }; + + public BmsInfo Info { get; set; } + public List Notes { get; set; } + public List NotesPercent { get; set; } + public string Md5 { get; set; } + public Dictionary NoteData { get; set; } + + public float Bpm => Info.Bpm; + + public static string GetNoteDataKey(string bmsId, int pathway, int speed, string scene) + { + return $"{bmsId}-{pathway}-{speed}-{scene}"; + } + + public void InitNoteData() + { + NoteData = new Dictionary(); + + // NOTE: PPG has officially gone mad. The new scene 12 does not appear in the scene list, so we can't really use the scene list anymore. + Span sceneSpan = stackalloc int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }; + + foreach (var config in NoteDataMananger.instance.noteDatas) { - get + // Ignore april fools variants (these are handled elsewhere) + if (config.IsAprilFools()) continue; + // Ignore phase 2 boss gears + if (config.IsPhase2BossGear()) continue; + + // Scene setting of "0" is a wildcard + var anyScene = config.IsAnyScene(); + // Notes with these values are extremely likely to be events, and get registered to all pathways + var anyPathway = config.IsAnyPathway(); + // Boss type, None type, boss mashers, and events get registered to all speeds + var anySpeed = config.IsAnySpeed(); + + var speeds = new List { config.speed }; + var scenes = new List { config.scene }; + var pathways = new List { config.pathway }; + + // Use all speeds if any speed + if (anySpeed) speeds = new List { 1, 2, 3 }; + // Use all scenes if any scene + if (anyScene) { - var bpmString = Info["BPM"]?.GetValue() ?? Info["BPM01"]?.GetValue() ?? string.Empty; - return bpmString.TryParseAsFloat(out var bpm) ? bpm : 0f; + scenes = new List(); + + foreach (var scene in sceneSpan) + { + // Special handling for collectibles in touhou scene + if (config.GetNoteType() is NoteType.Hp or NoteType.Music && scene == 8) + continue; + + var sceneSuffix = scene.ToString().PadLeft(2, '0'); + scenes.Add($"scene_{sceneSuffix}"); + } } - } - public static string GetNoteDataKey(string bmsId, int pathway, int speed, string scene) - { - return $"{bmsId}-{pathway}-{speed}-{scene}"; + // Use all pathways if any pathway + if (anyPathway) pathways = new List { 0, 1 }; + + foreach (var key in pathways.SelectMany(pathway => speeds.SelectMany(speed => + scenes.Select(scene => GetNoteDataKey(config.ibms_id, pathway, speed, scene))))) + NoteData.TryAdd(key, config); } + } + + public Il2CppSystem.Collections.Generic.List GetSceneEvents() + { + var sceneEvents = new Il2CppSystem.Collections.Generic.List(); - public void InitNoteData() + foreach (var note in Notes) { - NoteData = new Dictionary(); + if (string.IsNullOrEmpty(note.Value)) continue; - // NOTE: PPG has officially gone mad. The new scene 12 does not appear in the scene list, so we can't really use the scene list anymore. - Span sceneSpan = stackalloc int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }; + if (!Channels.TryGetValue(note.Tone, out var channel)) continue; - foreach (var config in NoteDataMananger.instance.noteDatas) - { - // Ignore april fools variants (these are handled elsewhere) - if (config.IsAprilFools()) continue; - // Ignore phase 2 boss gears - if (config.IsPhase2BossGear()) continue; - - // Scene setting of "0" is a wildcard - var anyScene = config.IsAnyScene(); - // Notes with these values are extremely likely to be events, and get registered to all pathways - var anyPathway = config.IsAnyPathway(); - // Boss type, None type, boss mashers, and events get registered to all speeds - var anySpeed = config.IsAnySpeed(); - - var speeds = new List { config.speed }; - var scenes = new List { config.scene }; - var pathways = new List { config.pathway }; - - // Use all speeds if any speed - if (anySpeed) speeds = new List { 1, 2, 3 }; - // Use all scenes if any scene - if (anyScene) + if (channel.HasFlag(ChannelType.Scene)) + sceneEvents.Add(new SceneEvent { - scenes = new List(); - - foreach (var scene in sceneSpan) - { - // Special handling for collectibles in touhou scene - if (config.GetNoteType() is NoteType.Hp or NoteType.Music && scene == 8) - continue; + time = (Decimal)(float)(decimal)note.Time, + uid = $"SceneEvent/{note.Value}" + }); + else if (channel.HasFlag(ChannelType.SpBpmDirect) || channel.HasFlag(ChannelType.SpBpmLookup)) + sceneEvents.Add(new SceneEvent + { + time = (Decimal)(float)(decimal)note.Time, + uid = "SceneEvent/OnBPMChanged", + value = note.Value + }); + } - var sceneSuffix = scene.ToString().PadLeft(2, '0'); - scenes.Add($"scene_{sceneSuffix}"); - } - } + return sceneEvents; + } - // Use all pathways if any pathway - if (anyPathway) pathways = new List { 0, 1 }; + public List GetNoteData() + { + if (NoteData is null || NoteData.Count == 0) InitNoteData(); + var processed = new List(); - foreach (var key in pathways.SelectMany(pathway => speeds.SelectMany(speed => - scenes.Select(scene => GetNoteDataKey(config.ibms_id, pathway, speed, scene))))) - NoteData.TryAdd(key, config); - } - } + var speedAir = Info.Player; + var speedGround = speedAir; - public Il2CppSystem.Collections.Generic.List GetSceneEvents() - { - var sceneEvents = new Il2CppSystem.Collections.Generic.List(); + var objectId = 1; - foreach (var note in Notes) - { - var bmsKey = note["value"]?.GetValue() ?? string.Empty; - if (string.IsNullOrEmpty(bmsKey)) continue; + // Track which note indices have been consumed as hold-note endpoints + var consumedIndices = new HashSet(); - var channelTone = note["tone"]?.GetValue() ?? string.Empty; - if (!Channels.TryGetValue(channelTone, out var channel)) continue; + for (var i = 0; i < Notes.Count; i++) + { + if (consumedIndices.Contains(i)) continue; - if (channel.HasFlag(ChannelType.Scene)) - sceneEvents.Add(new SceneEvent - { - time = note["time"].GetValueAsIl2CppDecimal(), - uid = $"SceneEvent/{bmsKey}" - }); - else if (channel.HasFlag(ChannelType.SpBpmDirect) || channel.HasFlag(ChannelType.SpBpmLookup)) - sceneEvents.Add(new SceneEvent - { - time = note["time"].GetValueAsIl2CppDecimal(), - uid = "SceneEvent/OnBPMChanged", - value = note["value"]?.GetValue() ?? string.Empty - }); - } + var note = Notes[i]; - return sceneEvents; - } + var bmsKey = note.Value; + if (string.IsNullOrEmpty(bmsKey)) continue; - public JsonArray GetNoteData() - { - if (NoteData is null || NoteData.Count == 0) InitNoteData(); - var processed = new JsonArray(); + var bmsId = BmsIds.GetValueOrDefault(bmsKey, BmsId.None); + var channel = note.Tone; + var channelType = Channels.GetValueOrDefault(channel, ChannelType.None); - var speedAir = (Info["PLAYER"]?.GetValue() ?? "1").ParseAsInt(); - var speedGround = speedAir; + // Handle lane type + var pathway = -1; + if (channelType.HasFlag(ChannelType.Air)) + pathway = 1; + else if (channelType.HasFlag(ChannelType.Ground) || channelType.HasFlag(ChannelType.Event)) + pathway = 0; - var objectId = 1; + if (pathway == -1) continue; - for (var i = 0; i < Notes.Count; i++) + // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault + // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault + switch (bmsId) { - var note = Notes[i]; - if (note is null) continue; - - var bmsKey = note["value"]?.GetValue() ?? "00"; - var bmsId = BmsIds.GetValueOrDefault(bmsKey, BmsId.None); - var channel = note["tone"]?.GetValue() ?? string.Empty; - var channelType = Channels.GetValueOrDefault(channel, ChannelType.None); - - // Handle lane type - var pathway = -1; - if (channelType.HasFlag(ChannelType.Air)) - pathway = 1; - else if (channelType.HasFlag(ChannelType.Ground) || channelType.HasFlag(ChannelType.Event)) - pathway = 0; - - if (pathway == -1) continue; - - // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault - // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault - switch (bmsId) - { - // Handle speed changes - case BmsId.Speed1Both: - speedGround = 1; - speedAir = 1; - break; - case BmsId.Speed2Both: - speedGround = 2; - speedAir = 2; - break; - case BmsId.Speed3Both: - speedGround = 3; - speedAir = 3; - break; - case BmsId.Speed1Low: - speedGround = 1; - break; - case BmsId.Speed1High: - speedAir = 1; - break; - case BmsId.Speed2Low: - speedGround = 2; - break; - case BmsId.Speed2High: - speedAir = 2; - break; - case BmsId.Speed3Low: - speedGround = 3; - break; - case BmsId.Speed3High: - speedAir = 3; - break; - } + // Handle speed changes + case BmsId.Speed1Both: + speedGround = 1; + speedAir = 1; + break; + case BmsId.Speed2Both: + speedGround = 2; + speedAir = 2; + break; + case BmsId.Speed3Both: + speedGround = 3; + speedAir = 3; + break; + case BmsId.Speed1Low: + speedGround = 1; + break; + case BmsId.Speed1High: + speedAir = 1; + break; + case BmsId.Speed2Low: + speedGround = 2; + break; + case BmsId.Speed2High: + speedAir = 2; + break; + case BmsId.Speed3Low: + speedGround = 3; + break; + case BmsId.Speed3High: + speedAir = 3; + break; + } - var speed = pathway == 1 ? speedAir : speedGround; - var scene = Info["GENRE"]?.GetValue(); + var speed = pathway == 1 ? speedAir : speedGround; + var scene = Info.Genre; - if (!NoteData!.TryGetValue(GetNoteDataKey(bmsKey, pathway, speed, scene), out var configData)) - continue; + if (!NoteData!.TryGetValue(GetNoteDataKey(bmsKey, pathway, speed, scene), out var configData)) + continue; - var time = note["time"]?.GetValueAsDecimal() ?? 0M; + var time = (decimal)note.Time; - // Hold note & masher - var holdLength = 0M; - var isHold = configData.GetNoteType() is NoteType.Press or NoteType.Mul; - if (isHold) - { - if (channelType.HasFlag(ChannelType.SpTapHolds)) - holdLength = 0.001M; - else - for (var j = i + 1; j < Notes.Count; j++) - { - var holdEndNote = Notes[j]; - var holdEndTime = holdEndNote?["time"]?.GetValueAsDecimal() ?? 0M; - var holdEndBmsKey = holdEndNote?["value"]?.GetValue() ?? string.Empty; - var holdEndChannel = holdEndNote?["tone"]?.GetValue() ?? string.Empty; - - if (holdEndBmsKey != bmsKey || holdEndChannel != channel) continue; - holdLength = holdEndTime - time; - Notes[j]!["value"] = ""; - break; - } - } + // Hold note & masher + var holdLength = 0M; + var isHold = configData.GetNoteType() is NoteType.Press or NoteType.Mul; + if (isHold) + { + if (channelType.HasFlag(ChannelType.SpTapHolds)) + holdLength = 0.001M; + else + for (var j = i + 1; j < Notes.Count; j++) + { + if (consumedIndices.Contains(j)) continue; - processed.Add(new JsonObject - { - ["id"] = objectId++, - ["time"] = time, - ["note_uid"] = configData.uid, - ["length"] = holdLength, - ["pathway"] = pathway, - ["blood"] = !isHold && channelType.HasFlag(ChannelType.SpBlood) - }); + var holdEndNote = Notes[j]; + var holdEndTime = (decimal)holdEndNote.Time; + var holdEndBmsKey = holdEndNote.Value; + var holdEndChannel = holdEndNote.Tone; + + if (holdEndBmsKey != bmsKey || holdEndChannel != channel) continue; + holdLength = holdEndTime - time; + consumedIndices.Add(j); + break; + } } - return processed; + processed.Add(new ProcessedNote( + objectId++, + time, + configData.uid, + holdLength, + pathway, + !isHold && channelType.HasFlag(ChannelType.SpBlood) + )); } + + return processed; } } \ No newline at end of file diff --git a/Data/BmsInfo.cs b/Data/BmsInfo.cs new file mode 100644 index 0000000..9e5770e --- /dev/null +++ b/Data/BmsInfo.cs @@ -0,0 +1,43 @@ +namespace CustomAlbums.Data; + +public class BmsInfo +{ + /// BMS chart title (e.g. song name). Parsed from #TITLE header. + public string Title { get; set; } = string.Empty; + + /// Artist name. Parsed from #ARTIST header. + public string Artist { get; set; } = string.Empty; + + /// Scene identifier (e.g. "scene_08"). Parsed from #GENRE header. + public string Genre { get; set; } = string.Empty; + + /// Banner image path. Parsed from #BANNER header, prefixed with "cover/". + public string Banner { get; set; } = "cover/none_cover.png"; + + /// Chart name. Set by the loader, not from a BMS header. + public string Name { get; set; } = string.Empty; + + /// Player/speed lane mode. Parsed from #PLAYER header. Defaults to 1. + public int Player { get; set; } = 1; + + /// Base BPM of the chart. Parsed from #BPM header. + public float Bpm { get; set; } + + /// Difficulty rank (1-4). Parsed from #RANK header. + public int Rank { get; set; } + + /// Long note type. Parsed from #LNTYPE header. + public int LnType { get; set; } + + /// Level designer name. Parsed from #LEVELDESIGN header. + public string LevelDesign { get; set; } = string.Empty; + + /// Play level. Parsed from #PLAYLEVEL header. + public string PlayLevel { get; set; } = string.Empty; + + /// + /// Fallback storage for any unrecognized BMS headers. + /// Key is the header name (without #), value is the full string value. + /// + public Dictionary Extra { get; } = new(); +} \ No newline at end of file diff --git a/Data/BmsStates.cs b/Data/BmsStates.cs index a079e80..7126c89 100644 --- a/Data/BmsStates.cs +++ b/Data/BmsStates.cs @@ -1,109 +1,108 @@ -namespace CustomAlbums.Data +namespace CustomAlbums.Data; + +internal class BmsStates { - internal class BmsStates + public enum AnimAlignment { - public enum AnimAlignment - { - Left = -1, - Right = 1 - } + Left = -1, + Right = 1 + } - public enum BossState - { - OffScreen, - Idle, - Phase1, - Phase2 - } + public enum BossState + { + OffScreen, + Idle, + Phase1, + Phase2 + } - public static readonly Dictionary AnimStatesLeft = new() - { - { "in", BossState.OffScreen }, - { "out", BossState.Idle }, - { "boss_close_atk_1", BossState.Idle }, - { "boss_close_atk_2", BossState.Idle }, - { "multi_atk_48", BossState.Idle }, - { "multi_atk_48_end", BossState.Idle }, - { "boss_far_atk_1_L", BossState.Phase1 }, - { "boss_far_atk_1_R", BossState.Phase1 }, - { "boss_far_atk_2", BossState.Phase2 }, - { "boss_far_atk_1_start", BossState.Idle }, - { "boss_far_atk_2_start", BossState.Idle }, - { "boss_far_atk_1_end", BossState.Phase1 }, - { "boss_far_atk_2_end", BossState.Phase2 }, - { "atk_1_to_2", BossState.Phase1 }, - { "atk_2_to_1", BossState.Phase2 } - }; + public static readonly Dictionary AnimStatesLeft = new() + { + { "in", BossState.OffScreen }, + { "out", BossState.Idle }, + { "boss_close_atk_1", BossState.Idle }, + { "boss_close_atk_2", BossState.Idle }, + { "multi_atk_48", BossState.Idle }, + { "multi_atk_48_end", BossState.Idle }, + { "boss_far_atk_1_L", BossState.Phase1 }, + { "boss_far_atk_1_R", BossState.Phase1 }, + { "boss_far_atk_2", BossState.Phase2 }, + { "boss_far_atk_1_start", BossState.Idle }, + { "boss_far_atk_2_start", BossState.Idle }, + { "boss_far_atk_1_end", BossState.Phase1 }, + { "boss_far_atk_2_end", BossState.Phase2 }, + { "atk_1_to_2", BossState.Phase1 }, + { "atk_2_to_1", BossState.Phase2 } + }; - public static readonly Dictionary AnimStatesRight = new() - { - { "in", BossState.Idle }, - { "out", BossState.OffScreen }, - { "boss_close_atk_1", BossState.Idle }, - { "boss_close_atk_2", BossState.Idle }, - { "multi_atk_48", BossState.Idle }, - { "multi_atk_48_end", BossState.OffScreen }, - { "boss_far_atk_1_L", BossState.Phase1 }, - { "boss_far_atk_1_R", BossState.Phase1 }, - { "boss_far_atk_2", BossState.Phase2 }, - { "boss_far_atk_1_start", BossState.Phase1 }, - { "boss_far_atk_2_start", BossState.Phase2 }, - { "boss_far_atk_1_end", BossState.Idle }, - { "boss_far_atk_2_end", BossState.Idle }, - { "atk_1_to_2", BossState.Phase2 }, - { "atk_2_to_1", BossState.Phase1 } - }; + public static readonly Dictionary AnimStatesRight = new() + { + { "in", BossState.Idle }, + { "out", BossState.OffScreen }, + { "boss_close_atk_1", BossState.Idle }, + { "boss_close_atk_2", BossState.Idle }, + { "multi_atk_48", BossState.Idle }, + { "multi_atk_48_end", BossState.OffScreen }, + { "boss_far_atk_1_L", BossState.Phase1 }, + { "boss_far_atk_1_R", BossState.Phase1 }, + { "boss_far_atk_2", BossState.Phase2 }, + { "boss_far_atk_1_start", BossState.Phase1 }, + { "boss_far_atk_2_start", BossState.Phase2 }, + { "boss_far_atk_1_end", BossState.Idle }, + { "boss_far_atk_2_end", BossState.Idle }, + { "atk_1_to_2", BossState.Phase2 }, + { "atk_2_to_1", BossState.Phase1 } + }; - public static readonly Dictionary> StateTransferAnims = new() + public static readonly Dictionary> StateTransferAnims = new() + { { + BossState.OffScreen, new Dictionary { - BossState.OffScreen, new Dictionary - { - { BossState.OffScreen, "0" }, - { BossState.Idle, "in" }, - { BossState.Phase1, "boss_far_atk_1_start" }, - { BossState.Phase2, "boss_far_atk_2_start" } - } - }, + { BossState.OffScreen, "0" }, + { BossState.Idle, "in" }, + { BossState.Phase1, "boss_far_atk_1_start" }, + { BossState.Phase2, "boss_far_atk_2_start" } + } + }, + { + BossState.Idle, new Dictionary { - BossState.Idle, new Dictionary - { - { BossState.OffScreen, "out" }, - { BossState.Idle, "0" }, - { BossState.Phase1, "boss_far_atk_1_start" }, - { BossState.Phase2, "boss_far_atk_2_start" } - } - }, + { BossState.OffScreen, "out" }, + { BossState.Idle, "0" }, + { BossState.Phase1, "boss_far_atk_1_start" }, + { BossState.Phase2, "boss_far_atk_2_start" } + } + }, + { + BossState.Phase1, new Dictionary { - BossState.Phase1, new Dictionary - { - { BossState.OffScreen, "out" }, - { BossState.Idle, "boss_far_atk_1_end" }, - { BossState.Phase1, "0" }, - { BossState.Phase2, "atk_1_to_2" } - } - }, + { BossState.OffScreen, "out" }, + { BossState.Idle, "boss_far_atk_1_end" }, + { BossState.Phase1, "0" }, + { BossState.Phase2, "atk_1_to_2" } + } + }, + { + BossState.Phase2, new Dictionary { - BossState.Phase2, new Dictionary - { - { BossState.OffScreen, "out" }, - { BossState.Idle, "boss_far_atk_2_end" }, - { BossState.Phase1, "atk_2_to_1" }, - { BossState.Phase2, "0" } - } + { BossState.OffScreen, "out" }, + { BossState.Idle, "boss_far_atk_2_end" }, + { BossState.Phase1, "atk_2_to_1" }, + { BossState.Phase2, "0" } } - }; + } + }; - public static readonly Dictionary TransferAlignment = new() - { - { "in", AnimAlignment.Right }, - { "out", AnimAlignment.Left }, - { "boss_far_atk_1_start", AnimAlignment.Right }, - { "boss_far_atk_2_start", AnimAlignment.Right }, - { "boss_far_atk_1_end", AnimAlignment.Left }, - { "boss_far_atk_2_end", AnimAlignment.Left }, - { "atk_1_to_2", AnimAlignment.Right }, - { "atk_2_to_1", AnimAlignment.Right } - }; - } + public static readonly Dictionary TransferAlignment = new() + { + { "in", AnimAlignment.Right }, + { "out", AnimAlignment.Left }, + { "boss_far_atk_1_start", AnimAlignment.Right }, + { "boss_far_atk_2_start", AnimAlignment.Right }, + { "boss_far_atk_1_end", AnimAlignment.Left }, + { "boss_far_atk_2_end", AnimAlignment.Left }, + { "atk_1_to_2", AnimAlignment.Right }, + { "atk_2_to_1", AnimAlignment.Right } + }; } \ No newline at end of file diff --git a/Data/BmsTypes.cs b/Data/BmsTypes.cs new file mode 100644 index 0000000..4816159 --- /dev/null +++ b/Data/BmsTypes.cs @@ -0,0 +1,21 @@ +namespace CustomAlbums.Data; + +public readonly record struct BpmEntry(float Tick, float Freq) : IComparable +{ + public int CompareTo(BpmEntry other) + { + return Tick.CompareTo(other.Tick); + } +} + +public readonly record struct TimeSigEntry(int Beat, float Percent); + +public readonly record struct RawNote(float Time, string Value, string Tone); + +public readonly record struct ProcessedNote( + int Id, + decimal Time, + string NoteUid, + decimal Length, + int Pathway, + bool Blood); \ No newline at end of file diff --git a/Data/ChartSave.cs b/Data/ChartSave.cs index 324dc58..e95f210 100644 --- a/Data/ChartSave.cs +++ b/Data/ChartSave.cs @@ -1,14 +1,13 @@ -namespace CustomAlbums.Data +namespace CustomAlbums.Data; + +public class ChartSave { - public class ChartSave - { - public int Evaluate { get; set; } = 0; - public int Score { get; set; } = 0; - public int Combo { get; set; } = 0; - public float Accuracy { get; set; } = 0f; - public string AccuracyStr { get; set; } = "0"; - public float Clear { get; set; } = 0f; - public int FailCount { get; set; } = 0; - public bool Passed { get; set; } = false; - } + public int Evaluate { get; set; } = 0; + public int Score { get; set; } = 0; + public int Combo { get; set; } = 0; + public float Accuracy { get; set; } = 0f; + public string AccuracyStr { get; set; } = "0"; + public float Clear { get; set; } = 0f; + public int FailCount { get; set; } = 0; + public bool Passed { get; set; } = false; } \ No newline at end of file diff --git a/Data/CustomAlbumsSave.cs b/Data/CustomAlbumsSave.cs index aa408b3..8ac14ff 100644 --- a/Data/CustomAlbumsSave.cs +++ b/Data/CustomAlbumsSave.cs @@ -1,26 +1,25 @@ -namespace CustomAlbums.Data +namespace CustomAlbums.Data; + +public class CustomAlbumsSave { - public class CustomAlbumsSave - { - public string SelectedAlbum { get; set; } = string.Empty; - public float Ability { get; set; } = 0; - public HashSet UnlockedMasters { get; set; } = new(); - public HashSet Collections { get; set; } = new(); - public HashSet Hides { get; set; } = new(); - public List History { get; set; } = new(); - public Dictionary> Highest { get; set; } = new(); - public Dictionary> FullCombo { get; set; } = new(); + public string SelectedAlbum { get; set; } = string.Empty; + public float Ability { get; set; } = 0; + public HashSet UnlockedMasters { get; set; } = new(); + public HashSet Collections { get; set; } = new(); + public HashSet Hides { get; set; } = new(); + public List History { get; set; } = new(); + public Dictionary> Highest { get; set; } = new(); + public Dictionary> FullCombo { get; set; } = new(); - internal bool IsEmpty() - { - return SelectedAlbum == string.Empty - && Ability == 0 - && UnlockedMasters.Count == 0 - && Collections.Count == 0 - && Hides.Count == 0 - && History.Count == 0 - && Highest.Count == 0 - && FullCombo.Count == 0; - } + internal bool IsEmpty() + { + return SelectedAlbum == string.Empty + && Ability == 0 + && UnlockedMasters.Count == 0 + && Collections.Count == 0 + && Hides.Count == 0 + && History.Count == 0 + && Highest.Count == 0 + && FullCombo.Count == 0; } } \ No newline at end of file diff --git a/Data/Pack.cs b/Data/Pack.cs index b6957c7..40a5511 100644 --- a/Data/Pack.cs +++ b/Data/Pack.cs @@ -1,14 +1,52 @@ -using CustomAlbums.Managers; +using System.IO.Compression; +using CustomAlbums.Managers; +using CustomAlbums.Utilities; -namespace CustomAlbums.Data +namespace CustomAlbums.Data; + +public class Pack { - public class Pack + internal int Length; + + internal int StartIndex; + public string Title { get; set; } = AlbumManager.GetCustomAlbumsTitle(); + public string TitleColorHex { get; set; } = "#ffffff"; + public bool LongTextScroll { get; set; } = false; + + public string Path { get; set; } = string.Empty; + public List Albums { get; set; } = new(); + + public bool HasFile(string name) + { + if (string.IsNullOrEmpty(Path)) return false; + + try + { + using var zip = ZipFile.OpenRead(Path); + return zip.GetEntry(name) != null; + } + catch + { + return false; + } + } + + public Stream OpenNullableStream(string file) { - public string Title { get; set; } = AlbumManager.GetCustomAlbumsTitle(); - public string TitleColorHex { get; set; } = "#ffffff"; - public bool LongTextScroll { get; set; } = false; + if (string.IsNullOrEmpty(Path)) return null; + + try + { + using var zip = ZipFile.OpenRead(Path); + var entry = zip.GetEntry(file); + + if (entry != null) return entry.Open().ToMemoryStream(); - internal int StartIndex; - internal int Length; + return null; + } + catch + { + return null; + } } -} +} \ No newline at end of file diff --git a/Data/SceneEggs.cs b/Data/SceneEggs.cs index e29a451..e7fb969 100644 --- a/Data/SceneEggs.cs +++ b/Data/SceneEggs.cs @@ -1,20 +1,19 @@ -namespace CustomAlbums.Data +namespace CustomAlbums.Data; + +public class SceneEgg { - public class SceneEgg + public enum SceneEggs { - public enum SceneEggs - { - None = -1, - Cytus = 0, - Wacca = 1, - Queen = 2, - Touhou = 3, - Arknights = 4, - Miku = 5, - RinLen = 6, - BlueArchive = 7, - BadApple = 129, - Christmas = 999 - } + None = -1, + Cytus = 0, + Wacca = 1, + Queen = 2, + Touhou = 3, + Arknights = 4, + Miku = 5, + RinLen = 6, + BlueArchive = 7, + BadApple = 129, + Christmas = 999 } } \ No newline at end of file diff --git a/Data/Sheet.cs b/Data/Sheet.cs index 9fcb073..72db3ad 100644 --- a/Data/Sheet.cs +++ b/Data/Sheet.cs @@ -1,81 +1,92 @@ -using System.Text.Json.Nodes; +using System.Text.Json.Nodes; using CustomAlbums.Utilities; using Il2CppAssets.Scripts.Database; using Il2CppAssets.Scripts.GameCore; using Il2CppAssets.Scripts.Structs; -namespace CustomAlbums.Data +namespace CustomAlbums.Data; + +public class Sheet { - public class Sheet + private static readonly Logger Logger = new(nameof(Sheet)); + private string _md5; + + public Sheet(Album parentAlbum, int difficulty) { - private static readonly Logger Logger = new(nameof(Sheet)); + ParentAlbum = parentAlbum; + Difficulty = difficulty; + MapName = $"{parentAlbum.AlbumName}_map{difficulty}"; + } - public Sheet(Album parentAlbum, int difficulty) - { - ParentAlbum = parentAlbum; - Difficulty = difficulty; - MapName = $"{parentAlbum.AlbumName}_map{difficulty}"; - } + public Album ParentAlbum { get; } + public string MapName { get; } + public int Difficulty { get; } + public bool TalkFileVersion2 { get; set; } - public Album ParentAlbum { get; } - public string MapName { get; } - public int Difficulty { get; } - public bool TalkFileVersion2 { get; set; } - public string Md5 + public string Md5 + { + get { - get + if (!string.IsNullOrEmpty(_md5)) return _md5; + try { using var stream = ParentAlbum.OpenMemoryStream($"map{Difficulty}.bms"); - return stream.GetHash(); + if (stream != null) _md5 = stream.GetHash(); + } + catch (Exception e) + { + Logger.Warning($"Could not compute MD5 for {MapName}: {e.Message}"); } + + return _md5 ?? string.Empty; } + } - public StageInfo GetStage() - { - // If opening a FileStream is possible (i.e. reading from a folder) then open it as FileStream - // Otherwise open it as a MemoryStream - // This allows writing to the map BMS file while it is being read - using var stream = ParentAlbum.OpenFileStreamIfPossible($"map{Difficulty}.bms"); + public StageInfo GetStage() + { + // If opening a FileStream is possible (i.e. reading from a folder) then open it as FileStream + // Otherwise open it as a MemoryStream + // This allows writing to the map BMS file while it is being read + using var stream = ParentAlbum.OpenFileStreamIfPossible($"map{Difficulty}.bms"); - var bms = BmsLoader.Load(stream, MapName); - if (bms is null) return null; + var bms = BmsLoader.Load(stream, MapName); + if (bms is null) return null; - var stageInfo = BmsLoader.TransmuteData(bms); - stageInfo.mapName = MapName; - stageInfo.scene = bms.Info["GENRE"]?.GetValue() ?? string.Empty; - stageInfo.music = $"{ParentAlbum.Index}"; - stageInfo.difficulty = Difficulty; - stageInfo.bpm = bms.Bpm; - stageInfo.md5 = Md5; - stageInfo.sceneEvents = bms.GetSceneEvents(); - stageInfo.name = ParentAlbum.Info.Name; + var stageInfo = BmsLoader.TransmuteData(bms); + stageInfo.mapName = MapName; + stageInfo.scene = bms.Info.Genre; + stageInfo.music = ParentAlbum.Uid; + stageInfo.difficulty = Difficulty; + stageInfo.bpm = bms.Bpm; + stageInfo.md5 = Md5; + stageInfo.sceneEvents = bms.GetSceneEvents(); + stageInfo.name = ParentAlbum.Info.Name; - if (ParentAlbum.HasFile($"map{Difficulty}.talk")) + if (ParentAlbum.HasFile($"map{Difficulty}.talk")) + { + using var talkStream = ParentAlbum.OpenFileStreamIfPossible($"map{Difficulty}.talk"); + if (talkStream.Length > 0) { - using var talkStream = ParentAlbum.OpenFileStreamIfPossible($"map{Difficulty}.talk"); - if (talkStream.Length > 0) + var talkFile = Json.Deserialize(talkStream); + if (talkFile.TryGetPropertyValue("version", out var node) && node?.GetValue() == 2) { - var talkFile = Json.Deserialize(talkStream); - if (talkFile.TryGetPropertyValue("version", out var node) && node?.GetValue() == 2) - { - Logger.Msg("Version 2 talk file!"); - TalkFileVersion2 = true; - } - - talkFile.Remove("version"); - var dict = Json - .Il2CppJsonDeserialize< - Il2CppSystem.Collections.Generic.Dictionary>>(talkFile.ToJsonString()); - stageInfo.dialogEvents = - new Il2CppSystem.Collections.Generic.Dictionary>(); - foreach (var dialogEvent in dict) stageInfo.dialogEvents.Add(dialogEvent.Key, dialogEvent.Value); + Logger.Msg("Version 2 talk file!"); + TalkFileVersion2 = true; } - } - GlobalDataBase.dbStageInfo.SetStageInfo(stageInfo); - return stageInfo; + talkFile.Remove("version"); + var dict = Json + .Il2CppJsonDeserialize< + Il2CppSystem.Collections.Generic.Dictionary>>(talkFile.ToJsonString()); + stageInfo.dialogEvents = + new Il2CppSystem.Collections.Generic.Dictionary>(); + foreach (var dialogEvent in dict) stageInfo.dialogEvents.Add(dialogEvent.Key, dialogEvent.Value); + } } + + GlobalDataBase.dbStageInfo.SetStageInfo(stageInfo); + return stageInfo; } } \ No newline at end of file diff --git a/Data/Talk.cs b/Data/Talk.cs index 949f268..12a049f 100644 --- a/Data/Talk.cs +++ b/Data/Talk.cs @@ -1,28 +1,27 @@ using Il2CppAssets.Scripts.Structs; -namespace CustomAlbums.Data +namespace CustomAlbums.Data; + +public class Talk { - public class Talk - { - public int Version { get; set; } = 1; - public Il2CppSystem.Collections.Generic.List English { get; set; } - public Il2CppSystem.Collections.Generic.List ChineseS { get; set; } - public Il2CppSystem.Collections.Generic.List ChineseT { get; set; } - public Il2CppSystem.Collections.Generic.List Japanese { get; set; } - public Il2CppSystem.Collections.Generic.List Korean { get; set; } + public int Version { get; set; } = 1; + public Il2CppSystem.Collections.Generic.List English { get; set; } + public Il2CppSystem.Collections.Generic.List ChineseS { get; set; } + public Il2CppSystem.Collections.Generic.List ChineseT { get; set; } + public Il2CppSystem.Collections.Generic.List Japanese { get; set; } + public Il2CppSystem.Collections.Generic.List Korean { get; set; } - public Il2CppSystem.Collections.Generic.Dictionary>.Enumerator GetEnumerator() - { - var dict = - new Il2CppSystem.Collections.Generic.Dictionary>(5); - dict["English"] = English; - dict["ChineseS"] = ChineseS; - dict["ChineseT"] = ChineseT; - dict["Japanese"] = Japanese; - dict["Korean"] = Korean; - return dict.GetEnumerator(); - } + public Il2CppSystem.Collections.Generic.Dictionary>. + Enumerator GetEnumerator() + { + var dict = + new Il2CppSystem.Collections.Generic.Dictionary>(5); + dict["English"] = English; + dict["ChineseS"] = ChineseS; + dict["ChineseT"] = ChineseT; + dict["Japanese"] = Japanese; + dict["Korean"] = Korean; + return dict.GetEnumerator(); } } \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index 8f8cf5d..eb640a3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -20,13 +20,13 @@ $(MelonAssemblies)\Assembly-CSharp.dll + Include="Assembly-CSharp-firstpass, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"> false false $(MelonAssemblies)\Assembly-CSharp-firstpass.dll + Include="Il2Cppcom.prpr.log, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"> false false $(MelonAssemblies)\Il2Cppcom.prpr.log.dll @@ -37,25 +37,25 @@ $(MelonAssemblies)\Il2CppDOTween.dll + Include="Il2CppGoogle.Protobuf, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"> false false $(MelonAssemblies)\Il2CppGoogle.Protobuf.dll + Include="Il2CppInterop.Common, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"> false false $(MelonNET6)\Il2CppInterop.Common.dll + Include="Il2CppInterop.Generator, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"> false false $(MelonNET6)\Il2CppInterop.Generator.dll + Include="Il2CppInterop.Runtime, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"> false false $(MelonNET6)\Il2CppInterop.Runtime.dll @@ -71,7 +71,7 @@ $(MelonAssemblies)\Il2CppNAudio.dll + Include="Il2CppNewtonsoft.Json, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"> false false $(MelonAssemblies)\Il2CppNewtonsoft.Json.dll @@ -87,25 +87,25 @@ $(MelonAssemblies)\Il2CppPeroTools2.dll + Include="Il2CppPeroTools2.Account, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"> false false $(MelonAssemblies)\Il2CppPeroTools2.Account.dll + Include="Il2CppPeroTools2.Resources, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"> false false $(MelonAssemblies)\Il2CppPeroTools2.Resources.dll + Include="Il2CppSirenix.Serialization, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"> false false $(MelonAssemblies)\Il2CppSirenix.Serialization.dll + Include="Il2Cppspine-unity, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"> false false $(MelonAssemblies)\Il2Cppspine-unity.dll @@ -116,13 +116,13 @@ $(MelonAssemblies)\Il2CppSystem.dll + Include="Il2CppSystem.Core, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"> false false $(MelonAssemblies)\Il2CppSystem.Core.dll + Include="Il2CppSystem.Runtime.InteropServices, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"> false false $(MelonAssemblies)\Il2CppSystem.Runtime.InteropServices.dll @@ -133,37 +133,37 @@ $(MelonNET6)\MelonLoader.dll + Include="Unity.Addressables, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"> false false $(MelonAssemblies)\Unity.Addressables.dll + Include="Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"> false false $(MelonAssemblies)\Unity.ResourceManager.dll + Include="UnityEngine.AnimationModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"> false false $(MelonAssemblies)\UnityEngine.AnimationModule.dll + Include="UnityEngine.AudioModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"> false false $(MelonAssemblies)\UnityEngine.AudioModule.dll + Include="UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"> false false $(MelonAssemblies)\UnityEngine.CoreModule.dll + Include="UnityEngine.ImageConversionModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"> false false $(MelonAssemblies)\UnityEngine.ImageConversionModule.dll @@ -174,10 +174,16 @@ $(MelonAssemblies)\UnityEngine.UI.dll + Include="UnityEngine.UnityWebRequestModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"> false false $(MelonAssemblies)\UnityEngine.UnityWebRequestModule.dll + + false + false + $(MelonAssemblies)\UnityEngine.UnityWebRequestTextureModule.dll + \ No newline at end of file diff --git a/ILRepack.targets b/ILRepack.targets index b189cb7..e971865 100644 --- a/ILRepack.targets +++ b/ILRepack.targets @@ -9,7 +9,6 @@ - diff --git a/Main.cs b/Main.cs index 4c995ba..59d5849 100644 --- a/Main.cs +++ b/Main.cs @@ -1,63 +1,51 @@ -using CustomAlbums.Managers; +using CustomAlbums.Managers; using CustomAlbums.Patches; using CustomAlbums.Utilities; using MelonLoader; -using static CustomAlbums.Patches.AnimatedCoverPatch; +using MelonLoader.Utils; -namespace CustomAlbums +namespace CustomAlbums; + +public class Main : MelonMod { - public class Main : MelonMod + public const string MelonName = "CustomAlbums"; + public const string MelonAuthor = "Two Fellas"; + public const string MelonVersion = "4.2.0"; + private static readonly Logger Logger = new("CustomAlbums"); + + public override void OnInitializeMelon() + { + base.OnInitializeMelon(); + + if (!Directory.Exists(AlbumManager.SearchPath)) Directory.CreateDirectory(AlbumManager.SearchPath); + + var cacheDir = Path.Combine(MelonEnvironment.UserDataDirectory, "CustomAlbums", "Cache"); + if (Directory.Exists(cacheDir)) Directory.Delete(cacheDir, true); + + ModSettings.Register(); + AssetPatch.AttachHook(); + SavePatch.AttachHook(); + AlbumManager.LoadAlbums(); + SaveManager.LoadSaveFile(); + Logger.Msg("Initialized CustomAlbums!", false); + } + + public override void OnLateInitializeMelon() + { + base.OnLateInitializeMelon(); + HotReloadManager.OnLateInitializeMelon(); + } + + public override void OnFixedUpdate() + { + base.OnFixedUpdate(); + HotReloadManager.FixedUpdate(); + } + + public override void OnDeinitializeMelon() { - private static readonly Logger Logger = new("CustomAlbums"); - - public const string MelonName = "CustomAlbums"; - public const string MelonAuthor = "Two Fellas"; - public const string MelonVersion = "4.1.9"; - - public override void OnInitializeMelon() - { - base.OnInitializeMelon(); - - if (!Directory.Exists(AlbumManager.SearchPath)) Directory.CreateDirectory(AlbumManager.SearchPath); - - ModSettings.Register(); - AssetPatch.AttachHook(); - SavePatch.AttachHook(); - AlbumManager.LoadAlbums(); - SaveManager.LoadSaveFile(); - Logger.Msg("Initialized CustomAlbums!", false); - } - - public override void OnLateInitializeMelon() - { - base.OnLateInitializeMelon(); - // TODO: Actually write HotReload - // HotReloadManager.OnLateInitializeMelon(); - } - - /// - /// This override adds support for animated covers. - /// - public override void OnUpdate() - { - base.OnUpdate(); - MusicStageCellPatch.AnimateCoversUpdate(); - } - - /// - /// This override adds support for hot reloading. - /// - public override void OnFixedUpdate() - { - base.OnFixedUpdate(); - // TODO: Actually write HotReload - // HotReloadManager.FixedUpdate(); - } - - public override void OnSceneWasLoaded(int buildIndex, string sceneName) - { - base.OnSceneWasLoaded(buildIndex, sceneName); - MusicStageCellPatch.CurrentScene = sceneName; - } + base.OnDeinitializeMelon(); + var cacheDir = Path.Combine(MelonEnvironment.UserDataDirectory, "CustomAlbums", "Cache"); + if (Directory.Exists(cacheDir)) Directory.Delete(cacheDir, true); } } \ No newline at end of file diff --git a/Managers/AlbumManager.cs b/Managers/AlbumManager.cs index 03a2522..03988f4 100644 --- a/Managers/AlbumManager.cs +++ b/Managers/AlbumManager.cs @@ -1,4 +1,6 @@ -using CustomAlbums.Data; +using System.Diagnostics; +using System.IO.Compression; +using CustomAlbums.Data; using CustomAlbums.ModExtensions; using CustomAlbums.Utilities; using Il2CppAssets.Scripts.PeroTools.Commons; @@ -7,141 +9,220 @@ using UnityEngine; using Logger = CustomAlbums.Utilities.Logger; -namespace CustomAlbums.Managers +namespace CustomAlbums.Managers; + +public static class AlbumManager { - public static class AlbumManager + public const int Uid = 999; + public const string SearchPath = "Custom_Albums"; + public const string SearchPattern = "*.mdm"; + public const string PackSearchPattern = "*.mdp"; + public static readonly string JsonName = $"ALBUM{Uid + 1}"; + public static readonly string MusicPackage = $"music_package_{Uid}"; + + public static readonly Dictionary Languages = new() { - public const int Uid = 999; - public const string SearchPath = "Custom_Albums"; - public const string SearchPattern = "*.mdm"; - public static readonly string JsonName = $"ALBUM{Uid + 1}"; - public static readonly string MusicPackage = $"music_package_{Uid}"; - - public static readonly Dictionary Languages = new() - { - { "English", "Custom Albums" }, - { "ChineseS", "自定义" }, - { "ChineseT", "自定義" }, - { "Japanese", "カスタムアルバム" }, - { "Korean", "커스텀앨범" } - }; + { "English", "Custom Albums" }, + { "ChineseS", "自定义" }, + { "ChineseT", "自定義" }, + { "Japanese", "カスタムアルバム" }, + { "Korean", "커스텀앨범" } + }; - private static readonly Logger Logger = new(nameof(AlbumManager)); - internal static readonly FileSystemWatcher AlbumWatcher = new(); - internal static Events.LoadAlbumEvent OnAlbumLoaded; + private static readonly Logger Logger = new(nameof(AlbumManager)); + internal static readonly FileSystemWatcher AlbumWatcher = new(); + internal static Events.LoadAlbumEvent OnAlbumLoaded; + private static readonly Dictionary UidHistory = new(); - private static int MaxCount { get; set; } - internal static string CurrentPack { get; set; } = null; - public static Dictionary LoadedAlbums { get; } = new(); + private static int MaxCount { get; set; } + internal static string CurrentPack { get; set; } + public static Dictionary LoadedAlbums { get; } = new(); - public static void LoadMany(string directory) + public static Pack LoadPack(string directory) + { + // Get the files from the directory + try { - // Get the files from the directory - var files = Directory.EnumerateFiles(directory); + var zipFiles = ZipFile.OpenRead(directory); // Filter for .mdm files and find the pack.json file - var mdms = files.Where(file => Path.GetExtension(file).EqualsCaseInsensitive(".mdm")).ToList(); - var json = files.FirstOrDefault(file => Path.GetFileName(file).EqualsCaseInsensitive("pack.json")); + var mdms = zipFiles.Entries.Where(file => file.Name.EndsWith(".mdm")); + var json = zipFiles.Entries.FirstOrDefault(file => file.Name.EndsWith(".json")); // Initialize pack and variables - var pack = PackManager.CreatePack(json); + var pack = PackManager.CreatePack(json, directory); CurrentPack = pack.Title; + pack.StartIndex = MaxCount; // Count successfully loaded .mdm files - pack.Length = mdms.Count(file => LoadOne(file) != null); + pack.Length = mdms.Count(mdm => + { + var album = LoadOne(directory, mdm, mdm.FullName); + if (album is not null) + { + pack.Albums.Add(album); + return true; + } + + return false; + }); // Set the current pack to null and add the pack to the pack list CurrentPack = null; PackManager.AddPack(pack); - } - public static Album LoadOne(string path) + return pack; + } + catch (Exception ex) { - MaxCount = Math.Max(LoadedAlbums.Count, MaxCount); - var isDirectory = File.GetAttributes(path).HasFlag(FileAttributes.Directory); - var fileName = isDirectory ? Path.GetFileName(path) : Path.GetFileNameWithoutExtension(path); - - if (LoadedAlbums.ContainsKey(fileName)) return null; - - try - { - if (isDirectory && Directory.EnumerateFiles(path) - .Any(file => Path.GetFileName(file) - .EqualsCaseInsensitive("pack.json"))) - { - LoadMany(path); - return null; - } + Logger.Warning($"Failed to load album at {directory}. Reason: {ex.Message}"); + Logger.Warning(ex.StackTrace); - var album = new Album(path, MaxCount, CurrentPack); - if (album.Info is null) return null; + return null; + } + } - var albumName = album.AlbumName; - - LoadedAlbums.Add(albumName, album); + public static Album LoadOne(string directory, ZipArchiveEntry mdm, string fullFileName) + { + var fileName = Path.GetFileNameWithoutExtension(fullFileName); + try + { + var album = new Album(directory, mdm, 0, CurrentPack); + if (album.Info is null) return null; - if (album.HasFile("cover.png") || album.HasFile("cover.gif")) - ResourcesManager.instance.LoadFromName($"{albumName}_cover").hideFlags |= - HideFlags.DontUnloadUnusedAsset; + var albumName = album.AlbumName; + if (LoadedAlbums.ContainsKey(albumName)) return null; - Logger.Msg($"Loaded {albumName}: {album.Info.Name}"); - OnAlbumLoaded?.Invoke(typeof(AlbumManager), new AlbumEventArgs(album)); - return album; + if (UidHistory.TryGetValue(albumName, out var oldIndex)) + { + album.Index = oldIndex; } - catch (Exception ex) + else { - Logger.Warning($"Failed to load album at {fileName}. Reason: {ex.Message}"); - Logger.Warning(ex.StackTrace); + album.Index = MaxCount; + MaxCount++; + UidHistory[albumName] = album.Index; } - return null; - } + Logger.Msg("Adding " + albumName + " as a pack!"); - public static void LoadAlbums() - { - LoadedAlbums.Clear(); + LoadedAlbums.Add(albumName, album); - var files = new List(); - files.AddRange(Directory.GetFiles(SearchPath, SearchPattern)); - files.AddRange(Directory.GetDirectories(SearchPath)); + if (album.HasPng) + ResourcesManager.instance.LoadFromName($"{albumName}_cover").hideFlags |= + HideFlags.DontUnloadUnusedAsset; - foreach (var file in files) LoadOne(file); - Logger.Msg($"Finished loading {LoadedAlbums.Count} albums.", false); + Logger.Msg($"Loaded {albumName}: {album.Info.Name}"); + OnAlbumLoaded?.Invoke(typeof(AlbumManager), new AlbumEventArgs(album)); + return album; } - - public static IEnumerable GetAllUid() + catch (Exception ex) { - return LoadedAlbums.Select(album => $"{Uid}-{album.Value.Index}"); + Logger.Warning($"Failed to load album at {fileName}. Reason: {ex.Message}"); + Logger.Warning(ex.StackTrace); } - public static Album GetByUid(string uid) - { - return LoadedAlbums.FirstOrDefault(album => album.Value.Index == uid[4..].ParseAsInt()).Value; - } - public static string GetAlbumNameFromUid(string uid) + return null; + } + + public static Album LoadOne(string path) + { + var isDirectory = File.GetAttributes(path).HasFlag(FileAttributes.Directory); + var fileName = isDirectory ? Path.GetFileName(path) : Path.GetFileNameWithoutExtension(path); + try { - var album = GetByUid(uid); - return album is null ? string.Empty : album.AlbumName; + var album = new Album(path, 0, CurrentPack); + if (album.Info is null) return null; + + var albumName = album.AlbumName; + if (LoadedAlbums.ContainsKey(albumName)) return null; + + if (UidHistory.TryGetValue(albumName, out var oldIndex)) + { + album.Index = oldIndex; + } + else + { + album.Index = MaxCount; + MaxCount++; + UidHistory[albumName] = album.Index; + } + + LoadedAlbums.Add(albumName, album); + + if (album.HasPng) + ResourcesManager.instance.LoadFromName($"{albumName}_cover").hideFlags |= + HideFlags.DontUnloadUnusedAsset; + + Logger.Msg($"Loaded {albumName}: {album.Info.Name}"); + OnAlbumLoaded?.Invoke(typeof(AlbumManager), new AlbumEventArgs(album)); + return album; } - public static IEnumerable GetAlbumUidsFromNames(this IEnumerable albumNames) + catch (Exception ex) { - return albumNames.Where(name => LoadedAlbums.ContainsKey(name)) - .Select(name => $"{Uid}-{LoadedAlbums[name].Index}"); + Logger.Warning($"Failed to load album at {fileName}. Reason: {ex.Message}"); + Logger.Warning(ex.StackTrace); } - /// - /// Gets the current "Custom Albums" title based on language. - /// - /// The current "Custom Albums" title based on language. - public static string GetCustomAlbumsTitle() - { - return Languages.GetValueOrDefault( - SingletonScriptableObject + return null; + } + + public static void LoadAlbums() + { + LoadedAlbums.Clear(); + UidHistory.Clear(); + MaxCount = 0; + + var stopwatch = Stopwatch.StartNew(); + + var packs = new List(); + var files = new List(); + files.AddRange(Directory.GetFiles(SearchPath, SearchPattern)); + files.AddRange(Directory.GetDirectories(SearchPath)); + packs.AddRange(Directory.GetFiles(SearchPath, PackSearchPattern)); + + foreach (var pack in packs) LoadPack(pack); + foreach (var file in files) LoadOne(file); + + stopwatch.Stop(); + + Logger.Msg($"Finished loading {LoadedAlbums.Count} albums in {stopwatch.Elapsed}", false); + } + + public static IEnumerable GetAllUid() + { + return LoadedAlbums.Select(album => $"{Uid}-{album.Value.Index}"); + } + + public static Album GetByUid(string uid) + { + return LoadedAlbums.FirstOrDefault(album => album.Value.Index == uid[4..].ParseAsInt()).Value; + } + + public static string GetAlbumNameFromUid(string uid) + { + var album = GetByUid(uid); + return album is null ? string.Empty : album.AlbumName; + } + + public static IEnumerable GetAlbumUidsFromNames(this IEnumerable albumNames) + { + return albumNames.Where(name => LoadedAlbums.ContainsKey(name)) + .Select(name => $"{Uid}-{LoadedAlbums[name].Index}"); + } + + /// + /// Gets the current "Custom Albums" title based on language. + /// + /// The current "Custom Albums" title based on language. + public static string GetCustomAlbumsTitle() + { + return Languages.GetValueOrDefault( + SingletonScriptableObject .instance? .GetActiveOption("Language") ?? "English"); - } } } \ No newline at end of file diff --git a/Managers/AudioManager.cs b/Managers/AudioManager.cs index 5039716..35da6e2 100644 --- a/Managers/AudioManager.cs +++ b/Managers/AudioManager.cs @@ -1,191 +1,183 @@ using CustomAlbums.Data; +using CustomAlbums.Utilities; using Il2CppAssets.Scripts.PeroTools.Commons; using Il2CppAssets.Scripts.PeroTools.Managers; using NAudio.Vorbis; using NLayer; using UnityEngine; -using Action = Il2CppSystem.Action; using Logger = CustomAlbums.Utilities.Logger; // ReSharper disable AccessToModifiedClosure -namespace CustomAlbums.Managers +namespace CustomAlbums.Managers; + +public static class AudioManager { - public static class AudioManager - { - public const int AsyncReadSpeed = 4096; + public const int AsyncReadSpeed = 4096; - private static Coroutine _currentCoroutine; - private static readonly Dictionary Coroutines = new(); - private static readonly Logger Logger = new(nameof(AudioManager)); + private static Coroutine _currentCoroutine; + private static readonly Dictionary Coroutines = new(); + private static readonly Logger Logger = new(nameof(AudioManager)); - public static bool SwitchLoad(string name) - { - if (!Coroutines.TryGetValue(name, out var routine)) return false; - _currentCoroutine = routine; + public static bool SwitchLoad(string name) + { + if (!Coroutines.TryGetValue(name, out var routine)) return false; + _currentCoroutine = routine; - Logger.Msg($"Switching to async load of {name}"); - return true; - } + Logger.Msg($"Switching to async load of {name}"); + return true; + } - public static AudioClip LoadClipFromMp3(MemoryStream stream, string name) - { - var mp3 = new MpegFile(stream); - var sampleCount = mp3.Length / sizeof(float); - var audioClip = - AudioClip.Create(name, (int)sampleCount / mp3.Channels, mp3.Channels, mp3.SampleRate, false); + public static AudioClip LoadClipFromMp3(MemoryStream stream, string name) + { + var mp3 = new MpegFile(stream); + var sampleCount = mp3.Length / sizeof(float); + var audioClip = + AudioClip.Create(name, (int)sampleCount / mp3.Channels, mp3.Channels, mp3.SampleRate, false); - var remainingSamples = sampleCount; - var index = 0; + var remainingSamples = sampleCount; + var index = 0; - if (name.EndsWith("_music") && mp3.SampleRate != 44100) - Logger.Warning( - $"{name}.mp3 is not 44.1khz, desyncs may occur! Consider switching to .ogg format or using 44.1khz"); + if (name.EndsWith("_music") && mp3.SampleRate != 44100) + Logger.Warning( + $"{name}.mp3 is not 44.1khz, desyncs may occur! Consider switching to .ogg format or using 44.1khz"); - Coroutine coroutine = null; - coroutine = CreateCoroutine((Il2CppSystem.Func)delegate + Coroutine coroutine = null; + coroutine = CreateCoroutine(new Func(delegate + { + // Stop coroutine if the asset is unloaded + if (audioClip == null) { - // Stop coroutine if the asset is unloaded - if (audioClip == null) - { - Coroutines.Remove(name); - if (_currentCoroutine == coroutine) _currentCoroutine = null; + Coroutines.Remove(name); + if (_currentCoroutine == coroutine) _currentCoroutine = null; - Logger.Msg($"Aborting async load of {name}.mp3"); - return true; - } + Logger.Msg($"Aborting async load of {name}.mp3"); + return true; + } - // Pause coroutine if it is not active - if (coroutine != _currentCoroutine) return false; + // Pause coroutine if it is not active + if (coroutine != _currentCoroutine) return false; - var sampleArray = new float[Math.Min(AsyncReadSpeed, remainingSamples)]; - var readCount = mp3.ReadSamples(sampleArray, 0, sampleArray.Length); + var sampleArray = new float[Math.Min(AsyncReadSpeed, remainingSamples)]; + var readCount = mp3.ReadSamples(sampleArray, 0, sampleArray.Length); - audioClip.SetData(sampleArray, index / mp3.Channels); + audioClip.SetData(sampleArray, index / mp3.Channels); - index += readCount; - remainingSamples -= readCount; + index += readCount; + remainingSamples -= readCount; - if (remainingSamples > 0 && readCount != 0) return false; + if (remainingSamples > 0 && readCount != 0) return false; - mp3.Dispose(); - stream.Dispose(); + mp3.Dispose(); + stream.Dispose(); - Coroutines.Remove(name); - _currentCoroutine = null; + Coroutines.Remove(name); + _currentCoroutine = null; - Logger.Msg($"Finished async load of {name}.mp3"); - return true; - }); + Logger.Msg($"Finished async load of {name}.mp3"); + return true; + })); + + Coroutines[name] = coroutine; + _currentCoroutine = coroutine; + + return audioClip; + } - Coroutines[name] = coroutine; - _currentCoroutine = coroutine; + public static AudioClip LoadClipFromOgg(MemoryStream stream, string name) + { + var ogg = new VorbisWaveReader(stream); + var sampleCount = (int)(ogg.Length / (ogg.WaveFormat.BitsPerSample / 8)); + var audioClip = AudioClip.Create(name, sampleCount / ogg.WaveFormat.Channels, ogg.WaveFormat.Channels, + ogg.WaveFormat.SampleRate, false); - return audioClip; - } + var remainingSamples = sampleCount; + var index = 0; - public static AudioClip LoadClipFromOgg(MemoryStream stream, string name) + Coroutine coroutine = null; + coroutine = CreateCoroutine(new Func(delegate { - var ogg = new VorbisWaveReader(stream); - var sampleCount = (int)(ogg.Length / (ogg.WaveFormat.BitsPerSample / 8)); - var audioClip = AudioClip.Create(name, sampleCount / ogg.WaveFormat.Channels, ogg.WaveFormat.Channels, - ogg.WaveFormat.SampleRate, false); + // Stop coroutine if the asset is unloaded + if (audioClip == null) + { + Coroutines.Remove(name); + if (_currentCoroutine == coroutine) _currentCoroutine = null; - var remainingSamples = sampleCount; - var index = 0; + Logger.Msg($"Aborting async load of {name}.ogg"); + return true; + } - Coroutine coroutine = null; - coroutine = CreateCoroutine((Il2CppSystem.Func)delegate + // Pause coroutine if it is not active + if (coroutine != _currentCoroutine) return false; + + var sampleArray = new float[Math.Min(AsyncReadSpeed, remainingSamples)]; + var readCount = ogg.Read(sampleArray, 0, sampleArray.Length); + + try + { + audioClip.SetData(sampleArray, index / ogg.WaveFormat.Channels); + } + catch (Exception e) { - // Stop coroutine if the asset is unloaded - if (audioClip == null) - { - Coroutines.Remove(name); - if (_currentCoroutine == coroutine) _currentCoroutine = null; - - Logger.Msg($"Aborting async load of {name}.ogg"); - return true; - } - - // Pause coroutine if it is not active - if (coroutine != _currentCoroutine) return false; - - var sampleArray = new float[Math.Min(AsyncReadSpeed, remainingSamples)]; - var readCount = ogg.Read(sampleArray, 0, sampleArray.Length); - - try - { - audioClip.SetData(sampleArray, index / ogg.WaveFormat.Channels); - } - catch (Exception e) - { - if ((double)ogg.Position / 1000 > ogg.Length) - Logger.Warning( - "Possible over-read of audio file. This is a file-dependent anomaly. Consider making a minimal edit and re-saving."); - Logger.Error( - $"Exception while reading at offset {index} of {(double)ogg.Position / 1000 / ogg.Length} in {name}, aborting: {e.Message}"); - - ogg.Dispose(); - stream.Dispose(); - - Coroutines.Remove(name); - _currentCoroutine = null; - return true; - } - - index += readCount; - remainingSamples -= readCount; - - if (remainingSamples > 0 && readCount != 0) return false; + if ((double)ogg.Position / 1000 > ogg.Length) + Logger.Warning( + "Possible over-read of audio file. This is a file-dependent anomaly. Consider making a minimal edit and re-saving."); + Logger.Error( + $"Exception while reading at offset {index} of {(double)ogg.Position / 1000 / ogg.Length} in {name}, aborting: {e.Message}"); ogg.Dispose(); stream.Dispose(); Coroutines.Remove(name); _currentCoroutine = null; - - Logger.Msg($"Finished async load of {name}.ogg"); return true; - }); + } - Coroutines[name] = coroutine; - _currentCoroutine = coroutine; + index += readCount; + remainingSamples -= readCount; - return audioClip; - } + if (remainingSamples > 0 && readCount != 0) return false; - private static Coroutine CreateCoroutine(Il2CppSystem.Func update) - { - return SingletonMonoBehaviour.instance.StartCoroutine( - (Action)delegate { }, update); - } - - /// - /// Gets the audio clip for the specified album. - /// - /// The specified album. - /// This is "music" or "demo". Defaults to "music". - /// - public static AudioClip GetAudio(this Album album, string name = "music") - { - var key = $"{album.AlbumName}_{name}"; - if (album.HasFile($"{name}.ogg")) - { - // Load music.ogg - var stream = album.OpenMemoryStream($"{name}.ogg"); - return LoadClipFromOgg(stream, key); - } + ogg.Dispose(); + stream.Dispose(); - if (album.HasFile($"{name}.mp3")) - { - // Load music.mp3 - var stream = album.OpenMemoryStream($"{name}.mp3"); - return LoadClipFromMp3(stream, key); - } + Coroutines.Remove(name); + _currentCoroutine = null; + + Logger.Msg($"Finished async load of {name}.ogg"); + return true; + })); + + Coroutines[name] = coroutine; + _currentCoroutine = coroutine; + + return audioClip; + } + + private static Coroutine CreateCoroutine(Il2CppSystem.Func update) + { + return SingletonMonoBehaviour.instance.StartCoroutine( + new Action(delegate { }), update); + } + + /// + /// Gets the audio clip for the specified album. + /// + /// The specified album. + /// This is "music" or "demo". Defaults to "music". + /// + public static AudioClip GetAudio(this Album album, string name = "music") + { + var key = $"{album.AlbumName}_{name}"; + + if (album.OpenNullableStream($"{name}.ogg") is { } oggStream) + return LoadClipFromOgg(oggStream.ToMemoryStream(), key); + + if (album.OpenNullableStream($"{name}.mp3") is { } mp3Stream) + return LoadClipFromMp3(mp3Stream.ToMemoryStream(), key); - // No music file found - Logger.Error($"Could not find audio file for {name} in {album.Info.Name}"); - return null; - } + // No music file found + Logger.Error($"Could not find audio file for {name} in {album.Info.Name}"); + return null; } } \ No newline at end of file diff --git a/Managers/CoverManager.cs b/Managers/CoverManager.cs deleted file mode 100644 index 294faa5..0000000 --- a/Managers/CoverManager.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System.Runtime.CompilerServices; -using CustomAlbums.Data; -using CustomAlbums.Utilities; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; -using UnityEngine; -using Logger = CustomAlbums.Utilities.Logger; - -namespace CustomAlbums.Managers -{ - public static class CoverManager - { - internal static readonly Dictionary CachedCovers = new(); - internal static readonly Dictionary CachedAnimatedCovers = new(); - private static readonly Logger Logger = new(nameof(CoverManager)); - - private static readonly Configuration Config = Configuration.Default; - - public static Sprite GetCover(this Album album) - { - if (!album.HasFile("cover.png")) return null; - if (CachedCovers.TryGetValue(album.Index, out var cached)) return cached; - - using var stream = album.OpenMemoryStream("cover.png"); - - var bytes = stream.ReadFully(); - - // Create the textures - var texture = new Texture2D(2, 2, TextureFormat.ARGB32, false) - { - wrapMode = TextureWrapMode.MirrorOnce - }; - texture.LoadImage(bytes); - - var cover = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f)); - CachedCovers.Add(album.Index, cover); - - return cover; - } - - public static unsafe AnimatedCover GetAnimatedCover(this Album album) - { - // Early return statements - if (!album.HasFile("cover.gif")) return null; - if (CachedAnimatedCovers.TryGetValue(album.Index, out var cached)) return cached; - - Config.PreferContiguousImageBuffers = true; - - // Open and load the gif - using var stream = album.OpenMemoryStream("cover.gif"); - using var gif = Image.Load(new DecoderOptions { Configuration = Config }, stream); - - // For some reason Unity loads textures upside down? - // Flip the frames - gif.Mutate(c => c.Flip(FlipMode.Vertical)); - - var sprites = new Sprite[gif.Frames.Count]; - - for (var i = 0; i < gif.Frames.Count; i++) - { - // Get frame data - var frame = gif.Frames[i]; - var width = frame.Width; - var height = frame.Height; - - // Get frame pixel data - // - // This should really be done with CopyPixelData and a byte array - // but that causes a 6MB+ copy of an array that slows things down by a bit - // The more efficient way is to retrieve an IntPtr that stores the data and pass that with a size instead - var getPixelDataResult = frame.DangerousTryGetSinglePixelMemory(out var memory); - if (!getPixelDataResult) - { - Logger.Error("Failed to get pixel data."); - return null; - } - - using var handle = memory.Pin(); - - // Create the textures - var texture = new Texture2D(width, height, TextureFormat.RGBA32, false) - { - wrapMode = TextureWrapMode.MirrorOnce - }; - texture.LoadRawTextureData((IntPtr)handle.Pointer, memory.Length * Unsafe.SizeOf()); - texture.Apply(false); - - // Create the sprite with the given texture and add it to the sprites array - var sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), - new Vector2(0.5f, 0.5f)); - sprite.hideFlags |= HideFlags.DontUnloadUnusedAsset; - sprites[i] = sprite; - } - - // Create and add cover to cache - var cover = new AnimatedCover(sprites, gif.Frames.RootFrame.Metadata.GetGifMetadata().FrameDelay * 10); - CachedAnimatedCovers.Add(album.Index, cover); - - return cover; - } - } -} \ No newline at end of file diff --git a/Managers/HotReloadManager.cs b/Managers/HotReloadManager.cs index 04c0bdf..24ab785 100644 --- a/Managers/HotReloadManager.cs +++ b/Managers/HotReloadManager.cs @@ -1,139 +1,722 @@ -using CustomAlbums.Patches; +using System.Collections.Concurrent; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Nodes; +using CustomAlbums.Data; +using CustomAlbums.Patches; using CustomAlbums.Utilities; using HarmonyLib; using Il2CppAssets.Scripts.Database; +using Il2CppAssets.Scripts.PeroTools.Commons; +using Il2CppAssets.Scripts.PeroTools.Managers; using Il2CppAssets.Scripts.UI.Panels; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using Il2CppPeroTools2.Resources; +using UnityEngine; +using Logger = CustomAlbums.Utilities.Logger; +using Object = UnityEngine.Object; -namespace CustomAlbums.Managers +namespace CustomAlbums.Managers; + +internal static class HotReloadManager { - // TODO: Fix all of this with album.AlbumName - internal static class HotReloadManager - { - private static readonly Logger Logger = new(nameof(HotReloadManager)); - private static Queue AlbumsAdded { get; } = new(); - private static Queue AlbumsDeleted { get; } = new(); - private static List AlbumUidsAdded { get; } = new(); - private static PnlStage PnlStageInstance { get; set; } + private static readonly Logger Logger = new(nameof(HotReloadManager)); + + // Hot-loaded MusicInfo cache: uid -> MusicInfo + // Used by GetMusicInfoFromAll Harmony Postfix + private static readonly Dictionary HotLoadedMusicInfos = new(); + private static readonly Dictionary HotLoadedMusicNames = new(); + private static readonly Dictionary HotLoadedMusicAuthors = new(); + + private static float _lastProcessTime; + + private static ConcurrentQueue AlbumsToAdd { get; } = new(); - private static bool IsFileUnlocked(string path) + private static ConcurrentQueue AlbumsToDelete { get; } = new(); + private static ConcurrentDictionary LastFileEvent { get; } = new(); + internal static PnlStage PnlStageInstance { get; set; } + + private static bool IsFileUnlocked(string path) + { + try { + if (Directory.Exists(path)) + { + var infoPath = Path.Combine(path, "info.json"); + if (!File.Exists(infoPath)) return false; + + using var fileStream = File.Open(infoPath, FileMode.Open, FileAccess.Read, FileShare.None); + return fileStream.Length > 0; + } + else + { + if (!File.Exists(path)) return false; + using var fileStream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.None); + return fileStream.Length > 0; + } + } + catch (Exception ex) when (ex is FileNotFoundException or IOException or UnauthorizedAccessException) + { + return false; + } + } + + + private static string GetTopLevelName(string relativePath) + { + var separatorIndex = relativePath.IndexOfAny(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }); + return separatorIndex >= 0 ? relativePath.Substring(0, separatorIndex) : relativePath; + } + + private static int ParseDifficulty(string difficulty) + { + return int.TryParse(difficulty, out var value) ? value : 0; + } + + private static MusicExInfo CreateMusicExInfo(Album album) + { + var albumInfo = album.Info; + var changedDiff = new Il2CppStructArray(5); + changedDiff[0] = ParseDifficulty(albumInfo.Difficulty1); + changedDiff[1] = ParseDifficulty(albumInfo.Difficulty2); + changedDiff[2] = ParseDifficulty(albumInfo.Difficulty3); + changedDiff[3] = ParseDifficulty(albumInfo.Difficulty4); + changedDiff[4] = ParseDifficulty(albumInfo.Difficulty5); + + var musicExInfo = new MusicExInfo(); + musicExInfo.m_AlbumIndex = AlbumManager.Uid + 1; + musicExInfo.m_AlbumUidIndex = AlbumManager.Uid; + musicExInfo.m_MusicIndex = album.Index; + musicExInfo.m_AlbumUidName = $"music_package_{AlbumManager.Uid}"; + musicExInfo.m_AlbumJsonName = AlbumManager.JsonName; + musicExInfo.m_ChangedDiff = changedDiff; + return musicExInfo; + } + + private static int ProcessAdditions() + { + var addedCount = 0; + + if (AlbumsToAdd.TryDequeue(out var path)) try { - using var fileStream = File.Open(path, FileMode.Open); - if (fileStream.Length <= 0) return false; + var album = AlbumManager.LoadOne(path); + if (album == null) Logger.Warning($"Failed to load album from {path}"); + + var albumName = album.AlbumName; + var uid = $"{AlbumManager.Uid}-{album.Index}"; + var albumInfo = album.Info; + Logger.Msg($"Adding {albumName} (UID: {uid})"); + + var masterAlbums = Singleton.instance.GetConfigObject(); + var albumsInfo = masterAlbums?.GetAlbumsInfoByUid(AlbumManager.MusicPackage); + var globalAlbumConfig = albumsInfo != null + ? Singleton.instance.GetConfigObject(albumsInfo.albumJsonIndex) + : null; + + if (globalAlbumConfig != null) + { + var jsonArray = new JsonArray(); + var localJsonArray = new JsonArray(); + + foreach (var (albumStr, albumObj) in AlbumManager.LoadedAlbums) + { + var aInfo = albumObj.Info; + var displayName = aInfo.Name ?? ""; + + var customChartJson = new + { + uid = albumObj.Uid, + name = displayName, + author = aInfo.Author ?? "", + bpm = aInfo.Bpm ?? "0", + music = $"{albumStr}_music", + demo = $"{albumStr}_demo", + cover = $"{albumStr}_cover", + noteJson = $"{albumStr}_map", + scene = aInfo.Scene ?? "scene_01", + unlockLevel = "0", + levelDesigner = aInfo.LevelDesigner ?? "", + levelDesigner1 = aInfo.LevelDesigner1 ?? aInfo.LevelDesigner ?? "", + levelDesigner2 = aInfo.LevelDesigner2 ?? aInfo.LevelDesigner ?? "", + levelDesigner3 = aInfo.LevelDesigner3 ?? aInfo.LevelDesigner ?? "", + levelDesigner4 = aInfo.LevelDesigner4 ?? aInfo.LevelDesigner ?? "", + levelDesigner5 = aInfo.LevelDesigner5 ?? aInfo.LevelDesigner ?? "", + difficulty1 = aInfo.Difficulty1 ?? "0", + difficulty2 = aInfo.Difficulty2 ?? "0", + difficulty3 = aInfo.Difficulty3 ?? "0", + difficulty4 = aInfo.Difficulty4 ?? "0", + difficulty5 = aInfo.Difficulty5 ?? "0" + }; + jsonArray.Add(JsonSerializer.SerializeToNode(customChartJson)); - Logger.Msg("The added album is ready to be read!"); - return true; + localJsonArray.Add(JsonSerializer.SerializeToNode(new + { + name = displayName, + author = aInfo.Author ?? "" + })); + } + + var fullJsonStr = JsonSerializer.Serialize(jsonArray); + var fullLocalJsonStr = JsonSerializer.Serialize(localJsonArray); + + globalAlbumConfig.Deserialize(fullJsonStr); + + var localDicProp = + typeof(BaseDBConfigLocalObject).GetProperty("m_LocalDic", + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (localDicProp != null) + { + var globalLocalDic = + localDicProp.GetValue(globalAlbumConfig) as + Il2CppSystem.Collections.Generic.Dictionary; + if (globalLocalDic != null) + { + globalLocalDic.Clear(); + for (var i = 0; i <= 15; i++) + { + var newLocalAlbum = new DBConfigLocalALBUM(); + newLocalAlbum.Deserialize(fullLocalJsonStr); + globalLocalDic.Add(i, newLocalAlbum); + } + } + } + + GlobalDataBase.s_DbMusicTag.AddAllMusicInfo(globalAlbumConfig); + + var newMusicInfoList = new Il2CppSystem.Collections.Generic.List(); + globalAlbumConfig.GetAllMusicInfo(newMusicInfoList); + var idx = 0; + foreach (var m in newMusicInfoList) + { + var uidSplit = m.uid.Split('-'); + if (uidSplit.Length < 2) + { + idx++; + continue; + } + + if (!int.TryParse(uidSplit[1], out var parsedIndex)) + { + idx++; + continue; + } + + var aObj = AlbumManager.LoadedAlbums.Values.FirstOrDefault(a => a.Index == parsedIndex); + if (aObj != null) + { + m.Init(idx); + m.InitExInfo(); + m.m_MusicExInfo = CreateMusicExInfo(aObj); + + HotLoadedMusicInfos[m.uid] = m; + HotLoadedMusicNames[m.uid] = aObj.Info.Name ?? ""; + HotLoadedMusicAuthors[m.uid] = aObj.Info.Author ?? ""; + } + + idx++; + } + + Logger.Msg($"Transmuted MusicInfo for {uid}"); + } + else + { + Logger.Error("globalAlbumConfig is null! Cannot inject metadata."); + } + + try + { + var dhColBase = DataHelper.collections; + var dhColPtr = dhColBase != null ? dhColBase.Pointer : IntPtr.Zero; + var uids = GlobalDataBase.s_DbMusicTag.m_StageShowMusicUids; + if (uids != null) + if (!uids.Contains(uid)) + if (dhColPtr == IntPtr.Zero || uids.Pointer != dhColPtr) + { + uids.Add(uid); + Logger.Msg($"Added {uid} to m_StageShowMusicUids"); + } + + var allMusicTag = GlobalDataBase.dbMusicTag.GetAlbumTagInfo(0); + if (allMusicTag != null) + { + if (allMusicTag.m_MusicUids != null && !allMusicTag.m_MusicUids.Contains(uid)) + if (allMusicTag.m_MusicUids.Pointer != dhColPtr) + allMusicTag.m_MusicUids.Add(uid); + if (allMusicTag.m_DisplayMusicUids != null) + foreach (var d in allMusicTag.m_DisplayMusicUids) + if (d.musicUids != null && !d.musicUids.Contains(uid)) + if (d.musicUids.Pointer != dhColPtr) + d.musicUids.Add(uid); + } + } + catch (Exception ex) + { + Logger.Warning($"Failed to add to view lists: {ex.Message}"); + } + + // Inject search tags into ConfigManager + try + { + var config = Singleton.instance.GetConfigObject(); + var searchTag = new MusicSearchTagInfo + { + uid = uid, + listIndex = config.count + }; + + var tags = new List { "custom albums" }; + if (albumInfo.SearchTags != null) tags.AddRange(albumInfo.SearchTags); + if (!string.IsNullOrEmpty(albumInfo.NameRomanized)) tags.Add(albumInfo.NameRomanized); + for (var i = 0; i < tags.Count; i++) tags[i] = tags[i].ToLower(); + searchTag.tag = new Il2CppStringArray(tags.ToArray()); + + if (!config.m_Dictionary.ContainsKey(uid)) + { + config.m_Dictionary.Add(uid, searchTag); + config.list.Add(searchTag); + } + } + catch (Exception ex) + { + Logger.Warning($"Failed to add search tags: {ex.Message}"); + } + + // Preload cover resource + try + { + if (album.HasFile("cover.png")) + ResourcesManager.instance + .LoadFromName($"{albumName}_cover") + .hideFlags |= HideFlags.DontUnloadUnusedAsset; + } + catch (Exception ex) + { + Logger.Warning($"Failed to preload cover: {ex.Message}"); + } + + addedCount++; + Logger.Msg($"Successfully added {albumInfo.Name}"); } - catch (Exception ex) when (ex is FileNotFoundException or IOException) + catch (Exception ex) { - return false; + Logger.Warning($"Error adding album: {ex.Message}"); + Logger.Warning(ex.StackTrace); } - } - private static void RemoveAllCachedAssets(string albumName) - { - Logger.Msg("Removing " + albumName + "!"); + return addedCount; + } + + private static int ProcessDeletions() + { + var deletedCount = 0; - if (!AlbumManager.LoadedAlbums.TryGetValue($"album_{albumName}", out var album)) return; + if (AlbumsToDelete.TryDequeue(out var albumKey)) + try + { + Logger.Msg($"Removing {albumKey}"); - // Remove cached album information, not needed anymore since the album has been deleted - AlbumManager.LoadedAlbums.Remove($"album_{albumName}"); - CoverManager.CachedAnimatedCovers.Remove(album.Index); - CoverManager.CachedCovers.Remove(album.Index); - AssetPatch.RemoveFromCache($"album_{albumName}_demo"); - AssetPatch.RemoveFromCache($"album_{albumName}_music"); - AssetPatch.RemoveFromCache($"album_{albumName}_cover"); + if (!AlbumManager.LoadedAlbums.TryGetValue(albumKey, out var album)) + Logger.Warning($"Album {albumKey} not found"); - // Get the music info from the UID, remove it from the ShowMusic list, and refresh the UI - var musicInfo = GlobalDataBase.s_DbMusicTag.GetMusicInfoFromAll($"{AlbumManager.Uid}-{album.Index}"); - GlobalDataBase.s_DbMusicTag.RemoveShowMusicUid(musicInfo); - PnlStageInstance.m_MusicRootAnimator?.Play(PnlStageInstance.animNameAlbumIn); - PnlStageInstance.RefreshMusicFSV(); + var uid = $"{AlbumManager.Uid}-{album.Index}"; - // TODO: Remove the album information from the Custom Albums tag menu + HotLoadedMusicInfos.Remove(uid); + HotLoadedMusicNames.Remove(uid); + HotLoadedMusicAuthors.Remove(uid); - // TODO: Only change the selected album if the selected album was the album that was deleted - Logger.Msg("Successfully removed from cache!"); - } + // Remove from game music database + try + { + var musicInfo = GlobalDataBase.s_DbMusicTag.GetMusicInfoFromAll(uid); + if (musicInfo != null) GlobalDataBase.s_DbMusicTag.RemoveShowMusicUid(musicInfo); + + var allMusicInfo = GlobalDataBase.s_DbMusicTag.m_AllMusicInfo; + if (allMusicInfo != null && allMusicInfo.ContainsKey(uid)) allMusicInfo.Remove(uid); + + if (GlobalDataBase.s_DbMusicTag.m_AllAlbumTagData != null) + foreach (var tag in GlobalDataBase.s_DbMusicTag.m_AllAlbumTagData) + { + var tagInfo = tag.Value; + if (tagInfo.m_MusicUids != null && tagInfo.m_MusicUids.Contains(uid)) + tagInfo.m_MusicUids.Remove(uid); + if (tagInfo.m_DisplayMusicUids != null) + foreach (var d in tagInfo.m_DisplayMusicUids) + if (d.musicUids != null && d.musicUids.Contains(uid)) + d.musicUids.Remove(uid); + } + } + catch (Exception ex) + { + Logger.Warning($"Failed to remove from database: {ex.Message}"); + } + + // Remove from ConfigManager + try + { + var config = Singleton.instance.GetConfigObject(); + if (config != null) + if (config.m_Dictionary.ContainsKey(uid)) + { + var tagInfo = config.m_Dictionary[uid]; + config.m_Dictionary.Remove(uid); + config.list.Remove(tagInfo); + } + } + catch (Exception ex) + { + Logger.Warning($"Failed to remove search tags: {ex.Message}"); + } + + // Clear resource cache + AlbumManager.LoadedAlbums.Remove(albumKey); + AssetPatch.RemoveFromCache($"{albumKey}_demo"); + AssetPatch.RemoveFromCache($"{albumKey}_music"); + AssetPatch.RemoveFromCache($"{albumKey}_cover"); + + deletedCount++; + Logger.Msg($"Removed {albumKey}"); + } + catch (Exception ex) + { + Logger.Warning($"Error removing: {ex.Message}"); + Logger.Warning(ex.StackTrace); + } - private static void RenameAllCachedAssets(string oldAlbumName, string newAlbumName) + return deletedCount; + } + + private static void RebuildCustomAlbumsTag() + { + try { - Logger.Msg($"Renaming {oldAlbumName} to {newAlbumName}!"); - var success = AlbumManager.LoadedAlbums.Remove(oldAlbumName, out var album); - if (!success) return; + var existingTag = GlobalDataBase.dbMusicTag.GetAlbumTagInfo(AlbumManager.Uid); + if (existingTag != null) + { + var uids = AlbumManager.GetAllUid().ToList(); + + if (existingTag.customInfo != null) existingTag.customInfo.music_list = uids.ToIl2Cpp(); - // rename the assets to the new name - AlbumManager.LoadedAlbums.TryAdd(newAlbumName, album); - AssetPatch.ModifyCacheKey($"{oldAlbumName}_demo", $"{newAlbumName}_demo"); - AssetPatch.ModifyCacheKey($"{oldAlbumName}_music", $"{newAlbumName}_music"); - AssetPatch.ModifyCacheKey($"{oldAlbumName}_cover", $"{newAlbumName}_cover"); + if (existingTag.m_MusicUids != null) + { + existingTag.m_MusicUids.Clear(); + foreach (var uid in uids) existingTag.m_MusicUids.Add(uid); + } - Logger.Msg("Successfully modified cache!"); + if (existingTag.m_DisplayMusicUids != null) + foreach (var d in existingTag.m_DisplayMusicUids) + if (d.musicUids != null) + { + d.musicUids.Clear(); + foreach (var uid in uids) d.musicUids.Add(uid); + } + + Logger.Msg($"Tag updated ({uids.Count} albums)"); + } + } + catch (Exception ex) + { + Logger.Warning($"Tag rebuild failed: {ex.Message}"); } + } - private static void AddNewAlbums(int previousSize) + private static void RefreshUI() + { + try { - var index = previousSize; - for (var i = previousSize; i < AlbumManager.LoadedAlbums.Count; i++) + var stage = Object.FindObjectOfType(); + if (stage != null && stage.gameObject.activeInHierarchy) { - // TODO: Write added album hot reloading logic here + stage.RefreshStageUI(); + Logger.Msg("UI refreshed"); } - - PnlStageInstance.m_MusicRootAnimator?.Play(PnlStageInstance.animNameAlbumIn); - PnlStageInstance.RefreshMusicFSV(); } + catch (Exception ex) + { + Logger.Warning($"Failed to refresh UI: {ex.Message}"); + } + } + + internal static void FixedUpdate() + { + if (AlbumsToAdd.IsEmpty && AlbumsToDelete.IsEmpty) return; - /// - /// Runs the logic for hot reloading on Unity's FixedUpdate - /// - internal static void FixedUpdate() + if (PnlStageInstance == null) return; + + if (Time.unscaledTime - _lastProcessTime < 0.02f) return; + _lastProcessTime = Time.unscaledTime; + + var oldSelectedUid = DataHelper.selectedMusicUidFromInfoList; + var oldSelectedAlbumName = AlbumManager.GetAlbumNameFromUid(oldSelectedUid); + + var deletedCount = ProcessDeletions(); + var addedCount = 0; + if (deletedCount == 0) addedCount = ProcessAdditions(); + + if (deletedCount > 0 || addedCount > 0) { - var previousSize = AlbumManager.LoadedAlbums.Count; - while (AlbumsAdded.Count > 0) AlbumManager.LoadOne(AlbumsAdded.Dequeue()); - while (AlbumsDeleted.Count > 0) RemoveAllCachedAssets(AlbumsDeleted.Dequeue()); + PnlStageInstance?.m_MusicRootAnimator?.Play(PnlStageInstance.animNameAlbumIn); + + if (!string.IsNullOrEmpty(oldSelectedAlbumName) && oldSelectedUid != "0-0") + { + if (AlbumManager.LoadedAlbums.TryGetValue(oldSelectedAlbumName, out var newAlbum)) + { + var newUid = $"{AlbumManager.Uid}-{newAlbum.Index}"; + if (newUid != oldSelectedUid) + { + DataHelper.selectedMusicUidFromInfoList = newUid; + Logger.Msg($"Updated selected UID from {oldSelectedUid} to {newUid}"); + } + } + else + { + DataHelper.selectedMusicUidFromInfoList = "0-0"; + Logger.Msg("Selected album was deleted. Resetting selection to 0-0"); + } + } + + RebuildCustomAlbumsTag(); + RefreshUI(); } + } - /// - /// Initializes the AlbumWatcher for adding/deleting/renaming new custom charts. - /// - internal static void OnLateInitializeMelon() + internal static void OnLateInitializeMelon() + { + try { - AlbumManager.AlbumWatcher.Path = AlbumManager.SearchPath; - AlbumManager.AlbumWatcher.Filter = AlbumManager.SearchPattern; + var watchPath = Path.GetFullPath(AlbumManager.SearchPath); + Logger.Msg($"Watching directory: {watchPath}"); + + if (!Directory.Exists(watchPath)) + { + Logger.Warning($"Directory does not exist: {watchPath}"); + return; + } + + AlbumManager.AlbumWatcher.Path = watchPath; + AlbumManager.AlbumWatcher.IncludeSubdirectories = true; AlbumManager.AlbumWatcher.Created += (_, e) => { - Logger.Msg("Added file " + e.Name); - while (!IsFileUnlocked(e.FullPath)) - // Thread sleep added to not poll the drive a ton - Thread.Sleep(200); - AlbumsAdded.Enqueue(e.FullPath); + var topLevelName = GetTopLevelName(e.Name); + var fullPath = Path.Combine(watchPath, topLevelName); + + var now = DateTime.Now; + if (LastFileEvent.TryGetValue(topLevelName, out var lastTime) && + (now - lastTime).TotalMilliseconds < 500) return; + LastFileEvent[topLevelName] = now; + + var isMdm = topLevelName.EndsWith(".mdm"); + if (!isMdm && !Directory.Exists(fullPath)) return; + + Logger.Msg($"Detected new file or folder: {topLevelName}"); + Task.Run(() => + { + var attempts = 0; + while (!IsFileUnlocked(fullPath) && attempts < 50) + { + Thread.Sleep(200); + attempts++; + } + + if (attempts < 50) + { + AlbumsToAdd.Enqueue(fullPath); + Logger.Msg($"Queued for addition: {topLevelName}"); + } + else + { + Logger.Warning($"Timed out waiting for file: {topLevelName}"); + } + }); + }; + + AlbumManager.AlbumWatcher.Deleted += (_, e) => + { + var topLevelName = GetTopLevelName(e.Name); + var isInnerFile = e.Name != topLevelName; + var fullPath = Path.Combine(watchPath, topLevelName); + + var now = DateTime.Now; + if (LastFileEvent.TryGetValue(topLevelName, out var lastTime) && + (now - lastTime).TotalMilliseconds < 500) return; + LastFileEvent[topLevelName] = now; + + var isMdm = topLevelName.EndsWith(".mdm"); + var albumKey = isMdm ? $"album_{Path.GetFileNameWithoutExtension(topLevelName)}" : $"album_{topLevelName}_folder"; + if (!isMdm && !AlbumManager.LoadedAlbums.ContainsKey(albumKey)) return; + + if (isInnerFile) + { + Logger.Msg($"Detected inner deletion: {e.Name}, reloading {topLevelName}"); + Task.Run(() => + { + var attempts = 0; + while (!IsFileUnlocked(fullPath) && attempts < 50) + { + Thread.Sleep(200); + attempts++; + } + if (attempts < 50) + { + AlbumsToDelete.Enqueue(albumKey); + AlbumsToAdd.Enqueue(fullPath); + Logger.Msg($"Queued for reload: {topLevelName}"); + } + }); + } + else + { + Logger.Msg($"Detected deletion: {topLevelName}"); + AlbumsToDelete.Enqueue(albumKey); + } }; - AlbumManager.AlbumWatcher.Deleted += (s, e) => + + AlbumManager.AlbumWatcher.Changed += (_, e) => { - Logger.Msg("Deleted file " + e.Name); - AlbumsDeleted.Enqueue(Path.GetFileNameWithoutExtension(e.Name)); + if (e.ChangeType != WatcherChangeTypes.Changed) return; + var topLevelName = GetTopLevelName(e.Name); + var fullPath = Path.Combine(watchPath, topLevelName); + + var now = DateTime.Now; + if (LastFileEvent.TryGetValue(topLevelName, out var lastTime) && + (now - lastTime).TotalMilliseconds < 500) return; + LastFileEvent[topLevelName] = now; + + var isMdm = topLevelName.EndsWith(".mdm"); + var albumKey = isMdm ? $"album_{Path.GetFileNameWithoutExtension(topLevelName)}" : $"album_{topLevelName}_folder"; + if (!isMdm && !AlbumManager.LoadedAlbums.ContainsKey(albumKey)) return; + + Logger.Msg($"Detected change: {e.Name}"); + Task.Run(() => + { + var attempts = 0; + while (!IsFileUnlocked(fullPath) && attempts < 50) + { + Thread.Sleep(200); + attempts++; + } + + if (attempts < 50) + { + AlbumsToDelete.Enqueue(albumKey); + AlbumsToAdd.Enqueue(fullPath); + Logger.Msg($"Queued for reload: {topLevelName}"); + } + else + { + Logger.Warning($"Timed out waiting for file: {topLevelName}"); + } + }); }; - AlbumManager.AlbumWatcher.Renamed += (s, e) => + + AlbumManager.AlbumWatcher.Renamed += (_, e) => { - Logger.Msg("Renamed file " + e.OldName + " -> " + e.Name); - RenameAllCachedAssets($"album_{Path.GetFileNameWithoutExtension(e.OldName)}", - $"album_{Path.GetFileNameWithoutExtension(e.Name)}"); + var oldTopLevelName = GetTopLevelName(e.OldName); + var newTopLevelName = GetTopLevelName(e.Name); + var isInnerRename = e.OldName != oldTopLevelName || e.Name != newTopLevelName; + var fullPath = Path.Combine(watchPath, newTopLevelName); + + var now = DateTime.Now; + if (LastFileEvent.TryGetValue(newTopLevelName, out var lastTime) && + (now - lastTime).TotalMilliseconds < 500) return; + LastFileEvent[newTopLevelName] = now; + + if (isInnerRename) + { + var isMdm = newTopLevelName.EndsWith(".mdm"); + var albumKey = isMdm ? $"album_{Path.GetFileNameWithoutExtension(newTopLevelName)}" : $"album_{newTopLevelName}_folder"; + if (!isMdm && !AlbumManager.LoadedAlbums.ContainsKey(albumKey)) return; + + Logger.Msg($"Detected inner rename: {e.OldName} -> {e.Name}, reloading {newTopLevelName}"); + Task.Run(() => + { + var attempts = 0; + while (!IsFileUnlocked(fullPath) && attempts < 50) + { + Thread.Sleep(200); + attempts++; + } + if (attempts < 50) + { + AlbumsToDelete.Enqueue(albumKey); + AlbumsToAdd.Enqueue(fullPath); + Logger.Msg($"Queued for reload: {newTopLevelName}"); + } + }); + return; + } + + var isOldMdm = oldTopLevelName.EndsWith(".mdm"); + var isNewMdm = newTopLevelName.EndsWith(".mdm"); + + var oldKey = isOldMdm ? $"album_{Path.GetFileNameWithoutExtension(oldTopLevelName)}" : $"album_{oldTopLevelName}_folder"; + var newKey = isNewMdm ? $"album_{Path.GetFileNameWithoutExtension(newTopLevelName)}" : $"album_{newTopLevelName}_folder"; + + if (!isOldMdm && !isNewMdm && !AlbumManager.LoadedAlbums.ContainsKey(oldKey) && !Directory.Exists(fullPath)) return; + + Logger.Msg($"Detected rename: {oldTopLevelName} -> {newTopLevelName}"); + + if (AlbumManager.LoadedAlbums.Remove(oldKey, out var album)) + { + AlbumManager.LoadedAlbums.TryAdd(newKey, album); + AssetPatch.ModifyCacheKey($"{oldKey}_demo", $"{newKey}_demo"); + AssetPatch.ModifyCacheKey($"{oldKey}_music", $"{newKey}_music"); + AssetPatch.ModifyCacheKey($"{oldKey}_cover", $"{newKey}_cover"); + Logger.Msg($"Renamed {oldKey} -> {newKey}"); + } + else + { + Logger.Msg($"Old file was not loaded, treating as new file: {newTopLevelName}"); + Task.Run(() => + { + var attempts = 0; + while (!IsFileUnlocked(fullPath) && attempts < 50) + { + Thread.Sleep(200); + attempts++; + } + + if (attempts < 50) + { + AlbumsToAdd.Enqueue(fullPath); + Logger.Msg($"Queued for addition: {newTopLevelName}"); + } + else + { + Logger.Warning($"Timed out waiting for file: {newTopLevelName}"); + } + }); + } }; - // Start the AlbumWatcher AlbumManager.AlbumWatcher.EnableRaisingEvents = true; + Logger.Msg("FileSystemWatcher initialized!"); + } + catch (Exception ex) + { + Logger.Warning($"Init failed: {ex.Message}"); + Logger.Warning(ex.StackTrace); + } + } + + [HarmonyPatch(typeof(DBMusicTag), nameof(DBMusicTag.GetMusicInfoFromAll))] + internal static class GetMusicInfoFromAllPatch + { + private static void Postfix(string musicUid, ref MusicInfo __result) + { + if (string.IsNullOrEmpty(musicUid)) return; + + if (HotLoadedMusicInfos.TryGetValue(musicUid, out var hotMusicInfo)) __result = hotMusicInfo; } + } - [HarmonyPatch(typeof(PnlStage), nameof(PnlStage.PreWarm))] - internal static class StagePreWarmPatch + [HarmonyPatch(typeof(PnlStage), nameof(PnlStage.PreWarm))] + internal static class StagePreWarmPatch + { + private static void Postfix(PnlStage __instance) { - private static void Postfix(PnlStage __instance) - { - Logger.Msg($"PnlStage instance retrieved. Null = {__instance == null}"); - PnlStageInstance = __instance; - } + PnlStageInstance = __instance; } } } \ No newline at end of file diff --git a/Managers/MusicDataManager.cs b/Managers/MusicDataManager.cs index 55382fe..edc6bdd 100644 --- a/Managers/MusicDataManager.cs +++ b/Managers/MusicDataManager.cs @@ -3,68 +3,67 @@ using Il2CppGameLogic; using Decimal = Il2CppSystem.Decimal; -namespace CustomAlbums.Managers +namespace CustomAlbums.Managers; + +internal static class MusicDataManager { - internal static class MusicDataManager - { - private static readonly List MusicDataList = new(); - private static readonly Logger Logger = new(nameof(MusicDataManager)); + private static readonly List MusicDataList = new(); + private static readonly Logger Logger = new(nameof(MusicDataManager)); - static MusicDataManager() - { - // Add an empty music data to the list as a placeholder - MusicDataList.Add(Interop.CreateTypeValue()); - } + static MusicDataManager() + { + // Add an empty music data to the list as a placeholder + MusicDataList.Add(Interop.CreateTypeValue()); + } - public static ReadOnlyCollection Data => MusicDataList.AsReadOnly(); + public static ReadOnlyCollection Data => MusicDataList.AsReadOnly(); - public static void Add(MusicData data) + public static void Add(MusicData data) + { + if (MusicDataList.Count >= short.MaxValue - 1) { - if (MusicDataList.Count >= short.MaxValue - 1) - { - Logger.Warning($"BMS is too large to load. Max MusicData allowed is {short.MaxValue}!"); - return; - } - - MusicDataList.Add(data); + Logger.Warning($"BMS is too large to load. Max MusicData allowed is {short.MaxValue}!"); + return; } - public static void Sort() - { - // Remove placeholder music data - MusicDataList.RemoveAt(0); + MusicDataList.Add(data); + } - // Sort the list - MusicDataList.Sort((l, r) => !(r.tick - r.dt - (l.tick - l.dt) > 0) ? 1 : -1); + public static void Sort() + { + // Remove placeholder music data + MusicDataList.RemoveAt(0); - // Add the placeholder music data back - MusicDataList.Insert(0, Interop.CreateTypeValue()); + // Sort the list + MusicDataList.Sort((l, r) => !(r.tick - r.dt - (l.tick - l.dt) > 0) ? 1 : -1); - // Reapply object IDs and round tick to nearest 3 decimal places - for (var i = 1; i < MusicDataList.Count; i++) - { - var musicData = MusicDataList[i]; - musicData.tick = Decimal.Round(musicData.tick, 3); - musicData.objId = (short)i; + // Add the placeholder music data back + MusicDataList.Insert(0, Interop.CreateTypeValue()); - MusicDataList[i] = musicData; - } + // Reapply object IDs and round tick to nearest 3 decimal places + for (var i = 1; i < MusicDataList.Count; i++) + { + var musicData = MusicDataList[i]; + musicData.tick = Decimal.Round(musicData.tick, 3); + musicData.objId = (short)i; - Logger.Msg("Sorted MusicData"); + MusicDataList[i] = musicData; } - public static void Set(int index, MusicData data) - { - MusicDataList[index] = data; - } + Logger.Msg("Sorted MusicData"); + } - public static void Clear() - { - // Clear the list - MusicDataList.Clear(); + public static void Set(int index, MusicData data) + { + MusicDataList[index] = data; + } - // Add an empty music data to the list as a placeholder - MusicDataList.Add(Interop.CreateTypeValue()); - } + public static void Clear() + { + // Clear the list + MusicDataList.Clear(); + + // Add an empty music data to the list as a placeholder + MusicDataList.Add(Interop.CreateTypeValue()); } } \ No newline at end of file diff --git a/Managers/PackManager.cs b/Managers/PackManager.cs index 87b636e..dbe35b3 100644 --- a/Managers/PackManager.cs +++ b/Managers/PackManager.cs @@ -1,33 +1,36 @@ -using CustomAlbums.Data; +using System.IO.Compression; +using CustomAlbums.Data; using CustomAlbums.Utilities; -namespace CustomAlbums.Managers +namespace CustomAlbums.Managers; + +public class PackManager { - internal class PackManager + private static readonly List Packs = new(); + + public static Pack GetPackFromUid(string uid) { - private static readonly List Packs = new(); - internal static Pack GetPackFromUid(string uid) - { - // If the uid is not custom or parsing the index fails - if (!uid.StartsWith($"{AlbumManager.Uid}-") || - !uid[4..].TryParseAsInt(out var uidIndex)) return null; + // If the uid is not custom or parsing the index fails + if (!uid.StartsWith($"{AlbumManager.Uid}-") || + !uid[4..].TryParseAsInt(out var uidIndex)) return null; - // Retrieve the pack that the uid belongs to - var pack = Packs.FirstOrDefault(pack => - uidIndex >= pack.StartIndex && uidIndex < pack.StartIndex + pack.Length); + // Retrieve the pack that the uid belongs to + var retrievedPack = Packs.FirstOrDefault(pack => + uidIndex >= pack.StartIndex && uidIndex < pack.StartIndex + pack.Length); - // If the pack has no albums in it return null, otherwise return pack (will be null if it doesn't exist) - return pack?.Length == 0 ? null : pack; - } + // If the pack has no albums in it return null, otherwise return pack (will be null if it doesn't exist) + return retrievedPack?.Length is 0 ? null : retrievedPack; + } - internal static Pack CreatePack(string file) - { - return Json.Deserialize(File.OpenRead(file)); - } + internal static Pack CreatePack(ZipArchiveEntry json, string path) + { + var pack = Json.Deserialize(json.Open()); + pack.Path = path; + return pack; + } - internal static void AddPack(Pack pack) - { - Packs.Add(pack); - } + internal static void AddPack(Pack pack) + { + Packs.Add(pack); } -} +} \ No newline at end of file diff --git a/Managers/SaveManager.cs b/Managers/SaveManager.cs index 7d53de0..c6c3f61 100644 --- a/Managers/SaveManager.cs +++ b/Managers/SaveManager.cs @@ -1,228 +1,233 @@ -using System.Text; +using System.IO.Compression; +using System.Text; using System.Text.Json; using CustomAlbums.Data; using CustomAlbums.Utilities; -using System.IO.Compression; -namespace CustomAlbums.Managers +namespace CustomAlbums.Managers; + +public class SaveManager { - internal class SaveManager + private const string SaveLocation = "UserData"; + internal static Logger Logger = new(nameof(SaveManager)); + public static CustomAlbumsSave SaveData { get; private set; } + internal static string PreviousScore { get; set; } = "-"; + + /// + /// Fixes the save file since this version of CAM uses a different naming scheme. + /// This allows cross-compatibility between CAM 3 and CAM 4, but not from CAM 4 to CAM 3. + /// + internal static void FixSaveFile() { - private const string SaveLocation = "UserData"; - internal static CustomAlbumsSave SaveData; - internal static Logger Logger = new(nameof(SaveManager)); - internal static string PreviousScore { get; set; } = "-"; - - /// - /// Fixes the save file since this version of CAM uses a different naming scheme. - /// This allows cross-compatibility between CAM 3 and CAM 4, but not from CAM 4 to CAM 3. - /// - internal static void FixSaveFile() + if (!ModSettings.SavingEnabled) return; + var firstHistory = SaveData.History.FirstOrDefault(); + var firstHighest = SaveData.Highest.FirstOrDefault(); + var firstFullCombo = SaveData.FullCombo.FirstOrDefault(); + var stringBuilder = new StringBuilder(); + + // If we need to fix the history + if (firstHistory != null && firstHistory.StartsWith("pkg_")) { - if (!ModSettings.SavingEnabled) return; - var firstHistory = SaveData.History.FirstOrDefault(); - var firstHighest = SaveData.Highest.FirstOrDefault(); - var firstFullCombo = SaveData.FullCombo.FirstOrDefault(); - var stringBuilder = new StringBuilder(); - - // If we need to fix the history - if (firstHistory != null && firstHistory.StartsWith("pkg_")) - { - var fixedList = new List(SaveData.History.Count); - foreach (var history in SaveData.History.Where(history => history.StartsWith("pkg_"))) - { - stringBuilder.Clear(); - stringBuilder.Append(history); - stringBuilder.Remove(0, 4); - stringBuilder.Insert(0, "album_"); - fixedList.Add(stringBuilder.ToString()); - } - - SaveData.History = fixedList; - } - - // If we need to fix the highest - if (!firstHighest.Equals(default(KeyValuePair>)) && - firstHighest.Key.StartsWith("pkg_")) - { - var fixedDictionaryHighest = - new Dictionary>(SaveData.Highest.Count); - foreach (var (key, value) in SaveData.Highest.Where(kv => kv.Key.StartsWith("pkg_"))) - { - stringBuilder.Clear(); - stringBuilder.Append(key); - stringBuilder.Remove(0, 4); - stringBuilder.Insert(0, "album_"); - fixedDictionaryHighest.Add(stringBuilder.ToString(), value); - } - - SaveData.Highest = fixedDictionaryHighest; - } - - if (!SaveData.UnlockedMasters.Any()) + var fixedList = new List(SaveData.History.Count); + foreach (var history in SaveData.History.Where(history => history.StartsWith("pkg_"))) { - var unlockedHighest = SaveData.Highest.Where(kv => - kv.Value.ContainsKey(3) && kv.Value.TryGetValue(2, out var chartSave) && chartSave.Evaluate >= 4).Select(kv => kv.Key); - var folderCharts = AlbumManager.LoadedAlbums.Where(kv => kv.Value.HasDifficulty(2) && kv.Value.HasDifficulty(3) && !kv.Value.IsPackaged).Select(kv => kv.Key); - var concat = unlockedHighest.Concat(folderCharts); - SaveData.UnlockedMasters.UnionWith(concat); + stringBuilder.Clear(); + stringBuilder.Append(history); + stringBuilder.Remove(0, 4); + stringBuilder.Insert(0, "album_"); + fixedList.Add(stringBuilder.ToString()); } - // If we don't need to fix the FullCombo then return - if (!firstFullCombo.Equals(default(KeyValuePair>)) && - !firstFullCombo.Key.StartsWith("pkg_")) return; + SaveData.History = fixedList; + } - var fixedDictionaryFc = new Dictionary>(SaveData.FullCombo.Count); - foreach (var (key, value) in SaveData.FullCombo.Where(kv => kv.Key.StartsWith("pkg_"))) + // If we need to fix the highest + if (!firstHighest.Equals(default(KeyValuePair>)) && + firstHighest.Key.StartsWith("pkg_")) + { + var fixedDictionaryHighest = + new Dictionary>(SaveData.Highest.Count); + foreach (var (key, value) in SaveData.Highest.Where(kv => kv.Key.StartsWith("pkg_"))) { - if (!key.StartsWith("pkg_")) continue; stringBuilder.Clear(); stringBuilder.Append(key); stringBuilder.Remove(0, 4); stringBuilder.Insert(0, "album_"); - fixedDictionaryFc.Add(stringBuilder.ToString(), value); + fixedDictionaryHighest.Add(stringBuilder.ToString(), value); } - SaveData.FullCombo = fixedDictionaryFc; + SaveData.Highest = fixedDictionaryHighest; } - private static void RestoreBackup() + if (!SaveData.UnlockedMasters.Any()) { - var backupPath = Path.Join(SaveLocation, "Backups", "Backups.zip"); - - // If a backup does not exist at all - if (!File.Exists(backupPath)) - { - Logger.Fail("No backups found. Please delete the CustomAlbums.json file in UserData folder to create a new save."); - return; - } + var unlockedHighest = SaveData.Highest.Where(kv => + kv.Value.ContainsKey(3) && kv.Value.TryGetValue(2, out var chartSave) && chartSave.Evaluate >= 4) + .Select(kv => kv.Key); + var folderCharts = AlbumManager.LoadedAlbums + .Where(kv => kv.Value.HasDifficulty(2) && kv.Value.HasDifficulty(3) && !kv.Value.IsPackaged) + .Select(kv => kv.Key); + var concat = unlockedHighest.Concat(folderCharts); + SaveData.UnlockedMasters.UnionWith(concat); + } - // Traverse the .zip file, trying every single backup in this archive - using var backupEntries = ZipFile.OpenRead(backupPath); - foreach (var backup in backupEntries.Entries.OrderByDescending(bak => bak.LastWriteTime) - .Where(bak => bak.Name.EndsWith("CustomAlbums.json.bak"))) - { - try - { - SaveData = Json.Deserialize(backup.Open()); - } - catch (Exception) - { - continue; - } - - // If our backup that we are trying to load is valid but empty, continue to try and find the last save with data in it - if (SaveData.IsEmpty()) continue; - Logger.Success($"Restored backup from {backup.LastWriteTime.DateTime}."); - return; - } - Logger.Fail("Could not restore save file. Please delete the CustomAlbums.json file in UserData folder to create a new save."); + // If we don't need to fix the FullCombo then return + if (!firstFullCombo.Equals(default(KeyValuePair>)) && + !firstFullCombo.Key.StartsWith("pkg_")) return; + + var fixedDictionaryFc = new Dictionary>(SaveData.FullCombo.Count); + foreach (var (key, value) in SaveData.FullCombo.Where(kv => kv.Key.StartsWith("pkg_"))) + { + if (!key.StartsWith("pkg_")) continue; + stringBuilder.Clear(); + stringBuilder.Append(key); + stringBuilder.Remove(0, 4); + stringBuilder.Insert(0, "album_"); + fixedDictionaryFc.Add(stringBuilder.ToString(), value); } - internal static void LoadSaveFile() + SaveData.FullCombo = fixedDictionaryFc; + } + + private static void RestoreBackup() + { + var backupPath = Path.Join(SaveLocation, "Backups", "Backups.zip"); + + // If a backup does not exist at all + if (!File.Exists(backupPath)) + { + Logger.Fail( + "No backups found. Please delete the CustomAlbums.json file in UserData folder to create a new save."); + return; + } + + // Traverse the .zip file, trying every single backup in this archive + using var backupEntries = ZipFile.OpenRead(backupPath); + foreach (var backup in backupEntries.Entries.OrderByDescending(bak => bak.LastWriteTime) + .Where(bak => bak.Name.EndsWith("CustomAlbums.json.bak"))) { - if (!ModSettings.SavingEnabled) return; try { - using var fileStream = File.OpenRead(Path.Join(SaveLocation, "CustomAlbums.json")); - SaveData = Json.Deserialize(fileStream); - FixSaveFile(); + SaveData = Json.Deserialize(backup.Open()); } - catch (Exception ex) + catch (Exception) { - if (ex is FileNotFoundException) - { - SaveData = new CustomAlbumsSave(); - } - else - { - Logger.Warning("Could not load save file. Attempting to restore backup..."); - RestoreBackup(); - } + continue; } + + // If our backup that we are trying to load is valid but empty, continue to try and find the last save with data in it + if (SaveData.IsEmpty()) continue; + Logger.Success($"Restored backup from {backup.LastWriteTime.DateTime}."); + return; } - internal static void SaveSaveFile() + Logger.Fail( + "Could not restore save file. Please delete the CustomAlbums.json file in UserData folder to create a new save."); + } + + internal static void LoadSaveFile() + { + if (!ModSettings.SavingEnabled) return; + try { - if (!ModSettings.SavingEnabled) return; - try + using var fileStream = File.OpenRead(Path.Join(SaveLocation, "CustomAlbums.json")); + SaveData = Json.Deserialize(fileStream); + FixSaveFile(); + } + catch (Exception ex) + { + if (ex is FileNotFoundException) { - if (SaveData is null) - { - Logger.Warning("Trying to save null data, not saving."); - return; - } - - File.WriteAllText(Path.Join(SaveLocation, "CustomAlbums.json"), JsonSerializer.Serialize(SaveData)); + SaveData = new CustomAlbumsSave(); } - catch (Exception ex) + else { - Logger.Warning("Failed to save save file. " + ex.StackTrace); + Logger.Warning("Could not load save file. Attempting to restore backup..."); + RestoreBackup(); } } + } - /// - /// Saves custom score given scoring information. - /// - /// The UID of the chart. - /// The difficulty index of the chart played. - /// The score of the play. - /// The accuracy of the play. - /// The maximum combo of the play. - /// The judgement ranking of the play. - /// The amount of misses in the play. - internal static void SaveScore(string uid, int musicDifficulty, int score, float accuracy, int maxCombo, - string evaluate, int miss) + internal static void SaveSaveFile() + { + if (!ModSettings.SavingEnabled) return; + try { - if (!ModSettings.SavingEnabled) return; + if (SaveData is null) + { + Logger.Warning("Trying to save null data, not saving."); + return; + } + + File.WriteAllText(Path.Join(SaveLocation, "CustomAlbums.json"), JsonSerializer.Serialize(SaveData)); + } + catch (Exception ex) + { + Logger.Warning("Failed to save save file. " + ex.StackTrace); + } + } - var album = AlbumManager.GetByUid(uid); - if (!album?.IsPackaged ?? true) return; + /// + /// Saves custom score given scoring information. + /// + /// The UID of the chart. + /// The difficulty index of the chart played. + /// The score of the play. + /// The accuracy of the play. + /// The maximum combo of the play. + /// The judgement ranking of the play. + /// The amount of misses in the play. + internal static void SaveScore(string uid, int musicDifficulty, int score, float accuracy, int maxCombo, + string evaluate, int miss) + { + if (!ModSettings.SavingEnabled) return; - var newEvaluate = evaluate switch - { - "sss" => 6, - "ss" => 5, - "s" => 4, - "a" => 3, - "b" => 2, - "c" => 1, - _ => 0 - }; + var album = AlbumManager.GetByUid(uid); + if (!album?.IsPackaged ?? true) return; - var albumName = album.AlbumName; + var newEvaluate = evaluate switch + { + "sss" => 6, + "ss" => 5, + "s" => 4, + "a" => 3, + "b" => 2, + "c" => 1, + _ => 0 + }; - // Create new album save - SaveData.Highest.TryAdd(albumName, new Dictionary()); + var albumName = album.AlbumName; - var currChartScore = SaveData.Highest[albumName]; + // Create new album save + SaveData.Highest.TryAdd(albumName, new Dictionary()); - // Create new save data if the difficulty doesn't exist - currChartScore.TryAdd(musicDifficulty, new ChartSave()); + var currChartScore = SaveData.Highest[albumName]; - // Set previous score for PnlVictory logic - var newScore = currChartScore[musicDifficulty]; - PreviousScore = newScore.Passed ? newScore.Score.ToString() : "-"; + // Create new save data if the difficulty doesn't exist + currChartScore.TryAdd(musicDifficulty, new ChartSave()); - // Set the correct new score, taking the max of everything - newScore.Passed = true; - newScore.Accuracy = Math.Max(accuracy, newScore.Accuracy); - newScore.Score = Math.Max(score, newScore.Score); - newScore.Combo = Math.Max(maxCombo, newScore.Combo); - newScore.Evaluate = Math.Max(newEvaluate, newScore.Evaluate); - newScore.AccuracyStr = (newScore.Accuracy / 100).ToStringInvariant("P2"); - newScore.Clear++; + // Set previous score for PnlVictory logic + var newScore = currChartScore[musicDifficulty]; + PreviousScore = newScore.Passed ? newScore.Score.ToString() : "-"; - if (musicDifficulty is 2 && AlbumManager.LoadedAlbums[albumName].HasDifficulty(3) && newScore.Evaluate >= 4) - SaveData.UnlockedMasters.Add(albumName); + // Set the correct new score, taking the max of everything + newScore.Passed = true; + newScore.Accuracy = Math.Max(accuracy, newScore.Accuracy); + newScore.Score = Math.Max(score, newScore.Score); + newScore.Combo = Math.Max(maxCombo, newScore.Combo); + newScore.Evaluate = Math.Max(newEvaluate, newScore.Evaluate); + newScore.AccuracyStr = (newScore.Accuracy / 100).ToStringInvariant("P2"); + newScore.Clear++; - // If there were no misses then add the chart/difficulty to the FullCombo list - if (miss != 0) return; + if (musicDifficulty is 2 && AlbumManager.LoadedAlbums[albumName].HasDifficulty(3) && newScore.Evaluate >= 4) + SaveData.UnlockedMasters.Add(albumName); - SaveData.FullCombo.TryAdd(albumName, new List()); + // If there were no misses then add the chart/difficulty to the FullCombo list + if (miss != 0) return; - if (!SaveData.FullCombo[albumName].Contains(musicDifficulty)) - SaveData.FullCombo[albumName].Add(musicDifficulty); - } + SaveData.FullCombo.TryAdd(albumName, new List()); + + if (!SaveData.FullCombo[albumName].Contains(musicDifficulty)) + SaveData.FullCombo[albumName].Add(musicDifficulty); } } \ No newline at end of file diff --git a/ModExtensions/AlbumEventArgs.cs b/ModExtensions/AlbumEventArgs.cs index f407596..ea21d08 100644 --- a/ModExtensions/AlbumEventArgs.cs +++ b/ModExtensions/AlbumEventArgs.cs @@ -1,14 +1,13 @@ using CustomAlbums.Data; -namespace CustomAlbums.ModExtensions +namespace CustomAlbums.ModExtensions; + +public class AlbumEventArgs : EventArgs { - public class AlbumEventArgs : EventArgs - { - public Album Album; + public Album Album; - public AlbumEventArgs(Album album) - { - Album = album; - } + public AlbumEventArgs(Album album) + { + Album = album; } -} +} \ No newline at end of file diff --git a/ModExtensions/AssetEventArgs.cs b/ModExtensions/AssetEventArgs.cs index bb2b084..839a5f5 100644 --- a/ModExtensions/AssetEventArgs.cs +++ b/ModExtensions/AssetEventArgs.cs @@ -1,14 +1,13 @@ -namespace CustomAlbums.ModExtensions -{ - public class AssetEventArgs : EventArgs - { - public string AssetName; - public IntPtr AssetPtr; +namespace CustomAlbums.ModExtensions; + +public class AssetEventArgs : EventArgs +{ + public string AssetName; + public IntPtr AssetPtr; - public AssetEventArgs(string assetName, IntPtr assetPtr) - { - AssetName = assetName; - AssetPtr = assetPtr; - } + public AssetEventArgs(string assetName, IntPtr assetPtr) + { + AssetName = assetName; + AssetPtr = assetPtr; } -} +} \ No newline at end of file diff --git a/ModExtensions/Events.cs b/ModExtensions/Events.cs index 97fb71d..66137fe 100644 --- a/ModExtensions/Events.cs +++ b/ModExtensions/Events.cs @@ -1,25 +1,23 @@ using CustomAlbums.Managers; using CustomAlbums.Patches; -namespace CustomAlbums.ModExtensions -{ - public static class Events - { - public delegate void LoadAssetEvent(object s, AssetEventArgs e); +namespace CustomAlbums.ModExtensions; - public static event LoadAssetEvent OnAssetLoaded - { - add => AssetPatch.OnAssetLoaded += value; - remove => AssetPatch.OnAssetLoaded -= value; - } +public static class Events +{ + public delegate void LoadAlbumEvent(object s, AlbumEventArgs e); - public delegate void LoadAlbumEvent(object s, AlbumEventArgs e); + public delegate void LoadAssetEvent(object s, AssetEventArgs e); - public static event LoadAlbumEvent OnAlbumLoaded - { - add => AlbumManager.OnAlbumLoaded += value; - remove => AlbumManager.OnAlbumLoaded -= value; - } + public static event LoadAssetEvent OnAssetLoaded + { + add => AssetPatch.OnAssetLoaded += value; + remove => AssetPatch.OnAssetLoaded -= value; + } + public static event LoadAlbumEvent OnAlbumLoaded + { + add => AlbumManager.OnAlbumLoaded += value; + remove => AlbumManager.OnAlbumLoaded -= value; } -} +} \ No newline at end of file diff --git a/ModSettings.cs b/ModSettings.cs index aab8ecf..9a7bd06 100644 --- a/ModSettings.cs +++ b/ModSettings.cs @@ -1,24 +1,23 @@ using MelonLoader; -namespace CustomAlbums +namespace CustomAlbums; + +internal static class ModSettings { - internal static class ModSettings - { - private static MelonPreferences_Entry _verboseLogging; - private static MelonPreferences_Entry _savingEnabled; - private static MelonPreferences_Entry _enableLogWriteToFile; - public static bool VerboseLogging => _verboseLogging.Value; - public static bool SavingEnabled => _savingEnabled.Value; - public static bool LoggingToFileEnabled => _enableLogWriteToFile.Value; + private static MelonPreferences_Entry _verboseLogging; + private static MelonPreferences_Entry _savingEnabled; + private static MelonPreferences_Entry _enableLogWriteToFile; + public static bool VerboseLogging => _verboseLogging.Value; + public static bool SavingEnabled => _savingEnabled.Value; + public static bool LoggingToFileEnabled => _enableLogWriteToFile.Value; - internal static void Register() - { - var category = MelonPreferences.CreateCategory("CustomAlbums", "Custom Albums"); + internal static void Register() + { + var category = MelonPreferences.CreateCategory("CustomAlbums", "Custom Albums"); - _verboseLogging = category.CreateEntry("VerboseLogging", false, "Verbose Logging"); - _savingEnabled = category.CreateEntry("SavingEnabled", true, "Enable Saving"); - _enableLogWriteToFile = category.CreateEntry("LogWriteToFile", false, "Enable Log Write to File"); - } + _verboseLogging = category.CreateEntry("VerboseLogging", false, "Verbose Logging"); + _savingEnabled = category.CreateEntry("SavingEnabled", true, "Enable Saving"); + _enableLogWriteToFile = category.CreateEntry("LogWriteToFile", false, "Enable Log Write to File"); } } \ No newline at end of file diff --git a/Patches/AnalyticsPatch.cs b/Patches/AnalyticsPatch.cs index 994f7c3..08eb6b3 100644 --- a/Patches/AnalyticsPatch.cs +++ b/Patches/AnalyticsPatch.cs @@ -1,109 +1,107 @@ -using CustomAlbums.Managers; +using System.Reflection; +using CustomAlbums.Managers; using HarmonyLib; using Il2CppAssets.Scripts.Database; using Il2CppPeroPeroGames.DataStatistics; -using System.Reflection; using UnityEngine; using Logger = CustomAlbums.Utilities.Logger; -namespace CustomAlbums.Patches +namespace CustomAlbums.Patches; + +internal class AnalyticsPatch { - internal class AnalyticsPatch + internal static readonly Logger Logger = new(nameof(AnalyticsPatch)); + + /// + /// Blocks tag analytics from being sent if tag is CustomAlbums. + /// + [HarmonyPatch(typeof(ThinkingDataPeripheralHelper), nameof(ThinkingDataPeripheralHelper.CommonSendString))] + internal class CommonSendStringPatch { - internal static readonly Logger Logger = new(nameof(AnalyticsPatch)); - - /// - /// Blocks tag analytics from being sent if tag is CustomAlbums. - /// - [HarmonyPatch(typeof(ThinkingDataPeripheralHelper), nameof(ThinkingDataPeripheralHelper.CommonSendString))] - internal class CommonSendStringPatch + private static bool Prefix(string eventName, string propertyName, string info) { - private static bool Prefix(string eventName, string propertyName, string info) - { - var runCond = info != AlbumManager.Languages["ChineseS"]; - if (!runCond) Logger.Msg("Blocked custom albums tag analytics."); - return runCond; - } + var runCond = info != AlbumManager.Languages["ChineseS"]; + if (!runCond) Logger.Msg("Blocked custom albums tag analytics."); + return runCond; } + } - /// - /// Blocks chart analytics from being sent if the chart is custom. - /// - [HarmonyPatch(typeof(ThinkingDataBattleHelper))] - internal class SendMDPatch + /// + /// Blocks chart analytics from being sent if the chart is custom. + /// + [HarmonyPatch(typeof(ThinkingDataBattleHelper))] + internal class SendMDPatch + { + private static IEnumerable TargetMethods() { - private static IEnumerable TargetMethods() - { - return typeof(ThinkingDataBattleHelper).GetMethods(BindingFlags.Instance | BindingFlags.Public) - .Where(method => method.Name.StartsWith("Send")); - } + return typeof(ThinkingDataBattleHelper).GetMethods(BindingFlags.Instance | BindingFlags.Public) + .Where(method => method.Name.StartsWith("Send")); + } - private static bool Prefix() - { - var runCond = !BattleHelper.MusicInfo().uid.StartsWith($"{AlbumManager.Uid}-"); - if (!runCond) Logger.Msg("Blocked sending analytics of custom chart."); - return runCond; - } + private static bool Prefix() + { + var runCond = !BattleHelper.MusicInfo().uid.StartsWith($"{AlbumManager.Uid}-"); + if (!runCond) Logger.Msg("Blocked sending analytics of custom chart."); + return runCond; } + } - /// - /// Prevents the game from sending any analytics if the musicInfo is custom. - /// - [HarmonyPatch(typeof(ThinkingDataPeripheralHelper), nameof(ThinkingDataPeripheralHelper.PostToThinkingData))] - internal class PostToThinkingDataPatch + /// + /// Prevents the game from sending any analytics if the musicInfo is custom. + /// + [HarmonyPatch(typeof(ThinkingDataPeripheralHelper), nameof(ThinkingDataPeripheralHelper.PostToThinkingData))] + internal class PostToThinkingDataPatch + { + private static bool Prefix(string dataStatisticsEventDefinesName, MusicInfo musicInfo) { - private static bool Prefix(string dataStatisticsEventDefinesName, MusicInfo musicInfo) - { - var runCond = !musicInfo.uid.StartsWith($"{AlbumManager.Uid}-"); - if (!runCond) Logger.Msg("Blocked thinking data post of custom chart."); - return runCond; - } + var runCond = !musicInfo.uid.StartsWith($"{AlbumManager.Uid}-"); + if (!runCond) Logger.Msg("Blocked thinking data post of custom chart."); + return runCond; } + } - /// - /// Prevents the game from sending any analytics if the favorited chart is custom. - /// - [HarmonyPatch(typeof(ThinkingDataPeripheralHelper), - nameof(ThinkingDataPeripheralHelper.SendFavoriteMusicBehavior))] - internal class SendFavoriteMusicBehaviorPatch + /// + /// Prevents the game from sending any analytics if the favorited chart is custom. + /// + [HarmonyPatch(typeof(ThinkingDataPeripheralHelper), + nameof(ThinkingDataPeripheralHelper.SendFavoriteMusicBehavior))] + internal class SendFavoriteMusicBehaviorPatch + { + private static bool Prefix(string dataStatisticsEventDefinesNameMusicInfo, MusicInfo musicInfo) { - private static bool Prefix(string dataStatisticsEventDefinesNameMusicInfo, MusicInfo musicInfo) - { - var runCond = !musicInfo.uid.StartsWith($"{AlbumManager.Uid}-"); - if (!runCond) Logger.Msg("Blocked sending favorite chart analytics of custom chart."); - return runCond; - } + var runCond = !musicInfo.uid.StartsWith($"{AlbumManager.Uid}-"); + if (!runCond) Logger.Msg("Blocked sending favorite chart analytics of custom chart."); + return runCond; } + } - /// - /// Prevents the game from sending any analytics if the musicInfo is custom. - /// - [HarmonyPatch(typeof(ThinkingDataPeripheralHelper), nameof(ThinkingDataPeripheralHelper.PostMusicChooseInfo))] - internal class PostMusicChooseInfoPatch + /// + /// Prevents the game from sending any analytics if the musicInfo is custom. + /// + [HarmonyPatch(typeof(ThinkingDataPeripheralHelper), nameof(ThinkingDataPeripheralHelper.PostMusicChooseInfo))] + internal class PostMusicChooseInfoPatch + { + private static bool Prefix(Vector2Int diffValue, string chooseMusicType, string searchName, MusicInfo musicInfo) { - private static bool Prefix(Vector2Int diffValue, string chooseMusicType, string searchName, MusicInfo musicInfo) - { - var runCond = !musicInfo.uid.StartsWith($"{AlbumManager.Uid}-"); - if (!runCond) Logger.Msg("Blocked sending chosen music from search analytics of custom chart."); - return runCond; - } + var runCond = !musicInfo.uid.StartsWith($"{AlbumManager.Uid}-"); + if (!runCond) Logger.Msg("Blocked sending chosen music from search analytics of custom chart."); + return runCond; } + } - /// - /// Cleans search result analytics of custom charts. - /// - [HarmonyPatch(typeof(ThinkingDataPeripheralHelper), nameof(ThinkingDataPeripheralHelper.GetSearchResultInfo))] - internal class GetSearchResultInfoPatch + /// + /// Cleans search result analytics of custom charts. + /// + [HarmonyPatch(typeof(ThinkingDataPeripheralHelper), nameof(ThinkingDataPeripheralHelper.GetSearchResultInfo))] + internal class GetSearchResultInfoPatch + { + private static bool Prefix(MusicInfo musicInfo) { - - private static bool Prefix(MusicInfo musicInfo) - { - var runCond = !musicInfo.uid.StartsWith($"{AlbumManager.Uid}-"); - if (!runCond) Logger.Msg("Blocking custom album from being added to search analytics."); - return runCond; - } + var runCond = !musicInfo.uid.StartsWith($"{AlbumManager.Uid}-"); + if (!runCond) Logger.Msg("Blocking custom album from being added to search analytics."); + return runCond; } } -} +} \ No newline at end of file diff --git a/Patches/AnimatedCoverPatch.cs b/Patches/AnimatedCoverPatch.cs deleted file mode 100644 index 4ab74b5..0000000 --- a/Patches/AnimatedCoverPatch.cs +++ /dev/null @@ -1,81 +0,0 @@ -using CustomAlbums.Managers; -using HarmonyLib; -using Il2Cpp; -using Il2CppAssets.Scripts.Database; -using Il2CppAssets.Scripts.PeroTools.Nice.Interface; -using UnityEngine; -using Logger = CustomAlbums.Utilities.Logger; - -namespace CustomAlbums.Patches -{ - internal class AnimatedCoverPatch - { - /// - /// Enables animated album covers. - /// - [HarmonyPatch(typeof(MusicStageCell), nameof(MusicStageCell.Awake))] - internal static class MusicStageCellPatch - { - private static readonly Logger Logger = new(nameof(MusicStageCellPatch)); - private static readonly LinkedList Cells = new(); - internal static string CurrentScene { get; set; } - - public static void AnimateCoversUpdate() - { - if (CurrentScene is not "UISystem_PC") return; - var dbMusicTag = GlobalDataBase.dbMusicTag; - - if (dbMusicTag == null) return; - - for (var node = Cells.First; node is not null;) - { - var next = node.Next; - var cell = node.Value; - var index = cell?.m_VariableBehaviour?.Cast().GetResult() ?? -1; - - var uid = dbMusicTag?.GetShowStageUidByIndex(index) ?? "?"; - - // If uid isn't defined - if (uid is "?") - { - Cells.Remove(node); - node = next; - continue; - } - - var musicInfo = dbMusicTag?.GetMusicInfoFromAll(uid); - - // If cell is not custom - if (musicInfo?.albumJsonIndex < AlbumManager.Uid) - { - Cells.Remove(node); - node = next; - continue; - } - - var album = AlbumManager.GetByUid(uid); - var animatedCover = album?.AnimatedCover; - - // If cell is not animated - if (animatedCover is null || animatedCover.FramesPerSecond is 0) - { - Cells.Remove(node); - node = next; - continue; - } - - // Animate the cell, with one last null check - var frame = (int)Mathf.Floor(Time.time * 1000) % - (animatedCover.FramesPerSecond * animatedCover.FrameCount) / animatedCover.FramesPerSecond; - if (cell != null) cell.m_StageImg.sprite = animatedCover.Frames[frame]; - node = next; - } - } - - private static void Prefix(MusicStageCell __instance) - { - Cells.AddLast(__instance); - } - } - } -} \ No newline at end of file diff --git a/Patches/AssetPatch.cs b/Patches/AssetPatch.cs index 1818ec2..8c84f5e 100644 --- a/Patches/AssetPatch.cs +++ b/Patches/AssetPatch.cs @@ -1,4 +1,4 @@ -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text.Json; using System.Text.Json.Nodes; @@ -21,343 +21,351 @@ using Logger = CustomAlbums.Utilities.Logger; using Object = UnityEngine.Object; -namespace CustomAlbums.Patches +namespace CustomAlbums.Patches; + +internal class AssetPatch { - internal class AssetPatch + private static readonly NativeHook Hook = new(); + private static readonly Dictionary AssetCache = new(); + private static readonly Dictionary> AssetHandler = new(); + private static readonly Logger Logger = new(nameof(AssetPatch)); + + private static int _latestAlbumNum; + private static bool _doneLoadingAlbumsFlag; + internal static Events.LoadAssetEvent OnAssetLoaded; + + /// + /// Binds the asset to a new key, while removing the old asset. + /// + /// The old assetName. + /// The new assetName where the value should be bound. + /// if the key was successfully bound; otherwise, . + internal static bool ModifyCacheKey(string oldAssetName, string newAssetName) { - private static readonly NativeHook Hook = new(); - private static readonly Dictionary AssetCache = new(); - private static readonly Dictionary> AssetHandler = new(); - private static readonly Logger Logger = new(nameof(AssetPatch)); - - private static int _latestAlbumNum; - private static bool _doneLoadingAlbumsFlag; - internal static Events.LoadAssetEvent OnAssetLoaded; - - /// - /// Binds the asset to a new key, while removing the old asset. - /// - /// The old assetName. - /// The new assetName where the value should be bound. - /// if the key was successfully bound; otherwise, . - internal static bool ModifyCacheKey(string oldAssetName, string newAssetName) - { - var success = AssetCache.Remove(oldAssetName, out var asset); - return success && AssetCache.TryAdd(newAssetName, asset); - } + var success = AssetCache.Remove(oldAssetName, out var asset); + return success && AssetCache.TryAdd(newAssetName, asset); + } - /// - /// Removes a KeyValuePair from the asset cache. - /// - /// The key corresponding to the value to be removed. - /// if the entry was successfully removed; otherwise, . - internal static bool RemoveFromCache(string key) - { - return AssetCache.Remove(key); - } + /// + /// Updates the cache with a new asset. + /// + internal static void UpdateCache(string key, Object value) + { + AssetCache[key] = value; + } - /// - /// Adds methods to the AssetHandler. - /// The AssetHandler modifies certain assets based on their name. - /// - internal static void InitializeHandler() + /// + /// Removes a KeyValuePair from the asset cache. + /// + /// The key corresponding to the value to be removed. + /// if the entry was successfully removed; otherwise, . + internal static bool RemoveFromCache(string key) + { + return AssetCache.Remove(key); + } + + /// + /// Adds methods to the AssetHandler. + /// The AssetHandler modifies certain assets based on their name. + /// + internal static void InitializeHandler() + { + AssetHandler.Add("albums", (assetName, assetPtr, _) => { - AssetHandler.Add("albums", (assetName, assetPtr, _) => + var jsonArray = Json.Deserialize(new TextAsset(assetPtr).text); + + // adds the CustomAlbums "dlc" to the game + jsonArray.Add(new { - var jsonArray = Json.Deserialize(new TextAsset(assetPtr).text); + uid = AlbumManager.MusicPackage, + title = "Custom Albums", + prefabsName = $"AlbumDisco{AlbumManager.Uid}", + price = "¥25.00", + jsonName = AlbumManager.JsonName, + needPurchase = false, + free = true + }); - // adds the CustomAlbums "dlc" to the game - jsonArray.Add(new - { - uid = AlbumManager.MusicPackage, - title = "Custom Albums", - prefabsName = $"AlbumDisco{AlbumManager.Uid}", - price = "¥25.00", - jsonName = AlbumManager.JsonName, - needPurchase = false, - free = true - }); + // Create and add the new asset with the CustomAlbums "dlc" + var newAsset = CreateTextAsset(assetName, JsonSerializer.Serialize(jsonArray)); + if (!Singleton.instance.m_Dictionary.ContainsKey(assetName)) + Singleton.instance.Add(assetName, newAsset.text); - // Create and add the new asset with the CustomAlbums "dlc" - var newAsset = CreateTextAsset(assetName, JsonSerializer.Serialize(jsonArray)); - if (!Singleton.instance.m_Dictionary.ContainsKey(assetName)) - Singleton.instance.Add(assetName, newAsset.text); + // Set cache and return newAsset's pointer if it non-null + AssetCache[assetName] = newAsset; + return newAsset?.Pointer ?? assetPtr; + }); - // Set cache and return newAsset's pointer if it non-null - AssetCache[assetName] = newAsset; - return newAsset?.Pointer ?? assetPtr; - }); + AssetHandler.Add(AlbumManager.JsonName, (assetName, assetPtr, _) => + { + var jsonArray = new JsonArray(); - AssetHandler.Add(AlbumManager.JsonName, (assetName, assetPtr, _) => + // Add each custom charts' data to the album + foreach (var (albumStr, albumObj) in AlbumManager.LoadedAlbums) { - var jsonArray = new JsonArray(); - - // Add each custom charts' data to the album - foreach (var (albumStr, albumObj) in AlbumManager.LoadedAlbums) + var albumInfo = albumObj.Info; + var customChartJson = new { - var albumInfo = albumObj.Info; - var customChartJson = new - { - uid = albumObj.Uid, - name = albumInfo.Name, - author = albumInfo.Author, - bpm = albumInfo.Bpm, - music = $"{albumStr}_music", - demo = $"{albumStr}_demo", - cover = $"{albumStr}_cover", - noteJson = $"{albumStr}_map", - scene = albumInfo.Scene, - unlockLevel = "0", - levelDesigner = albumInfo.LevelDesigner, - levelDesigner1 = albumInfo.LevelDesigner1 ?? albumInfo.LevelDesigner, - levelDesigner2 = albumInfo.LevelDesigner2 ?? albumInfo.LevelDesigner, - levelDesigner3 = albumInfo.LevelDesigner3 ?? albumInfo.LevelDesigner, - levelDesigner4 = albumInfo.LevelDesigner4 ?? albumInfo.LevelDesigner, - levelDesigner5 = albumInfo.LevelDesigner5 ?? albumInfo.LevelDesigner, - difficulty1 = albumInfo.Difficulty1 ?? "0", - difficulty2 = albumInfo.Difficulty2, - difficulty3 = albumInfo.Difficulty3 ?? "0", - difficulty4 = albumInfo.Difficulty4 ?? "0", - difficulty5 = albumInfo.Difficulty5 ?? "0" - }; - jsonArray.Add(customChartJson); - - // Configure the searchtaginfo - var config = Singleton.instance.GetConfigObject(); - var searchTag = new MusicSearchTagInfo - { - uid = customChartJson.uid, - listIndex = config.count - }; + uid = albumObj.Uid, + name = albumInfo.Name, + author = albumInfo.Author, + bpm = albumInfo.Bpm, + music = $"{albumStr}_music", + demo = $"{albumStr}_demo", + cover = $"{albumStr}_cover", + noteJson = $"{albumStr}_map", + scene = albumInfo.Scene, + unlockLevel = "0", + levelDesigner = albumInfo.LevelDesigner, + levelDesigner1 = albumInfo.LevelDesigner1 ?? albumInfo.LevelDesigner, + levelDesigner2 = albumInfo.LevelDesigner2 ?? albumInfo.LevelDesigner, + levelDesigner3 = albumInfo.LevelDesigner3 ?? albumInfo.LevelDesigner, + levelDesigner4 = albumInfo.LevelDesigner4 ?? albumInfo.LevelDesigner, + levelDesigner5 = albumInfo.LevelDesigner5 ?? albumInfo.LevelDesigner, + difficulty1 = albumInfo.Difficulty1 ?? "0", + difficulty2 = albumInfo.Difficulty2, + difficulty3 = albumInfo.Difficulty3 ?? "0", + difficulty4 = albumInfo.Difficulty4 ?? "0", + difficulty5 = albumInfo.Difficulty5 ?? "0" + }; + jsonArray.Add(customChartJson); + + // Configure the searchtaginfo + var config = Singleton.instance.GetConfigObject(); + var searchTag = new MusicSearchTagInfo + { + uid = customChartJson.uid, + listIndex = config.count + }; - // Create the search tag itself - var tags = new List { "custom albums" }; - if (albumInfo.SearchTags != null) tags.AddRange(albumInfo.SearchTags); - if (!string.IsNullOrEmpty(albumInfo.NameRomanized)) tags.Add(albumInfo.NameRomanized); - for (var i = 0; i < tags.Count; i++) tags[i] = tags[i].ToLower(); + // Create the search tag itself + var tags = new List { "custom albums" }; + if (albumInfo.SearchTags != null) tags.AddRange(albumInfo.SearchTags); + if (!string.IsNullOrEmpty(albumInfo.NameRomanized)) tags.Add(albumInfo.NameRomanized); + for (var i = 0; i < tags.Count; i++) tags[i] = tags[i].ToLower(); - searchTag.tag = new Il2CppStringArray(tags.ToArray()); + searchTag.tag = new Il2CppStringArray(tags.ToArray()); - // Add it to the game - config.m_Dictionary.Add(searchTag.uid, searchTag); - config.list.Add(searchTag); - } + // Add it to the game + config.m_Dictionary.Add(searchTag.uid, searchTag); + config.list.Add(searchTag); + } - // Create and add the new asset with the custom charts' data - var newAsset = CreateTextAsset(assetName, JsonSerializer.Serialize(jsonArray)); - if (!Singleton.instance.m_Dictionary.ContainsKey(assetName)) - Singleton.instance.Add(assetName, newAsset.text); + // Create and add the new asset with the custom charts' data + var newAsset = CreateTextAsset(assetName, JsonSerializer.Serialize(jsonArray)); + if (!Singleton.instance.m_Dictionary.ContainsKey(assetName)) + Singleton.instance.Add(assetName, newAsset.text); - // Set cache and return newAsset's pointer if it non-null - AssetCache[assetName] = newAsset; - return newAsset?.Pointer ?? assetPtr; - }); + // Set cache and return newAsset's pointer if it non-null + AssetCache[assetName] = newAsset; + return newAsset?.Pointer ?? assetPtr; + }); - AssetHandler.Add("albums_", (assetName, assetPtr, language) => + AssetHandler.Add("albums_", (assetName, assetPtr, language) => + { + var jsonArray = Json.Deserialize(new TextAsset(assetPtr).text); + + // Adds the correct language for the "Custom Albums" album + jsonArray.Add(new { - var jsonArray = Json.Deserialize(new TextAsset(assetPtr).text); + title = AlbumManager.Languages[language] + }); - // Adds the correct language for the "Custom Albums" album + // Create and add the new asset with the correct lingual name of "Custom Albums" + var newAsset = CreateTextAsset(assetName, JsonSerializer.Serialize(jsonArray)); + if (!Singleton.instance.m_Dictionary.ContainsKey(assetName)) + Singleton.instance.Add(assetName, newAsset.text); + + // Set cache and return newAsset's pointer if it non-null + AssetCache[assetName] = newAsset; + return newAsset?.Pointer ?? assetPtr; + }); + + AssetHandler.Add($"{AlbumManager.JsonName}_", (assetName, assetPtr, _) => + { + var jsonArray = new JsonArray(); + + // This should technically be written to retrieve and add the name of the chart based on your selected language + // However this only grabs whatever name is given in the info.json + // Very likely not a big deal + foreach (var (_, value) in AlbumManager.LoadedAlbums) jsonArray.Add(new { - title = AlbumManager.Languages[language] + name = value.Info.Name, + author = value.Info.Author }); - // Create and add the new asset with the correct lingual name of "Custom Albums" - var newAsset = CreateTextAsset(assetName, JsonSerializer.Serialize(jsonArray)); - if (!Singleton.instance.m_Dictionary.ContainsKey(assetName)) - Singleton.instance.Add(assetName, newAsset.text); + // Create and add the new asset with the names of each custom chart + var newAsset = CreateTextAsset(assetName, JsonSerializer.Serialize(jsonArray)); + if (!Singleton.instance.m_Dictionary.ContainsKey(assetName)) + Singleton.instance.Add(assetName, newAsset.text); - // Set cache and return newAsset's pointer if it non-null - AssetCache[assetName] = newAsset; - return newAsset?.Pointer ?? assetPtr; - }); + // Set cache and return newAsset's pointer if it non-null + AssetCache[assetName] = newAsset; + return newAsset?.Pointer ?? assetPtr; + }); - AssetHandler.Add($"{AlbumManager.JsonName}_", (assetName, assetPtr, _) => - { - var jsonArray = new JsonArray(); - - // This should technically be written to retrieve and add the name of the chart based on your selected language - // However this only grabs whatever name is given in the info.json - // Very likely not a big deal - foreach (var (_, value) in AlbumManager.LoadedAlbums) - jsonArray.Add(new - { - name = value.Info.Name, - author = value.Info.Author - }); - - // Create and add the new asset with the names of each custom chart - var newAsset = CreateTextAsset(assetName, JsonSerializer.Serialize(jsonArray)); - if (!Singleton.instance.m_Dictionary.ContainsKey(assetName)) - Singleton.instance.Add(assetName, newAsset.text); - - // Set cache and return newAsset's pointer if it non-null - AssetCache[assetName] = newAsset; - return newAsset?.Pointer ?? assetPtr; - }); - - AssetHandler.Add("album_", (assetName, assetPtr, _) => + AssetHandler.Add("album_", (assetName, assetPtr, _) => + { + var cache = true; + Object newAsset = null; + var suffix = AssetIdentifiers.AssetSuffixes.FirstOrDefault(assetName.EndsWith); + if (!string.IsNullOrEmpty(suffix)) { - var cache = true; - Object newAsset = null; - var suffix = AssetIdentifiers.AssetSuffixes.FirstOrDefault(assetName.EndsWith); - if (!string.IsNullOrEmpty(suffix)) + var albumKey = assetName.Remove(assetName.Length - suffix.Length); + AlbumManager.LoadedAlbums.TryGetValue(albumKey, out var album); + if (suffix.StartsWith("_map")) { - var albumKey = assetName.Remove(assetName.Length - suffix.Length); - AlbumManager.LoadedAlbums.TryGetValue(albumKey, out var album); - if (suffix.StartsWith("_map")) - { - newAsset = album?.Sheets.GetValueOrDefault(suffix[^1].ToString().ParseAsInt())?.GetStage(); - // Do not cache the StageInfos, this should be loaded into memory only when we need it - cache = false; - } - else + newAsset = album?.Sheets.GetValueOrDefault(suffix[^1].ToString().ParseAsInt())?.GetStage(); + // Do not cache the StageInfos, this should be loaded into memory only when we need it + cache = false; + } + else + { + // Set the newAsset to the objects held in the album + switch (suffix) { - // Set the newAsset to the objects held in the album - switch (suffix) - { - case "_demo": - newAsset = album?.Demo; - break; - case "_music": - newAsset = album?.Music; - break; - case "_cover": - newAsset = album?.AnimatedCover?.Frames[0] ?? album?.Cover; - break; - default: - Logger.Error($"Unknown suffix: {suffix}"); - break; - } + case "_demo": + newAsset = album?.Demo; + break; + case "_music": + newAsset = album?.Music; + break; + case "_cover": + newAsset = album?.Cover; + break; + default: + Logger.Error($"Unknown suffix: {suffix}"); + break; } } + } - // Set cache if we should and return newAsset's pointer if it non-null - if (cache) AssetCache[assetName] = newAsset; - return newAsset?.Pointer ?? assetPtr; - }); - } + // Set cache if we should and return newAsset's pointer if it non-null + if (cache) AssetCache[assetName] = newAsset; + return newAsset?.Pointer ?? assetPtr; + }); + } - /// - /// Gets where T : and detours it using a - /// where T : to LoadFromNamePatch. - /// - internal static unsafe void AttachHook() - { - var loadFromNameMethod = AccessTools.Method(typeof(ResourcesManager), - nameof(ResourcesManager.LoadFromName), new[] { typeof(string) }, - new[] { typeof(TextAsset) }); + /// + /// Gets where T : and detours it using a + /// where T : to LoadFromNamePatch. + /// + internal static unsafe void AttachHook() + { + var loadFromNameMethod = AccessTools.Method(typeof(ResourcesManager), + nameof(ResourcesManager.LoadFromName), new[] { typeof(string) }, + new[] { typeof(TextAsset) }); - if (loadFromNameMethod is null) - { - Logger.Error("FATAL ERROR: AssetPatch failed."); - Thread.Sleep(1000); - Environment.Exit(1); - } + if (loadFromNameMethod is null) + { + Logger.Error("FATAL ERROR: AssetPatch failed."); + Thread.Sleep(1000); + Environment.Exit(1); + } - // AttachHook should only be run once; create the handler - InitializeHandler(); + // AttachHook should only be run once; create the handler + InitializeHandler(); - var loadFromNamePointer = *(IntPtr*)(IntPtr)Il2CppInteropUtils - .GetIl2CppMethodInfoPointerFieldForGeneratedMethod(loadFromNameMethod).GetValue(null)!; + var loadFromNamePointer = *(IntPtr*)(IntPtr)Il2CppInteropUtils + .GetIl2CppMethodInfoPointerFieldForGeneratedMethod(loadFromNameMethod).GetValue(null)!; - // Create a pointer for our new method to be called instead - // This is Cdecl because this is going to be called in an unmanaged context - delegate* unmanaged[Cdecl] detourPointer = &LoadFromNamePatch; + // Create a pointer for our new method to be called instead + // This is Cdecl because this is going to be called in an unmanaged context + delegate* unmanaged[Cdecl] detourPointer = &LoadFromNamePatch; - // Set the hook so that LoadFromNamePatch runs instead of the original LoadFromName - Hook.Detour = (IntPtr)detourPointer; - Hook.Target = loadFromNamePointer; - Hook.Attach(); - } + // Set the hook so that LoadFromNamePatch runs instead of the original LoadFromName + Hook.Detour = (IntPtr)detourPointer; + Hook.Target = loadFromNamePointer; + Hook.Attach(); + } - /// - /// Modifies certain game data as it get loaded. - /// The game data that is modified directly adds support for custom albums. - /// - /// The instance of ResourceManager calling LoadFromName. - /// The pointer to the string assetName. - /// Method info used by the original method. - /// - /// An of either a newly created asset if it exists or the original asset if a new one was not - /// created. - /// - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static IntPtr LoadFromNamePatch(IntPtr instance, IntPtr assetNamePtr, IntPtr nativeMethodInfo) - { - // Retrieve the pointer of the asset and the name of the asset - var assetPtr = Hook.Trampoline(instance, assetNamePtr, nativeMethodInfo); - var assetName = IL2CPP.Il2CppStringToManaged(assetNamePtr) ?? string.Empty; + /// + /// Modifies certain game data as it get loaded. + /// The game data that is modified directly adds support for custom albums. + /// + /// The instance of ResourceManager calling LoadFromName. + /// The pointer to the string assetName. + /// Method info used by the original method. + /// + /// An of either a newly created asset if it exists or the original asset + /// if a new one was not + /// created. + /// + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static IntPtr LoadFromNamePatch(IntPtr instance, IntPtr assetNamePtr, IntPtr nativeMethodInfo) + { + // Retrieve the pointer of the asset and the name of the asset + var assetPtr = Hook.Trampoline(instance, assetNamePtr, nativeMethodInfo); + var assetName = IL2CPP.Il2CppStringToManaged(assetNamePtr) ?? string.Empty; - Logger.Msg($"Loading {assetName}!"); + Logger.Msg($"Loading {assetName}!"); - OnAssetLoaded?.Invoke(typeof(AssetPatch), new AssetEventArgs(assetName, assetPtr)); + OnAssetLoaded?.Invoke(typeof(AssetPatch), new AssetEventArgs(assetName, assetPtr)); - if (assetName.StartsWith("ALBUM") && assetName[5..].TryParseAsInt(out var albumNum) && - albumNum != AlbumManager.Uid + 1) - { - // If done loading albums, we've found the maximum actual album - // If there's an attempt to load other albums (there will be if you open the tag menu), early return zero pointer - // This provides a noticeable speedup when opening tags for the first time - if (_doneLoadingAlbumsFlag && albumNum > _latestAlbumNum) - return IntPtr.Zero; - - // Otherwise get the maximum number X of ALBUMX - _latestAlbumNum = Math.Max(_latestAlbumNum, albumNum); - } + if (assetName.StartsWith("ALBUM") && assetName[5..].TryParseAsInt(out var albumNum) && + albumNum != AlbumManager.Uid + 1) + { + // If done loading albums, we've found the maximum actual album + // If there's an attempt to load other albums (there will be if you open the tag menu), early return zero pointer + // This provides a noticeable speedup when opening tags for the first time + if (_doneLoadingAlbumsFlag && albumNum > _latestAlbumNum) + return IntPtr.Zero; + + // Otherwise get the maximum number X of ALBUMX + _latestAlbumNum = Math.Max(_latestAlbumNum, albumNum); + } - // If we're loading music_search_tag we're done loading albums - if (assetName == "music_search_tag") _doneLoadingAlbumsFlag = true; + // If we're loading music_search_tag we're done loading albums + if (assetName == "music_search_tag") _doneLoadingAlbumsFlag = true; - // If the asset exists in the cache then retrieve it - if (AssetCache.TryGetValue(assetName, out var cachedAsset)) + // If the asset exists in the cache then retrieve it + if (AssetCache.TryGetValue(assetName, out var cachedAsset)) + { + if (cachedAsset != null) { - if (cachedAsset != null) - { - Logger.Msg($"Returning {assetName} from cache"); - AudioManager.SwitchLoad(assetName); - return cachedAsset.Pointer; - } - - Logger.Msg("Removing null asset from cache"); - AssetCache.Remove(assetName); + Logger.Msg($"Returning {assetName} from cache"); + AudioManager.SwitchLoad(assetName); + return cachedAsset.Pointer; } - // Allow original LoadFromName to run with LocalizationSettings - if (assetName is "LocalizationSettings") return assetPtr; + Logger.Msg("Removing null asset from cache"); + AssetCache.Remove(assetName); + } - var language = SingletonScriptableObject.instance.GetActiveOption("Language"); + // Allow original LoadFromName to run with LocalizationSettings + if (assetName is "LocalizationSettings") return assetPtr; - // Programmatically alters the asset name to remove the language if the language exists - var handledAssetName = assetName.StartsWith("albums_") ? "albums_" : assetName; - handledAssetName = handledAssetName.StartsWith($"{AlbumManager.JsonName}_") - ? $"{AlbumManager.JsonName}_" - : handledAssetName; - handledAssetName = handledAssetName.StartsWith("album_") ? "album_" : handledAssetName; + var language = SingletonScriptableObject.instance.GetActiveOption("Language"); - // Get the method from the AssetHandler if it exists and run it, otherwise return assetPtr - return AssetHandler.TryGetValue(handledAssetName, out var value) - ? value(assetName, assetPtr, language) - : assetPtr; - } + // Programmatically alters the asset name to remove the language if the language exists + var handledAssetName = assetName.StartsWith("albums_") ? "albums_" : assetName; + handledAssetName = handledAssetName.StartsWith($"{AlbumManager.JsonName}_") + ? $"{AlbumManager.JsonName}_" + : handledAssetName; + handledAssetName = handledAssetName.StartsWith("album_") ? "album_" : handledAssetName; - /// - /// Simple helper method to create a TextAsset. - /// - /// - /// - /// A new initialized with the parameters. - private static TextAsset CreateTextAsset(string name, string text) - { - return new TextAsset(text) - { - name = name - }; - } + // Get the method from the AssetHandler if it exists and run it, otherwise return assetPtr + return AssetHandler.TryGetValue(handledAssetName, out var value) + ? value(assetName, assetPtr, language) + : assetPtr; + } - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate IntPtr LoadFromNameDelegate(IntPtr instance, IntPtr assetNamePtr, IntPtr nativeMethodInfo); + /// + /// Simple helper method to create a TextAsset. + /// + /// + /// + /// A new initialized with the parameters. + private static TextAsset CreateTextAsset(string name, string text) + { + return new TextAsset(text) + { + name = name + }; } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate IntPtr LoadFromNameDelegate(IntPtr instance, IntPtr assetNamePtr, IntPtr nativeMethodInfo); } \ No newline at end of file diff --git a/Patches/ChartDialogPatch.cs b/Patches/ChartDialogPatch.cs index a368675..c6e24ae 100644 --- a/Patches/ChartDialogPatch.cs +++ b/Patches/ChartDialogPatch.cs @@ -6,69 +6,68 @@ using Il2CppAssets.Scripts.GameCore; using Il2CppAssets.Scripts.Structs; -namespace CustomAlbums.Patches +namespace CustomAlbums.Patches; + +internal class ChartDialogPatch { - internal class ChartDialogPatch + /// + /// This method runs once when the chart loads, so init basic values to avoid computing again + /// + [HarmonyPatch(typeof(DialogMasterControl), nameof(DialogMasterControl.Init))] + internal class InitPatch { - /// - /// This method runs once when the chart loads, so init basic values to avoid computing again - /// - [HarmonyPatch(typeof(DialogMasterControl), nameof(DialogMasterControl.Init))] - internal class InitPatch + private static readonly Logger Logger = new(nameof(InitPatch)); + + private static void Prefix(DialogMasterControl __instance) { - private static readonly Logger Logger = new(nameof(InitPatch)); + Logger.Msg("Setting talk file values."); + var currentStageUid = GlobalDataBase.dbBattleStage.musicUid; + PlayDialogAnimPatch.CurrentStageInfo = GlobalDataBase.dbStageInfo.m_StageInfo; - private static void Prefix(DialogMasterControl __instance) + if (!currentStageUid.StartsWith($"{AlbumManager.Uid}-")) { - Logger.Msg("Setting talk file values."); - var currentStageUid = GlobalDataBase.dbBattleStage.musicUid; - PlayDialogAnimPatch.CurrentStageInfo = GlobalDataBase.dbStageInfo.m_StageInfo; - - if (!currentStageUid.StartsWith($"{AlbumManager.Uid}-")) - { - PlayDialogAnimPatch.HasVersion2 = false; - return; - } + PlayDialogAnimPatch.HasVersion2 = false; + return; + } - var currentAlbum = AlbumManager.GetByUid(currentStageUid); + var currentAlbum = AlbumManager.GetByUid(currentStageUid); - // Reset values that may have changed - PlayDialogAnimPatch.HasVersion2 = - (currentAlbum?.Sheets.TryGetValue(PlayDialogAnimPatch.CurrentStageInfo.difficulty, out var sheet) ?? - false) && sheet.TalkFileVersion2; - PlayDialogAnimPatch.Index = 0; - PlayDialogAnimPatch.CurrentLanguage = DataHelper.userLanguage; + // Reset values that may have changed + PlayDialogAnimPatch.HasVersion2 = + (currentAlbum?.Sheets.TryGetValue(PlayDialogAnimPatch.CurrentStageInfo.difficulty, out var sheet) ?? + false) && sheet.TalkFileVersion2; + PlayDialogAnimPatch.Index = 0; + PlayDialogAnimPatch.CurrentLanguage = DataHelper.userLanguage; - if (PlayDialogAnimPatch.CurrentStageInfo.dialogEvents == null) return; - // Set this here to avoid repeatedly checking a dictionary - PlayDialogAnimPatch.DialogEvents = - PlayDialogAnimPatch.CurrentStageInfo.dialogEvents.ContainsKey(PlayDialogAnimPatch.CurrentLanguage) - ? PlayDialogAnimPatch.CurrentStageInfo.dialogEvents[PlayDialogAnimPatch.CurrentLanguage] - : PlayDialogAnimPatch.CurrentStageInfo.dialogEvents["English"]; - } + if (PlayDialogAnimPatch.CurrentStageInfo.dialogEvents == null) return; + // Set this here to avoid repeatedly checking a dictionary + PlayDialogAnimPatch.DialogEvents = + PlayDialogAnimPatch.CurrentStageInfo.dialogEvents.ContainsKey(PlayDialogAnimPatch.CurrentLanguage) + ? PlayDialogAnimPatch.CurrentStageInfo.dialogEvents[PlayDialogAnimPatch.CurrentLanguage] + : PlayDialogAnimPatch.CurrentStageInfo.dialogEvents["English"]; } + } - /// - /// This patch allows support for setting the transparency of dialog boxes. Eventually, this patch will allow stronger - /// control of dialog boxes using the "version": 2 property. - /// - [HarmonyPatch(typeof(DialogSubControl), nameof(DialogSubControl.PlayDialogAnim))] - internal class PlayDialogAnimPatch - { - private static readonly Logger Logger = new(nameof(PlayDialogAnimPatch)); - internal static int Index { get; set; } - internal static StageInfo CurrentStageInfo { get; set; } - internal static bool HasVersion2 { get; set; } - internal static string CurrentLanguage { get; set; } = string.Empty; - internal static Il2CppSystem.Collections.Generic.List DialogEvents { get; set; } + /// + /// This patch allows support for setting the transparency of dialog boxes. Eventually, this patch will allow stronger + /// control of dialog boxes using the "version": 2 property. + /// + [HarmonyPatch(typeof(DialogSubControl), nameof(DialogSubControl.PlayDialogAnim))] + internal class PlayDialogAnimPatch + { + private static readonly Logger Logger = new(nameof(PlayDialogAnimPatch)); + internal static int Index { get; set; } + internal static StageInfo CurrentStageInfo { get; set; } + internal static bool HasVersion2 { get; set; } + internal static string CurrentLanguage { get; set; } = string.Empty; + internal static Il2CppSystem.Collections.Generic.List DialogEvents { get; set; } - private static void Prefix(DialogSubControl __instance) - { - if (!HasVersion2 || DialogEvents == null) return; + private static void Prefix(DialogSubControl __instance) + { + if (!HasVersion2 || DialogEvents == null) return; - __instance.m_BgImg.color = DialogEvents[Index++].bgColor; - } + __instance.m_BgImg.color = DialogEvents[Index++].bgColor; } } } \ No newline at end of file diff --git a/Patches/CoverRefreshPatch.cs b/Patches/CoverRefreshPatch.cs new file mode 100644 index 0000000..38735ad --- /dev/null +++ b/Patches/CoverRefreshPatch.cs @@ -0,0 +1,36 @@ +using HarmonyLib; +using Il2Cpp; + +namespace CustomAlbums.Patches; + +internal static class CoverRefreshPatch +{ + private static readonly Dictionary ActiveCells = new(); + + internal static void Refresh(string musicUid) + { + // Iterate over a copy to avoid enumeration modification exceptions if a cell destroys itself + var cells = new Dictionary(ActiveCells); + foreach (var kvp in cells) + if (kvp.Value == musicUid && kvp.Key != null && kvp.Key.gameObject.activeInHierarchy) + kvp.Key.ForceRefreshData(); + } + + [HarmonyPatch(typeof(MusicStageCell), nameof(MusicStageCell.SetCoverLogic))] + internal static class SetCoverLogicPatch + { + private static void Postfix(MusicStageCell __instance, MusicStageCell.MusicStageCellInfo cellInfo) + { + ActiveCells[__instance] = cellInfo.musicUid; + } + } + + [HarmonyPatch(typeof(MusicStageCell), nameof(MusicStageCell.OnDestroy))] + internal static class OnDestroyPatch + { + private static void Postfix(MusicStageCell __instance) + { + ActiveCells.Remove(__instance); + } + } +} \ No newline at end of file diff --git a/Patches/DifficultyGradeIconPatch.cs b/Patches/DifficultyGradeIconPatch.cs index e619284..c1b38f9 100644 --- a/Patches/DifficultyGradeIconPatch.cs +++ b/Patches/DifficultyGradeIconPatch.cs @@ -1,4 +1,5 @@ -using CustomAlbums.Managers; +using CustomAlbums.Data; +using CustomAlbums.Managers; using CustomAlbums.Utilities; using HarmonyLib; using Il2Cpp; @@ -6,64 +7,67 @@ using Il2CppAssets.Scripts.PeroTools.Commons; using Il2CppAssets.Scripts.UI.Panels; -namespace CustomAlbums.Patches +namespace CustomAlbums.Patches; + +[HarmonyPatch(typeof(PnlStage), nameof(PnlStage.RefreshDiffUI))] +internal class DifficultyGradeIconPatch { + private static int GetEvaluate(Dictionary highest, int diff) + { + return highest.GetValueOrDefault(diff)?.Evaluate ?? -1; + } - [HarmonyPatch(typeof(PnlStage), nameof(PnlStage.RefreshDiffUI))] - internal class DifficultyGradeIconPatch - { - private static int GetEvaluate(Dictionary highest, int diff) => highest.GetValueOrDefault(diff)?.Evaluate ?? -1; + /// + /// Enables the S logos on the difficulties for custom charts. + /// + // ReSharper disable once InconsistentNaming + private static void Postfix(MusicInfo musicInfo, PnlStage __instance) + { + var uid = musicInfo?.uid; + var specialSongInstance = Singleton.instance; - /// - /// Enables the S logos on the difficulties for custom charts. - /// - // ReSharper disable once InconsistentNaming - private static void Postfix(MusicInfo musicInfo, PnlStage __instance) - { - var uid = musicInfo?.uid; - var specialSongInstance = Singleton.instance; + if (string.IsNullOrEmpty(uid)) return; - if (string.IsNullOrEmpty(uid)) return; + // For custom charts, we need to set the S logos for each difficulty + if (uid.StartsWith($"{AlbumManager.Uid}-")) + { + // Gets the highest data from save data + var customHighest = SaveManager.SaveData.GetChartSaveDataFromUid(uid)?.Highest; - // For custom charts, we need to set the S logos for each difficulty - if (uid.StartsWith($"{AlbumManager.Uid}-")) - { - // Gets the highest data from save data - var customHighest = SaveManager.SaveData.GetChartSaveDataFromUid(uid).Highest; + if (customHighest == null) return; - // Get the Evaluate value from each, set diff3 to diff4 if hidden is invoked - var diff1 = GetEvaluate(customHighest, 1); - var diff2 = GetEvaluate(customHighest, 2); - var diff3 = Singleton.instance.IsInvokeHideBms(uid) - ? GetEvaluate(customHighest, 4) - : GetEvaluate(customHighest, 3); + // Get the Evaluate value from each, set diff3 to diff4 if hidden is invoked + var diff1 = GetEvaluate(customHighest, 1); + var diff2 = GetEvaluate(customHighest, 2); + var diff3 = Singleton.instance.IsInvokeHideBms(uid) + ? GetEvaluate(customHighest, 4) + : GetEvaluate(customHighest, 3); - // Set the S logos for each difficulty - __instance.m_Diff1Item.ChangeSchemeByEvalue(diff1, __instance.globalData); - __instance.m_Diff2Item.ChangeSchemeByEvalue(diff2, __instance.globalData); - __instance.m_Diff3Item.ChangeSchemeByEvalue(diff3, __instance.globalData); - } + // Set the S logos for each difficulty + __instance.m_Diff1Item.ChangeSchemeByEvalue(diff1, __instance.globalData); + __instance.m_Diff2Item.ChangeSchemeByEvalue(diff2, __instance.globalData); + __instance.m_Diff3Item.ChangeSchemeByEvalue(diff3, __instance.globalData); + } - // For vanilla and custom charts, this fixes the hidden difficulty icon for non-master charts - if (!specialSongInstance.IsInvokeHideBms(uid)) return; - if (!specialSongInstance.m_HideBmsInfos.TryGetValue(uid, out var info)) return; + // For vanilla and custom charts, this fixes the hidden difficulty icon for non-master charts + if (!specialSongInstance.IsInvokeHideBms(uid)) return; + if (!specialSongInstance.m_HideBmsInfos.TryGetValue(uid, out var info)) return; - // Get the difficulty evaluates - var hiddenEval = DataHelper.highest.GetIDataByUid(uid, 4).ToChartSave().Evaluate; - var masterEval = DataHelper.highest.GetIDataByUid(uid, 3).ToChartSave().Evaluate; + // Get the difficulty evaluates + var hiddenEval = DataHelper.highest.GetIDataByUid(uid, 4).ToChartSave().Evaluate; + var masterEval = DataHelper.highest.GetIDataByUid(uid, 3).ToChartSave().Evaluate; - // The game handles case 3 already, don't need to reinvent the wheel here - switch (info.triggerDiff) - { - case 1: - __instance.m_Diff3Item.ChangeSchemeByEvalue(masterEval, __instance.globalData); - __instance.m_Diff1Item.ChangeSchemeByEvalue(hiddenEval, __instance.globalData); - break; - case 2: - __instance.m_Diff3Item.ChangeSchemeByEvalue(masterEval, __instance.globalData); - __instance.m_Diff2Item.ChangeSchemeByEvalue(hiddenEval, __instance.globalData); - break; - } + // The game handles case 3 already, don't need to reinvent the wheel here + switch (info.triggerDiff) + { + case 1: + __instance.m_Diff3Item.ChangeSchemeByEvalue(masterEval, __instance.globalData); + __instance.m_Diff1Item.ChangeSchemeByEvalue(hiddenEval, __instance.globalData); + break; + case 2: + __instance.m_Diff3Item.ChangeSchemeByEvalue(masterEval, __instance.globalData); + __instance.m_Diff2Item.ChangeSchemeByEvalue(hiddenEval, __instance.globalData); + break; } } } \ No newline at end of file diff --git a/Patches/FilterPatch.cs b/Patches/FilterPatch.cs new file mode 100644 index 0000000..778fbc3 --- /dev/null +++ b/Patches/FilterPatch.cs @@ -0,0 +1,103 @@ +using CustomAlbums.Managers; +using HarmonyLib; +using Il2Cpp; +using Il2CppAssets.Scripts.Database; +using Il2CppAssets.Scripts.Helpers; + +namespace CustomAlbums.Patches; + +internal class FilterPatch +{ + /// + /// Applies "Assisted screening" filters to custom charts + /// + [HarmonyPatch(typeof(MusicTagHelper), nameof(MusicTagHelper.AdditionalFilter))] + internal static class AdditionalFilterPatch + { + private static bool AllMatchingMeetCondition(string albumName, MusicInfo musicInfo, int starLevel, + Func condition) + { + var starStr = starLevel.ToString(); + var foundMatchingStar = false; + var allMeet = true; + + for (var i = 1; i <= 4; i++) + { + var diffStr = i switch + { + 1 => musicInfo.difficulty1, + 2 => musicInfo.difficulty2, + 3 => musicInfo.difficulty3, + 4 => musicInfo.difficulty4, + _ => "0" + }; + + if (diffStr == starStr || (starLevel == 0 && diffStr == "?")) + { + foundMatchingStar = true; + if (!condition(albumName, i)) allMeet = false; + } + } + + return foundMatchingStar && allMeet; + } + + private static void Postfix(Il2CppSystem.Collections.Generic.List buffer, int difficultyIndex) + { + var filter = MusicTagHelper.GetAdditionalConditions(); + if (filter == DifficultyFilterDefines.All) return; + + // Iterate backward to safely remove elements + for (var i = buffer.Count - 1; i >= 0; i--) + { + var musicInfo = buffer[i]; + if (!musicInfo.uid.StartsWith($"{AlbumManager.Uid}-")) continue; + + var albumName = AlbumManager.GetAlbumNameFromUid(musicInfo.uid); + var remove = false; + + switch (filter) + { + case DifficultyFilterDefines.NoClear: + remove = AllMatchingMeetCondition(albumName, musicInfo, difficultyIndex, (album, slot) => + { + return SaveManager.SaveData.Highest.TryGetValue(album, out var highest) + && highest.ContainsKey(slot); + }); + break; + + case DifficultyFilterDefines.NoFullCombo: + remove = AllMatchingMeetCondition(albumName, musicInfo, difficultyIndex, (album, slot) => + { + return SaveManager.SaveData.FullCombo.TryGetValue(album, out var fcs) + && fcs.Contains(slot); + }); + break; + + case DifficultyFilterDefines.NoRankS: + remove = AllMatchingMeetCondition(albumName, musicInfo, difficultyIndex, (album, slot) => + { + return SaveManager.SaveData.Highest.TryGetValue(album, out var highest) + && highest.TryGetValue(slot, out var chartSave) + && chartSave.Evaluate >= 4; + }); + break; + + case DifficultyFilterDefines.NoAllPerfect: + remove = AllMatchingMeetCondition(albumName, musicInfo, difficultyIndex, (album, slot) => + { + return SaveManager.SaveData.Highest.TryGetValue(album, out var highest) + && highest.TryGetValue(slot, out var chartSave) + && chartSave.Accuracy >= 100f; + }); + break; + + case DifficultyFilterDefines.NoLevelAchievement: + break; + } + + if (remove) buffer.RemoveAt(i); + } + } + } +} \ No newline at end of file diff --git a/Patches/HiddenSupportPatch.cs b/Patches/HiddenSupportPatch.cs index f5cb7e8..87f48e4 100644 --- a/Patches/HiddenSupportPatch.cs +++ b/Patches/HiddenSupportPatch.cs @@ -8,210 +8,209 @@ using Il2CppAssets.Scripts.UI.Tips; using Il2CppInterop.Runtime.InteropTypes.Arrays; using Il2CppPeroPeroGames.GlobalDefines; -using MelonLoader; -namespace CustomAlbums.Patches +namespace CustomAlbums.Patches; + +internal class HiddenSupportPatch { - internal class HiddenSupportPatch + internal static HashSet LoadedHiddens = new(); + + internal static void UpdateHiddenCharts() { - internal static HashSet LoadedHiddens = new(); + HideBmsInfoDicPatch.HasUpdate = true; + MusicTagPatch.HasUpdate = true; + } - internal static void UpdateHiddenCharts() - { - HideBmsInfoDicPatch.HasUpdate = true; - MusicTagPatch.HasUpdate = true; - } + [HarmonyPatch(typeof(SpecialSongManager), nameof(SpecialSongManager.InitHideBmsInfoDic))] + internal static class HideBmsInfoDicPatch + { + private static readonly Logger Logger = new(nameof(HideBmsInfoDicPatch)); + internal static bool HasUpdate { get; set; } = true; - [HarmonyPatch(typeof(SpecialSongManager), nameof(SpecialSongManager.InitHideBmsInfoDic))] - internal static class HideBmsInfoDicPatch + private static void Postfix(SpecialSongManager __instance) { - internal static bool HasUpdate { get; set; } = true; - private static readonly Logger Logger = new(nameof(HideBmsInfoDicPatch)); + if (!HasUpdate) return; - private static void Postfix(SpecialSongManager __instance) + foreach (var (key, value) in AlbumManager.LoadedAlbums.Where(kv => + kv.Value.HasDifficulty(4) || kv.Value.HasDifficulty(5))) { - if (!HasUpdate) return; + // Quick check for Touhou charts + if (value.HasDifficulty(5)) + { + var newTouhouArray = new Il2CppStringArray(DBMusicTagDefine.s_BarrageModeSongUid.Length + 1); + for (var i = 0; i < DBMusicTagDefine.s_BarrageModeSongUid.Length; i++) + newTouhouArray[i] = DBMusicTagDefine.s_BarrageModeSongUid[i]; + newTouhouArray[^1] = value.Uid; + DBMusicTagDefine.s_BarrageModeSongUid = newTouhouArray; + } + + // Enable hidden mode for charts containing map4 + if (!LoadedHiddens.Contains(value.Uid)) continue; - foreach (var (key, value) in AlbumManager.LoadedAlbums.Where(kv => kv.Value.HasDifficulty(4) || kv.Value.HasDifficulty(5))) + var albumUid = value.Uid; + + __instance.m_HideBmsInfos.Add(albumUid, + new SpecialSongManager.HideBmsInfo( + albumUid, + value.Info.HideBmsDifficulty == "0" + ? value.HasDifficulty(3) ? 3 : 2 + : value.Info.HideBmsDifficulty.ParseAsInt(), + 4, + $"{key}_map4", + new Func(() => __instance.IsInvokeHideBms(albumUid)) + )); + + // Add chart to the appropriate list for their hidden type + var hideMusicInfo = new HideMusicInfo { - // Quick check for Touhou charts - if (value.HasDifficulty(5)) + musicUid = albumUid, + invokeType = value.Info.HideBmsMode switch { - var newTouhouArray = new Il2CppStringArray(DBMusicTagDefine.s_BarrageModeSongUid.Length + 1); - for (var i = 0; i < DBMusicTagDefine.s_BarrageModeSongUid.Length; i++) - newTouhouArray[i] = DBMusicTagDefine.s_BarrageModeSongUid[i]; - newTouhouArray[^1] = value.Uid; - DBMusicTagDefine.s_BarrageModeSongUid = newTouhouArray; + "CLICK" => (int)HiddenInvokeType.Click, + "PRESS" => (int)HiddenInvokeType.LongPress, + "TOGGLE" => (int)HiddenInvokeType.Toggle, + _ => (int)HiddenInvokeType.None } - // Enable hidden mode for charts containing map4 - if (!LoadedHiddens.Contains(value.Uid)) continue; - - var albumUid = value.Uid; - - __instance.m_HideBmsInfos.Add(albumUid, - new SpecialSongManager.HideBmsInfo( - albumUid, - value.Info.HideBmsDifficulty == "0" - ? value.HasDifficulty(3) ? 3 : 2 - : value.Info.HideBmsDifficulty.ParseAsInt(), - 4, - $"{key}_map4", - new Func(() => __instance.IsInvokeHideBms(albumUid)) - )); - - // Add chart to the appropriate list for their hidden type - var hideMusicInfo = new HideMusicInfo - { - musicUid = albumUid, - invokeType = value.Info.HideBmsMode switch - { - "CLICK" => (int)HiddenInvokeType.Click, - "PRESS" => (int)HiddenInvokeType.LongPress, - "TOGGLE" => (int)HiddenInvokeType.Toggle, - _ => (int)HiddenInvokeType.None - } - }; - - __instance.m_ConfigHideMusic.m_HideMusicObjectMapping[albumUid] = hideMusicInfo; - } - HasUpdate = false; + }; + + __instance.m_ConfigHideMusic.m_HideMusicObjectMapping[albumUid] = hideMusicInfo; } - } - /// - /// Activates hidden charts when the conditions are met - /// - [HarmonyPatch(typeof(SpecialSongManager), nameof(SpecialSongManager.InvokeHideBms))] - internal static class InvokeHideBmsPatch - { - private static readonly Logger Logger = new(nameof(InvokeHideBmsPatch)); + HasUpdate = false; + } + } - private static bool Prefix(MusicInfo musicInfo, SpecialSongManager __instance) - { - if (!musicInfo.uid.StartsWith($"{AlbumManager.Uid}-") || - !LoadedHiddens.Contains(musicInfo.uid)) return true; + /// + /// Activates hidden charts when the conditions are met + /// + [HarmonyPatch(typeof(SpecialSongManager), nameof(SpecialSongManager.InvokeHideBms))] + internal static class InvokeHideBmsPatch + { + private static readonly Logger Logger = new(nameof(InvokeHideBmsPatch)); - var hideBms = __instance.m_HideBmsInfos[musicInfo.uid]; - __instance.m_IsInvokeHideDic[hideBms.uid] = true; + private static bool Prefix(MusicInfo musicInfo, SpecialSongManager __instance) + { + if (!musicInfo.uid.StartsWith($"{AlbumManager.Uid}-") || + !LoadedHiddens.Contains(musicInfo.uid)) return true; - if (!hideBms.extraCondition.Invoke()) return false; + var hideBms = __instance.m_HideBmsInfos[musicInfo.uid]; + __instance.m_IsInvokeHideDic[hideBms.uid] = true; - var album = AlbumManager.LoadedAlbums.FirstOrDefault(kv => kv.Value.Index == musicInfo.musicIndex) - .Value; + if (!hideBms.extraCondition.Invoke()) return false; - ActivateHidden(hideBms); + var album = AlbumManager.LoadedAlbums.FirstOrDefault(kv => kv.Value.Index == musicInfo.musicIndex) + .Value; - if (album.Info.HideBmsMessage != null) - { - var pnlTips = PnlTipsManager.instance; - var msgBox = pnlTips.GetMessageBox("PnlSpecialsBmsAsk"); - msgBox.Show("TIPS", album.Info.HideBmsMessage); - } + ActivateHidden(hideBms); - SpecialSongManager.onTriggerHideBmsEvent?.Invoke(); - if (album.Info.HideBmsMode == "PRESS") Singleton.instance.Invoke("UI/OnSpecialsMusic"); - return false; + if (album.Info.HideBmsMessage != null) + { + var pnlTips = PnlTipsManager.instance; + var msgBox = pnlTips.GetMessageBox("PnlSpecialsBmsAsk"); + msgBox.Show("TIPS", album.Info.HideBmsMessage); } - private static void ActivateHidden(SpecialSongManager.HideBmsInfo hideBms) - { - if (hideBms == null) return; + SpecialSongManager.onTriggerHideBmsEvent?.Invoke(); + if (album.Info.HideBmsMode == "PRESS") Singleton.instance.Invoke("UI/OnSpecialsMusic"); + return false; + } - var info = GlobalDataBase.dbMusicTag.GetMusicInfoFromAll(hideBms.uid); - if (hideBms.triggerDiff == 0) return; + private static void ActivateHidden(SpecialSongManager.HideBmsInfo hideBms) + { + if (hideBms == null) return; - var targetDifficulty = hideBms.triggerDiff; + var info = GlobalDataBase.dbMusicTag.GetMusicInfoFromAll(hideBms.uid); + if (hideBms.triggerDiff == 0) return; - if (targetDifficulty == -1) - { - targetDifficulty = 2; + var targetDifficulty = hideBms.triggerDiff; - // Disable the other difficulty options - info.AddMaskValue("difficulty1", "0"); - info.AddMaskValue("difficulty3", "0"); - } + if (targetDifficulty == -1) + { + targetDifficulty = 2; - var difficultyToHide = "difficulty" + targetDifficulty; - var levelDesignToHide = "levelDesigner" + targetDifficulty; - var difficulty = "?"; - var levelDesignStr = info.levelDesigner; - switch (hideBms.m_HideDiff) - { - case 1: - difficulty = info.difficulty1; - levelDesignStr = info.levelDesigner1 ?? info.levelDesigner; - break; - case 2: - difficulty = info.difficulty2; - levelDesignStr = info.levelDesigner2 ?? info.levelDesigner; - break; - case 3: - difficulty = info.difficulty3; - levelDesignStr = info.levelDesigner3 ?? info.levelDesigner; - break; - case 4: - difficulty = info.difficulty4; - levelDesignStr = info.levelDesigner4 ?? info.levelDesigner; - break; - case 5: - difficulty = info.difficulty5; - levelDesignStr = info.levelDesigner5 ?? info.levelDesigner; - break; - } + // Disable the other difficulty options + info.AddMaskValue("difficulty1", "0"); + info.AddMaskValue("difficulty3", "0"); + } - info.AddMaskValue(difficultyToHide, difficulty); - info.AddMaskValue(levelDesignToHide, levelDesignStr); - info.SetDifficulty(targetDifficulty, hideBms.m_HideDiff); + var difficultyToHide = "difficulty" + targetDifficulty; + var levelDesignToHide = "levelDesigner" + targetDifficulty; + var difficulty = "?"; + var levelDesignStr = info.levelDesigner; + switch (hideBms.m_HideDiff) + { + case 1: + difficulty = info.difficulty1; + levelDesignStr = info.levelDesigner1 ?? info.levelDesigner; + break; + case 2: + difficulty = info.difficulty2; + levelDesignStr = info.levelDesigner2 ?? info.levelDesigner; + break; + case 3: + difficulty = info.difficulty3; + levelDesignStr = info.levelDesigner3 ?? info.levelDesigner; + break; + case 4: + difficulty = info.difficulty4; + levelDesignStr = info.levelDesigner4 ?? info.levelDesigner; + break; + case 5: + difficulty = info.difficulty5; + levelDesignStr = info.levelDesigner5 ?? info.levelDesigner; + break; } + + info.AddMaskValue(difficultyToHide, difficulty); + info.AddMaskValue(levelDesignToHide, levelDesignStr); + info.SetDifficulty(targetDifficulty, hideBms.m_HideDiff); } + } + + [HarmonyPatch(typeof(MusicTagManager), nameof(MusicTagManager.InitDefaultInfo))] + internal class MusicTagPatch + { + private const int HiddenId = 32776; + private static readonly Logger Logger = new(nameof(MusicTagPatch)); + internal static bool HasUpdate { get; set; } = true; - [HarmonyPatch(typeof(MusicTagManager), nameof(MusicTagManager.InitDefaultInfo))] - internal class MusicTagPatch + private static void Postfix() { - private const int HiddenId = 32776; - private static readonly Logger Logger = new(nameof(MusicTagPatch)); - internal static bool HasUpdate { get; set; } = true; + if (!HasUpdate) return; - private static void Postfix() - { - if (!HasUpdate) return; + Il2CppSystem.Collections.Generic.List newHiddenAlbums = new(); + foreach (var (_, value) in AlbumManager.LoadedAlbums) + if (value.HasDifficulty(DifficultyDefine.hide) && LoadedHiddens.Add(value.Uid)) + newHiddenAlbums.Add(value.Uid); - Il2CppSystem.Collections.Generic.List newHiddenAlbums = new(); - foreach (var (_, value) in AlbumManager.LoadedAlbums) - { - if (value.HasDifficulty(DifficultyDefine.hide) && LoadedHiddens.Add(value.Uid)) - newHiddenAlbums.Add(value.Uid); - } + var newHiddenArray = + new Il2CppStringArray(DBMusicTagDefine.s_HiddenLocal.Length + newHiddenAlbums.Count); - var newHiddenArray = - new Il2CppStringArray(DBMusicTagDefine.s_HiddenLocal.Length + newHiddenAlbums.Count); + // copying s_HiddenLocal to newHiddenArray + for (var i = 0; i < DBMusicTagDefine.s_HiddenLocal.Length; ++i) + newHiddenArray[i] = DBMusicTagDefine.s_HiddenLocal[i]; - // copying s_HiddenLocal to newHiddenArray - for (var i = 0; i < DBMusicTagDefine.s_HiddenLocal.Length; ++i) - newHiddenArray[i] = DBMusicTagDefine.s_HiddenLocal[i]; + // adding all new charts to newHiddenArray + for (var i = 0; i < newHiddenAlbums.Count; i++) + newHiddenArray[i + DBMusicTagDefine.s_HiddenLocal.Length] = newHiddenAlbums[i]; - // adding all new charts to newHiddenArray - for (var i = 0; i < newHiddenAlbums.Count; i++) - newHiddenArray[i + DBMusicTagDefine.s_HiddenLocal.Length] = newHiddenAlbums[i]; + // setting the local hidden data to our new one that has custom charts + DBMusicTagDefine.s_HiddenLocal = newHiddenArray; - // setting the local hidden data to our new one that has custom charts - DBMusicTagDefine.s_HiddenLocal = newHiddenArray; - - // adding that data to the tag - var tagInfo = GlobalDataBase.dbMusicTag.GetAlbumTagInfo(HiddenId); - tagInfo.AddTagUids(newHiddenAlbums); - HasUpdate = false; - } + // adding that data to the tag + var tagInfo = GlobalDataBase.dbMusicTag.GetAlbumTagInfo(HiddenId); + tagInfo.AddTagUids(newHiddenAlbums); + HasUpdate = false; } } +} - internal enum HiddenInvokeType - { - None = 0, - LongPress = 1, - Toggle = 2, - Click = 3, - Special = 4 - } +internal enum HiddenInvokeType +{ + None = 0, + LongPress = 1, + Toggle = 2, + Click = 3, + Special = 4 } \ No newline at end of file diff --git a/Patches/LongChartPatch.cs b/Patches/LongChartPatch.cs index 8c42f9f..8d426be 100644 --- a/Patches/LongChartPatch.cs +++ b/Patches/LongChartPatch.cs @@ -1,18 +1,17 @@ using HarmonyLib; using Il2CppDYUnityLib; -namespace CustomAlbums.Patches +namespace CustomAlbums.Patches; + +/// +/// Changes the maximum length of a chart from 4 minutes to about 357913 minutes. +/// This does not change the fact that the maximum amount of chart elements is 32767. +/// +[HarmonyPatch(typeof(FixUpdateTimer), "Run")] +internal class LongChartPatch { - /// - /// Changes the maximum length of a chart from 4 minutes to about 357913 minutes. - /// This does not change the fact that the maximum amount of chart elements is 32767. - /// - [HarmonyPatch(typeof(FixUpdateTimer), "Run")] - internal class LongChartPatch + private static void Prefix(FixUpdateTimer __instance) { - private static void Prefix(FixUpdateTimer __instance) - { - if (__instance.totalTick is >= 24000 and < int.MaxValue) __instance.totalTick = int.MaxValue; - } + if (__instance.totalTick is >= 24000 and < int.MaxValue) __instance.totalTick = int.MaxValue; } } \ No newline at end of file diff --git a/Patches/MikuSceneSupportPatch.cs b/Patches/MikuSceneSupportPatch.cs index 733bf4e..e3c4138 100644 --- a/Patches/MikuSceneSupportPatch.cs +++ b/Patches/MikuSceneSupportPatch.cs @@ -2,27 +2,25 @@ using HarmonyLib; using Il2CppGameLogic; -namespace CustomAlbums.Patches +namespace CustomAlbums.Patches; + +internal class MikuSceneSupportPatch { - internal class MikuSceneSupportPatch + /// + /// Fixes the purple background issue when using the BMS ID "1X" to switch to the Miku scene. + /// + [HarmonyPatch(typeof(GameMusicScene), nameof(GameMusicScene.SceneFestival))] + internal class MikuFixPatch { - - /// - /// Fixes the purple background issue when using the BMS ID "1X" to switch to the Miku scene. - /// - [HarmonyPatch(typeof(GameMusicScene), nameof(GameMusicScene.SceneFestival))] - internal class MikuFixPatch + private static void Finalizer(string sceneFestivalName, ref string __result) { - private static void Finalizer(string sceneFestivalName, ref string __result) - { - var subStr = sceneFestivalName[6..]; - - if (subStr.Length <= 2 || !subStr.TryParseAsInt(out var sceneIndex)) return; - - // For some reason, scene change 1X sets scene to scene_010, which is invalid - // Just trim off "scene_" and parse the numbers after the underscore to int again to get 10 instead of 010 - __result = $"scene_{sceneIndex}"; - } + var subStr = sceneFestivalName[6..]; + + if (subStr.Length <= 2 || !subStr.TryParseAsInt(out var sceneIndex)) return; + + // For some reason, scene change 1X sets scene to scene_010, which is invalid + // Just trim off "scene_" and parse the numbers after the underscore to int again to get 10 instead of 010 + __result = $"scene_{sceneIndex}"; } } -} +} \ No newline at end of file diff --git a/Patches/MusicTagManagerPatch.cs b/Patches/MusicTagManagerPatch.cs index b44557a..843fb68 100644 --- a/Patches/MusicTagManagerPatch.cs +++ b/Patches/MusicTagManagerPatch.cs @@ -1,25 +1,24 @@ -using Il2Cpp; +using HarmonyLib; +using Il2Cpp; using Il2CppAssets.Scripts.Database; -using HarmonyLib; using Il2CppAssets.Scripts.PeroTools.Commons; using Il2CppAssets.Scripts.PeroTools.Managers; -namespace CustomAlbums.Patches +namespace CustomAlbums.Patches; + +internal class MusicTagManagerPatch { - internal class MusicTagManagerPatch + /// + /// Makes the game think (correctly) that there are not 1000 albums upon loading the tag menu. + /// Increases performance substantially upon loading the tag menu. + /// + [HarmonyPatch(typeof(MusicTagManager), nameof(MusicTagManager.InitDatas))] + internal static class Fix1000AlbumsPatch { - /// - /// Makes the game think (correctly) that there are not 1000 albums upon loading the tag menu. - /// Increases performance substantially upon loading the tag menu. - /// - [HarmonyPatch(typeof(MusicTagManager), nameof(MusicTagManager.InitDatas))] - internal static class Fix1000AlbumsPatch + private static void Postfix() { - private static void Postfix() - { - var configObject = Singleton.instance.GetConfigObject(); - configObject.m_MaxAlbumUid = configObject.count - 3; - } + var configObject = Singleton.instance.GetConfigObject(); + configObject.m_MaxAlbumUid = configObject.count - 3; } } -} +} \ No newline at end of file diff --git a/Patches/PackPatch.cs b/Patches/PackPatch.cs index a2f8995..38aec6f 100644 --- a/Patches/PackPatch.cs +++ b/Patches/PackPatch.cs @@ -1,40 +1,43 @@ using CustomAlbums.Data; using CustomAlbums.Managers; -using CustomAlbums.Utilities; using HarmonyLib; using Il2Cpp; using Il2CppAssets.Scripts.Database; +using UnityEngine; +using Logger = CustomAlbums.Utilities.Logger; -namespace CustomAlbums.Patches +namespace CustomAlbums.Patches; + +internal class PackPatch { - internal class PackPatch + private static readonly Logger Logger = new(nameof(PackPatch)); + + [HarmonyPatch(typeof(LongSongNameController), nameof(LongSongNameController.Refresh), typeof(string), typeof(bool), + typeof(float))] + internal class RefreshPatch { - private static readonly Logger Logger = new(nameof(PackPatch)); - - [HarmonyPatch(typeof(LongSongNameController), nameof(LongSongNameController.Refresh), new[] { typeof(string), typeof(bool), typeof(float) })] - internal class RefreshPatch { - private static void SetColor(string colorHex, LongSongNameController instance) - { - var fixedColor = UnityEngine.ColorUtility.TryParseHtmlString(colorHex, out var color) ? color : UnityEngine.Color.white; - if (instance.m_MidSimpleName != null) instance.m_MidSimpleName.color = fixedColor; - if (instance.m_TxtBackupName != null) instance.m_TxtBackupName.color = fixedColor; - if (instance.m_TxtSimpleName != null) instance.m_TxtSimpleName.color = fixedColor; - } + private static void SetColor(string colorHex, LongSongNameController instance) + { + var fixedColor = ColorUtility.TryParseHtmlString(colorHex, out var color) ? color : Color.white; + if (instance.m_MidSimpleName != null) instance.m_MidSimpleName.color = fixedColor; + if (instance.m_TxtBackupName != null) instance.m_TxtBackupName.color = fixedColor; + if (instance.m_TxtSimpleName != null) instance.m_TxtSimpleName.color = fixedColor; + } - private static void Prefix(ref string text, ref Pack __state) - { - var currentUid = DataHelper.selectedMusicUid; - if (currentUid == null || !currentUid.StartsWith($"{AlbumManager.Uid}-") || text != AlbumManager.GetCustomAlbumsTitle()) return; - - __state = PackManager.GetPackFromUid(currentUid); - text = __state?.Title ?? text; - } + private static void Prefix(ref string text, ref Pack __state) + { + var currentUid = DataHelper.selectedMusicUid; + if (currentUid == null || !currentUid.StartsWith($"{AlbumManager.Uid}-") || + text != AlbumManager.GetCustomAlbumsTitle()) return; + + __state = PackManager.GetPackFromUid(currentUid); + text = __state?.Title ?? text; + } - private static void Postfix(ref Pack __state, LongSongNameController __instance) - { - if (__instance == null) return; - SetColor(__state?.TitleColorHex, __instance); - } + private static void Postfix(ref Pack __state, LongSongNameController __instance) + { + if (__instance == null) return; + SetColor(__state?.TitleColorHex, __instance); } } -} +} \ No newline at end of file diff --git a/Patches/ReportCardPatch.cs b/Patches/ReportCardPatch.cs index 797139b..273ffa7 100644 --- a/Patches/ReportCardPatch.cs +++ b/Patches/ReportCardPatch.cs @@ -8,70 +8,64 @@ using Il2CppAssets.Scripts.PeroTools.Commons; using Il2CppAssets.Scripts.UI.Panels; -namespace CustomAlbums.Patches +namespace CustomAlbums.Patches; + +[HarmonyPatch(typeof(PnlReportCard), nameof(PnlReportCard.RefreshBestRecord))] +internal static class ReportCardPatch { - [HarmonyPatch(typeof(PnlReportCard), nameof(PnlReportCard.RefreshBestRecord))] - internal static class ReportCardPatch + // ReSharper disable once InconsistentNaming + private static bool Prefix(PnlReportCard __instance) { - // ReSharper disable once InconsistentNaming - private static bool Prefix(PnlReportCard __instance) - { - var musicInfo = GlobalDataBase.s_DbMusicTag.CurMusicInfo(); - if (musicInfo.albumIndex != 999) return true; + var musicInfo = GlobalDataBase.s_DbMusicTag.CurMusicInfo(); + if (musicInfo.albumIndex != 999) return true; - var mapDifficulty = GlobalDataBase.s_DbBattleStage.m_MapDifficulty; - var curMusicBestRankOrder = Singleton.instance.GetCurMusicBestRankOrder(); + var mapDifficulty = GlobalDataBase.s_DbBattleStage.m_MapDifficulty; + var curMusicBestRankOrder = Singleton.instance.GetCurMusicBestRankOrder(); - var album = AlbumManager.GetByUid(musicInfo.uid); - if (album == null) return false; + var album = AlbumManager.GetByUid(musicInfo.uid); + if (album == null) return false; - var save = SaveManager.SaveData.Highest[album.AlbumName][mapDifficulty]; - if (save == null) return false; + var save = SaveManager.SaveData.Highest.GetValueOrDefault(album.AlbumName)?.GetValueOrDefault(mapDifficulty); + if (save == null) return false; - __instance.RefreshRecord(musicInfo, mapDifficulty, save.Score, save.Combo, save.AccuracyStr, save.Evaluate, save.Clear.ToString(CultureInfo.InvariantCulture), curMusicBestRankOrder); - return false; - } + __instance.RefreshRecord(musicInfo, mapDifficulty, save.Score, save.Combo, save.AccuracyStr, save.Evaluate, + save.Clear.ToString(CultureInfo.InvariantCulture), curMusicBestRankOrder); + return false; } +} - [HarmonyPatch] - internal static class TogglePatch +[HarmonyPatch] +internal static class TogglePatch +{ + private static IEnumerable TargetMethods() { - private static IEnumerable TargetMethods() - { - return new[] { nameof(PnlPreparation.OnDiffTglChanged), nameof(PnlPreparation.OnEnable) } - .Select(methodName => typeof(PnlPreparation).GetMethod(methodName)) - .ToArray(); - } - // ReSharper disable once InconsistentNaming - private static void Postfix(PnlPreparation __instance) - { - var musicInfo = GlobalDataBase.s_DbMusicTag.CurMusicInfo(); - if (musicInfo.albumIndex != 999) return; - - var mapDifficulty = GlobalDataBase.s_DbBattleStage.m_MapDifficulty; - var gameObject = __instance.btnDownloadReport.gameObject; + return new[] { nameof(PnlPreparation.OnDiffTglChanged), nameof(PnlPreparation.OnEnable) } + .Select(methodName => typeof(PnlPreparation).GetMethod(methodName)) + .ToArray(); + } - var album = AlbumManager.GetByUid(musicInfo.uid); - if (album == null) - { - gameObject.SetActive(false); - return; - } + // ReSharper disable once InconsistentNaming + private static void Postfix(PnlPreparation __instance) + { + var musicInfo = GlobalDataBase.s_DbMusicTag.CurMusicInfo(); + if (musicInfo.albumIndex != 999) return; - if (!SaveManager.SaveData.Highest.TryGetValue(album.AlbumName, out var chart)) - { - gameObject.SetActive(false); - return; - } + var mapDifficulty = GlobalDataBase.s_DbBattleStage.m_MapDifficulty; + var gameObject = __instance.btnDownloadReport.gameObject; - if (!chart.ContainsKey(mapDifficulty)) - { - gameObject.SetActive(false); - return; - } + var album = AlbumManager.GetByUid(musicInfo.uid); + if (album == null) + { + gameObject.SetActive(false); + return; + } - gameObject.SetActive(true); + if (!SaveManager.SaveData.Highest.TryGetValue(album.AlbumName, out var chart)) + { + gameObject.SetActive(false); + return; } - } -} + gameObject.SetActive(chart.ContainsKey(mapDifficulty)); + } +} \ No newline at end of file diff --git a/Patches/SavePatch.cs b/Patches/SavePatch.cs index a059573..1d9c714 100644 --- a/Patches/SavePatch.cs +++ b/Patches/SavePatch.cs @@ -17,569 +17,589 @@ using Il2CppInterop.Common; using MelonLoader; using MelonLoader.NativeUtils; +using UnityEngine.UI; using static CustomAlbums.Managers.SaveManager; using ArgumentOutOfRangeException = System.ArgumentOutOfRangeException; using Environment = System.Environment; using IntPtr = System.IntPtr; using Logger = CustomAlbums.Utilities.Logger; -namespace CustomAlbums.Patches +namespace CustomAlbums.Patches; + +internal static class SavePatch { - internal static class SavePatch - { - internal static bool? _hqPresent = null; - internal static bool HQPresent => _hqPresent ??= MelonBase.FindMelon("Headquarters", "AshtonMemer") is not null; + internal static bool? _hqPresent; - private static string OriginalNoNetText; + private static string OriginalRepairingText; - internal static readonly Logger Logger = new(nameof(SavePatch)); + internal static readonly Logger Logger = new(nameof(SavePatch)); - // A mapping of Evaluate->letter grade - internal static readonly string[] EvalToGrade = - { - "D", - "C", - "B", - "A", - "S", - "S", - "S" - }; - - private static readonly NativeHook Hook = new(); - - // - // PANEL INJECTION SECTION - // - - /// - /// Sets the (score, combo, accuracy) to the custom chart data. - /// - /// The instance to set. - /// The custom chart data. - /// If the selected chart has been FCed. - private static void SetPanelWithData(PnlRecord panel, ChartSave data, bool isFullCombo) + // A mapping of Evaluate->letter grade + internal static readonly string[] EvalToGrade = + { + "D", + "C", + "B", + "A", + "S", + "S", + "S" + }; + + private static readonly NativeHook Hook = new(); + internal static bool HQPresent => _hqPresent ??= MelonBase.FindMelon("Headquarters", "AshtonMemer") is not null; + + // + // PANEL INJECTION SECTION + // + + /// + /// Sets the (score, combo, accuracy) to the custom chart data. + /// + /// The instance to set. + /// The custom chart data. + /// If the selected chart has been FCed. + private static void SetPanelWithData(PnlRecord panel, ChartSave data, bool isFullCombo) + { + // Enables the FC icon if chart has been FCed + // Also sets the combo text to a gold color if it has been FCed + if (isFullCombo) { - // Enables the FC icon if chart has been FCed - // Also sets the combo text to a gold color if it has been FCed - if (isFullCombo) - { - panel.imgIconFc.SetActive(true); - panel.txtCombo.color = panel.gradeColor[6]; - } - - // Sets all the PnlRecord data to custom chart data. - var evaluate = data.Evaluate; - panel.txtAccuracy.text = data.AccuracyStr; - panel.txtClear.text = data.Clear.ToStringInvariant(); - panel.txtCombo.text = data.Combo.ToStringInvariant(); - panel.txtGrade.text = EvalToGrade[evaluate]; - panel.txtGrade.color = panel.gradeColor[evaluate]; - panel.txtScore.text = data.Score.ToStringInvariant(); + panel.imgIconFc.SetActive(true); + panel.txtCombo.color = panel.gradeColor[6]; } - /// - /// Clears the current pnlRecord and refreshes the panel if needed. - /// - /// The instance. - /// Whether panelPreparation should force reload the leaderboards. - /// - private static void ClearAndRefreshPanels(PnlPreparation panelPreparation, bool reload) - { - panelPreparation.pnlRecord.Clear(); - panelPreparation.pnlRecord.imgIconFc.SetActive(false); - foreach (var panel in panelPreparation.pnlRanks) panel.Refresh(reload); - } + // Sets all the PnlRecord data to custom chart data. + var evaluate = data.Evaluate; + panel.txtAccuracy.text = data.AccuracyStr; + panel.txtClear.text = data.Clear.ToStringInvariant(); + panel.txtCombo.text = data.Combo.ToStringInvariant(); + panel.txtGrade.text = EvalToGrade[evaluate]; + panel.txtGrade.color = panel.gradeColor[evaluate]; + panel.txtScore.text = data.Score.ToStringInvariant(); + } - /// - /// Gets the correct current difficulty for the selected chart, accounting for hidden activation. - /// - /// The uid of the chart. - /// The current difficulty of the selected chart. - private static int GetDifficulty(string uid) - { - // Get current index and null-check m_HideBmsInfos - var selectedIndex = GlobalDataBase.s_DbMusicTag.selectedDiffTglIndex; - var hideBmsInfos = Singleton.instance.m_HideBmsInfos; - if (hideBmsInfos == null || !hideBmsInfos.ContainsKey(uid)) return selectedIndex; + /// + /// Clears the current pnlRecord and refreshes the panel if needed. + /// + /// The instance. + /// Whether panelPreparation should force reload the leaderboards. + private static void ClearAndRefreshPanels(PnlPreparation panelPreparation, bool reload) + { + panelPreparation.pnlRecord.Clear(); + panelPreparation.pnlRecord.imgIconFc.SetActive(false); + foreach (var panel in panelPreparation.pnlRanks) panel.Refresh(reload); + } - // Null-check the invoked hidden dictionary and make sure that if it's non-null the hideBms exists and is non-null - var invokedHides = Singleton.instance.m_IsInvokeHideDic; - if (invokedHides == null || !invokedHides.ContainsKey(uid) || !hideBmsInfos.TryGetValue(uid, out var hideBms) || hideBms == null) return selectedIndex; + /// + /// Gets the correct current difficulty for the selected chart, accounting for hidden activation. + /// + /// The uid of the chart. + /// The current difficulty of the selected chart. + private static int GetDifficulty(string uid) + { + // Get current index and null-check m_HideBmsInfos + var selectedIndex = GlobalDataBase.s_DbMusicTag.selectedDiffTglIndex; + var hideBmsInfos = Singleton.instance.m_HideBmsInfos; + if (hideBmsInfos == null || !hideBmsInfos.ContainsKey(uid)) return selectedIndex; - return invokedHides[uid] && selectedIndex == hideBms.triggerDiff ? hideBms.m_HideDiff : selectedIndex; - } + // Null-check the invoked hidden dictionary and make sure that if it's non-null the hideBms exists and is non-null + var invokedHides = Singleton.instance.m_IsInvokeHideDic; + if (invokedHides == null || !invokedHides.ContainsKey(uid) || !hideBmsInfos.TryGetValue(uid, out var hideBms) || + hideBms == null) return selectedIndex; - /// - /// Grabs the custom chart data and injects the PnlRecord with the chart data. - /// - /// The instance. - /// Whether the instance should force reload the leaderboards. - /// - private static bool InjectPnlPreparation(PnlPreparation __instance, bool forceReload) - { - var currentMusicInfo = GlobalDataBase.s_DbMusicTag.CurMusicInfo(); + return invokedHides[uid] && selectedIndex == hideBms.triggerDiff ? hideBms.m_HideDiff : selectedIndex; + } - // If the chart is not custom, run the original method; otherwise, run our modified one - if (currentMusicInfo.albumJsonIndex != AlbumManager.Uid + 1) return true; + /// + /// Grabs the custom chart data and injects the PnlRecord with the chart data. + /// + /// The instance. + /// Whether the instance should force reload the leaderboards. + /// + private static bool InjectPnlPreparation(PnlPreparation __instance, bool forceReload) + { + var currentMusicInfo = GlobalDataBase.s_DbMusicTag.CurMusicInfo(); - // Reset the panel to its default - ClearAndRefreshPanels(__instance, forceReload); - __instance.designerLongNameController?.Refresh(); + // If the chart is not custom, run the original method; otherwise, run our modified one + if (currentMusicInfo.albumJsonIndex != AlbumManager.Uid + 1) return true; - if (!ModSettings.SavingEnabled) return false; + // Reset the panel to its default + ClearAndRefreshPanels(__instance, forceReload); + __instance.designerLongNameController?.Refresh(); - var recordPanel = __instance.pnlRecord; - var currentChartData = SaveData.GetChartSaveDataFromUid(currentMusicInfo.uid); + if (!ModSettings.SavingEnabled) return false; - // If no highest data exists then early return - if ((currentChartData.Highest?.Count ?? 0) == 0) - { - Logger.Msg($"No save data found for {currentMusicInfo.uid}, nothing to inject"); - return false; - } + var recordPanel = __instance.pnlRecord; + var currentChartData = SaveData.GetChartSaveDataFromUid(currentMusicInfo.uid); - var difficulty = GetDifficulty(currentMusicInfo.uid); + // If no highest data exists then early return + if ((currentChartData.Highest?.Count ?? 0) == 0) + { + Logger.Msg($"No save data found for {currentMusicInfo.uid}, nothing to inject"); + return false; + } - var isFullCombo = currentChartData.FullCombo?.Contains(difficulty) ?? false; - var currentChartHighest = currentChartData.Highest.GetValueOrDefault(difficulty); + var difficulty = GetDifficulty(currentMusicInfo.uid); - if (currentChartHighest is null) - { - Logger.Msg($"Save data was found for the chart, but not for difficulty {difficulty}"); - return false; - } + var isFullCombo = currentChartData.FullCombo?.Contains(difficulty) ?? false; + var currentChartHighest = currentChartData.Highest.GetValueOrDefault(difficulty); - // Set the panel with the custom score data - SetPanelWithData(recordPanel, currentChartHighest, isFullCombo); - Logger.Msg($"Injecting {currentMusicInfo.uid} with difficulty {difficulty}"); - + if (currentChartHighest is null) + { + Logger.Msg($"Save data was found for the chart, but not for difficulty {difficulty}"); return false; } - /// - /// Adds proper support for different levelDesigners as well as fixes the levelDesigner sticking issue. - /// - [HarmonyPatch(typeof(MusicInfo), nameof(MusicInfo.GetLevelDesignerStringByIndex))] - internal class LevelDesignerPatch - { - private static bool Prefix(MusicInfo __instance, ref string __result, int index) - { - - __result = index switch - { - 1 => __instance.m_MaskValue.TryGetValue("levelDesigner1", out var d1) ? d1.ToString() : __instance.levelDesigner1, - 2 => __instance.m_MaskValue.TryGetValue("levelDesigner2", out var d2) ? d2.ToString() : __instance.levelDesigner2, - 3 => __instance.m_MaskValue.TryGetValue("levelDesigner3", out var d3) ? d3.ToString() : __instance.levelDesigner3, - 4 => __instance.m_MaskValue.TryGetValue("levelDesigner4", out var d4) ? d4.ToString() : __instance.levelDesigner4, - 5 => __instance.m_MaskValue.TryGetValue("levelDesigner5", out var d5) ? d5.ToString() : __instance.levelDesigner5, - _ => throw new ArgumentOutOfRangeException(nameof(index), index, "The difficulty is not a valid index.") - }; + // Set the panel with the custom score data + SetPanelWithData(recordPanel, currentChartHighest, isFullCombo); + Logger.Msg($"Injecting {currentMusicInfo.uid} with difficulty {difficulty}"); - if (string.IsNullOrEmpty(__result) || __result == "?") __result = __instance.m_MaskValue.TryGetValue("levelDesigner", out var d) ? d.ToString() : __instance.levelDesigner; - if (!string.IsNullOrEmpty(__result)) return false; + return false; + } - __result = "?????"; - return false; + // + // HACK SECTION + // - } - } + // TODO: Find a way to inject hidden and favorite charts without using vanilla save -- below are workarounds for quick release. + private static void CleanCustomData() + { + DataHelper.hides.RemoveAll((Il2CppSystem.Predicate)(uid => uid.StartsWith($"{AlbumManager.Uid}-"))); + DataHelper.history.RemoveAll( + (Il2CppSystem.Predicate)(uid => uid.StartsWith($"{AlbumManager.Uid}-"))); + DataHelper.collections.RemoveAll( + (Il2CppSystem.Predicate)(uid => uid.StartsWith($"{AlbumManager.Uid}-"))); + DataHelper.highest.RemoveAll( + (Il2CppSystem.Predicate)(data => data.GetUid().StartsWith($"{AlbumManager.Uid}-"))); - /// - /// Stops the game from loading leaderboards on custom charts if Headquarters is not installed. - /// - [HarmonyPatch(typeof(PnlRank), nameof(PnlRank.UIRefresh))] - internal class PnlRankPatch - { - private static bool FirstRun = true; - private static bool Prefix(string uid, PnlRank __instance) - { - var noNetComp = __instance.noNet.GetComponent(); - if (FirstRun) - { - OriginalNoNetText = noNetComp.text; - FirstRun = false; - } - // Check first run case when on a custom and HQ is not present - if (uid.StartsWith($"{AlbumManager.Uid}-") && _hqPresent == null && !HQPresent) - { - Logger.Warning("Headquarters is not installed! Custom chart leaderboards will not function."); - } - - // Vanilla chart or HQ present - if (!uid.StartsWith($"{AlbumManager.Uid}-") || HQPresent) - { - noNetComp.text = OriginalNoNetText; - return true; - } - - // Custom and HQ not present - noNetComp.text = "Headquarters mod is not loaded! ~(*´Д`)"; - __instance.noNet.SetActive(true); - return false; - } - } + var keysToRemove = new List(); - // - // HACK SECTION - // + foreach (var kvp in DataHelper.dicLevelConfig) + if (kvp.key.StartsWith("999")) + keysToRemove.Add(kvp.key); - // TODO: Find a way to inject hidden and favorite charts without using vanilla save -- below are workarounds for quick release. - private static void CleanCustomData() - { + foreach (var key in keysToRemove) + DataHelper.dicLevelConfig.Remove(key); - DataHelper.hides.RemoveAll((Il2CppSystem.Predicate)(uid => uid.StartsWith($"{AlbumManager.Uid}-"))); - DataHelper.history.RemoveAll( - (Il2CppSystem.Predicate)(uid => uid.StartsWith($"{AlbumManager.Uid}-"))); - DataHelper.collections.RemoveAll( - (Il2CppSystem.Predicate)(uid => uid.StartsWith($"{AlbumManager.Uid}-"))); - DataHelper.highest.RemoveAll( - (Il2CppSystem.Predicate)(data => data.GetUid().StartsWith($"{AlbumManager.Uid}-"))); + if (DataHelper.selectedAlbumUid == "music_package_999") + DataHelper.selectedAlbumUid = "music_package_0"; - var keysToRemove = new List(); + if (DataHelper.selectedAlbumTagIndex == AlbumManager.Uid) + DataHelper.selectedAlbumTagIndex = 0; - foreach (var kvp in DataHelper.dicLevelConfig) - if (kvp.key.StartsWith("999")) - keysToRemove.Add(kvp.key); + if (!DataHelper.selectedMusicUidFromInfoList.StartsWith($"{AlbumManager.Uid}-")) return; - foreach (var key in keysToRemove) - DataHelper.dicLevelConfig.Remove(key); + if (ModSettings.SavingEnabled) + SaveData.SelectedAlbum = AlbumManager.GetAlbumNameFromUid(DataHelper.selectedMusicUidFromInfoList); + DataHelper.selectedMusicUidFromInfoList = "0-0"; + } - if (DataHelper.selectedAlbumUid == "music_package_999") - DataHelper.selectedAlbumUid = "music_package_0"; + private static void InjectCustomData() + { + if (!ModSettings.SavingEnabled) return; - if (DataHelper.selectedAlbumTagIndex == AlbumManager.Uid) - DataHelper.selectedAlbumTagIndex = 0; + DataHelper.hides.AddManagedRange(SaveData.Hides.GetAlbumUidsFromNames()); + DataHelper.history.AddManagedRange(SaveData.History.GetAlbumUidsFromNames()); + DataHelper.collections.AddManagedRange(SaveData.Collections.GetAlbumUidsFromNames()); - if (!DataHelper.selectedMusicUidFromInfoList.StartsWith($"{AlbumManager.Uid}-")) return; - - if (ModSettings.SavingEnabled) SaveData.SelectedAlbum = AlbumManager.GetAlbumNameFromUid(DataHelper.selectedMusicUidFromInfoList); - DataHelper.selectedMusicUidFromInfoList = "0-0"; - } + if (!SaveData.SelectedAlbum.StartsWith("album_")) return; + DataHelper.selectedAlbumUid = "music_package_999"; + DataHelper.selectedAlbumTagIndex = 999; + DataHelper.selectedMusicUidFromInfoList = + AlbumManager.LoadedAlbums.TryGetValue(SaveData.SelectedAlbum, out var album) ? album.Uid : "0-0"; + } - private static void InjectCustomData() - { - if (!ModSettings.SavingEnabled) return; + // + // SCORE SECTION + // - DataHelper.hides.AddManagedRange(SaveData.Hides.GetAlbumUidsFromNames()); - DataHelper.history.AddManagedRange(SaveData.History.GetAlbumUidsFromNames()); - DataHelper.collections.AddManagedRange(SaveData.Collections.GetAlbumUidsFromNames()); + /// + /// Stops the game from saving custom chart score data. + /// + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static IntPtr RecordBattleArgsPatch(IntPtr instance, IntPtr args, IntPtr isSuccess, IntPtr nativeMethodInfo) + { + return !GlobalDataBase.s_DbBattleStage.musicUid.StartsWith($"{AlbumManager.Uid}-") + ? Hook.Trampoline(instance, args, isSuccess, nativeMethodInfo) + : IntPtr.Zero; + } - if (!SaveData.SelectedAlbum.StartsWith("album_")) return; - DataHelper.selectedAlbumUid = "music_package_999"; - DataHelper.selectedAlbumTagIndex = 999; - DataHelper.selectedMusicUidFromInfoList = AlbumManager.LoadedAlbums.TryGetValue(SaveData.SelectedAlbum, out var album) ? album.Uid : "0-0"; - } + /// + /// Gets RecordBattleArgs and detours it using a + /// NativeHook<RecordBattleArgsDelegate> to RecordBattleArgsPatch. + /// + internal static unsafe void AttachHook() + { + // If you are interested in what's happening here, check out AssetPatch + var recordBattleArgsMethod = AccessTools.Method(typeof(AchievementManager), + nameof(AchievementManager.RecordBattleArgs), + new[] { typeof(GlobalAchievementArgs).MakeByRefType(), typeof(bool) }); - [HarmonyPatch(typeof(DataHelper), nameof(DataHelper.CheckMusicUnlockMaster), new Type[] { typeof(MusicInfo), typeof(bool) })] - internal class CheckUnlockMasterPatch + if (recordBattleArgsMethod is null) { - private static bool Prefix(MusicInfo musicInfo, ref bool __result) - { - SaveData.Ability = Math.Max(SaveData.Ability, GlobalDataBase.dbUi.ability); - var ability = GameAccountSystem.instance.IsLoggedIn() ? SaveData.Ability : 0; - var uid = musicInfo?.uid; - - // If musicInfo or uid is null, run original - if (uid is null) return true; - - // Bugged vanilla state, do manual logic - if (GlobalDataBase.dbUi.ability == 0 && ability != 0) - { - Logger.Msg("Fixing bugged vanilla state for master lock"); - var vanillaParse = Formatting.TryParseAsInt(musicInfo.difficulty3, out var difficulty); - var cond = !vanillaParse || ability >= difficulty; - __result = cond || DataHelper.unlockMasters.Contains(uid) || SaveData.UnlockedMasters.Contains(AlbumManager.GetAlbumNameFromUid(uid)) || (!AlbumManager.GetByUid(musicInfo.uid)?.IsPackaged ?? false); - return false; - } - - // Non-bugged vanilla case - if (!uid.StartsWith($"{AlbumManager.Uid}-")) return true; - - // Non-bugged custom case - var successParse = Formatting.TryParseAsInt(musicInfo.difficulty3, out var diffNum); - var abilityConditionOrGimmick = !successParse || ability >= diffNum; - - __result = abilityConditionOrGimmick || SaveData.UnlockedMasters.Contains(AlbumManager.GetAlbumNameFromUid(uid)) || !AlbumManager.GetByUid(musicInfo.uid).IsPackaged; - return false; - } + Logger.Error("FATAL ERROR: SavePatch failed."); + Thread.Sleep(1000); + Environment.Exit(1); } - [HarmonyPatch(typeof(PnlPreparation), nameof(PnlPreparation.OnEnable))] - internal class PnlPreparationEnablePatch + var recordBattleArgsPointer = *(IntPtr*)(IntPtr)Il2CppInteropUtils + .GetIl2CppMethodInfoPointerFieldForGeneratedMethod(recordBattleArgsMethod).GetValue(null)!; + + delegate* unmanaged[Cdecl] detourPointer = &RecordBattleArgsPatch; + + Hook.Detour = (IntPtr)detourPointer; + Hook.Target = recordBattleArgsPointer; + Hook.Attach(); + } + + /// + /// Adds proper support for different levelDesigners as well as fixes the levelDesigner sticking issue. + /// + [HarmonyPatch(typeof(MusicInfo), nameof(MusicInfo.GetLevelDesignerStringByIndex))] + internal class LevelDesignerPatch + { + private static bool Prefix(MusicInfo __instance, ref string __result, int index) { - // Inject and force load all leaderboards - private static bool Prefix(PnlPreparation __instance) + __result = index switch { - return InjectPnlPreparation(__instance, true); - } + 1 => __instance.m_MaskValue.TryGetValue("levelDesigner1", out var d1) + ? d1.ToString() + : __instance.levelDesigner1, + 2 => __instance.m_MaskValue.TryGetValue("levelDesigner2", out var d2) + ? d2.ToString() + : __instance.levelDesigner2, + 3 => __instance.m_MaskValue.TryGetValue("levelDesigner3", out var d3) + ? d3.ToString() + : __instance.levelDesigner3, + 4 => __instance.m_MaskValue.TryGetValue("levelDesigner4", out var d4) + ? d4.ToString() + : __instance.levelDesigner4, + 5 => __instance.m_MaskValue.TryGetValue("levelDesigner5", out var d5) + ? d5.ToString() + : __instance.levelDesigner5, + _ => throw new ArgumentOutOfRangeException(nameof(index), index, "The difficulty is not a valid index.") + }; + + if (string.IsNullOrEmpty(__result) || __result == "?") + __result = __instance.m_MaskValue.TryGetValue("levelDesigner", out var d) + ? d.ToString() + : __instance.levelDesigner; + if (!string.IsNullOrEmpty(__result)) return false; + + __result = "?????"; + return false; } + } + + /// + /// Stops the game from loading leaderboards on custom charts if Headquarters is not installed. + /// + [HarmonyPatch(typeof(PnlRank), nameof(PnlRank.UIRefresh))] + internal class PnlRankPatch + { + private static bool FirstRun = true; - [HarmonyPatch(typeof(PnlPreparation), nameof(PnlPreparation.OnDiffTglChanged))] - internal class PnlPreparationDiffTogglePatch + private static bool Prefix(string uid, PnlRank __instance) { - // Inject and load all leaderboards if needed - private static bool Prefix(PnlPreparation __instance) + var repairingComp = __instance.repairing.GetComponent(); + if (FirstRun) { - return InjectPnlPreparation(__instance, false); + OriginalRepairingText = repairingComp.text; + FirstRun = false; } - } - // - // ACCOUNT SECTION - // + // Check first run case when on a custom and HQ is not present + if (uid.StartsWith($"{AlbumManager.Uid}-") && _hqPresent == null && !HQPresent) + Logger.Warning("Headquarters is not installed! Custom chart leaderboards will not function."); - // TODO: Figure all of this out without using vanilla save - [HarmonyPatch(typeof(DBMusicTag), nameof(DBMusicTag.AddHide))] - internal class AddHidePatch - { - private static bool Prefix(MusicInfo musicInfo) + // Vanilla chart or HQ present + if (!uid.StartsWith($"{AlbumManager.Uid}-") || HQPresent) { - if (!musicInfo.uid.StartsWith($"{AlbumManager.Uid}-")) return true; - if (!ModSettings.SavingEnabled) return false; - - SaveData.Hides.Add(AlbumManager.GetAlbumNameFromUid(musicInfo.uid)); + repairingComp.text = OriginalRepairingText; return true; } + + // Custom and HQ not present + repairingComp.text = "Headquarters mod is not loaded! ~(*´Д`)"; + __instance.repairing.SetActive(true); + return false; } + } - [HarmonyPatch(typeof(DBMusicTag), nameof(DBMusicTag.RemoveHide))] - internal class RemoveHidePatch + [HarmonyPatch(typeof(DataHelper), nameof(DataHelper.CheckMusicUnlockMaster), typeof(MusicInfo), typeof(bool))] + internal class CheckUnlockMasterPatch + { + private static bool Prefix(MusicInfo musicInfo, ref bool __result) { - private static bool Prefix(MusicInfo musicInfo) - { - if (!musicInfo.uid.StartsWith($"{AlbumManager.Uid}-")) return true; - if (!ModSettings.SavingEnabled) return false; + SaveData.Ability = Math.Max(SaveData.Ability, GlobalDataBase.dbUi.ability); + var ability = GameAccountSystem.instance.IsLoggedIn() ? SaveData.Ability : 0; + var uid = musicInfo?.uid; - SaveData.Hides.Remove(AlbumManager.GetAlbumNameFromUid(musicInfo.uid)); - return true; + // If musicInfo or uid is null, run original + if (uid is null) return true; + + // Bugged vanilla state, do manual logic + if (GlobalDataBase.dbUi.ability == 0 && ability != 0) + { + Logger.Msg("Fixing bugged vanilla state for master lock"); + var vanillaParse = musicInfo.difficulty3.TryParseAsInt(out var difficulty); + var cond = !vanillaParse || ability >= difficulty; + __result = cond || DataHelper.unlockMasters.Contains(uid) || + SaveData.UnlockedMasters.Contains(AlbumManager.GetAlbumNameFromUid(uid)) || + (!AlbumManager.GetByUid(musicInfo.uid)?.IsPackaged ?? false); + return false; } + + // Non-bugged vanilla case + if (!uid.StartsWith($"{AlbumManager.Uid}-")) return true; + + // Non-bugged custom case + var successParse = musicInfo.difficulty3.TryParseAsInt(out var diffNum); + var abilityConditionOrGimmick = !successParse || ability >= diffNum; + + __result = abilityConditionOrGimmick || + SaveData.UnlockedMasters.Contains(AlbumManager.GetAlbumNameFromUid(uid)) || + !AlbumManager.GetByUid(musicInfo.uid).IsPackaged; + return false; } + } - [HarmonyPatch(typeof(DBMusicTag), nameof(DBMusicTag.AddCollection))] - internal class AddCollectionPatch + [HarmonyPatch(typeof(PnlPreparation), nameof(PnlPreparation.OnEnable))] + internal class PnlPreparationEnablePatch + { + // Inject and force load all leaderboards + private static bool Prefix(PnlPreparation __instance) { - private static bool Prefix(MusicInfo musicInfo) - { - if (!musicInfo.uid.StartsWith($"{AlbumManager.Uid}-")) return true; - if (!ModSettings.SavingEnabled) return false; + return InjectPnlPreparation(__instance, true); + } + } - SaveData.Collections.Add(AlbumManager.GetAlbumNameFromUid(musicInfo.uid)); - return true; - } + [HarmonyPatch(typeof(PnlPreparation), nameof(PnlPreparation.OnDiffTglChanged))] + internal class PnlPreparationDiffTogglePatch + { + // Inject and load all leaderboards if needed + private static bool Prefix(PnlPreparation __instance) + { + return InjectPnlPreparation(__instance, false); } + } - [HarmonyPatch(typeof(DBMusicTag), nameof(DBMusicTag.RemoveCollection))] - internal class RemoveCollectionPatch + // + // ACCOUNT SECTION + // + + // TODO: Figure all of this out without using vanilla save + [HarmonyPatch(typeof(DBMusicTag), nameof(DBMusicTag.AddHide))] + internal class AddHidePatch + { + private static bool Prefix(MusicInfo musicInfo) { - private static bool Prefix(MusicInfo musicInfo) - { - if (!musicInfo.uid.StartsWith($"{AlbumManager.Uid}-")) return true; - if (!ModSettings.SavingEnabled) return false; + if (!musicInfo.uid.StartsWith($"{AlbumManager.Uid}-")) return true; + if (!ModSettings.SavingEnabled) return false; - SaveData.Collections.Remove(AlbumManager.GetAlbumNameFromUid(musicInfo.uid)); - return true; - } + SaveData.Hides.Add(AlbumManager.GetAlbumNameFromUid(musicInfo.uid)); + return true; } + } - [HarmonyPatch(typeof(DBMusicTag), nameof(DBMusicTag.AddHistory))] - internal class AddHistoryPatch + [HarmonyPatch(typeof(DBMusicTag), nameof(DBMusicTag.RemoveHide))] + internal class RemoveHidePatch + { + private static bool Prefix(MusicInfo musicInfo) { - private static bool Prefix(string musicUid) - { - if (!musicUid.StartsWith($"{AlbumManager.Uid}-")) return true; - if (!ModSettings.SavingEnabled) return false; - - var albumName = AlbumManager.GetAlbumNameFromUid(musicUid); + if (!musicInfo.uid.StartsWith($"{AlbumManager.Uid}-")) return true; + if (!ModSettings.SavingEnabled) return false; - // Remove album from history if it exists - SaveData.History.Remove(albumName); - SaveData.History.Add(albumName); + SaveData.Hides.Remove(AlbumManager.GetAlbumNameFromUid(musicInfo.uid)); + return true; + } + } - if (SaveData.History.Count > 10) - SaveData.History.RemoveAt(0); + [HarmonyPatch(typeof(DBMusicTag), nameof(DBMusicTag.AddCollection))] + internal class AddCollectionPatch + { + private static bool Prefix(MusicInfo musicInfo) + { + if (!musicInfo.uid.StartsWith($"{AlbumManager.Uid}-")) return true; + if (!ModSettings.SavingEnabled) return false; - return true; - } + SaveData.Collections.Add(AlbumManager.GetAlbumNameFromUid(musicInfo.uid)); + return true; } + } - // - // SCORE SECTION - // - - /// - /// Stops the game from saving custom chart score data. - /// - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static IntPtr RecordBattleArgsPatch(IntPtr instance, IntPtr args, IntPtr isSuccess, IntPtr nativeMethodInfo) + [HarmonyPatch(typeof(DBMusicTag), nameof(DBMusicTag.RemoveCollection))] + internal class RemoveCollectionPatch + { + private static bool Prefix(MusicInfo musicInfo) { - return !GlobalDataBase.s_DbBattleStage.musicUid.StartsWith($"{AlbumManager.Uid}-") ? Hook.Trampoline(instance, args, isSuccess, nativeMethodInfo) : IntPtr.Zero; + if (!musicInfo.uid.StartsWith($"{AlbumManager.Uid}-")) return true; + if (!ModSettings.SavingEnabled) return false; + + SaveData.Collections.Remove(AlbumManager.GetAlbumNameFromUid(musicInfo.uid)); + return true; } + } - /// - /// Gets RecordBattleArgs and detours it using a - /// NativeHook<RecordBattleArgsDelegate> to RecordBattleArgsPatch. - /// - internal static unsafe void AttachHook() + [HarmonyPatch(typeof(DBMusicTag), nameof(DBMusicTag.AddHistory))] + internal class AddHistoryPatch + { + private static bool Prefix(string musicUid) { - // If you are interested in what's happening here, check out AssetPatch - var recordBattleArgsMethod = AccessTools.Method(typeof(AchievementManager), - nameof(AchievementManager.RecordBattleArgs), - new[] { typeof(GlobalAchievementArgs).MakeByRefType(), typeof(bool) }); + if (!musicUid.StartsWith($"{AlbumManager.Uid}-")) return true; + if (!ModSettings.SavingEnabled) return false; - if (recordBattleArgsMethod is null) - { - Logger.Error("FATAL ERROR: SavePatch failed."); - Thread.Sleep(1000); - Environment.Exit(1); - } + var albumName = AlbumManager.GetAlbumNameFromUid(musicUid); - var recordBattleArgsPointer = *(IntPtr*)(IntPtr)Il2CppInteropUtils - .GetIl2CppMethodInfoPointerFieldForGeneratedMethod(recordBattleArgsMethod).GetValue(null)!; + // Remove album from history if it exists + SaveData.History.Remove(albumName); + SaveData.History.Add(albumName); - delegate* unmanaged[Cdecl] detourPointer = &RecordBattleArgsPatch; + if (SaveData.History.Count > 10) + SaveData.History.RemoveAt(0); - Hook.Detour = (IntPtr)detourPointer; - Hook.Target = recordBattleArgsPointer; - Hook.Attach(); + return true; } + } - /// - /// Gets the score data of the custom chart and sends it to SaveManager for processing. - /// - [HarmonyPatch(typeof(GameAccountSystem), nameof(GameAccountSystem.UploadScore))] - internal class UploadScorePatch + /// + /// Gets the score data of the custom chart and sends it to SaveManager for processing. + /// + [HarmonyPatch(typeof(GameAccountSystem), nameof(GameAccountSystem.UploadScore))] + internal class UploadScorePatch + { + private static void Postfix(SceneUploadResultData resData) { - private static void Postfix(SceneUploadResultData resData) - { - if (!resData.musicUid.StartsWith($"{AlbumManager.Uid}-")) return; - SaveScore(resData.musicUid, resData.musicDifficulty, resData.score, resData.accuracy, resData.combo, resData.evaluate, resData.miss); - } + if (!resData.musicUid.StartsWith($"{AlbumManager.Uid}-")) return; + SaveScore(resData.musicUid, resData.musicDifficulty, resData.score, resData.accuracy, resData.combo, + resData.evaluate, resData.miss); } + } - /// - /// Enables the PnlVictory screen logic when the chart is custom. - /// - [HarmonyPatch(typeof(PnlVictory), nameof(PnlVictory.SetScore))] - internal class SetScorePatch + /// + /// Enables the PnlVictory screen logic when the chart is custom. + /// + [HarmonyPatch(typeof(PnlVictory), nameof(PnlVictory.SetScore))] + internal class SetScorePatch + { + private static void Postfix(PnlVictory __instance) { - private static void Postfix(PnlVictory __instance) + // Fix for DJMax scrolling text bug + if (__instance.m_CurControls.mainPnl.transform.parent.name is "Djmax") { - // Fix for DJMax scrolling text bug - if (__instance.m_CurControls.mainPnl.transform.parent.name is "Djmax") - { - var titleMask = __instance.m_CurControls.mainPnl.transform - .Find("PnlVictory_3D").Find("SongTittle").Find("ImgSongTittleMask"); + var titleMask = __instance.m_CurControls.mainPnl.transform + .Find("PnlVictory_3D").Find("SongTittle").Find("ImgSongTittleMask"); - var titleText = titleMask.Find("TxtSongTittle").gameObject; - if (!titleText.active) titleMask.Find("MaskPos").gameObject.SetActive(true); - } + var titleText = titleMask.Find("TxtSongTittle").gameObject; + if (!titleText.active) titleMask.Find("MaskPos").gameObject.SetActive(true); + } - if (!ModSettings.SavingEnabled || !GlobalDataBase.dbBattleStage.musicUid.StartsWith($"{AlbumManager.Uid}-")) return; + if (!ModSettings.SavingEnabled || + !GlobalDataBase.dbBattleStage.musicUid.StartsWith($"{AlbumManager.Uid}-")) return; - var albumName = AlbumManager.GetAlbumNameFromUid(GlobalDataBase.dbBattleStage.musicUid); + var albumName = AlbumManager.GetAlbumNameFromUid(GlobalDataBase.dbBattleStage.musicUid); - if (!SaveData.Highest.TryGetValue(albumName, out var highest)) - return; + if (!SaveData.Highest.TryGetValue(albumName, out var highest)) + return; - // If the chart has been played then enable the "HI-SCORE" UI element - var difficulty = GlobalDataBase.s_DbBattleStage.m_MapDifficulty; - __instance.m_CurControls.highScoreTitle.enabled = PreviousScore != "-"; - __instance.m_CurControls.highScoreTxt.enabled = PreviousScore != "-"; + // If the chart has been played then enable the "HI-SCORE" UI element + var difficulty = GlobalDataBase.s_DbBattleStage.m_MapDifficulty; + __instance.m_CurControls.highScoreTitle.enabled = PreviousScore != "-"; + __instance.m_CurControls.highScoreTxt.enabled = PreviousScore != "-"; - // Gets the current run score - var score = Singleton.instance.GetScore(); + // Gets the current run score + var score = Singleton.instance.GetScore(); - // Enable "New Best!!" UI element if the chart hasn't been played, or the new play is a higher score - if (!highest.ContainsKey(difficulty)) - __instance.m_CurControls.newBest.SetActive(true); - if (PreviousScore != "-") - __instance.m_CurControls.newBest.SetActive(score > int.Parse(PreviousScore)); + // Enable "New Best!!" UI element if the chart hasn't been played, or the new play is a higher score + if (!highest.ContainsKey(difficulty)) + __instance.m_CurControls.newBest.SetActive(true); + if (PreviousScore != "-") + __instance.m_CurControls.newBest.SetActive(score > int.Parse(PreviousScore)); - __instance.m_CurControls.highScoreTxt.text = PreviousScore; - } + __instance.m_CurControls.highScoreTxt.text = PreviousScore; } + } - [HarmonyPatch(typeof(PnlPreparation), nameof(PnlPreparation.OnBattleStart))] - internal class OnBattleStartPatch + [HarmonyPatch(typeof(PnlPreparation), nameof(PnlPreparation.OnBattleStart))] + internal class OnBattleStartPatch + { + private static void Postfix() { - private static void Postfix() - { - if (ModSettings.SavingEnabled && !DataHelper.selectedMusicUidFromInfoList.StartsWith("album_")) - SaveData.SelectedAlbum = DataHelper.selectedMusicUidFromInfoList; - } + if (ModSettings.SavingEnabled && !DataHelper.selectedMusicUidFromInfoList.StartsWith("album_")) + SaveData.SelectedAlbum = DataHelper.selectedMusicUidFromInfoList; } + } - // - // HACKS SECTION - TODO: Remove all the methods below once we don't put data into the official game at all - // - - [HarmonyPatch(typeof(SteamSync), nameof(SteamSync.SaveLocal))] - internal class SaveLocalPatch - { - private static bool BackedUp { get; set; } - - private static void Prefix() - { + // + // HACKS SECTION - TODO: Remove all the methods below once we don't put data into the official game at all + // - if (ModSettings.SavingEnabled && DataHelper.selectedMusicUidFromInfoList.StartsWith($"{AlbumManager.Uid}-")) - SaveData.SelectedAlbum = - $"{AlbumManager.GetAlbumNameFromUid(DataHelper.selectedMusicUidFromInfoList)}"; + [HarmonyPatch(typeof(SteamSync), nameof(SteamSync.SaveLocal))] + internal class SaveLocalPatch + { + private static bool BackedUp { get; set; } - if (ModSettings.SavingEnabled) SaveSaveFile(); - CleanCustomData(); - } + private static void Prefix() + { + if (ModSettings.SavingEnabled && DataHelper.selectedMusicUidFromInfoList.StartsWith($"{AlbumManager.Uid}-")) + SaveData.SelectedAlbum = + $"{AlbumManager.GetAlbumNameFromUid(DataHelper.selectedMusicUidFromInfoList)}"; - private static void Postfix() - { - if (!ModSettings.SavingEnabled) return; - if (!BackedUp) Backup.InitBackups(); - BackedUp = true; - InjectCustomData(); - } + if (ModSettings.SavingEnabled) SaveSaveFile(); + CleanCustomData(); } - [HarmonyPatch(typeof(SteamSync), nameof(SteamSync.LoadLocal))] - internal class LoadLocalPatch + private static void Postfix() { - private static void Postfix() - { - CleanCustomData(); - if (ModSettings.SavingEnabled) InjectCustomData(); - } + if (!ModSettings.SavingEnabled) return; + if (!BackedUp) Backup.InitBackups(); + BackedUp = true; + InjectCustomData(); } + } - [HarmonyPatch(typeof(GameAccountSystem), nameof(GameAccountSystem.RefreshDatas))] - internal class RefreshDatasPatch + [HarmonyPatch(typeof(SteamSync), nameof(SteamSync.LoadLocal))] + internal class LoadLocalPatch + { + private static void Postfix() { - private static void Prefix() - { - Backup.InitBackups(); - SaveSaveFile(); - CleanCustomData(); - if (ModSettings.SavingEnabled) InjectCustomData(); - } + CleanCustomData(); + if (ModSettings.SavingEnabled) InjectCustomData(); } + } - [HarmonyPatch(typeof(GameAccountSystem), nameof(GameAccountSystem.OnSaveSelectCallback))] - internal class OnSaveSelectPatch + [HarmonyPatch(typeof(GameAccountSystem), nameof(GameAccountSystem.RefreshDatas))] + internal class RefreshDatasPatch + { + private static void Prefix() { - private static void Prefix(ref bool isLocal) - { - if (!isLocal) return; + Backup.InitBackups(); + SaveSaveFile(); + CleanCustomData(); + if (ModSettings.SavingEnabled) InjectCustomData(); + } + } - if (ModSettings.SavingEnabled) SaveSaveFile(); - CleanCustomData(); - } + [HarmonyPatch(typeof(GameAccountSystem), nameof(GameAccountSystem.OnSaveSelectCallback))] + internal class OnSaveSelectPatch + { + private static void Prefix(ref bool isLocal) + { + if (!isLocal) return; - private static void Postfix(ref bool isLocal) - { - if (!ModSettings.SavingEnabled || !isLocal) return; - InjectCustomData(); - } + if (ModSettings.SavingEnabled) SaveSaveFile(); + CleanCustomData(); } - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate IntPtr RecordBattleArgsDelegate(IntPtr instance, IntPtr args, IntPtr isSuccess, IntPtr nativeMethodInfo); + private static void Postfix(ref bool isLocal) + { + if (!ModSettings.SavingEnabled || !isLocal) return; + InjectCustomData(); + } } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate IntPtr RecordBattleArgsDelegate(IntPtr instance, IntPtr args, IntPtr isSuccess, + IntPtr nativeMethodInfo); } \ No newline at end of file diff --git a/Patches/SceneEggPatch.cs b/Patches/SceneEggPatch.cs index 62806df..b03dcb7 100644 --- a/Patches/SceneEggPatch.cs +++ b/Patches/SceneEggPatch.cs @@ -10,121 +10,119 @@ using Il2CppPeroPeroGames.GlobalDefines; using static CustomAlbums.Data.SceneEgg; -namespace CustomAlbums.Patches +namespace CustomAlbums.Patches; + +internal class SceneEggPatch { - internal class SceneEggPatch + private static readonly Logger Logger = new(nameof(SceneEggPatch)); + + internal static bool IgnoreSceneEggs(out Album outAlbum, params SceneEggs[] sceneEggs) { - private static readonly Logger Logger = new(nameof(SceneEggPatch)); + outAlbum = null; + // If the chart is not custom then leave + var uid = DataHelper.selectedMusicUid; + if (!uid.StartsWith($"{AlbumManager.Uid}-")) return true; + + // If the album doesn't exist (?) or if there are no SceneEggs or if it's christmas SceneEgg (not really a SceneEgg) then leave + var album = AlbumManager.GetByUid(uid); + if (album is null) return true; + + outAlbum = album; + return sceneEggs.Any(sceneEgg => sceneEgg == album.Info.SceneEgg); + } - internal static bool IgnoreSceneEggs(out Album outAlbum, params SceneEggs[] sceneEggs) + /// + /// Adds support for SceneEggs. + /// + [HarmonyPatch(typeof(SceneEggAbstractController), nameof(SceneEggAbstractController.SceneEggHandle))] + internal class ControllerPatch + { + private static void Prefix(Il2CppSystem.Collections.Generic.List sceneEggIdsBuffer) { - outAlbum = null; - // If the chart is not custom then leave - var uid = DataHelper.selectedMusicUid; - if (!uid.StartsWith($"{AlbumManager.Uid}-")) return true; + // Return if there are no scene eggs or if the scene eggs have special logic + if (IgnoreSceneEggs(out var album, SceneEggs.None, SceneEggs.Christmas, SceneEggs.BadApple)) return; - // If the album doesn't exist (?) or if there are no SceneEggs or if it's christmas SceneEgg (not really a SceneEgg) then leave - var album = AlbumManager.GetByUid(uid); - if (album is null) return true; + // Adds the scene egg to the buffer + sceneEggIdsBuffer.Add((int)album.Info.SceneEgg); + Logger.Msg("Added SceneEgg " + Enum.GetName(typeof(SceneEggs), album.Info.SceneEgg)); - outAlbum = album; - return sceneEggs.Any(sceneEgg => sceneEgg == album.Info.SceneEgg); + // Removes all scene eggs from the buffer that are not the one that was added + // This prevents a hierarchical issue where character choice would be used over chart choice + sceneEggIdsBuffer.RemoveAll((Il2CppSystem.Predicate)(eggId => eggId != (int)album.Info.SceneEgg)); } + } - /// - /// Adds support for SceneEggs. - /// - [HarmonyPatch(typeof(SceneEggAbstractController), nameof(SceneEggAbstractController.SceneEggHandle))] - internal class ControllerPatch + /// + /// Makes scene_05 be scene_05_christmas if the Christmas SceneEgg is enabled. + /// + [HarmonyPatch(typeof(GameMusicScene), nameof(GameMusicScene.SceneFestival))] + internal class SceneFestivalPatch + { + private static bool Prefix(string sceneFestivalName, ref string __result) { - private static void Prefix(Il2CppSystem.Collections.Generic.List sceneEggIdsBuffer) - { - // Return if there are no scene eggs or if the scene eggs have special logic - if (IgnoreSceneEggs(out var album, SceneEggs.None, SceneEggs.Christmas, SceneEggs.BadApple)) return; - - // Adds the scene egg to the buffer - sceneEggIdsBuffer.Add((int)album.Info.SceneEgg); - Logger.Msg("Added SceneEgg " + Enum.GetName(typeof(SceneEggs), album.Info.SceneEgg)); - - // Removes all scene eggs from the buffer that are not the one that was added - // This prevents a hierarchical issue where character choice would be used over chart choice - sceneEggIdsBuffer.RemoveAll((Il2CppSystem.Predicate)(eggId => eggId != (int)album.Info.SceneEgg)); - } - } + // If the scene is not scene_05 or scene_08 then there is no Christmas + if (sceneFestivalName != "scene_05") return true; - /// - /// Makes scene_05 be scene_05_christmas if the Christmas SceneEgg is enabled. - /// - [HarmonyPatch(typeof(GameMusicScene), nameof(GameMusicScene.SceneFestival))] - internal class SceneFestivalPatch - { - private static bool Prefix(string sceneFestivalName, ref string __result) - { - // If the scene is not scene_05 or scene_08 then there is no Christmas - if (sceneFestivalName != "scene_05") return true; - - // Ignore the actual SceneEggs - if (IgnoreSceneEggs(out _, SceneEggs.Arknights, SceneEggs.Cytus, SceneEggs.None, - SceneEggs.Queen, SceneEggs.Touhou, SceneEggs.Wacca, SceneEggs.Miku, SceneEggs.BadApple, SceneEggs.RinLen, SceneEggs.BlueArchive)) return true; - - if (sceneFestivalName == "scene_05") __result = "scene_05_christmas"; - return false; - } + // Ignore the actual SceneEggs + if (IgnoreSceneEggs(out _, SceneEggs.Arknights, SceneEggs.Cytus, SceneEggs.None, + SceneEggs.Queen, SceneEggs.Touhou, SceneEggs.Wacca, SceneEggs.Miku, SceneEggs.BadApple, SceneEggs.RinLen, SceneEggs.BlueArchive)) return true; + + if (sceneFestivalName == "scene_05") __result = "scene_05_christmas"; + return false; } + } - /// - /// Makes the boss be the christmas boss if the Christmas SceneEgg is enabled. - /// - [HarmonyPatch(typeof(Boss), nameof(Boss.BossFestival))] - internal class BossFestivalPatch + /// + /// Makes the boss be the christmas boss if the Christmas SceneEgg is enabled. + /// + [HarmonyPatch(typeof(Boss), nameof(Boss.BossFestival))] + internal class BossFestivalPatch + { + private static bool Prefix(string bossFestivalName, ref string __result) { - private static bool Prefix(string bossFestivalName, ref string __result) - { - // If the boss is not 0501_boss then there is no Christmas - if (bossFestivalName != "0501_boss") return true; - if (IgnoreSceneEggs(out _, SceneEggs.Arknights, SceneEggs.Cytus, SceneEggs.None, - SceneEggs.Queen, SceneEggs.Touhou, SceneEggs.Wacca, SceneEggs.Miku, SceneEggs.BadApple, SceneEggs.RinLen, SceneEggs.BlueArchive)) return true; - - __result = "0501_boss_christmas"; - return false; - } + // If the boss is not 0501_boss then there is no Christmas + if (bossFestivalName != "0501_boss") return true; + if (IgnoreSceneEggs(out _, SceneEggs.Arknights, SceneEggs.Cytus, SceneEggs.None, + SceneEggs.Queen, SceneEggs.Touhou, SceneEggs.Wacca, SceneEggs.Miku, SceneEggs.BadApple, SceneEggs.RinLen, SceneEggs.BlueArchive)) return true; + + __result = "0501_boss_christmas"; + return false; } + } - /// - /// Makes the game think (temporarily) that the chart is Bad Apple when the BadApple SceneEgg is enabled to load all - /// the assets properly. - /// - [HarmonyPatch(typeof(DBTouhou), nameof(DBTouhou.AwakeInit))] - internal class BadApplePatch + /// + /// Makes the game think (temporarily) that the chart is Bad Apple when the BadApple SceneEgg is enabled to load all + /// the assets properly. + /// + [HarmonyPatch(typeof(DBTouhou), nameof(DBTouhou.AwakeInit))] + internal class BadApplePatch + { + private static void Postfix() { - private static void Postfix() - { - if (IgnoreSceneEggs(out _, SceneEggs.Arknights, SceneEggs.Cytus, SceneEggs.None, - SceneEggs.Queen, SceneEggs.Touhou, SceneEggs.Wacca, SceneEggs.Miku, SceneEggs.Christmas, SceneEggs.RinLen, SceneEggs.BlueArchive)) return; + if (IgnoreSceneEggs(out _, SceneEggs.Arknights, SceneEggs.Cytus, SceneEggs.None, + SceneEggs.Queen, SceneEggs.Touhou, SceneEggs.Wacca, SceneEggs.Miku, SceneEggs.Christmas, + SceneEggs.RinLen, SceneEggs.BlueArchive)) return; - GlobalDataBase.dbTouhou.isBadApple = true; + GlobalDataBase.dbTouhou.isBadApple = true; - GlobalDataBase.s_DbOther.m_HpFx = TouhouLogic.ReplaceBadAppleString("fx_hp_ground"); - GlobalDataBase.s_DbOther.m_MusicFx = TouhouLogic.ReplaceBadAppleString("fx_score_ground"); - GlobalDataBase.s_DbOther.m_DustFx = TouhouLogic.ReplaceBadAppleString("dust_fx"); - } + GlobalDataBase.s_DbOther.m_HpFx = TouhouLogic.ReplaceBadAppleString("fx_hp_ground"); + GlobalDataBase.s_DbOther.m_MusicFx = TouhouLogic.ReplaceBadAppleString("fx_score_ground"); + GlobalDataBase.s_DbOther.m_DustFx = TouhouLogic.ReplaceBadAppleString("dust_fx"); } + } - /// - /// Makes the game think that the chart is a RinLen chart when the RinLen SceneEgg is enabled to load all - /// the assets properly. - /// - [HarmonyPatch(typeof(DBMusicTagDefine), nameof(DBMusicTagDefine.IsRinLenEggSong))] - internal class RinLenPatch + /// + /// Makes the game think that the chart is a RinLen chart when the RinLen SceneEgg is enabled to load all + /// the assets properly. + /// + [HarmonyPatch(typeof(DBMusicTagDefine), nameof(DBMusicTagDefine.IsRinLenEggSong))] + internal class RinLenPatch + { + private static void Postfix(string uid, ref bool __result) { - private static void Postfix(string uid, ref bool __result) - { - if (uid.StartsWith($"{AlbumManager.Uid}-") && AlbumManager.GetByUid(uid).Info.SceneEgg is SceneEggs.RinLen) - { - __result = true; - } - } + if (uid.StartsWith($"{AlbumManager.Uid}-") && AlbumManager.GetByUid(uid).Info.SceneEgg is SceneEggs.RinLen) + __result = true; } } } \ No newline at end of file diff --git a/Patches/SilenceLogPatch.cs b/Patches/SilenceLogPatch.cs index 52eb069..32a647d 100644 --- a/Patches/SilenceLogPatch.cs +++ b/Patches/SilenceLogPatch.cs @@ -1,22 +1,20 @@ using HarmonyLib; using Il2CppPeroTools2.Log; -namespace CustomAlbums.Patches +namespace CustomAlbums.Patches; + +internal class SilenceLogPatch { - internal class SilenceLogPatch + /// + /// Disables log write-to-file if it is set in MelonPreferences (it is by default). + /// + [HarmonyPatch(typeof(PeroLogConfig), nameof(PeroLogConfig.instance), MethodType.Getter)] + internal class LoadConfigPatch { - /// - /// Disables log write-to-file if it is set in MelonPreferences (it is by default). - /// - [HarmonyPatch(typeof(PeroLogConfig), nameof(PeroLogConfig.instance), MethodType.Getter)] - internal class LoadConfigPatch + private static void Postfix(ref PeroLogConfig __result) { - private static void Postfix(ref PeroLogConfig __result) - { - if (ModSettings.LoggingToFileEnabled) return; - __result.m_IsLogToFile = false; - } + if (ModSettings.LoggingToFileEnabled) return; + __result.m_IsLogToFile = false; } - } -} +} \ No newline at end of file diff --git a/Patches/TagPatch.cs b/Patches/TagPatch.cs index c89dcef..bf80fc7 100644 --- a/Patches/TagPatch.cs +++ b/Patches/TagPatch.cs @@ -6,34 +6,33 @@ using Il2CppAssets.Scripts.Database.DataClass; using static Il2CppAssets.Scripts.Database.DBConfigCustomTags; -namespace CustomAlbums.Patches +namespace CustomAlbums.Patches; + +internal class TagPatch { - internal class TagPatch + [HarmonyPatch(typeof(MusicTagManager), nameof(MusicTagManager.InitAlbumTagInfo))] + internal class MusicTagPatch { - [HarmonyPatch(typeof(MusicTagManager), nameof(MusicTagManager.InitAlbumTagInfo))] - internal class MusicTagPatch + private static void Postfix() { - private static void Postfix() + var info = new AlbumTagInfo + { + name = AlbumManager.Languages["English"], + tagUid = "tag-custom-albums", + iconName = "IconCustomAlbums" + }; + var customInfo = new CustomTagInfo { - var info = new AlbumTagInfo - { - name = AlbumManager.Languages["English"], - tagUid = "tag-custom-albums", - iconName = "IconCustomAlbums" - }; - var customInfo = new CustomTagInfo - { - tag_name = AlbumManager.Languages.ToIl2Cpp(), - tag_picture = "https://cdn.mdmc.moe/static/melon.png", - music_list = AlbumManager.GetAllUid().ToIl2Cpp() - }; + tag_name = AlbumManager.Languages.ToIl2Cpp(), + tag_picture = "https://cdn.mdmc.moe/static/melon.png", + music_list = AlbumManager.GetAllUid().ToIl2Cpp() + }; - info.InitCustomTagInfo(customInfo); + info.InitCustomTagInfo(customInfo); - GlobalDataBase.dbMusicTag.m_AlbumTagsSort.Insert(GlobalDataBase.dbMusicTag.m_AlbumTagsSort.Count - 4, - AlbumManager.Uid); - GlobalDataBase.dbMusicTag.AddAlbumTagData(AlbumManager.Uid, info); - } + GlobalDataBase.dbMusicTag.m_AlbumTagsSort.Insert(GlobalDataBase.dbMusicTag.m_AlbumTagsSort.Count - 4, + AlbumManager.Uid); + GlobalDataBase.dbMusicTag.AddAlbumTagData(AlbumManager.Uid, info); } } } \ No newline at end of file diff --git a/Patches/TreeItemPatch.cs b/Patches/TreeItemPatch.cs index 5699ea6..f4eba81 100644 --- a/Patches/TreeItemPatch.cs +++ b/Patches/TreeItemPatch.cs @@ -1,36 +1,35 @@ -using Il2Cpp; -using CustomAlbums.Utilities; +using CustomAlbums.Utilities; using HarmonyLib; +using Il2Cpp; -namespace CustomAlbums.Patches +namespace CustomAlbums.Patches; + +internal class TreeItemPatch { - internal class TreeItemPatch - { - // TODO: Finish "Album" support - private static readonly Logger Logger = new(nameof(TreeItemPatch)); + // TODO: Finish "Album" support + private static readonly Logger Logger = new(nameof(TreeItemPatch)); - [HarmonyPatch(typeof(PnlMusicTagItem), nameof(PnlMusicTagItem.OnTagClicked))] - internal class OnMusicTagClickedPatch + [HarmonyPatch(typeof(PnlMusicTagItem), nameof(PnlMusicTagItem.OnTagClicked))] + internal class OnMusicTagClickedPatch + { + private static void Prefix(int tagIndex, PnlMusicTagItem __instance) { - private static void Prefix(int tagIndex, PnlMusicTagItem __instance) - { - // STUB - } + // STUB } + } - [HarmonyPatch(typeof(PnlMusicTagItem), nameof(PnlMusicTagItem.Enable))] - internal class EnablePatch + [HarmonyPatch(typeof(PnlMusicTagItem), nameof(PnlMusicTagItem.Enable))] + internal class EnablePatch + { + private static void Prefix(PnlMusicTagItem __instance) { - private static void Prefix(PnlMusicTagItem __instance) - { - // STUB - } + // STUB } + } - [HarmonyPatch(typeof(PnlMusicTagItem), nameof(PnlMusicTagItem.AddDataToMgr))] - internal class AddDataPatch - { - // STUB - } + [HarmonyPatch(typeof(PnlMusicTagItem), nameof(PnlMusicTagItem.AddDataToMgr))] + internal class AddDataPatch + { + // STUB } -} +} \ No newline at end of file diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index c985376..66516dc 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -20,4 +20,4 @@ [assembly: AssemblyFileVersion($"{Main.MelonVersion}.0")] [assembly: MelonInfo(typeof(Main), Main.MelonName, Main.MelonVersion, Main.MelonAuthor)] [assembly: MelonGame("PeroPeroGames", "MuseDash")] -[assembly: MelonColor(255, 0, 255, 150)] +[assembly: MelonColor(255, 0, 255, 150)] \ No newline at end of file diff --git a/Resources/default_cover.png b/Resources/default_cover.png new file mode 100644 index 0000000..2043bd1 Binary files /dev/null and b/Resources/default_cover.png differ diff --git a/Utilities/Backup.cs b/Utilities/Backup.cs index d76e2ef..09fc970 100644 --- a/Utilities/Backup.cs +++ b/Utilities/Backup.cs @@ -8,142 +8,141 @@ using Il2CppAssets.Scripts.PeroTools.Nice.Interface; using Il2CppInterop.Runtime.InteropTypes.Arrays; -namespace CustomAlbums.Utilities +namespace CustomAlbums.Utilities; + +internal class Backup { - internal class Backup + private static readonly Logger Logger = new(nameof(Backup)); + private static string BackupPath => Path.Combine(Directory.GetCurrentDirectory(), "UserData/Backups"); + private static string BackupVanilla => Path.Combine(BackupPath, "Vanilla.sav.bak"); + private static string BackupVanillaDebug => Path.Combine(BackupPath, "VanillaDebug.json"); + private static string BackupCustom => Path.Combine(BackupPath, "CustomAlbums.json.bak"); + private static string BackupZip => Path.Combine(BackupPath, "Backups.zip"); + private static TimeSpan MaxBackupTime => TimeSpan.FromDays(30); + + internal static void InitBackups() { - private static readonly Logger Logger = new(nameof(Backup)); - private static string BackupPath => Path.Combine(Directory.GetCurrentDirectory(), "UserData/Backups"); - private static string BackupVanilla => Path.Combine(BackupPath, "Vanilla.sav.bak"); - private static string BackupVanillaDebug => Path.Combine(BackupPath, "VanillaDebug.json"); - private static string BackupCustom => Path.Combine(BackupPath, "CustomAlbums.json.bak"); - private static string BackupZip => Path.Combine(BackupPath, "Backups.zip"); - private static TimeSpan MaxBackupTime => TimeSpan.FromDays(30); - - internal static void InitBackups() - { - Directory.CreateDirectory(BackupPath); - - CompressBackups(); - CreateBackup(BackupVanilla, Singleton.instance.ToBytes()); - CreateBackup(BackupVanillaDebug, - JsonSerializer.Serialize(ToJsonDict(Singleton.instance.datas))); - CreateBackup(BackupCustom, SaveManager.SaveData); - ClearOldBackups(); - } + Directory.CreateDirectory(BackupPath); + + CompressBackups(); + CreateBackup(BackupVanilla, Singleton.instance.ToBytes()); + CreateBackup(BackupVanillaDebug, + JsonSerializer.Serialize(ToJsonDict(Singleton.instance.datas))); + CreateBackup(BackupCustom, SaveManager.SaveData); + ClearOldBackups(); + } - private static void CreateBackup(string filePath, object data) + private static void CreateBackup(string filePath, object data) + { + try { - try + if (data is null) { - if (data is null) - { - Logger.Warning("Could not create backup of null data!"); - return; - } + Logger.Warning("Could not create backup of null data!"); + return; + } - var wroteFile = false; + var wroteFile = false; - switch (data) - { - case string str: - File.WriteAllText(filePath, str); - wroteFile = true; - break; - - case byte[] bytes: - File.WriteAllBytes(filePath, bytes); - wroteFile = true; - break; - - case Il2CppStructArray ilBytes: - File.WriteAllBytes(filePath, ilBytes); - wroteFile = true; - break; - - case CustomAlbumsSave save: - File.WriteAllText(filePath, JsonSerializer.Serialize(save)); - wroteFile = true; - break; - - default: - Logger.Warning("Could not create backup for unsupported data type " + data.GetType().FullName); - break; - } - - if (wroteFile) Logger.Msg($"Saved backup: {filePath}"); - } - catch (Exception e) + switch (data) { - Logger.Error("Backup failed: " + e); + case string str: + File.WriteAllText(filePath, str); + wroteFile = true; + break; + + case byte[] bytes: + File.WriteAllBytes(filePath, bytes); + wroteFile = true; + break; + + case Il2CppStructArray ilBytes: + File.WriteAllBytes(filePath, ilBytes); + wroteFile = true; + break; + + case CustomAlbumsSave save: + File.WriteAllText(filePath, JsonSerializer.Serialize(save)); + wroteFile = true; + break; + + default: + Logger.Warning("Could not create backup for unsupported data type " + data.GetType().FullName); + break; } + + if (wroteFile) Logger.Msg($"Saved backup: {filePath}"); + } + catch (Exception e) + { + Logger.Error("Backup failed: " + e); } + } - private static void ClearOldBackups() + private static void ClearOldBackups() + { + try { - try + var backups = Directory.EnumerateFiles(BackupPath).ToList(); + foreach (var backup in from backup in backups + let backupDate = Directory.GetLastWriteTime(backup) + where (DateTime.Now - backupDate).Duration() > MaxBackupTime.Duration() + select backup) { - var backups = Directory.EnumerateFiles(BackupPath).ToList(); - foreach (var backup in from backup in backups - let backupDate = Directory.GetLastWriteTime(backup) - where (DateTime.Now - backupDate).Duration() > MaxBackupTime.Duration() - select backup) - { - Logger.Msg("Removing old backup: " + backup); - File.Delete(backup); - } + Logger.Msg("Removing old backup: " + backup); + File.Delete(backup); + } - if (!File.Exists(BackupZip)) return; + if (!File.Exists(BackupZip)) return; - using var zip = ZipFile.Open(BackupZip, ZipArchiveMode.Update); - foreach (var entry in zip.Entries.ToList().Where(entry => - (DateTime.Now - entry.LastWriteTime).Duration() > MaxBackupTime.Duration())) - { - Logger.Msg("Removing compressed old backup: " + entry.Name); - zip.GetEntry(entry.Name)?.Delete(); - } - } - catch (Exception e) + using var zip = ZipFile.Open(BackupZip, ZipArchiveMode.Update); + foreach (var entry in zip.Entries.ToList().Where(entry => + (DateTime.Now - entry.LastWriteTime).Duration() > MaxBackupTime.Duration())) { - Logger.Error("Clearing old backups failed: " + e); + Logger.Msg("Removing compressed old backup: " + entry.Name); + zip.GetEntry(entry.Name)?.Delete(); } } - - private static void CompressBackups() + catch (Exception e) { - try - { - using var zip = ZipFile.Open(BackupZip, ZipArchiveMode.Update); - - var filesList = Directory.EnumerateFiles(BackupPath).Where(name => Path.GetExtension(name) != ".zip") - .ToList(); - if (filesList.Any()) - filesList.ForEach(entry => - { - var newFileName = Directory.GetLastWriteTime(entry).ToString("yyyy_MM_dd_H_mm_ss-") + - Path.GetFileName(Path.GetFileName(entry)); - zip.CreateEntryFromFile(entry, newFileName); - File.Delete(entry); - }); - } - catch (Exception e) - { - Logger.Error("Compressing previous backups failed: " + e); - } + Logger.Error("Clearing old backups failed: " + e); } + } - private static Dictionary ToJsonDict( - Il2CppSystem.Collections.Generic.Dictionary dataList) + private static void CompressBackups() + { + try { - var dictionary = new Dictionary(); - foreach (var keyValuePair in dataList) - { - var singletonDataObject = keyValuePair.Value?.TryCast(); - if (singletonDataObject != null) - dictionary.Add(keyValuePair.Key, Json.Deserialize(singletonDataObject.ToJson())); - } + using var zip = ZipFile.Open(BackupZip, ZipArchiveMode.Update); - return dictionary; + var filesList = Directory.EnumerateFiles(BackupPath).Where(name => Path.GetExtension(name) != ".zip") + .ToList(); + if (filesList.Any()) + filesList.ForEach(entry => + { + var newFileName = Directory.GetLastWriteTime(entry).ToString("yyyy_MM_dd_H_mm_ss-") + + Path.GetFileName(Path.GetFileName(entry)); + zip.CreateEntryFromFile(entry, newFileName); + File.Delete(entry); + }); + } + catch (Exception e) + { + Logger.Error("Compressing previous backups failed: " + e); } } + + private static Dictionary ToJsonDict( + Il2CppSystem.Collections.Generic.Dictionary dataList) + { + var dictionary = new Dictionary(); + foreach (var keyValuePair in dataList) + { + var singletonDataObject = keyValuePair.Value?.TryCast(); + if (singletonDataObject != null) + dictionary.Add(keyValuePair.Key, Json.Deserialize(singletonDataObject.ToJson())); + } + + return dictionary; + } } \ No newline at end of file diff --git a/Utilities/ConfigDataExtensions.cs b/Utilities/ConfigDataExtensions.cs index c5a81e1..81cf054 100644 --- a/Utilities/ConfigDataExtensions.cs +++ b/Utilities/ConfigDataExtensions.cs @@ -1,55 +1,58 @@ -using System.Text.Json.Nodes; +using CustomAlbums.Data; using Il2CppGameLogic; using Il2CppPeroPeroGames.GlobalDefines; +using Decimal = Il2CppSystem.Decimal; -namespace CustomAlbums.Utilities +namespace CustomAlbums.Utilities; + +public static class ConfigDataExtensions { - public static class ConfigDataExtensions + public static bool IsAprilFools(this NoteConfigData config) + { + return config.prefab_name.EndsWith("_fool"); + } + + public static NoteType GetNoteType(this NoteConfigData config) + { + return (NoteType)config.type; + } + + public static bool IsAnyScene(this NoteConfigData config) + { + return config.scene == "0"; + } + + public static bool IsAnyPathway(this NoteConfigData config) + { + return config.pathway == 0 && config.score == 0 && config.fever == 0 && config.damage == 0; + } + + public static bool IsPhase2BossGear(this NoteConfigData config) + { + return config.GetNoteType() == NoteType.Block && config.boss_action.EndsWith("_atk_2"); + } + + public static bool IsAnySpeed(this NoteConfigData config) + { + return config.GetNoteType() == NoteType.Boss + || config.GetNoteType() == NoteType.None + || config.ibms_id == "16" + || config.ibms_id == "17"; + } + + /// + /// Converts a to a Il2Cpp struct. + /// + public static MusicConfigData ToMusicConfigData(this ProcessedNote note) { - public static bool IsAprilFools(this NoteConfigData config) - { - return config.prefab_name.EndsWith("_fool"); - } - - public static NoteType GetNoteType(this NoteConfigData config) - { - return (NoteType)config.type; - } - - public static bool IsAnyScene(this NoteConfigData config) - { - return config.scene == "0"; - } - - public static bool IsAnyPathway(this NoteConfigData config) - { - return config.pathway == 0 && config.score == 0 && config.fever == 0 && config.damage == 0; - } - - public static bool IsPhase2BossGear(this NoteConfigData config) - { - return config.GetNoteType() == NoteType.Block && config.boss_action.EndsWith("_atk_2"); - } - - public static bool IsAnySpeed(this NoteConfigData config) - { - return config.GetNoteType() == NoteType.Boss - || config.GetNoteType() == NoteType.None - || config.ibms_id == "16" - || config.ibms_id == "17"; - } - - public static MusicConfigData ToMusicConfigData(this JsonNode node) - { - var config = Interop.CreateTypeValue(); - config.id = node["id"]?.GetValue() ?? -1; - config.time = node["time"].GetValueAsIl2CppDecimal(); - config.note_uid = node["note_uid"]?.GetValue() ?? string.Empty; - config.length = node["length"].GetValueAsIl2CppDecimal(); - config.pathway = node["pathway"]?.GetValue() ?? 0; - config.blood = node["blood"]?.GetValue() ?? false; - - return config; - } + var config = Interop.CreateTypeValue(); + config.id = note.Id; + config.time = (Decimal)(float)note.Time; + config.note_uid = note.NoteUid; + config.length = (Decimal)(float)note.Length; + config.pathway = note.Pathway; + config.blood = note.Blood; + + return config; } } \ No newline at end of file diff --git a/Utilities/Converters.cs b/Utilities/Converters.cs index c952081..4cafba2 100644 --- a/Utilities/Converters.cs +++ b/Utilities/Converters.cs @@ -1,21 +1,20 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace CustomAlbums.Utilities +namespace CustomAlbums.Utilities; + +internal class Converters { - internal class Converters + public class NumberConverter : JsonConverter { - public class NumberConverter : JsonConverter + public override string Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options) { - public override string Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options) - { - return reader.TokenType is JsonTokenType.Number ? reader.GetInt32().ToString() : reader.GetString(); - } + return reader.TokenType is JsonTokenType.Number ? reader.GetInt32().ToString() : reader.GetString(); + } - public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) - { - writer.WriteStringValue(value); - } + public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) + { + writer.WriteStringValue(value); } } } \ No newline at end of file diff --git a/Utilities/DataExtensions.cs b/Utilities/DataExtensions.cs index d20e1bb..cf7e1ba 100644 --- a/Utilities/DataExtensions.cs +++ b/Utilities/DataExtensions.cs @@ -1,77 +1,74 @@ using CustomAlbums.Data; using Il2CppAssets.Scripts.PeroTools.Nice.Interface; -namespace CustomAlbums.Utilities +namespace CustomAlbums.Utilities; + +internal static class DataExtensions { - internal static class DataExtensions + /// + /// Gets the uid field of the IData object. + /// + /// The IData object. + /// The uid or an empty string if not found. + public static string GetUid(this IData data) { - /// - /// Gets the uid field of the IData object. - /// - /// The IData object. - /// The uid or an empty string if not found. - public static string GetUid(this IData data) - { - var uidField = data.fields["uid"]; - return uidField == null ? string.Empty : uidField.GetResult(); - } + var uidField = data.fields["uid"]; + return uidField == null ? string.Empty : uidField.GetResult(); + } - /// - /// Gets the index of a chart in an IData list by its uid and difficulty. - /// - /// The IData list. - /// The uid of the chart. - /// The difficulty of the chart. - /// The index of the chart in the list, or -1 if not found. - public static int GetIndexByUid(this Il2CppSystem.Collections.Generic.List dataList, string uid, int difficulty) + /// + /// Gets the index of a chart in an IData list by its uid and difficulty. + /// + /// The IData list. + /// The uid of the chart. + /// The difficulty of the chart. + /// The index of the chart in the list, or -1 if not found. + public static int GetIndexByUid(this Il2CppSystem.Collections.Generic.List dataList, string uid, + int difficulty) + { + var i = 0; + var fullUid = $"{uid}_{difficulty}"; + + // For loop doesn't work here + foreach (var data in dataList) { - var i = 0; + if (data.GetUid() == fullUid) return i; - // For loop doesn't work here - foreach (var data in dataList) - { - if (data.GetUid() == $"{uid}_{difficulty}") - { - return i; - } + i++; + } - i++; - } + return -1; + } - return -1; - } + /// + /// Gets the IData object from a list by its uid and difficulty. + /// + /// The IData list. + /// The uid of the chart. + /// The difficulty of the chart. + /// The IData object, or null if not found. + public static IData GetIDataByUid(this Il2CppSystem.Collections.Generic.List dataList, string uid, + int difficulty) + { + var fullUid = $"{uid}_{difficulty}"; - /// - /// Gets the IData object from a list by its uid and difficulty. - /// - /// The IData list. - /// The uid of the chart. - /// The difficulty of the chart. - /// The IData object, or null if not found. - public static IData GetIDataByUid(this Il2CppSystem.Collections.Generic.List dataList, string uid, int difficulty) - { - foreach (var data in dataList) - { - if (data.GetUid() == $"{uid}_{difficulty}") - { - return data; - } - } + foreach (var data in dataList) + if (data.GetUid() == fullUid) + return data; - return null; - } + return null; + } - public static ChartSave ToChartSave(this IData data) + public static ChartSave ToChartSave(this IData data) + { + return new ChartSave { - return new ChartSave - { - Evaluate = data.fields["evaluate"].GetResult(), - Score = data.fields["score"].GetResult(), - Combo = data.fields["combo"].GetResult(), - Accuracy = data.fields["accuracy"].GetResult(), - AccuracyStr = data.fields["accuracyStr"].GetResult(), - Clear = data.fields["clear"].GetResult() - }; - } + Evaluate = data.fields["evaluate"].GetResult(), + Score = data.fields["score"].GetResult(), + Combo = data.fields["combo"].GetResult(), + Accuracy = data.fields["accuracy"].GetResult(), + AccuracyStr = data.fields["accuracyStr"].GetResult(), + Clear = data.fields["clear"].GetResult() + }; } -} +} \ No newline at end of file diff --git a/Utilities/Debug.cs b/Utilities/Debug.cs index eaf39b7..9aec671 100644 --- a/Utilities/Debug.cs +++ b/Utilities/Debug.cs @@ -1,19 +1,18 @@ using HarmonyLib; -namespace CustomAlbums.Utilities +namespace CustomAlbums.Utilities; + +internal class Debug { - internal class Debug + [HarmonyPatch("Il2CppInterop.HarmonySupport.Il2CppDetourMethodPatcher", "ReportException")] + internal static class Il2CppDetourMethodPatcherPatch { - [HarmonyPatch("Il2CppInterop.HarmonySupport.Il2CppDetourMethodPatcher", "ReportException")] - internal static class Il2CppDetourMethodPatcherPatch - { - private static readonly Logger Logger = new(nameof(Il2CppDetourMethodPatcherPatch)); + private static readonly Logger Logger = new(nameof(Il2CppDetourMethodPatcherPatch)); - private static bool Prefix(Exception ex) - { - Logger.Msg("During invoking native->managed trampoline: " + ex); - return false; - } + private static bool Prefix(Exception ex) + { + Logger.Msg("During invoking native->managed trampoline: " + ex); + return false; } } } \ No newline at end of file diff --git a/Utilities/Formatting.cs b/Utilities/Formatting.cs index 0aa3c1b..f866330 100644 --- a/Utilities/Formatting.cs +++ b/Utilities/Formatting.cs @@ -2,31 +2,50 @@ namespace CustomAlbums.Utilities; -public static class Formatting { +public static class Formatting +{ public static int ParseAsInt(this string value) - => int.Parse(value, NumberStyles.Number, CultureInfo.InvariantCulture); + { + return int.Parse(value, NumberStyles.Number, CultureInfo.InvariantCulture); + } public static float ParseAsFloat(this string value) - => float.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture); + { + return float.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture); + } public static decimal ParseAsDecimal(this string value) - => decimal.Parse(value, NumberStyles.Number, CultureInfo.InvariantCulture); - + { + return decimal.Parse(value, NumberStyles.Number, CultureInfo.InvariantCulture); + } + public static bool TryParseAsInt(this string value, out int result) - => int.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out result); - + { + return int.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out result); + } + public static bool TryParseAsFloat(this string value, out float result) - => float.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out result); - + { + return float.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out result); + } + public static bool TryParseAsDecimal(this string value, out decimal result) - => decimal.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out result); - + { + return decimal.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out result); + } + public static string ToStringInvariant(this int value, string format = "") - => value.ToString(format, CultureInfo.InvariantCulture); - + { + return value.ToString(format, CultureInfo.InvariantCulture); + } + public static string ToStringInvariant(this float value, string format = "") - => value.ToString(format, CultureInfo.InvariantCulture); - + { + return value.ToString(format, CultureInfo.InvariantCulture); + } + public static string ToStringInvariant(this decimal value, string format = "") - => value.ToString(format, CultureInfo.InvariantCulture); + { + return value.ToString(format, CultureInfo.InvariantCulture); + } } \ No newline at end of file diff --git a/Utilities/Il2CppExtensions.cs b/Utilities/Il2CppExtensions.cs index 16593aa..f12469e 100644 --- a/Utilities/Il2CppExtensions.cs +++ b/Utilities/Il2CppExtensions.cs @@ -1,75 +1,99 @@ using System.Collections.ObjectModel; +using Il2CppInterop.Runtime.InteropTypes.Arrays; -namespace CustomAlbums.Utilities +namespace CustomAlbums.Utilities; + +public static class Il2CppExtensions { - public static class Il2CppExtensions + public static Il2CppSystem.Collections.Generic.List ToIl2Cpp(this List list) { - public static Il2CppSystem.Collections.Generic.List ToIl2Cpp(this List list) - { - var il2Cpp = new Il2CppSystem.Collections.Generic.List(list.Count); - foreach (var item in list) il2Cpp.Add(item); + var il2Cpp = new Il2CppSystem.Collections.Generic.List(list.Count); + foreach (var item in list) il2Cpp.Add(item); - return il2Cpp; - } + return il2Cpp; + } - public static Il2CppSystem.Collections.Generic.List ToIl2Cpp(this IEnumerable list) - { - var array = list.ToArray(); - var il2Cpp = new Il2CppSystem.Collections.Generic.List(array.Length); - foreach (var item in array) il2Cpp.Add(item); + public static Il2CppSystem.Collections.Generic.List ToIl2Cpp(this IEnumerable list) + { + var array = list.ToArray(); + var il2Cpp = new Il2CppSystem.Collections.Generic.List(array.Length); + foreach (var item in array) il2Cpp.Add(item); - return il2Cpp; - } + return il2Cpp; + } - public static Il2CppSystem.Collections.Generic.List ToIl2Cpp(this ReadOnlyCollection collection) - { - var il2Cpp = new Il2CppSystem.Collections.Generic.List(collection.Count); - foreach (var item in collection) il2Cpp.Add(item); + public static Il2CppSystem.Collections.Generic.List ToIl2Cpp(this ReadOnlyCollection collection) + { + var il2Cpp = new Il2CppSystem.Collections.Generic.List(collection.Count); + foreach (var item in collection) il2Cpp.Add(item); - return il2Cpp; - } + return il2Cpp; + } - public static Il2CppSystem.Collections.Generic.Dictionary ToIl2Cpp( - this Dictionary dictionary) - { - var il2Cpp = new Il2CppSystem.Collections.Generic.Dictionary(dictionary.Count); - foreach (var (key, value) in dictionary) il2Cpp.Add(key, value); + public static Il2CppSystem.Collections.Generic.Dictionary ToIl2Cpp( + this Dictionary dictionary) + { + var il2Cpp = new Il2CppSystem.Collections.Generic.Dictionary(dictionary.Count); + foreach (var (key, value) in dictionary) il2Cpp.Add(key, value); - return il2Cpp; - } + return il2Cpp; + } - public static List ToManaged(this Il2CppSystem.Collections.Generic.List list) - { - var managed = new List(list.Count); - foreach (var item in list) managed.Add(item); + public static List ToManaged(this Il2CppSystem.Collections.Generic.List list) + { + var managed = new List(list.Count); + foreach (var item in list) managed.Add(item); - return managed; - } + return managed; + } - public static Dictionary ToManaged( - this Il2CppSystem.Collections.Generic.Dictionary dictionary) - { - var managed = new Dictionary(dictionary.Count); - foreach (var entry in dictionary) managed.Add(entry.Key, entry.Value); + public static Dictionary ToManaged( + this Il2CppSystem.Collections.Generic.Dictionary dictionary) + { + var managed = new Dictionary(dictionary.Count); + foreach (var entry in dictionary) managed.Add(entry.Key, entry.Value); - return managed; - } + return managed; + } - public static void AddManagedRange(this Il2CppSystem.Collections.Generic.List il2cpp, - IEnumerable managed) - { - var array = managed.ToArray(); - il2cpp.Capacity += array.Length; - foreach (var item in array) il2cpp.Add(item); - } + public static void AddManagedRange(this Il2CppSystem.Collections.Generic.List il2cpp, + IEnumerable managed) + { + var array = managed.ToArray(); + il2cpp.Capacity += array.Length; + foreach (var item in array) il2cpp.Add(item); + } - public static bool TryGetValuePossibleNullKey( - this Il2CppSystem.Collections.Generic.Dictionary dict, TKey key, out TValue outValue) + public static bool TryGetValuePossibleNullKey( + this Il2CppSystem.Collections.Generic.Dictionary dict, TKey key, out TValue outValue) + { + outValue = default; + if (key == null || !dict.ContainsKey(key)) return false; + outValue = dict[key]; + return true; + } + + public static unsafe Il2CppStructArray CopyFromManaged(this T[] arr, long? size = null) where T : unmanaged + { + var len = arr.Length; + if (size is not null && size < 0) + throw new ArgumentOutOfRangeException(nameof(size), "The size to copy cannot be negative."); + + var lenCopy = size ?? len; + if (lenCopy > len) + throw new ArgumentOutOfRangeException(nameof(size), "The size to copy is larger than the array length."); + + var il2CppArray = new Il2CppStructArray(len); + + fixed (void* managedArrayBase = &arr[0]) { - outValue = default; - if (key == null || !dict.ContainsKey(key)) return false; - outValue = dict[key]; - return true; + Buffer.MemoryCopy( + managedArrayBase, + IntPtr.Add(il2CppArray.Pointer, 4 * IntPtr.Size).ToPointer(), + checked(il2CppArray.Length * sizeof(T)), + checked(lenCopy * sizeof(T))); } + + return il2CppArray; } } \ No newline at end of file diff --git a/Utilities/Interop.cs b/Utilities/Interop.cs index 806bf25..ec9bc70 100644 --- a/Utilities/Interop.cs +++ b/Utilities/Interop.cs @@ -1,19 +1,18 @@ using Il2CppInterop.Runtime; -namespace CustomAlbums.Utilities +namespace CustomAlbums.Utilities; + +public class Interop { - public class Interop + /// + /// A workaround to a memory corruption issue of creating Il2CppSystem.TypeValue types using C#'s new operator. + /// This will likely be deleted/become deprecated in the future. + /// + /// An Il2CppSystem.TypeValue type + /// A new object of type T + public static T CreateTypeValue() { - /// - /// A workaround to a memory corruption issue of creating Il2CppSystem.TypeValue types using C#'s new operator. - /// This will likely be deleted/become deprecated in the future. - /// - /// An Il2CppSystem.TypeValue type - /// A new object of type T - public static T CreateTypeValue() - { - return (T)Activator.CreateInstance(typeof(T), - IL2CPP.il2cpp_object_new(Il2CppClassPointerStore.NativeClassPtr)); - } + return (T)Activator.CreateInstance(typeof(T), + IL2CPP.il2cpp_object_new(Il2CppClassPointerStore.NativeClassPtr)); } } \ No newline at end of file diff --git a/Utilities/Json.cs b/Utilities/Json.cs index c8feddd..1435529 100644 --- a/Utilities/Json.cs +++ b/Utilities/Json.cs @@ -5,60 +5,59 @@ using Decimal = Il2CppSystem.Decimal; using JsonSerializer = System.Text.Json.JsonSerializer; -namespace CustomAlbums.Utilities +namespace CustomAlbums.Utilities; + +public static class Json { - public static class Json + private static readonly JsonSerializerOptions DeserializeOptions = new() { - private static readonly JsonSerializerOptions DeserializeOptions = new() - { - PropertyNameCaseInsensitive = true, - AllowTrailingCommas = true, + PropertyNameCaseInsensitive = true, + AllowTrailingCommas = true, - // God this sucks - ReadCommentHandling = JsonCommentHandling.Skip, + // God this sucks + ReadCommentHandling = JsonCommentHandling.Skip, - Converters = - { - new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) - } - }; - - public static T Deserialize(string json) + Converters = { - return JsonSerializer.Deserialize(json, DeserializeOptions); + new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) } + }; - public static T Deserialize(Stream stream) - { - return JsonSerializer.Deserialize(stream, DeserializeOptions); - } + public static T Deserialize(string json) + { + return JsonSerializer.Deserialize(json, DeserializeOptions); + } - public static JsonArray ToJsonArray(this IEnumerable list) - { - var array = new JsonArray(); - foreach (var item in list) array.Add(item); + public static T Deserialize(Stream stream) + { + return JsonSerializer.Deserialize(stream, DeserializeOptions); + } - return array; - } + public static JsonArray ToJsonArray(this IEnumerable list) + { + var array = new JsonArray(); + foreach (var item in list) array.Add(item); - public static T Il2CppJsonDeserialize(string text) - { - return JsonConvert.DeserializeObject(text); - } + return array; + } - /// - /// Fixes strange issue where getting a single as a decimal does not work. - /// - /// A - /// The parsed value - public static decimal GetValueAsDecimal(this JsonNode node) - { - return node.ToString().TryParseAsDecimal(out var result) ? result : 0M; - } + public static T Il2CppJsonDeserialize(string text) + { + return JsonConvert.DeserializeObject(text); + } - public static Decimal GetValueAsIl2CppDecimal(this JsonNode node) - { - return (Decimal)(float)node.GetValueAsDecimal(); - } + /// + /// Fixes strange issue where getting a single as a decimal does not work. + /// + /// A + /// The parsed value + public static decimal GetValueAsDecimal(this JsonNode node) + { + return node.ToString().TryParseAsDecimal(out var result) ? result : 0M; + } + + public static Decimal GetValueAsIl2CppDecimal(this JsonNode node) + { + return (Decimal)(float)node.GetValueAsDecimal(); } } \ No newline at end of file diff --git a/Utilities/Logger.cs b/Utilities/Logger.cs index 8df8450..7517168 100644 --- a/Utilities/Logger.cs +++ b/Utilities/Logger.cs @@ -1,41 +1,40 @@ using System.Drawing; using MelonLoader; -namespace CustomAlbums.Utilities +namespace CustomAlbums.Utilities; + +public class Logger { - public class Logger + private readonly MelonLogger.Instance _logger; + + public Logger(string className) + { + _logger = new MelonLogger.Instance(className, Color.FromArgb(255, 0, 255, 150)); + } + + public void Msg(string message, bool verbose = true) + { + if (verbose && !ModSettings.VerboseLogging) return; + _logger.Msg(message); + } + + public void Success(string message) + { + _logger.Msg(ConsoleColor.Green, "Success: " + message); + } + + public void Warning(string message) + { + _logger.Msg(ConsoleColor.Yellow, "Warning: " + message); + } + + public void Fail(string message) + { + _logger.Msg(ConsoleColor.Red, "FAILED: " + message); + } + + public void Error(string message) { - private readonly MelonLogger.Instance _logger; - - public Logger(string className) - { - _logger = new MelonLogger.Instance(className, Color.FromArgb(255, 0, 255, 150)); - } - - public void Msg(string message, bool verbose = true) - { - if (verbose && !ModSettings.VerboseLogging) return; - _logger.Msg(message); - } - - public void Success(string message) - { - _logger.Msg(ConsoleColor.Green, "Success: " + message); - } - - public void Warning(string message) - { - _logger.Msg(ConsoleColor.Yellow, "Warning: " + message); - } - - public void Fail(string message) - { - _logger.Msg(ConsoleColor.Red, "FAILED: " + message); - } - - public void Error(string message) - { - _logger.Msg(ConsoleColor.Red, "ERROR: " + message); - } + _logger.Msg(ConsoleColor.Red, "ERROR: " + message); } } \ No newline at end of file diff --git a/Utilities/PackExtensions.cs b/Utilities/PackExtensions.cs new file mode 100644 index 0000000..98768a5 --- /dev/null +++ b/Utilities/PackExtensions.cs @@ -0,0 +1,16 @@ +using System.IO.Compression; + +namespace CustomAlbums.Utilities; + +internal static class PackExtensions +{ + private static readonly Logger Logger = new(nameof(PackExtensions)); + + public static ZipArchive GetNestedZip(this ZipArchive mdp, string entryName) + { + var mdm = mdp.GetEntry(entryName) ?? throw new ArgumentException($"Entry {entryName} not found."); + var mdmStream = mdm.Open(); + var openedMdm = new ZipArchive(mdmStream, ZipArchiveMode.Read, false); + return openedMdm; + } +} \ No newline at end of file diff --git a/Utilities/SaveExtensions.cs b/Utilities/SaveExtensions.cs index c42ae04..2c5008a 100644 --- a/Utilities/SaveExtensions.cs +++ b/Utilities/SaveExtensions.cs @@ -1,38 +1,37 @@ using CustomAlbums.Data; using CustomAlbums.Managers; -namespace CustomAlbums.Utilities +namespace CustomAlbums.Utilities; + +public static class SaveExtensions { - public static class SaveExtensions + private static readonly Logger Logger = new(nameof(SaveExtensions)); + + /// + /// Gets the chart save data given the chart UID. + /// + /// The save file data class. + /// The chart UID. + /// A object consisting of score information from the current chart's UID. + public static SaveData GetChartSaveDataFromUid(this CustomAlbumsSave save, string uid) { - private static readonly Logger Logger = new(nameof(SaveExtensions)); + var album = AlbumManager.GetByUid(uid); - // This class is only needed when you're accessing GetChartSaveDataFromUid - // No need to make this part of the Data area since it's a fragment of other data classes - public class SaveData - { - public Dictionary Highest { get; set; } - public List FullCombo { get; set; } - } + if (album is null) return null; - /// - /// Gets the chart save data given the chart UID. - /// - /// The save file data class. - /// The chart UID. - /// A object consisting of score information from the current chart's UID. - public static SaveData GetChartSaveDataFromUid(this CustomAlbumsSave save, string uid) + var key = album!.AlbumName; + return new SaveData { - var album = AlbumManager.GetByUid(uid); - - if (album is null) return null; + Highest = save.Highest.GetValueOrDefault(key), + FullCombo = save.FullCombo.GetValueOrDefault(key) + }; + } - var key = album!.AlbumName; - return new SaveData - { - Highest = save.Highest.GetValueOrDefault(key), - FullCombo = save.FullCombo.GetValueOrDefault(key) - }; - } + // This class is only needed when you're accessing GetChartSaveDataFromUid + // No need to make this part of the Data area since it's a fragment of other data classes + public class SaveData + { + public Dictionary Highest { get; set; } + public List FullCombo { get; set; } } } \ No newline at end of file diff --git a/Utilities/StreamExtensions.cs b/Utilities/StreamExtensions.cs index 38f01d0..5220359 100644 --- a/Utilities/StreamExtensions.cs +++ b/Utilities/StreamExtensions.cs @@ -1,46 +1,47 @@ using System.Security.Cryptography; -namespace CustomAlbums.Utilities +namespace CustomAlbums.Utilities; + +public static class StreamExtensions { - public static class StreamExtensions + private static readonly Logger Logger = new(nameof(StreamExtensions)); + + public static string GetHash(this Stream stream) { - private static readonly Logger Logger = new(nameof(StreamExtensions)); - public static string GetHash(this Stream stream) - { - byte[] hash; - if (stream is MemoryStream ms) - hash = MD5.Create().ComputeHash(ms.ToArray()); - else - hash = MD5.Create().ComputeHash(stream.ToMemoryStream().ToArray()); - - return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); - } + byte[] hash; + if (stream is MemoryStream ms) + hash = MD5.Create().ComputeHash(ms.ToArray()); + else + hash = MD5.Create().ComputeHash(stream.ToMemoryStream().ToArray()); - public static byte[] ReadFully(this MemoryStream stream) - { - var buffer = new byte[1024 * 16]; - int read; + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } - using var ms = new MemoryStream(); - while ((read = stream.Read(buffer, 0, buffer.Length)) > 0) - ms.Write(buffer, 0, read); + public static byte[] ReadFully(this MemoryStream stream) + { + var buffer = new byte[1024 * 16]; + int read; - return ms.ToArray(); + using var ms = new MemoryStream(); + while ((read = stream.Read(buffer, 0, buffer.Length)) > 0) + ms.Write(buffer, 0, read); + + return ms.ToArray(); + } + + public static MemoryStream ToMemoryStream(this Stream stream) + { + var ms = new MemoryStream(); + try + { + stream.CopyTo(ms); + ms.Position = 0; } - public static MemoryStream ToMemoryStream(this Stream stream) + catch (Exception ex) { - var ms = new MemoryStream(); - try - { - stream.CopyTo(ms); - ms.Position = 0; - } - catch (Exception ex) - { - Logger.Warning(ex.Message); - } - - return ms; + Logger.Warning(ex.Message); } + + return ms; } } \ No newline at end of file diff --git a/Utilities/StringExtensions.cs b/Utilities/StringExtensions.cs index bcd4449..e79e6bd 100644 --- a/Utilities/StringExtensions.cs +++ b/Utilities/StringExtensions.cs @@ -1,14 +1,18 @@ -namespace CustomAlbums.Utilities +namespace CustomAlbums.Utilities; + +public static class StringExtensions { - public static class StringExtensions + /// + /// Compares two strings using the comparison type. + /// + /// First string to compare + /// + /// + /// if the value of the is the same as this string; otherwise, + /// . + /// + public static bool EqualsCaseInsensitive(this string str1, string str2) { - /// - /// Compares two strings using the comparison type. - /// - /// First string to compare - /// - /// if the value of the is the same as this string; otherwise, . - public static bool EqualsCaseInsensitive(this string str1, string str2) - => str1.Equals(str2, StringComparison.OrdinalIgnoreCase); + return str1.Equals(str2, StringComparison.OrdinalIgnoreCase); } -} +} \ No newline at end of file