Add Preset System

This commit is contained in:
copecone committed 2026-10-10 01:51:38 +09:00
1 parent 34d766cb4d
commit d218bb835d
7 files changed
+931 -151

No files matched your search

+14 -6
View File
@@ -6,20 +6,28 @@ using Newtonsoft.Json;
namespace ADOFAIRenderer.Core.Data;
public class GlobalConfig
internal class GlobalConfig
{
public string? OutputDirectory { get; set; }
public string? LastPresetName { get; set; }
public bool DefaultPresetsInitialized { get; set; }
}
public static class GlobalSettings
internal static class GlobalSettings
{
private static string SettingsFilePath =>
Path.Combine(MelonEnvironment.GameRootDirectory, "UserData", "ADOFAIRenderer", "global_settings.json");
private static string GetGameRootDir()
{
var root = MelonEnvironment.GameRootDirectory;
return string.IsNullOrEmpty(root) ? AppDomain.CurrentDomain.BaseDirectory : root;
}
public static GlobalConfig Current { get; set; } = new();
private static string SettingsFilePath =>
Path.Combine(GetGameRootDir(), "UserData", "ADOFAIRenderer", "global_settings.json");
public static GlobalConfig Current { get; private set; } = new();
public static string DefaultOutputDirectory =>
Path.Combine(MelonEnvironment.GameRootDirectory, "Render");
Path.Combine(GetGameRootDir(), "Render");
public static string EffectiveOutputDirectory
{
+203 -10
View File
@@ -2,24 +2,118 @@ 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.Data;
namespace ADOFAIRenderer.Core;
internal class RenderPreset
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()
};
}
}
internal static class PresetManager
public static class PresetManager
{
private static readonly List<RenderPreset> 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()
{
return Path.Combine(MelonEnvironment.GameRootDirectory, "UserData", "ADOFAIRenderer", "Presets");
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)
@@ -27,6 +121,45 @@ internal static class PresetManager
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))
@@ -43,10 +176,16 @@ internal static class PresetManager
var preset = new RenderPreset
{
Name = name,
Config = config
Config = config.Clone()
};
string json = JsonConvert.SerializeObject(preset, Formatting.Indented);
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);
@@ -54,14 +193,26 @@ internal static class PresetManager
public static List<RenderPreset> GetPresets()
{
EnsureDefaultPresetsCreated();
var presets = new List<RenderPreset>();
var dir = GetPresetsDirectory();
if (!Directory.Exists(dir)) return presets;
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
@@ -146,9 +297,20 @@ internal static class PresetManager
}
}
var preset = jObject.ToObject<RenderPreset>();
if (preset != null && !string.IsNullOrEmpty(preset.Name))
var preset = jObject.ToObject<RenderPreset>(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);
}
}
@@ -177,13 +339,44 @@ internal static class PresetManager
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<JObject>(json);
var pName = (string?)jObject?["Name"];
if (string.Equals(pName, name, StringComparison.OrdinalIgnoreCase))
{
File.Delete(file);
return;
}
}
catch
{
// Ignore read errors during cleanup search
}
}
}
}
+109
View File
@@ -0,0 +1,109 @@
using System;
using System.Globalization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEngine;
namespace ADOFAIRenderer.Data;
/// <summary>
/// Converts Unity <see cref="Color"/> and nullable <see cref="Color"/> to and from hex string (#RRGGBBAA).
/// Pure C# implementation to completely avoid Unity internal native ECalls (such as Color.linear or ColorUtility.TryParseHtmlString).
/// </summary>
public class ColorJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(Color) || objectType == typeof(Color?);
}
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value == null)
{
writer.WriteNull();
return;
}
var color = (Color)value;
writer.WriteValue(ToHex(color));
}
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
bool isNullable = Nullable.GetUnderlyingType(objectType) != null || !objectType.IsValueType;
if (reader.TokenType == JsonToken.Null)
{
return isNullable ? null : (object)default(Color);
}
if (reader.TokenType == JsonToken.String)
{
var str = (string?)reader.Value;
if (TryParseHex(str, out var parsedColor))
{
return parsedColor;
}
return isNullable ? null : (object)default(Color);
}
if (reader.TokenType == JsonToken.StartObject)
{
var obj = JObject.Load(reader);
float r = (float?)obj["r"] ?? 0f;
float g = (float?)obj["g"] ?? 0f;
float b = (float?)obj["b"] ?? 0f;
float a = (float?)obj["a"] ?? 1f;
return new Color(r, g, b, a);
}
return isNullable ? null : (object)default(Color);
}
public static string ToHex(Color color)
{
byte r = (byte)Math.Clamp((int)Math.Round(color.r * 255f), 0, 255);
byte g = (byte)Math.Clamp((int)Math.Round(color.g * 255f), 0, 255);
byte b = (byte)Math.Clamp((int)Math.Round(color.b * 255f), 0, 255);
byte a = (byte)Math.Clamp((int)Math.Round(color.a * 255f), 0, 255);
return $"#{r:X2}{g:X2}{b:X2}{a:X2}";
}
public static bool TryParseHex(string? hex, out Color color)
{
color = default;
if (string.IsNullOrWhiteSpace(hex)) return false;
var str = hex.Trim();
if (str.StartsWith("#"))
{
str = str.Substring(1);
}
if (str.Length == 6)
{
if (byte.TryParse(str.Substring(0, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var r) &&
byte.TryParse(str.Substring(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var g) &&
byte.TryParse(str.Substring(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var b))
{
color = new Color(r / 255f, g / 255f, b / 255f, 1f);
return true;
}
}
else if (str.Length == 8)
{
if (byte.TryParse(str.Substring(0, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var r) &&
byte.TryParse(str.Substring(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var g) &&
byte.TryParse(str.Substring(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var b) &&
byte.TryParse(str.Substring(6, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var a))
{
color = new Color(r / 255f, g / 255f, b / 255f, a / 255f);
return true;
}
}
return false;
}
}
+156 -74
View File
@@ -1,74 +1,156 @@
using System.Diagnostics.CodeAnalysis;
using UnityEngine;
namespace ADOFAIRenderer.Data;
[SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")]
public class RenderConfig
{
public double RenderFPS { get; set; }
public double VideoFPS { get; set; }
public int RenderWidth { get; set; }
public int RenderHeight { get; set; }
public VideoEncodingConfig VideoConfig { get; set; } = new();
public AudioEncodingConfig AudioConfig { get; set; } = new();
public AdvancedConfig AdvancedConfig { get; set; } = new();
public double DelayAfterEnd { get; set; } = 15;
public bool ShowJudgementText { get; set; } = false;
}
[SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")]
public class AdvancedConfig
{
public int? RandomSeed { get; set; } = null;
public bool ProcessAudio { get; set; } = true;
public bool NonTile { get; set; } = false;
public string FileExtension { get; set; } = "mkv";
public Color? firePlanetColor = null;
public Color? icePlanetColor = null;
public Color? windPlanetColor = null;
}
public class VideoEncodingConfig
{
public Codec Codec { get; set; } = Codec.H264;
public RateControlMode RateControlMode { get; set; } = RateControlMode.CBR;
public int Bitrate { get; set; } = 30000;
public int MaxBitrate { get; set; } = 50000;
// ReSharper disable once InconsistentNaming
public int QP { get; set; } = 18;
public double KeyframeInterval { get; set; } = 2.0;
public bool ForceSoftwareEncoder { get; set; }
}
public class AudioEncodingConfig
{
private int _audioSampleRate = 48000;
private int _audioChannels = 2;
private int _audioBitrate = 256000;
public bool RenderAudio { get; set; } = true;
public int AudioSampleRate
{
get => _audioSampleRate <= 0 ? 48000 : _audioSampleRate;
set => _audioSampleRate = value <= 0 ? 48000 : value;
}
public int AudioChannels
{
get => _audioChannels <= 0 ? 2 : _audioChannels;
set => _audioChannels = value <= 0 ? 2 : value;
}
public int AudioBitrate
{
get => _audioBitrate <= 0 ? 256000 : _audioBitrate;
set => _audioBitrate = value <= 0 ? 256000 : value;
}
}
using System.Diagnostics.CodeAnalysis;
using Newtonsoft.Json;
using UnityEngine;
namespace ADOFAIRenderer.Data;
[SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")]
public class RenderConfig
{
public double RenderFPS { get; set; }
public double VideoFPS { get; set; }
public int RenderWidth { get; set; }
public int RenderHeight { get; set; }
public VideoEncodingConfig VideoConfig { get; set; } = new();
public AudioEncodingConfig AudioConfig { get; set; } = new();
public AdvancedConfig AdvancedConfig { get; set; } = new();
public double DelayAfterEnd { get; set; } = 15;
public bool ShowJudgementText { get; set; } = false;
public void CopyFrom(RenderConfig? source)
{
if (source == null) return;
RenderFPS = source.RenderFPS;
VideoFPS = source.VideoFPS;
RenderWidth = source.RenderWidth;
RenderHeight = source.RenderHeight;
DelayAfterEnd = source.DelayAfterEnd;
ShowJudgementText = source.ShowJudgementText;
VideoConfig.CopyFrom(source.VideoConfig);
AudioConfig.CopyFrom(source.AudioConfig);
AdvancedConfig.CopyFrom(source.AdvancedConfig);
}
public RenderConfig Clone()
{
var clone = new RenderConfig();
clone.CopyFrom(this);
return clone;
}
}
[SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")]
public class AdvancedConfig
{
public int? RandomSeed { get; set; } = null;
public bool ProcessAudio { get; set; } = true;
public bool NonTile { get; set; } = false;
public string FileExtension { get; set; } = "mkv";
[JsonConverter(typeof(ColorJsonConverter))]
public Color? firePlanetColor = null;
[JsonConverter(typeof(ColorJsonConverter))]
public Color? icePlanetColor = null;
[JsonConverter(typeof(ColorJsonConverter))]
public Color? windPlanetColor = null;
public void CopyFrom(AdvancedConfig? source)
{
if (source == null) return;
RandomSeed = source.RandomSeed;
ProcessAudio = source.ProcessAudio;
NonTile = source.NonTile;
FileExtension = source.FileExtension;
firePlanetColor = source.firePlanetColor;
icePlanetColor = source.icePlanetColor;
windPlanetColor = source.windPlanetColor;
}
public AdvancedConfig Clone()
{
var clone = new AdvancedConfig();
clone.CopyFrom(this);
return clone;
}
}
public class VideoEncodingConfig
{
public Codec Codec { get; set; } = Codec.H264;
public RateControlMode RateControlMode { get; set; } = RateControlMode.CBR;
public int Bitrate { get; set; } = 30000;
public int MaxBitrate { get; set; } = 50000;
// ReSharper disable once InconsistentNaming
public int QP { get; set; } = 18;
public double KeyframeInterval { get; set; } = 2.0;
public bool ForceSoftwareEncoder { get; set; }
public void CopyFrom(VideoEncodingConfig? source)
{
if (source == null) return;
Codec = source.Codec;
RateControlMode = source.RateControlMode;
Bitrate = source.Bitrate;
MaxBitrate = source.MaxBitrate;
QP = source.QP;
KeyframeInterval = source.KeyframeInterval;
ForceSoftwareEncoder = source.ForceSoftwareEncoder;
}
public VideoEncodingConfig Clone()
{
var clone = new VideoEncodingConfig();
clone.CopyFrom(this);
return clone;
}
}
public class AudioEncodingConfig
{
private int _audioSampleRate = 48000;
private int _audioChannels = 2;
private int _audioBitrate = 256000;
public bool RenderAudio { get; set; } = true;
public int AudioSampleRate
{
get => _audioSampleRate <= 0 ? 48000 : _audioSampleRate;
set => _audioSampleRate = value <= 0 ? 48000 : value;
}
public int AudioChannels
{
get => _audioChannels <= 0 ? 2 : _audioChannels;
set => _audioChannels = value <= 0 ? 2 : value;
}
public int AudioBitrate
{
get => _audioBitrate <= 0 ? 256000 : _audioBitrate;
set => _audioBitrate = value <= 0 ? 256000 : value;
}
public void CopyFrom(AudioEncodingConfig? source)
{
if (source == null) return;
RenderAudio = source.RenderAudio;
AudioSampleRate = source.AudioSampleRate;
AudioChannels = source.AudioChannels;
AudioBitrate = source.AudioBitrate;
}
public AudioEncodingConfig Clone()
{
var clone = new AudioEncodingConfig();
clone.CopyFrom(this);
return clone;
}
}␍
+13
View File
@@ -1,5 +1,18 @@
{
"ui.render_config.window_title": "Render Config",
"ui.render_config.presets": "Presets",
"ui.render_config.preset_select": "Preset",
"ui.render_config.preset_name_placeholder": "New preset name...",
"ui.render_config.preset_save": "Save",
"ui.render_config.preset_delete": "Delete",
"ui.render_config.preset_overwrite_title": "Overwrite Preset",
"ui.render_config.preset_overwrite_desc": "A preset named '{0}' already exists. Do you want to overwrite it?",
"ui.render_config.preset_delete_title": "Delete Preset",
"ui.render_config.preset_delete_desc": "Are you sure you want to delete preset '{0}'?",
"ui.render_config.preset_name_empty": "Please enter a preset name.",
"ui.render_config.preset_custom": "(Modified)",
"ui.render_config.confirm": "Confirm",
"ui.render_config.cancel": "Cancel",
"ui.render_config.render_fps": "Render FPS",
"ui.render_config.video_fps": "Video FPS",
"ui.render_config.render_width": "Render Width",
+17 -4
View File
@@ -1,15 +1,28 @@
{
"ui.render_config.window_title": "렌더 설정",
"ui.render_config.presets": "프리셋",
"ui.render_config.preset_select": "프리셋 선택",
"ui.render_config.preset_name_placeholder": "새 프리셋 이름...",
"ui.render_config.preset_save": "저장",
"ui.render_config.preset_delete": "삭제",
"ui.render_config.preset_overwrite_title": "프리셋 덮어쓰기",
"ui.render_config.preset_overwrite_desc": "'{0}' 프리셋이 이미 존재합니다. 덮어쓰시겠습니까?",
"ui.render_config.preset_delete_title": "프리셋 삭제",
"ui.render_config.preset_delete_desc": "'{0}' 프리셋을 정말 삭제하시겠습니까?",
"ui.render_config.preset_name_empty": "프리셋 이름을 입력해 주세요.",
"ui.render_config.preset_custom": "(수정됨)",
"ui.render_config.confirm": "확인",
"ui.render_config.cancel": "취소",
"ui.render_config.render_fps": "렌더링 FPS",
"ui.render_config.video_fps": "영상 FPS",
"ui.render_config.render_width": "렌더링 너비",
"ui.render_config.render_height": "렌더링 높이",
"ui.render_config.delay_after_end": "종료 후 대기 시간 (초)",
"ui.render_config.show_judgement": "판정 텍스트 표시",
"ui.render_config.video_encoding": "Video Encoding Config",
"ui.render_config.audio_encoding": "Audio Encoding Config",
"ui.render_config.global_settings": "Global Settings",
"ui.render_config.advanced_settings": "Advanced Settings",
"ui.render_config.video_encoding": "비디오 인코딩 설정",
"ui.render_config.audio_encoding": "오디오 인코딩 설정",
"ui.render_config.global_settings": "전역 설정",
"ui.render_config.advanced_settings": "고급 설정",
"ui.render_config.go_render": "렌더 시작",
"ui.render_config.codec": "코덱",
"ui.render_config.rate_control": "비트레이트 제어 모드",
+419 -57
View File
@@ -21,6 +21,13 @@ internal static class RenderConfigWindow
{
private static O5Window? _configWindow;
// Presets controls
private static O5Dropdown<string>? _presetDropdown;
private static O5InputField? _presetNameInput;
private static O5Button? _savePresetButton;
private static O5Button? _deletePresetButton;
private static string? _lastLoadedPresetName;
// Controls cache for validation & sync
private static O5Slider? _renderFpsSlider;
private static O5Slider? _videoFPSSlider;
@@ -89,6 +96,10 @@ internal static class RenderConfigWindow
BuildUI(_configWindow.Content);
}
else
{
ReloadPresetDropdown(GlobalSettings.Current.LastPresetName);
}
// ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract
var config = RenderManager.Instance.Configuration ??= new RenderConfig();
@@ -133,7 +144,117 @@ internal static class RenderConfigWindow
864, 960, 1536, 2048, 3072 // 4:3
};
// TODO: Presets
// Presets (Collapsible Card)
var (_, presetContent) = O5Factory.Card(
ctx, scrollContent,
LocalizationManager.Get("ui.render_config.presets"),
activeValue: true,
onActiveChanged: null,
onDeleteClick: null,
showDeleteButton: false,
showActiveToggle: false
);
var presets = PresetManager.GetPresets();
var presetNames = presets.Select(p => p.Name).ToList();
var initialPresetName = GlobalSettings.Current.LastPresetName;
if (string.IsNullOrEmpty(initialPresetName) || !presetNames.Contains(initialPresetName))
{
initialPresetName = presetNames.FirstOrDefault(n => n.StartsWith("QHD", StringComparison.OrdinalIgnoreCase))
?? (presetNames.Count > 0 ? presetNames[0] : "");
}
_lastLoadedPresetName = initialPresetName;
if (config.RenderFPS <= 0)
{
var initialPreset = presets.FirstOrDefault(p => string.Equals(p.Name, initialPresetName, StringComparison.OrdinalIgnoreCase)) ?? presets.FirstOrDefault();
if (initialPreset != null)
{
config.CopyFrom(initialPreset.Config);
}
}
EnsureDefaultConfig(config);
_presetDropdown = CreateDropdownField(
ctx, presetContent,
LocalizationManager.Get("ui.render_config.preset_select"),
initialPresetName, initialPresetName,
presetNames, p => p,
"dd_presets",
selectedName =>
{
OnPresetSelected(selectedName, config);
}
);
var actionsRowObj = new GameObject("PresetActionsRow");
actionsRowObj.transform.SetParent(presetContent, false);
var actionsRowRect = actionsRowObj.AddComponent<RectTransform>();
var actionsHLayout = actionsRowObj.AddComponent<HorizontalLayoutGroup>();
actionsHLayout.spacing = 8f;
actionsHLayout.childAlignment = TextAnchor.MiddleCenter;
actionsHLayout.childControlWidth = true;
actionsHLayout.childControlHeight = true;
actionsHLayout.childForceExpandWidth = false;
actionsHLayout.childForceExpandHeight = false;
// ReSharper disable once InconsistentNaming
var actionsRowLE = actionsRowObj.AddComponent<LayoutElement>();
actionsRowLE.preferredHeight = 40f;
actionsRowLE.flexibleWidth = 1f;
_presetNameInput = O5Factory.Input(
ctx: ctx,
parent: actionsRowRect,
defaultValue: "",
value: initialPresetName,
onChanged: null,
placeholder: LocalizationManager.Get("ui.render_config.preset_name_placeholder"),
icon: null,
id: "input_preset_name"
);
var inputLE = _presetNameInput.Rect.GetComponent<LayoutElement>() ?? _presetNameInput.Rect.gameObject.AddComponent<LayoutElement>();
inputLE.flexibleWidth = 1f;
inputLE.preferredHeight = 40f;
_savePresetButton = O5Factory.Button(
ctx: ctx,
parent: actionsRowRect,
onClick: () => OnSavePresetClicked(config),
text: LocalizationManager.Get("ui.render_config.preset_save"),
id: "btn_preset_save",
height: 40f
);
var saveLE = _savePresetButton.Rect.GetComponent<LayoutElement>() ?? _savePresetButton.Rect.gameObject.AddComponent<LayoutElement>();
saveLE.preferredWidth = 90f;
saveLE.flexibleWidth = 0f;
saveLE.preferredHeight = 40f;
var redTheme = ctx.Theme with
{
ObjectButton = ctx.Theme.SoftRed,
ButtonHover = ctx.Theme.SoftRed * 0.8f
};
var redCtx = new O5Context(
config: ctx.Config,
theme: redTheme,
fonts: ctx.Fonts,
tween: ctx.Tween
);
_deletePresetButton = O5Factory.Button(
ctx: redCtx,
parent: actionsRowRect,
onClick: () => OnDeletePresetClicked(config),
text: LocalizationManager.Get("ui.render_config.preset_delete"),
id: "btn_preset_delete",
height: 40f
);
var deleteLE = _deletePresetButton.Rect.GetComponent<LayoutElement>() ?? _deletePresetButton.Rect.gameObject.AddComponent<LayoutElement>();
deleteLE.preferredWidth = 90f;
deleteLE.flexibleWidth = 0f;
deleteLE.preferredHeight = 40f;
UpdateDeleteButtonState();
// 1-3. Top section: 좌/우 컨테이너 1개씩으로 3행을 쌓는다 (중앙선 1개)
var (topLeft, topRight) = CreateTwoColumnPanel(scrollContent, spacing: 8f, columnSpacing: 10f);
@@ -144,7 +265,7 @@ internal static class RenderConfigWindow
format: "F3", clampMode: ClampMode.All,
sliderFilter: value => Snap(renderFpsSnap, value),
inputFilter: null, onComplete: null, onChanged: val => { config.RenderFPS = val; },
inputFilter: null, onComplete: null, onChanged: val => { config.RenderFPS = val; MarkConfigModified(); },
text: LocalizationManager.Get("ui.render_config.render_fps"),
id: "slider_render_fps", showFill: true
);
@@ -155,7 +276,7 @@ internal static class RenderConfigWindow
format: "F3", clampMode: ClampMode.All,
sliderFilter: value => Snap(videoFpsSnap, value),
inputFilter: null, onComplete: null, onChanged: val => { config.VideoFPS = val; },
inputFilter: null, onComplete: null, onChanged: val => { config.VideoFPS = val; MarkConfigModified(); },
text: LocalizationManager.Get("ui.render_config.video_fps"),
id: "slider_video_fps", showFill: true
);
@@ -166,7 +287,7 @@ internal static class RenderConfigWindow
format: "F0", clampMode: ClampMode.All,
sliderFilter: value => Snap(renderWidthSnap, value), inputFilter: value => Math.Max(value, 2),
onComplete: null, onChanged: val => { config.RenderWidth = (int)Math.Round(val); },
onComplete: null, onChanged: val => { config.RenderWidth = (int)Math.Round(val); MarkConfigModified(); },
text: LocalizationManager.Get("ui.render_config.render_width"),
id: "slider_render_width", showFill: true
);
@@ -177,7 +298,7 @@ internal static class RenderConfigWindow
format: "F0", clampMode: ClampMode.All,
sliderFilter: value => Snap(renderHeightSnap, value), inputFilter: value => Math.Max(value, 2),
onComplete: null, onChanged: val => { config.RenderHeight = (int)Math.Round(val); },
onComplete: null, onChanged: val => { config.RenderHeight = (int)Math.Round(val); MarkConfigModified(); },
text: LocalizationManager.Get("ui.render_config.render_height"),
id: "slider_render_height", showFill: true
);
@@ -188,7 +309,7 @@ internal static class RenderConfigWindow
format: "F3", clampMode: ClampMode.All,
sliderFilter: null, inputFilter: null,
onComplete: null, onChanged: val => { config.DelayAfterEnd = val; },
onComplete: null, onChanged: val => { config.DelayAfterEnd = val; MarkConfigModified(); },
text: LocalizationManager.Get("ui.render_config.delay_after_end"),
id: "slider_delay_after_end", showFill: true
);
@@ -198,7 +319,7 @@ internal static class RenderConfigWindow
false, config.ShowJudgementText,
LocalizationManager.Get("ui.render_config.show_judgement"),
"toggle_show_judgement",
val => config.ShowJudgementText = val,
val => { config.ShowJudgementText = val; MarkConfigModified(); },
generateLabel: false
);
@@ -225,6 +346,7 @@ internal static class RenderConfigWindow
{
config.VideoConfig.Codec = c;
UpdateVideoCodecUI(c, config);
MarkConfigModified();
});
_bitrateInput = CreateInputField(
@@ -233,7 +355,7 @@ internal static class RenderConfigWindow
"30000", config.VideoConfig.Bitrate.ToString(),
"input_bitrate", out _bitrateLabel,
TMPro.TMP_InputField.CharacterValidation.Integer,
val => { if (int.TryParse(val, out var b) && b > 0) config.VideoConfig.Bitrate = b; }
val => { if (int.TryParse(val, out var b) && b > 0) config.VideoConfig.Bitrate = b; MarkConfigModified(); }
);
_qpInput = CreateInputField(
@@ -242,7 +364,7 @@ internal static class RenderConfigWindow
"18", config.VideoConfig.QP.ToString(),
"input_qp", out _qpLabel,
TMPro.TMP_InputField.CharacterValidation.Integer,
val => { if (int.TryParse(val, out var qp) && qp >= 0) config.VideoConfig.QP = qp; }
val => { if (int.TryParse(val, out var qp) && qp >= 0) config.VideoConfig.QP = qp; MarkConfigModified(); }
);
var rateModes = (RateControlMode[])Enum.GetValues(typeof(RateControlMode));
@@ -256,6 +378,7 @@ internal static class RenderConfigWindow
{
config.VideoConfig.RateControlMode = m;
UpdateRateControlUI(m);
MarkConfigModified();
}
);
@@ -265,7 +388,7 @@ internal static class RenderConfigWindow
"50000", config.VideoConfig.MaxBitrate.ToString(),
"input_max_bitrate", out _maxBitrateLabel,
TMPro.TMP_InputField.CharacterValidation.Integer,
val => { if (int.TryParse(val, out var mb) && mb > 0) config.VideoConfig.MaxBitrate = mb; }
val => { if (int.TryParse(val, out var mb) && mb > 0) config.VideoConfig.MaxBitrate = mb; MarkConfigModified(); }
);
_forceSoftwareToggle = CreateToggleField(
@@ -273,7 +396,7 @@ internal static class RenderConfigWindow
false, config.VideoConfig.ForceSoftwareEncoder,
LocalizationManager.Get("ui.render_config.force_software"),
"toggle_force_software",
val => config.VideoConfig.ForceSoftwareEncoder = val,
val => { config.VideoConfig.ForceSoftwareEncoder = val; MarkConfigModified(); },
generateLabel: true
);
@@ -283,7 +406,7 @@ internal static class RenderConfigWindow
format: "F1", clampMode: ClampMode.All,
sliderFilter: null, inputFilter: null,
onComplete: null, onChanged: val => { config.VideoConfig.KeyframeInterval = val; },
onComplete: null, onChanged: val => { config.VideoConfig.KeyframeInterval = val; MarkConfigModified(); },
text: LocalizationManager.Get("ui.render_config.keyframe_interval"),
id: "slider_keyframe_interval", showFill: true
);
@@ -305,7 +428,7 @@ internal static class RenderConfigWindow
true, config.AudioConfig.RenderAudio,
LocalizationManager.Get("ui.render_config.render_audio"),
"toggle_render_audio",
val => config.AudioConfig.RenderAudio = val,
val => { config.AudioConfig.RenderAudio = val; MarkConfigModified(); },
generateLabel: false
);
@@ -315,7 +438,7 @@ internal static class RenderConfigWindow
format: "F0", clampMode: ClampMode.All,
sliderFilter: value => Snap(audioBitrateSnap, value),
inputFilter: null, onComplete: null, onChanged: val => { config.AudioConfig.AudioBitrate = (int)Math.Round(val) * 1000; },
inputFilter: null, onComplete: null, onChanged: val => { config.AudioConfig.AudioBitrate = (int)Math.Round(val) * 1000; MarkConfigModified(); },
text: LocalizationManager.Get("ui.render_config.audio_bitrate"),
id: "slider_audio_bitrate", showFill: true
);
@@ -327,7 +450,7 @@ internal static class RenderConfigWindow
2, config.AudioConfig.AudioChannels,
channels, c => c == 1 ? "1 (Mono)" : "2 (Stereo)",
"dd_channels",
c => config.AudioConfig.AudioChannels = c
c => { config.AudioConfig.AudioChannels = c; MarkConfigModified(); }
);
_sampleRateDropdown = CreateDropdownField(
@@ -336,7 +459,7 @@ internal static class RenderConfigWindow
48000, config.AudioConfig.AudioSampleRate,
CodecUtil.AvailableSampleRate[CodecUtil.AudioCodecMatch[config.VideoConfig.Codec]], r => $"{r/1000.0} kHz",
"dd_sample_rate",
r => config.AudioConfig.AudioSampleRate = r
r => { config.AudioConfig.AudioSampleRate = r; MarkConfigModified(); }
);
// 6. Global Settings (Collapsible Card)
@@ -425,7 +548,11 @@ internal static class RenderConfigWindow
"mkv", config.AdvancedConfig.FileExtension,
extensions, e => e,
"dd_adv_file_extension",
e => { if (!string.IsNullOrEmpty(e)) config.AdvancedConfig.FileExtension = e; }
e =>
{
if (!string.IsNullOrEmpty(e)) config.AdvancedConfig.FileExtension = e;
MarkConfigModified();
}
);
_processAudioToggle = CreateToggleField(
@@ -433,7 +560,11 @@ internal static class RenderConfigWindow
true, config.AdvancedConfig.ProcessAudio,
LocalizationManager.Get("ui.render_config.process_audio"),
"toggle_process_audio",
val => config.AdvancedConfig.ProcessAudio = val,
val =>
{
config.AdvancedConfig.ProcessAudio = val;
MarkConfigModified();
},
generateLabel: false
);
@@ -447,6 +578,7 @@ internal static class RenderConfigWindow
{
if (int.TryParse(val, out var s)) config.AdvancedConfig.RandomSeed = s;
else if (string.IsNullOrWhiteSpace(val)) config.AdvancedConfig.RandomSeed = null;
MarkConfigModified();
}
);
@@ -455,7 +587,11 @@ internal static class RenderConfigWindow
false, config.AdvancedConfig.NonTile,
LocalizationManager.Get("ui.render_config.non_tile"),
"toggle_non_tile",
val => config.AdvancedConfig.NonTile = val,
val =>
{
config.AdvancedConfig.NonTile = val;
MarkConfigModified();
},
generateLabel: false
);
@@ -482,6 +618,7 @@ internal static class RenderConfigWindow
_firePlanetPicker?.SetBlocked(!val);
config.AdvancedConfig.firePlanetColor = val ? _firePlanetPicker?.Value : null;
if (!val) _firePlanetPicker?.SetExpanded(false);
MarkConfigModified();
},
generateLabel: false
);
@@ -493,7 +630,10 @@ internal static class RenderConfigWindow
val =>
{
if (_firePlanetToggle?.Value == true)
{
config.AdvancedConfig.firePlanetColor = val;
MarkConfigModified();
}
}
);
if (!config.AdvancedConfig.firePlanetColor.HasValue)
@@ -512,6 +652,7 @@ internal static class RenderConfigWindow
_icePlanetPicker?.SetBlocked(!val);
config.AdvancedConfig.icePlanetColor = val ? _icePlanetPicker?.Value : null;
if (!val) _icePlanetPicker?.SetExpanded(false);
MarkConfigModified();
},
generateLabel: false
);
@@ -523,7 +664,10 @@ internal static class RenderConfigWindow
val =>
{
if (_icePlanetToggle?.Value == true)
{
config.AdvancedConfig.icePlanetColor = val;
MarkConfigModified();
}
}
);
if (!config.AdvancedConfig.icePlanetColor.HasValue)
@@ -542,6 +686,7 @@ internal static class RenderConfigWindow
_windPlanetPicker?.SetBlocked(!val);
config.AdvancedConfig.windPlanetColor = val ? _windPlanetPicker?.Value : null;
if (!val) _windPlanetPicker?.SetExpanded(false);
MarkConfigModified();
},
generateLabel: false
);
@@ -553,7 +698,10 @@ internal static class RenderConfigWindow
val =>
{
if (_windPlanetToggle?.Value == true)
{
config.AdvancedConfig.windPlanetColor = val;
MarkConfigModified();
}
}
);
if (!config.AdvancedConfig.windPlanetColor.HasValue)
@@ -647,49 +795,19 @@ internal static class RenderConfigWindow
UpdateVideoCodecUI(config.VideoConfig.Codec, config);
UpdateRateControlUI(config.VideoConfig.RateControlMode, noAnimate: true);
if (!string.IsNullOrEmpty(GlobalSettings.Current.LastPresetName))
{
_presetDropdown?.Set(GlobalSettings.Current.LastPresetName, false);
_lastLoadedPresetName = GlobalSettings.Current.LastPresetName;
UpdateDeleteButtonState();
}
}
private static void TryGoRender()
{
var config = RenderManager.Instance.Configuration;
// Sync Video Config
if (_bitrateInput != null && int.TryParse(_bitrateInput.Value, out var bitrate) && bitrate > 0)
{
config.VideoConfig.Bitrate = bitrate;
}
if (_maxBitrateInput != null && int.TryParse(_maxBitrateInput.Value, out var maxBitrate) && maxBitrate > 0)
{
config.VideoConfig.MaxBitrate = maxBitrate;
}
if (_qpInput != null && int.TryParse(_qpInput.Value, out var qp) && qp >= 0)
{
config.VideoConfig.QP = qp;
}
// Sync Advanced Config
if (_randomSeedInput != null)
{
if (int.TryParse(_randomSeedInput.Value, out var seed))
{
config.AdvancedConfig.RandomSeed = seed;
}
else if (string.IsNullOrWhiteSpace(_randomSeedInput.Value))
{
config.AdvancedConfig.RandomSeed = null;
}
}
// Sync Planet Colors
config.AdvancedConfig.firePlanetColor = (_firePlanetToggle?.Value == true && _firePlanetPicker != null)
? _firePlanetPicker.Value
: null;
config.AdvancedConfig.icePlanetColor = (_icePlanetToggle?.Value == true && _icePlanetPicker != null)
? _icePlanetPicker.Value
: null;
config.AdvancedConfig.windPlanetColor = (_windPlanetToggle?.Value == true && _windPlanetPicker != null)
? _windPlanetPicker.Value
: null;
SyncUItoConfig(config);
// Sync Output Directory
if (_outputDirInput != null)
@@ -725,6 +843,250 @@ internal static class RenderConfigWindow
new RenderSession().Start(config, RenderManager.Instance.levelPath);
}
private static void SyncUItoConfig(RenderConfig target)
{
if (_renderFpsSlider != null) target.RenderFPS = _renderFpsSlider.Value;
if (_videoFPSSlider != null) target.VideoFPS = _videoFPSSlider.Value;
if (_renderWidthSlider != null) target.RenderWidth = (int)Math.Round(_renderWidthSlider.Value);
if (_renderHeightSlider != null) target.RenderHeight = (int)Math.Round(_renderHeightSlider.Value);
if (_delayAfterEndSlider != null) target.DelayAfterEnd = _delayAfterEndSlider.Value;
if (_showJudgementToggle != null) target.ShowJudgementText = _showJudgementToggle.Value;
if (_codecDropdown != null) target.VideoConfig.Codec = _codecDropdown.Value;
if (_rateControlDropdown != null) target.VideoConfig.RateControlMode = _rateControlDropdown.Value;
if (_bitrateInput != null && int.TryParse(_bitrateInput.Value, out var bitrate) && bitrate > 0)
{
target.VideoConfig.Bitrate = bitrate;
}
if (_maxBitrateInput != null && int.TryParse(_maxBitrateInput.Value, out var maxBitrate) && maxBitrate > 0)
{
target.VideoConfig.MaxBitrate = maxBitrate;
}
if (_qpInput != null && int.TryParse(_qpInput.Value, out var qp) && qp >= 0)
{
target.VideoConfig.QP = qp;
}
if (_keyframeIntervalSlider != null) target.VideoConfig.KeyframeInterval = _keyframeIntervalSlider.Value;
if (_forceSoftwareToggle != null) target.VideoConfig.ForceSoftwareEncoder = _forceSoftwareToggle.Value;
if (_renderAudioToggle != null) target.AudioConfig.RenderAudio = _renderAudioToggle.Value;
if (_sampleRateDropdown != null) target.AudioConfig.AudioSampleRate = _sampleRateDropdown.Value;
if (_channelsDropdown != null) target.AudioConfig.AudioChannels = _channelsDropdown.Value;
if (_audioBitrateSlider != null) target.AudioConfig.AudioBitrate = (int)Math.Round(_audioBitrateSlider.Value) * 1000;
if (_fileExtensionDropdown != null && !string.IsNullOrEmpty(_fileExtensionDropdown.Value))
target.AdvancedConfig.FileExtension = _fileExtensionDropdown.Value;
if (_randomSeedInput != null)
{
if (int.TryParse(_randomSeedInput.Value, out var seed))
{
target.AdvancedConfig.RandomSeed = seed;
}
else if (string.IsNullOrWhiteSpace(_randomSeedInput.Value))
{
target.AdvancedConfig.RandomSeed = null;
}
}
if (_processAudioToggle != null) target.AdvancedConfig.ProcessAudio = _processAudioToggle.Value;
if (_nonTileToggle != null) target.AdvancedConfig.NonTile = _nonTileToggle.Value;
// Planet Colors
target.AdvancedConfig.firePlanetColor = (_firePlanetToggle?.Value == true && _firePlanetPicker != null)
? _firePlanetPicker.Value
: null;
target.AdvancedConfig.icePlanetColor = (_icePlanetToggle?.Value == true && _icePlanetPicker != null)
? _icePlanetPicker.Value
: null;
target.AdvancedConfig.windPlanetColor = (_windPlanetToggle?.Value == true && _windPlanetPicker != null)
? _windPlanetPicker.Value
: null;
}
private static string CustomPresetLabel => LocalizationManager.Get("ui.render_config.preset_custom");
private static void MarkConfigModified()
{
if (_presetDropdown == null) return;
var customLabel = CustomPresetLabel;
if (_presetDropdown.Value == customLabel) return;
var currentValues = _presetDropdown.Values;
if (!currentValues.Contains(customLabel))
{
var list = new List<string>(currentValues);
list.Insert(0, customLabel);
_presetDropdown.SetValues(list);
}
_presetDropdown.Set(customLabel, false);
UpdateDeleteButtonState();
}
private static void OnPresetSelected(string? selectedName, RenderConfig config)
{
if (string.IsNullOrEmpty(selectedName) || selectedName == CustomPresetLabel) return;
var preset = PresetManager.GetPresets().FirstOrDefault(p => string.Equals(p.Name, selectedName, StringComparison.OrdinalIgnoreCase));
if (preset == null) return;
GlobalSettings.Current.LastPresetName = preset.Name;
GlobalSettings.Save();
_lastLoadedPresetName = preset.Name;
config.CopyFrom(preset.Config);
ApplyConfigToUI(config);
_presetNameInput?.Set(preset.Name, false);
if (_presetDropdown != null && _presetDropdown.Values.Contains(CustomPresetLabel))
{
var cleaned = _presetDropdown.Values.Where(v => v != CustomPresetLabel).ToList();
_presetDropdown.SetValues(cleaned);
_presetDropdown.Set(preset.Name, false);
}
UpdateDeleteButtonState();
}
private static void OnSavePresetClicked(RenderConfig config)
{
SyncUItoConfig(config);
var name = _presetNameInput?.Value?.Trim();
if (string.IsNullOrWhiteSpace(name) || name == CustomPresetLabel)
{
CommonWarningDialogUI.ShowDialog(
LocalizationManager.Get("ui.render_config.validation_title"),
LocalizationManager.Get("ui.render_config.preset_name_empty"),
new DialogButton(LocalizationManager.Get("ui.render_config.confirm"), () => { })
);
return;
}
bool isCurrentPreset = !string.IsNullOrEmpty(_lastLoadedPresetName) &&
string.Equals(_lastLoadedPresetName, name, StringComparison.OrdinalIgnoreCase);
var existing = PresetManager.GetPresets().FirstOrDefault(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase));
if (existing != null && !isCurrentPreset)
{
CommonWarningDialogUI.ShowDialog(
LocalizationManager.Get("ui.render_config.preset_overwrite_title"),
LocalizationManager.Get("ui.render_config.preset_overwrite_desc", name),
new DialogButton(
LocalizationManager.Get("ui.render_config.confirm"),
() => DoSavePreset(name, config)
),
new DialogButton(
LocalizationManager.Get("ui.render_config.cancel"),
() => { }
)
);
}
else
{
DoSavePreset(name, config);
}
}
private static void DoSavePreset(string name, RenderConfig config)
{
PresetManager.SavePreset(name, config);
GlobalSettings.Current.LastPresetName = name;
GlobalSettings.Save();
_lastLoadedPresetName = name;
ReloadPresetDropdown(selectName: name);
}
private static void OnDeletePresetClicked(RenderConfig config)
{
var targetPresetName = _presetDropdown?.Value;
if (string.IsNullOrWhiteSpace(targetPresetName) || targetPresetName == CustomPresetLabel)
{
targetPresetName = _lastLoadedPresetName;
if (string.IsNullOrWhiteSpace(targetPresetName) || targetPresetName == CustomPresetLabel)
{
return;
}
}
var ctx = RendererUIManager.Context;
CommonWarningDialogUI.ShowDialog(
LocalizationManager.Get("ui.render_config.preset_delete_title"),
LocalizationManager.Get("ui.render_config.preset_delete_desc", targetPresetName),
new DialogButton(
LocalizationManager.Get("ui.render_config.confirm"),
() => DoDeletePreset(targetPresetName, config),
ctx.Theme.SoftRed
),
new DialogButton(
LocalizationManager.Get("ui.render_config.cancel"),
() => { }
)
);
}
private static void DoDeletePreset(string name, RenderConfig config)
{
PresetManager.DeletePreset(name);
var presets = PresetManager.GetPresets();
var fallbackName = presets.Count > 0 ? presets[0].Name : "";
GlobalSettings.Current.LastPresetName = fallbackName;
GlobalSettings.Save();
_lastLoadedPresetName = fallbackName;
ReloadPresetDropdown(selectName: fallbackName);
var fallbackPreset = presets.FirstOrDefault(p => string.Equals(p.Name, fallbackName, StringComparison.OrdinalIgnoreCase));
if (fallbackPreset != null)
{
config.CopyFrom(fallbackPreset.Config);
ApplyConfigToUI(config);
_presetNameInput?.Set(fallbackPreset.Name, false);
}
}
private static void ReloadPresetDropdown(string? selectName = null)
{
var presets = PresetManager.GetPresets();
var names = presets.Select(p => p.Name).ToList();
if (names.Count == 0)
{
_presetDropdown?.SetValues(new List<string> { CustomPresetLabel });
_presetDropdown?.Set(CustomPresetLabel, false);
_lastLoadedPresetName = null;
UpdateDeleteButtonState();
return;
}
_presetDropdown?.SetValues(names);
var targetName = selectName;
if (string.IsNullOrEmpty(targetName) || !names.Contains(targetName))
{
targetName = names[0];
}
_presetDropdown?.Set(targetName, false);
_lastLoadedPresetName = targetName;
UpdateDeleteButtonState();
}
private static void UpdateDeleteButtonState(string? presetName = null)
{
var targetName = presetName ?? _presetDropdown?.Value;
if (targetName == CustomPresetLabel)
{
targetName = _lastLoadedPresetName;
}
bool canDelete = !string.IsNullOrEmpty(targetName) &&
targetName != CustomPresetLabel &&
PresetManager.GetPresets().Any(p => string.Equals(p.Name, targetName, StringComparison.OrdinalIgnoreCase));
_deletePresetButton?.SetBlocked(!canDelete);
}
private static void ShowValidationError(string message)
{
Melon<RenderMod>.Logger.Error(message);