using System; using System.Collections.Generic; using System.IO; using System.Linq; using ADOFAIRenderer.Core.Data; using ADOFAIRenderer.Data; using MelonLoader; using MelonLoader.Utils; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace ADOFAIRenderer.Core; public class RenderPreset { public string Name { get; set; } = string.Empty; public RenderConfig Config { get; set; } = null!; public RenderPreset Clone() { return new RenderPreset { Name = Name, Config = Config.Clone() }; } } public static class PresetManager { private static readonly List DefaultPresets = new() { new RenderPreset { Name = "FHD 60fps (1x)", Config = new RenderConfig { RenderWidth = 1920, RenderHeight = 1080, RenderFPS = 60, VideoFPS = 60, DelayAfterEnd = 15, ShowJudgementText = false, VideoConfig = new VideoEncodingConfig { Codec = Codec.H264, RateControlMode = RateControlMode.CBR, Bitrate = 20000, MaxBitrate = 35000, QP = 18, KeyframeInterval = 2.0 }, AudioConfig = new AudioEncodingConfig { RenderAudio = true, AudioSampleRate = 48000, AudioChannels = 2, AudioBitrate = 256000 }, AdvancedConfig = new AdvancedConfig { ProcessAudio = true, NonTile = false, FileExtension = "mkv" } } }, new RenderPreset { Name = "FHD 60fps (4x)", Config = new RenderConfig { RenderWidth = 1920, RenderHeight = 1080, RenderFPS = 240, VideoFPS = 60, DelayAfterEnd = 15, ShowJudgementText = false, VideoConfig = new VideoEncodingConfig { Codec = Codec.H264, RateControlMode = RateControlMode.CBR, Bitrate = 30000, MaxBitrate = 50000, QP = 18, KeyframeInterval = 2.0 }, AudioConfig = new AudioEncodingConfig { RenderAudio = true, AudioSampleRate = 48000, AudioChannels = 2, AudioBitrate = 256000 }, AdvancedConfig = new AdvancedConfig { ProcessAudio = true, NonTile = false, FileExtension = "mkv" } } }, new RenderPreset { Name = "QHD 60fps (4x)", Config = new RenderConfig { RenderWidth = 2560, RenderHeight = 1440, RenderFPS = 240, VideoFPS = 60, DelayAfterEnd = 15, ShowJudgementText = false, VideoConfig = new VideoEncodingConfig { Codec = Codec.H264, RateControlMode = RateControlMode.CBR, Bitrate = 35000, MaxBitrate = 60000, QP = 18, KeyframeInterval = 2.0 }, AudioConfig = new AudioEncodingConfig { RenderAudio = true, AudioSampleRate = 48000, AudioChannels = 2, AudioBitrate = 256000 }, AdvancedConfig = new AdvancedConfig { ProcessAudio = true, NonTile = false, FileExtension = "mkv" } } }, new RenderPreset { Name = "4K 60fps (4x)", Config = new RenderConfig { RenderWidth = 3840, RenderHeight = 2160, RenderFPS = 240, VideoFPS = 60, DelayAfterEnd = 15, ShowJudgementText = false, VideoConfig = new VideoEncodingConfig { Codec = Codec.H264, RateControlMode = RateControlMode.CBR, Bitrate = 60000, MaxBitrate = 100000, QP = 18, KeyframeInterval = 2.0 }, AudioConfig = new AudioEncodingConfig { RenderAudio = true, AudioSampleRate = 48000, AudioChannels = 2, AudioBitrate = 256000 }, AdvancedConfig = new AdvancedConfig { ProcessAudio = true, NonTile = false, FileExtension = "mkv" } } } }; private static string GetPresetsDirectory() { string? root = null; try { root = MelonEnvironment.GameRootDirectory; } catch { // Outside MelonLoader host environment (e.g. tests) } if (string.IsNullOrEmpty(root)) { root = AppDomain.CurrentDomain.BaseDirectory; } return Path.Combine(root, "UserData", "ADOFAIRenderer", "Presets"); } private static string GetSafeFileName(string name) { return Path.GetInvalidFileNameChars().Aggregate(name, (current, c) => current.Replace(c, '_')); } public static void EnsureDefaultPresetsCreated() { try { var dir = GetPresetsDirectory(); if (!Directory.Exists(dir)) { Directory.CreateDirectory(dir); } if (!GlobalSettings.Current.DefaultPresetsInitialized) { var settings = new JsonSerializerSettings { Formatting = Formatting.Indented }; settings.Converters.Add(new ColorJsonConverter()); foreach (var def in DefaultPresets) { var safeName = GetSafeFileName(def.Name); var filePath = Path.Combine(dir, safeName + ".json"); if (!File.Exists(filePath)) { var json = JsonConvert.SerializeObject(def, settings); File.WriteAllText(filePath, json); } } GlobalSettings.Current.DefaultPresetsInitialized = true; GlobalSettings.Save(); } } catch (Exception ex) { MelonLogger.Warning($"Error initializing default presets: {ex.Message}"); } } public static void SavePreset(string name, RenderConfig config) { if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentException("Preset name cannot be empty."); } string dir = GetPresetsDirectory(); if (!Directory.Exists(dir)) { Directory.CreateDirectory(dir); } var preset = new RenderPreset { Name = name, Config = config.Clone() }; var settings = new JsonSerializerSettings { Formatting = Formatting.Indented }; settings.Converters.Add(new ColorJsonConverter()); string json = JsonConvert.SerializeObject(preset, settings); string safeName = GetSafeFileName(name); string filePath = Path.Combine(dir, safeName + ".json"); File.WriteAllText(filePath, json); } public static List GetPresets() { EnsureDefaultPresetsCreated(); var presets = new List(); var dir = GetPresetsDirectory(); if (!Directory.Exists(dir)) { foreach (var b in DefaultPresets) { presets.Add(b.Clone()); } return presets; } try { var files = Directory.GetFiles(dir, "*.json"); var serializerSettings = new JsonSerializerSettings(); serializerSettings.Converters.Add(new ColorJsonConverter()); var serializer = JsonSerializer.Create(serializerSettings); foreach (var file in files) { try { var json = File.ReadAllText(file); var jObject = JsonConvert.DeserializeObject(json); if (jObject == null) continue; var configToken = jObject["Config"]; if (configToken is JObject configObj) { TryChangeKey(configObj, "DelayAfterLevelEnd", "DelayAfterEnd"); // 1. legacy EncodingConfig -> VideoConfig if (configObj["EncodingConfig"] is JObject oldEncodingConfigObj) { configObj["VideoConfig"] ??= oldEncodingConfigObj; configObj.Remove("EncodingConfig"); } // 2. legacy top-level Codec -> VideoConfig.Codec var legacyCodecToken = configObj["Codec"]; if (legacyCodecToken != null && legacyCodecToken.Type != JTokenType.Null) { if (configObj["VideoConfig"] == null || configObj["VideoConfig"]!.Type == JTokenType.Null) { configObj["VideoConfig"] = new JObject(); } var videoConfigObj = (JObject)configObj["VideoConfig"]!; videoConfigObj["Codec"] ??= legacyCodecToken; configObj.Remove("Codec"); } // 3. legacy top-level Audio properties -> AudioConfig string[] legacyAudioProps = { "RenderAudio", "AudioSampleRate", "AudioChannels", "AudioBitrate" }; foreach (var prop in legacyAudioProps) { var legacyToken = configObj[prop]; if (legacyToken != null && legacyToken.Type != JTokenType.Null) { if (configObj["AudioConfig"] == null || configObj["AudioConfig"]!.Type == JTokenType.Null) { configObj["AudioConfig"] = new JObject(); } var audioConfigObj = (JObject)configObj["AudioConfig"]!; audioConfigObj[prop] ??= legacyToken; } configObj.Remove(prop); } if (configObj["AudioConfig"] != null && configObj["AudioConfig"]!.Type == JTokenType.Null) { configObj.Remove("AudioConfig"); } // 4. legacy top-level Advanced properties -> AdvancedConfig string[] legacyAdvancedProps = { "RandomSeed", "ProcessAudio", "NonTile" }; foreach (var prop in legacyAdvancedProps) { var legacyToken = configObj[prop]; if (legacyToken != null && legacyToken.Type != JTokenType.Null) { if (configObj["AdvancedConfig"] == null || configObj["AdvancedConfig"]!.Type == JTokenType.Null) { configObj["AdvancedConfig"] = new JObject(); } var advancedConfigObj = (JObject)configObj["AdvancedConfig"]!; advancedConfigObj[prop] ??= legacyToken; } configObj.Remove(prop); } if (configObj["AdvancedConfig"] != null && configObj["AdvancedConfig"]!.Type == JTokenType.Null) { configObj.Remove("AdvancedConfig"); } } var preset = jObject.ToObject(serializer); if (preset != null) { var fileBaseName = Path.GetFileNameWithoutExtension(file); if (string.IsNullOrWhiteSpace(preset.Name)) { preset.Name = fileBaseName; } if (presets.Any(p => string.Equals(p.Name, preset.Name, StringComparison.OrdinalIgnoreCase))) { continue; } presets.Add(preset); } } catch (Exception ex) { MelonLogger.Warning($"Error loading preset file '{Path.GetFileName(file)}': {ex.Message}"); } } } catch (Exception ex) { MelonLogger.Error($"Error retrieving preset list: {ex.Message}"); } return presets; } private static void TryChangeKey(JObject configObj, string keyBefore, string targetKey) { var legacyToken = configObj[keyBefore]; if (legacyToken != null && legacyToken.Type != JTokenType.Null) configObj[targetKey] = legacyToken; configObj.Remove(keyBefore); } public static void DeletePreset(string name) { if (string.IsNullOrWhiteSpace(name)) return; var dir = GetPresetsDirectory(); if (!Directory.Exists(dir)) return; var safeName = GetSafeFileName(name); var filePath = Path.Combine(dir, safeName + ".json"); if (File.Exists(filePath)) { File.Delete(filePath); return; } foreach (var file in Directory.GetFiles(dir, "*.json")) { try { var fileBaseName = Path.GetFileNameWithoutExtension(file); if (string.Equals(fileBaseName, name, StringComparison.OrdinalIgnoreCase)) { File.Delete(file); return; } var json = File.ReadAllText(file); var jObject = JsonConvert.DeserializeObject(json); var pName = (string?)jObject?["Name"]; if (string.Equals(pName, name, StringComparison.OrdinalIgnoreCase)) { File.Delete(file); return; } } catch { // Ignore read errors during cleanup search } } } }