diff --git a/Bundle/Linux/scenes b/Bundle/Linux/scenes index fb40a3e..961b561 100644 Binary files a/Bundle/Linux/scenes and b/Bundle/Linux/scenes differ diff --git a/Bundle/Win64/scenes b/Bundle/Win64/scenes index 2ea775f..2bc30dc 100644 Binary files a/Bundle/Win64/scenes and b/Bundle/Win64/scenes differ diff --git a/Core/RenderManager.cs b/Core/RenderManager.cs index ca85512..f59dcda 100644 --- a/Core/RenderManager.cs +++ b/Core/RenderManager.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Threading; using System.Buffers; +using ADOFAIRenderer.Data; using ADOFAIRenderer.UI; using ADOFAIRenderer.Util; using ADOFAIRenderer.Util.FFmpeg; @@ -31,7 +32,7 @@ namespace ADOFAIRenderer.Core public bool skipPressToStart; public string levelPath = ""; public bool isRenderCancelled; - public RenderSettings Configuration; + public RenderConfig Configuration = null!; // Render Sessions State (OOP Instance Fields) public bool isRendering; @@ -52,47 +53,6 @@ namespace ADOFAIRenderer.Core public int throttleCount; public bool isThrottling; - public struct RenderSettings - { - public double TargetFPS; - public double VideoFPS; - - public int RenderWidth; - public int RenderHeight; - - public RateControlMode BitrateMode; - public int Bitrate; - public int MaxBitrate; - public int QP; - - public string Codec; - public double EndWaitTime; - public bool ShowHitText; - public int? RandomSeed; - - public RenderSettings(double targetFPS, double videoFPS, int renderWidth, int renderHeight, - RateControlMode bitrateMode, int bitrate, int maxBitrate, int qp, - string codec, double endWaitTime, bool showHitText, int? randomSeed) - { - TargetFPS = targetFPS; - VideoFPS = videoFPS; - - RenderWidth = renderWidth; - RenderHeight = renderHeight; - - BitrateMode = bitrateMode; - Bitrate = bitrate; - MaxBitrate = maxBitrate; - QP = qp; - - Codec = codec; - EndWaitTime = endWaitTime; - ShowHitText = showHitText; - - RandomSeed = randomSeed; - } - } - internal struct CapturedFrame { public IntPtr NativePointer; @@ -209,7 +169,7 @@ namespace ADOFAIRenderer.Core DontDestroyOnLoad(_escapeHandlerObj); } - Time.captureDeltaTime = (float)(1 / Configuration.TargetFPS); + Time.captureDeltaTime = (float)(1 / Configuration.RenderFPS); scrController.instance!.StartCoroutine(SetupPlay()); } @@ -227,7 +187,7 @@ namespace ADOFAIRenderer.Core wonFrame = frameCount; } - var extraFrames = (long)Math.Ceiling(Configuration.EndWaitTime * Configuration.TargetFPS); + var extraFrames = (long)Math.Ceiling(Configuration.DelayAfterLevelEnd * Configuration.RenderFPS); if (frameCount - wonFrame < extraFrames) return; isRendering = isCapturing = false; @@ -254,10 +214,10 @@ namespace ADOFAIRenderer.Core Melon.Logger.Error($"[Render Skipped] Frame {frameCount} rendering failed after 60 retries. Supplying black frame."); var currentFrame = frameCountForVideo++; - var targetFps = Configuration.TargetFPS; - var videoFps = Configuration.VideoFPS; - var effectiveVideoFps = Math.Min(videoFps, targetFps); - var videoFrameIndex = (long)(currentFrame * effectiveVideoFps / targetFps); + var renderFPS = Configuration.RenderFPS; + var videoFPS = Configuration.VideoFPS; + var effectiveVideoFps = Math.Min(videoFPS, renderFPS); + var videoFrameIndex = (long)(currentFrame * effectiveVideoFps / renderFPS); if (videoFrameIndex > lastVideoFrameIndex) { @@ -317,11 +277,11 @@ namespace ADOFAIRenderer.Core var currentFrame = frameCountForVideo++; - var targetFps = Configuration.TargetFPS; + var renderFPS = Configuration.RenderFPS; var videoFps = Configuration.VideoFPS; - var effectiveVideoFps = Math.Min(videoFps, targetFps); + var effectiveVideoFps = Math.Min(videoFps, renderFPS); - var videoFrameIndex = (long)(currentFrame * effectiveVideoFps / targetFps); + var videoFrameIndex = (long)(currentFrame * effectiveVideoFps / renderFPS); if (videoFrameIndex > lastVideoFrameIndex) { @@ -395,7 +355,9 @@ namespace ADOFAIRenderer.Core isCapturing = true; scrController.instance.paused = scrController.instance.audioPaused = false; scrController.instance.levelWasSkipped = true; - + + if (!Configuration.ProcessAudio) yield break; + if (!scrConductor.instance || !scrConductor.instance.song) yield break; AudioSpectrumGenerator.Initialize(scrConductor.instance.song.clip); scrConductor.instance.song.clip = null; @@ -425,19 +387,14 @@ namespace ADOFAIRenderer.Core endRequested = false; endSuccessful = false; - var targetFps = Configuration.TargetFPS; - var videoFps = Configuration.VideoFPS; - var bitrateMode = Configuration.BitrateMode; - var bitrate = Configuration.Bitrate; - var maxBitrate = Configuration.MaxBitrate; - var qp = Configuration.QP; - var codecStr = Configuration.Codec; + var renderFPS = Configuration.RenderFPS; + var videoFPS = Configuration.VideoFPS; + var bitrateMode = Configuration.EncodingConfig!.RateControlMode; + var bitrate = Configuration.EncodingConfig!.Bitrate; + var maxBitrate = Configuration.EncodingConfig!.MaxBitrate; + var qp = Configuration.EncodingConfig!.QP; - var selectedCodec = Codec.H264; - if (Enum.TryParse(codecStr, true, out var parsedCodec)) - { - selectedCodec = parsedCodec; - } + var selectedCodec = Configuration.Codec; var ext = ".mp4"; if (selectedCodec == Codec.AV1 || selectedCodec == Codec.VP9) @@ -458,7 +415,7 @@ namespace ADOFAIRenderer.Core FFmpegNativeEncoder? videoFFmpeg = null; try { - var effectiveVideoFps = Math.Min(videoFps, targetFps); + var effectiveVideoFps = Math.Min(videoFPS, renderFPS); var settings = new FFmpegNativeEncoder.EncoderSettings { Width = Configuration.RenderWidth, diff --git a/Data/DoubleInputValidator.cs b/Data/DoubleInputValidator.cs index 566582d..dac92cc 100644 --- a/Data/DoubleInputValidator.cs +++ b/Data/DoubleInputValidator.cs @@ -9,7 +9,6 @@ namespace ADOFAIRenderer.Data private struct FieldEntry { public List Limits; - public bool Optional; } private readonly VisualElement _rootVisualElement; @@ -25,34 +24,22 @@ namespace ADOFAIRenderer.Data /// Multiple limits can be added to the same field — they are all checked in order. /// /// The name of the DoubleField in the UXML. - /// - /// If true, the field is allowed to be empty. Limits are only enforced when a value is present. - /// /// The limits to apply. - public DoubleInputValidator AddLimits(string fieldName, bool optional, params DoubleLimit[] limits) + public DoubleInputValidator AddLimits(string fieldName, params DoubleLimit[] limits) { if (!_fields.TryGetValue(fieldName, out var entry)) { entry = new FieldEntry { - Limits = new List(), - Optional = optional + Limits = new List() }; } - entry.Optional = optional; entry.Limits.AddRange(limits); _fields[fieldName] = entry; return this; } - /// - /// Shorthand for required (non-optional) fields. - public DoubleInputValidator AddLimits(string fieldName, params DoubleLimit[] limits) - { - return AddLimits(fieldName, false, limits); - } - /// /// Queries all registered fields from the root VisualElement and /// registers value-changed callbacks that rollback invalid values. @@ -73,12 +60,10 @@ namespace ADOFAIRenderer.Data } var limits = entry.Limits; - var optional = entry.Optional; field.RegisterValueChangedCallback(evt => { - // Optional fields allow empty input — skip validation when cleared - if (optional && string.IsNullOrEmpty(field.text)) + if (string.IsNullOrEmpty(field.text)) { return; } diff --git a/Data/EnumDropdownBinder.cs b/Data/EnumDropdownBinder.cs index f449442..d200ee2 100644 --- a/Data/EnumDropdownBinder.cs +++ b/Data/EnumDropdownBinder.cs @@ -13,13 +13,16 @@ namespace ADOFAIRenderer.Data /// The enum type to bind. public class EnumDropdownBinder where T : struct, Enum { - private readonly DropdownField _dropdown; + private readonly DropdownField? _dropdown; private readonly List _values; + private Action? _onValueChanged; /// /// The currently selected enum value. /// - public T Value => _values[_dropdown.index >= 0 ? _dropdown.index : 0]; + public T Value => _dropdown != null && _dropdown.index >= 0 && _dropdown.index < _values.Count + ? _values[_dropdown.index] + : default; /// /// Queries a by name from the root VisualElement, @@ -30,18 +33,24 @@ namespace ADOFAIRenderer.Data /// The initial selected value. Defaults to the first enum member. public EnumDropdownBinder(VisualElement rootVisualElement, string fieldName, T? defaultValue = null) { + _values = Enum.GetValues(typeof(T)).Cast().ToList(); + _dropdown = rootVisualElement.Q(fieldName); if (_dropdown == null) { Melon.Logger.Error( $"DropdownField '{fieldName}'을(를) 찾을 수 없습니다."); - _values = new List(); return; } - _values = Enum.GetValues(typeof(T)).Cast().ToList(); _dropdown.choices = _values.Select(v => v.ToString()).ToList(); + // Register the callback BEFORE setting index so the initial selection fires it too + _dropdown.RegisterValueChangedCallback(_ => + { + _onValueChanged?.Invoke(Value); + }); + var initial = defaultValue ?? _values[0]; _dropdown.index = _values.IndexOf(initial); } @@ -51,10 +60,13 @@ namespace ADOFAIRenderer.Data /// public void OnValueChanged(Action callback) { - _dropdown?.RegisterValueChangedCallback(evt => + _onValueChanged = callback; + + // Fire immediately with current value so the caller gets the initial state + if (_dropdown != null) { callback(Value); - }); + } } } } diff --git a/Data/IntegerInputValidator.cs b/Data/IntegerInputValidator.cs index f9af349..85b006d 100644 --- a/Data/IntegerInputValidator.cs +++ b/Data/IntegerInputValidator.cs @@ -9,7 +9,6 @@ namespace ADOFAIRenderer.Data private struct FieldEntry { public List Limits; - public bool Optional; } private readonly VisualElement _rootVisualElement; @@ -25,34 +24,22 @@ namespace ADOFAIRenderer.Data /// Multiple limits can be added to the same field — they are all checked in order. /// /// The name of the IntegerField in the UXML. - /// - /// If true, the field is allowed to be empty. Limits are only enforced when a value is present. - /// /// The limits to apply. - public IntegerInputValidator AddLimits(string fieldName, bool optional, params IntegerLimit[] limits) + public IntegerInputValidator AddLimits(string fieldName, params IntegerLimit[] limits) { if (!_fields.TryGetValue(fieldName, out var entry)) { entry = new FieldEntry { - Limits = new List(), - Optional = optional + Limits = new List() }; } - entry.Optional = optional; entry.Limits.AddRange(limits); _fields[fieldName] = entry; return this; } - /// - /// Shorthand for required (non-optional) fields. - public IntegerInputValidator AddLimits(string fieldName, params IntegerLimit[] limits) - { - return AddLimits(fieldName, false, limits); - } - /// /// Queries all registered fields from the root VisualElement and /// registers value-changed callbacks that rollback invalid values. @@ -73,12 +60,10 @@ namespace ADOFAIRenderer.Data } var limits = entry.Limits; - var optional = entry.Optional; field.RegisterValueChangedCallback(evt => { - // Optional fields allow empty input — skip validation when cleared - if (optional && string.IsNullOrEmpty(field.text)) + if (string.IsNullOrEmpty(field.text)) { return; } diff --git a/Data/RenderConfig.cs b/Data/RenderConfig.cs index 53e8bbc..0e1da91 100644 --- a/Data/RenderConfig.cs +++ b/Data/RenderConfig.cs @@ -1,7 +1,31 @@ -namespace ADOFAIRenderer.Configuration +using ADOFAIRenderer.Util.FFmpeg; + +namespace ADOFAIRenderer.Data { public class RenderConfig { + public double RenderFPS; + public double VideoFPS; + + public int RenderWidth; + public int RenderHeight; + public Codec Codec; + public EncodingConfig? EncodingConfig = null; + + public double DelayAfterLevelEnd; + public bool ShowJudgementText; + + public int? RandomSeed = null; + public bool ProcessAudio; + } + + public class EncodingConfig + { + public RateControlMode RateControlMode; + + public int Bitrate; + public int MaxBitrate; + public int QP; } } \ No newline at end of file diff --git a/Patch/Engine/RenderEnvironmentPatches.cs b/Patch/Engine/RenderEnvironmentPatches.cs index 23d2e72..de9cdd6 100644 --- a/Patch/Engine/RenderEnvironmentPatches.cs +++ b/Patch/Engine/RenderEnvironmentPatches.cs @@ -9,6 +9,7 @@ namespace ADOFAIRenderer.Patch.Engine public static class RenderEnvironmentPatches { private const bool RenderPatch = true; + private static double TimeSpent => RenderManager.Instance.frameCount / RenderManager.Instance.Configuration.RenderFPS; [HarmonyPatch(typeof(Time), "time", MethodType.Getter)] public static class TimePatch @@ -17,7 +18,7 @@ namespace ADOFAIRenderer.Patch.Engine public static bool Prefix(ref float __result) { if (!RenderManager.IsCapturingActive || !RenderPatch || !RenderManager.Instance) return true; - __result = (float)(RenderManager.Instance.frameCount / RenderManager.Instance.Configuration.TargetFPS + RenderManager.Instance.timeBase); + __result = (float)(TimeSpent + RenderManager.Instance.timeBase); return false; } } @@ -29,7 +30,7 @@ namespace ADOFAIRenderer.Patch.Engine public static bool Prefix(ref double __result) { if (!RenderManager.IsCapturingActive || !RenderPatch || !RenderManager.Instance) return true; - __result = RenderManager.Instance.frameCount / RenderManager.Instance.Configuration.TargetFPS + RenderManager.Instance.timeBase; + __result = TimeSpent + RenderManager.Instance.timeBase; return false; } } @@ -41,7 +42,7 @@ namespace ADOFAIRenderer.Patch.Engine public static bool Prefix(ref float __result) { if (!RenderManager.IsCapturingActive || !RenderPatch || !RenderManager.Instance) return true; - __result = (float)(RenderManager.Instance.frameCount / RenderManager.Instance.Configuration.TargetFPS + RenderManager.Instance.unscaledTimeBase); + __result = (float)(TimeSpent + RenderManager.Instance.unscaledTimeBase); return false; } } @@ -53,7 +54,7 @@ namespace ADOFAIRenderer.Patch.Engine public static bool Prefix(ref double __result) { if (!RenderManager.IsCapturingActive || !RenderPatch || !RenderManager.Instance) return true; - __result = RenderManager.Instance.frameCount / RenderManager.Instance.Configuration.TargetFPS + RenderManager.Instance.unscaledTimeBase; + __result = TimeSpent + RenderManager.Instance.unscaledTimeBase; return false; } } @@ -65,7 +66,7 @@ namespace ADOFAIRenderer.Patch.Engine public static bool Prefix(ref double __result) { if (!RenderManager.IsCapturingActive || !RenderPatch || !RenderManager.Instance) return true; - __result = RenderManager.Instance.frameCount / RenderManager.Instance.Configuration.TargetFPS + RenderManager.Instance.dspTimeBase; + __result = TimeSpent + RenderManager.Instance.dspTimeBase; return false; } } @@ -99,7 +100,7 @@ namespace ADOFAIRenderer.Patch.Engine public static bool Prefix(ref float __result) { if (RenderManager.Instance && (!RenderManager.IsCapturingActive || !RenderPatch || !RenderManager.Instance)) return true; - __result = (float)(1 / RenderManager.Instance!.Configuration.TargetFPS); + __result = (float)(1 / RenderManager.Instance!.Configuration.RenderFPS); return false; } } @@ -111,7 +112,7 @@ namespace ADOFAIRenderer.Patch.Engine public static bool Prefix(ref float __result) { if (!RenderManager.IsCapturingActive || !RenderPatch || !RenderManager.Instance) return true; - __result = (float)(1 / RenderManager.Instance.Configuration.TargetFPS); + __result = (float)(1 / RenderManager.Instance.Configuration.RenderFPS); return false; } } @@ -146,7 +147,7 @@ namespace ADOFAIRenderer.Patch.Engine [SuppressMessage("ReSharper", "InconsistentNaming")] public static bool Prefix(AudioSource __instance, float[] samples, int channel, FFTWindow window) { - if (!RenderManager.IsCapturingActive || !RenderPatch) return true; + if (!RenderManager.IsCapturingActive || !RenderPatch || !RenderManager.Instance.Configuration.ProcessAudio) return true; AudioSpectrumGenerator.GetSpectrumData(samples, channel, window); return false; } @@ -158,7 +159,7 @@ namespace ADOFAIRenderer.Patch.Engine [SuppressMessage("ReSharper", "InconsistentNaming")] public static void Prefix(ref bool stream) { - if (RenderManager.Instance != null) + if (RenderManager.Instance && RenderManager.Instance.Configuration.ProcessAudio) { stream = false; } diff --git a/Patch/Game/scrHitTextManagerPatch.cs b/Patch/Game/scrHitTextManagerPatch.cs index 6e9195b..f4b5082 100644 --- a/Patch/Game/scrHitTextManagerPatch.cs +++ b/Patch/Game/scrHitTextManagerPatch.cs @@ -9,7 +9,7 @@ namespace ADOFAIRenderer.Patch.Game { public static bool Prefix() { - return !RenderManager.IsActive || RenderManager.Instance is { Configuration: { ShowHitText: true } }; + return !RenderManager.IsActive || RenderManager.Instance is { Configuration: { ShowJudgementText: true } }; } } } \ No newline at end of file diff --git a/Patch/Game/scrShowIfDebugPatch.cs b/Patch/Game/scrShowIfDebugPatch.cs index 8bde244..727213c 100644 --- a/Patch/Game/scrShowIfDebugPatch.cs +++ b/Patch/Game/scrShowIfDebugPatch.cs @@ -25,7 +25,7 @@ namespace ADOFAIRenderer.Patch.Game if (RenderManager.Instance.wonFrame != -1) { var elapsedFrames = RenderManager.Instance.frameCount - RenderManager.Instance.wonFrame; - var remainingTime = RenderManager.Instance.Configuration.EndWaitTime - elapsedFrames / RenderManager.Instance.Configuration.TargetFPS; + var remainingTime = RenderManager.Instance.Configuration.DelayAfterLevelEnd - elapsedFrames / RenderManager.Instance.Configuration.RenderFPS; if (remainingTime < 0) remainingTime = 0; ___txt.text += $"\nWaiting for {remainingTime:F1}s.."; } diff --git a/RenderMod.cs b/RenderMod.cs index f830cbf..1bac5f2 100644 --- a/RenderMod.cs +++ b/RenderMod.cs @@ -7,7 +7,7 @@ using MelonLoader.Utils; using UnityEngine; using UnityEngine.EventSystems; -[assembly: MelonInfo(typeof(RenderMod), "ADOFAI Renderer", "0.0.4-INDEV", "ADOFAI Editor Forum")] +[assembly: MelonInfo(typeof(RenderMod), "ADOFAI Renderer", "0.0.5-INDEV", "ADOFAI Editor Forum")] [assembly: MelonGame("7th Beat Games", "A Dance of Fire and Ice")] namespace ADOFAIRenderer diff --git a/UI/RenderButton.cs b/UI/RenderButton.cs deleted file mode 100644 index 70d5575..0000000 --- a/UI/RenderButton.cs +++ /dev/null @@ -1,284 +0,0 @@ -using System.Linq; -using ADOFAIRenderer.Core; -using ADOFAIRenderer.Util.FFmpeg; -using MelonLoader; -using TMPro; -using UnityEngine; -using UnityEngine.UI; - -namespace ADOFAIRenderer.UI -{ - public class RenderButton : MonoBehaviour - { - [Header("Input Fields")] - [SerializeField] private TMP_InputField targetFPSInput = null!; - [SerializeField] private TMP_InputField videoFPSInput = null!; - [SerializeField] private TMP_InputField renderWidthInput = null!; - [SerializeField] private TMP_InputField renderHeightInput = null!; - [SerializeField] private TMP_InputField waitAfterEndInput = null!; - [SerializeField] private TMP_InputField randomSeedInput = null!; - - [Header("Dropdown")] - [SerializeField] private TMP_Dropdown codecDropdown = null!; - [SerializeField] private TMP_Dropdown bitrateModeDropdown = null!; - - [Header("Toggle")] - [SerializeField] private Toggle showHitTextToggle = null!; - - [Header("Button")] - [SerializeField] private Button advancedSettingsButton = null!; - - [Header("Bitrate Dynamic UI Container")] - [SerializeField] private RectTransform bitrateContainer = null!; - - // Dynamically instantiated inputs - private TMP_InputField _bitrateInput = null!; - private TMP_InputField _maxBitrateInput = null!; - private TMP_InputField _qpInput = null!; - - private Button _buttonComponent = null!; - - private void Start() - { - _buttonComponent = GetComponent