diff --git a/ADOFAIRenderer.csproj b/ADOFAIRenderer.csproj index 358eafc..e399585 100644 --- a/ADOFAIRenderer.csproj +++ b/ADOFAIRenderer.csproj @@ -110,4 +110,68 @@ + + + + + + + + + + + + + +(); + + foreach (var member in members.Elements("member")) + { + var nameAttr = member.Attribute("name")?.Value; + if (string.IsNullOrEmpty(nameAttr)) continue; + + // XML 주석 형식: T:네임스페이스.타입, M:메서드, P:프로퍼티, F:필드/열거형, E:이벤트 등 + int colonIndex = nameAttr.IndexOf(':'); + string fullName = colonIndex >= 0 ? nameAttr.Substring(colonIndex + 1) : nameAttr; + + if (!fullName.StartsWith(prefix, StringComparison.Ordinal) && !fullName.Equals(TargetNamespace, StringComparison.Ordinal)) + { + toRemove.Add(member); + } + } + + foreach (var elem in toRemove) + { + elem.Remove(); + } + + doc.Save(XmlFilePath); + Log.LogMessage(Microsoft.Build.Framework.MessageImportance.High, $"[FilterXmlDoc] Kept '{TargetNamespace}' namespace docs. Removed {toRemove.Count} other members."); + } + } + catch (Exception ex) + { + Log.LogWarning($"[FilterXmlDoc] XML filtering failed: {ex.Message}"); + } +]]> + + + + + + + <_XmlDocPath Condition="'$(_XmlDocPath)' == '' and Exists('$(TargetDir)$(TargetName).xml')">$(TargetDir)$(TargetName).xml + <_XmlDocPath Condition="'$(_XmlDocPath)' == '' and Exists('$(DocumentationFile)')">$(DocumentationFile) + + + diff --git a/API/RenderResult.cs b/API/RenderResult.cs index eebcddf..d1ade45 100644 --- a/API/RenderResult.cs +++ b/API/RenderResult.cs @@ -5,9 +5,10 @@ using System.IO; namespace ADOFAIRenderer.API; /// -/// Represents the result and metadata of a completed or cancelled render session. +/// Represents the result and metadata of a completed or canceled render session. /// [SuppressMessage("ReSharper", "UnusedType.Global")] +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public class RenderResult { /// @@ -41,10 +42,20 @@ public class RenderResult public long FileSizeBytes { get; } /// - /// Error message or reason if rendering failed or was cancelled; null on success. + /// Error message or reason if rendering failed or was canceled; null on success. /// public string? ErrorMessage { get; } - + + /// + /// Initializes a new instance of the class. + /// + /// Whether the render session completed successfully. + /// The absolute output path of the rendered video file. + /// Total number of video frames sent to the encoder. + /// Calculated video duration in seconds. + /// Total wall-clock time spent rendering. + /// File size of the output video in bytes. + /// Error message or reason if failed or canceled; otherwise, null. public RenderResult( bool success, string outputPath, @@ -63,7 +74,15 @@ public class RenderResult ErrorMessage = errorMessage; } - public static RenderResult CreateSuccess(string outputPath, long totalFrames, double fps, TimeSpan renderDuration) + /// + /// Creates a successful instance from the specified render metrics. + /// + /// The path to the rendered video file. + /// Total number of video frames sent to the encoder. + /// The effective frames per second used to compute video duration. + /// Total wall-clock time spent rendering. + /// A new representing a successful render session. + internal static RenderResult CreateSuccess(string outputPath, long totalFrames, double fps, TimeSpan renderDuration) { var fileSizeBytes = 0L; if (!string.IsNullOrEmpty(outputPath) && File.Exists(outputPath)) @@ -76,12 +95,24 @@ public class RenderResult return new RenderResult(true, outputPath, totalFrames, videoDuration, renderDuration, fileSizeBytes); } - public static RenderResult CreateFailure(string? errorMessage, long totalFrames, double fps, TimeSpan renderDuration) + /// + /// Creates a failed or cancelled instance. + /// + /// Error message or reason for failure/cancellation. + /// Total number of video frames sent to the encoder prior to termination. + /// The effective frames per second used to compute video duration. + /// Total wall-clock time spent rendering prior to termination. + /// A new representing a failed or canceled render session. + internal static RenderResult CreateFailure(string? errorMessage, long totalFrames, double fps, TimeSpan renderDuration) { var videoDuration = fps > 0 ? totalFrames / fps : 0.0; return new RenderResult(false, string.Empty, totalFrames, videoDuration, renderDuration, 0, errorMessage ?? "Rendering failed or was cancelled."); } + /// + /// Returns a string summarizing the render result metrics or failure reason. + /// + /// A string representation of the . public override string ToString() { return Success diff --git a/API/RenderSession.cs b/API/RenderSession.cs index 3319303..3ef1286 100644 --- a/API/RenderSession.cs +++ b/API/RenderSession.cs @@ -92,12 +92,22 @@ public class RenderSession return this; } + /// + /// Sets whether desktop notifications should be displayed when rendering completes. + /// + /// true to enable desktop notifications; otherwise, false. + /// The current instance for method chaining. public RenderSession WithNotification(bool enabled) { _notification = enabled; return this; } + /// + /// Adds a callback action to be invoked when the render session completes or fails. + /// + /// The callback action that receives the resulting . + /// The current instance for method chaining. public RenderSession OnComplete(Action callback) { _onComplete += callback; @@ -109,6 +119,7 @@ public class RenderSession /// /// The configuration settings for the render session. /// The file path of the level to render. + /// Thrown when the output directory cannot be determined from the output path. public void Start(RenderConfig renderConfig, string levelPath) { RenderContext.Auto = _auto; diff --git a/Core/FirstUpdateHook.cs b/Core/FirstUpdateHook.cs index e395d0a..caa6caf 100644 --- a/Core/FirstUpdateHook.cs +++ b/Core/FirstUpdateHook.cs @@ -1,66 +1,65 @@ using ADOFAIRenderer.UI; using UnityEngine; -namespace ADOFAIRenderer.Core +namespace ADOFAIRenderer.Core; + +[DefaultExecutionOrder(int.MinValue)] +internal class FirstUpdateHook : MonoBehaviour { - [DefaultExecutionOrder(int.MinValue)] - internal class FirstUpdateHook : MonoBehaviour + private bool _checkedFFmpeg; + private bool _checkedNvidiaDriver; + + private void Update() { - private bool _checkedFFmpeg; - private bool _checkedNvidiaDriver; - - private void Update() + if (!_checkedFFmpeg) { - if (!_checkedFFmpeg) + if (FFmpegDownloadUI.Instance) return; + + if (!FFmpegDownloadUI.CheckFFmpegExists()) { - if (FFmpegDownloadUI.Instance) return; - - if (!FFmpegDownloadUI.CheckFFmpegExists()) - { - FFmpegDownloadUI.ShowDownloadUI(); - return; - } - _checkedFFmpeg = true; - } - - if (!_checkedNvidiaDriver) - { - if (CommonWarningDialogUI.Instance) return; - - _checkedNvidiaDriver = true; - CheckNvidiaDriverWarning(); - } - - if (RenderManager.Instance) - { - RenderManager.Instance.OnPreRenderUpdate(); + FFmpegDownloadUI.ShowDownloadUI(); + return; } + _checkedFFmpeg = true; } - private void CheckNvidiaDriverWarning() + if (!_checkedNvidiaDriver) { - var driverVer = Util.GPUUtil.GetNvidiaDriverVersion(); - if (driverVer is null or >= 610.00f) return; + if (CommonWarningDialogUI.Instance) return; - var currentVersion = driverVer.Value.ToString("F2"); - - CommonWarningDialogUI.ShowDialog( - "NVIDIA 그래픽 드라이버 업데이트 권장", - $"현재 감지된 NVIDIA 그래픽 드라이버 버전: {currentVersion}\n\n하드웨어 가속(NVENC)을 사용하여 영상을 렌더링하려면 드라이버 버전 610.00 이상이 필요합니다.\n\n드라이버 버전이 낮으면 인코딩이 실패하거나 소프트웨어(CPU) 인코딩으로 대체되어 렌더링이 매우 느려집니다. 드라이버 업데이트를 진행해 주시기 바랍니다.", - new DialogButton("드라이버 업데이트", () => - { - Application.OpenURL("https://www.nvidia.com/Download/index.aspx"); - Application.Quit(); - }, new Color(0.0f, 0.8f, 1.0f, 1.0f)), - new DialogButton("기능 비활성화", () => - { - RenderMod.IsFeatureDisabled = true; - }), - new DialogButton("이번만 그냥 진행", () => - { - // 아무 동작 안 함 - }) - ); + _checkedNvidiaDriver = true; + CheckNvidiaDriverWarning(); + } + + if (RenderManager.Instance) + { + RenderManager.Instance.OnPreRenderUpdate(); } } + + private void CheckNvidiaDriverWarning() + { + var driverVer = Util.GPUUtil.GetNvidiaDriverVersion(); + if (driverVer is null or >= 610.00f) return; + + var currentVersion = driverVer.Value.ToString("F2"); + + CommonWarningDialogUI.ShowDialog( + "NVIDIA 그래픽 드라이버 업데이트 권장", + $"현재 감지된 NVIDIA 그래픽 드라이버 버전: {currentVersion}\n\n하드웨어 가속(NVENC)을 사용하여 영상을 렌더링하려면 드라이버 버전 610.00 이상이 필요합니다.\n\n드라이버 버전이 낮으면 인코딩이 실패하거나 소프트웨어(CPU) 인코딩으로 대체되어 렌더링이 매우 느려집니다. 드라이버 업데이트를 진행해 주시기 바랍니다.", + new DialogButton("드라이버 업데이트", () => + { + Application.OpenURL("https://www.nvidia.com/Download/index.aspx"); + Application.Quit(); + }, new Color(0.0f, 0.8f, 1.0f, 1.0f)), + new DialogButton("기능 비활성화", () => + { + RenderMod.IsFeatureDisabled = true; + }), + new DialogButton("이번만 그냥 진행", () => + { + // 아무 동작 안 함 + }) + ); + } } \ No newline at end of file diff --git a/Core/PostRenderHook.cs b/Core/PostRenderHook.cs index f0a5d14..2a86d4d 100644 --- a/Core/PostRenderHook.cs +++ b/Core/PostRenderHook.cs @@ -3,33 +3,32 @@ using System.Reflection; using HarmonyLib; using UnityEngine; -namespace ADOFAIRenderer.Core +namespace ADOFAIRenderer.Core; + +[DefaultExecutionOrder(int.MaxValue)] +internal class PostRenderHook : MonoBehaviour { - [DefaultExecutionOrder(int.MaxValue)] - internal class PostRenderHook : MonoBehaviour + private FieldInfo? _camQuadMeshField; + + private void Awake() { - private FieldInfo? _camQuadMeshField; - - private void Awake() - { - _camQuadMeshField = AccessTools.Field(typeof(scrCamera), "camQuadMesh"); - } - - private void LateUpdate() - { - if (RenderManager.Instance) - RenderManager.Instance.OnLastUpdate(); - - StartCoroutine(PostFrameCoroutine()); - } - - private IEnumerator PostFrameCoroutine() - { - yield return new WaitForEndOfFrame(); - - var val = (MeshRenderer)_camQuadMeshField!.GetValue(scrCamera.instance); - if (RenderManager.Instance) - RenderManager.Instance.OnPostFrame(val.sharedMaterial.mainTexture as RenderTexture); - } + _camQuadMeshField = AccessTools.Field(typeof(scrCamera), "camQuadMesh"); } -} + + private void LateUpdate() + { + if (RenderManager.Instance) + RenderManager.Instance.OnLastUpdate(); + + StartCoroutine(PostFrameCoroutine()); + } + + private IEnumerator PostFrameCoroutine() + { + yield return new WaitForEndOfFrame(); + + var val = (MeshRenderer)_camQuadMeshField!.GetValue(scrCamera.instance); + if (RenderManager.Instance) + RenderManager.Instance.OnPostFrame(val.sharedMaterial.mainTexture as RenderTexture); + } +} \ No newline at end of file diff --git a/Data/DoubleLimit.cs b/Data/DoubleLimit.cs index 4c3d728..92fe984 100644 --- a/Data/DoubleLimit.cs +++ b/Data/DoubleLimit.cs @@ -1,78 +1,79 @@ -namespace ADOFAIRenderer.Data +namespace ADOFAIRenderer.Data; + +/// +/// Base class for double input validation limits. +/// Multiple limits can be composed on a single input field — all are checked in order. +/// +internal abstract class DoubleLimit { /// - /// Base class for double input validation limits. - /// Multiple limits can be composed on a single input field — all are checked in order. + /// Validates the given double value against this limit. /// - internal abstract class DoubleLimit + /// The parsed double value to validate. + /// + /// When validation fails, set to a human-readable error message. + /// When validation succeeds, set to null. + /// + /// true if the value satisfies this limit; false otherwise. + public abstract bool Validate(double value, out string errorMessage); +} + +/// +/// Requires the double value to be strictly positive (> 0). +/// +internal class PositiveOnlyDoubleLimit : DoubleLimit +{ + public override bool Validate(double value, out string errorMessage) { - /// - /// Validates the given double value against this limit. - /// - /// The parsed double value to validate. - /// - /// When validation fails, set to a human-readable error message. - /// When validation succeeds, set to null. - /// - /// true if the value satisfies this limit; false otherwise. - public abstract bool Validate(double value, out string errorMessage); - } - - /// - /// Requires the double value to be strictly positive (> 0). - /// - internal class PositiveOnlyDoubleLimit : DoubleLimit - { - public override bool Validate(double value, out string errorMessage) + if (value > 0) { - if (value > 0) - { - errorMessage = null!; - return true; - } - - errorMessage = "값은 0보다 큰 양수여야 합니다."; - return false; - } - } - - /// - /// Requires the double value to be within an inclusive [Min, Max] range. - /// Either bound can be null to represent an unbounded side. - /// - internal class RangeDoubleLimit : DoubleLimit - { - public double? Min { get; } - public double? Max { get; } - - /// Inclusive lower bound, or null for no lower bound. - /// Inclusive upper bound, or null for no upper bound. - public RangeDoubleLimit(double? min, double? max) - { - Min = min; - Max = max; - } - - public override bool Validate(double value, out string errorMessage) - { - if (Min.HasValue && value < Min.Value) - { - errorMessage = Max.HasValue - ? $"값은 {Min.Value}에서 {Max.Value} 사이의 숫자여야 합니다." - : $"값은 {Min.Value} 이상의 숫자여야 합니다."; - return false; - } - - if (Max.HasValue && value > Max.Value) - { - errorMessage = Min.HasValue - ? $"값은 {Min.Value}에서 {Max.Value} 사이의 숫자여야 합니다." - : $"값은 {Max.Value} 이하의 숫자여야 합니다."; - return false; - } - errorMessage = null!; return true; } + + errorMessage = "값은 0보다 큰 양수여야 합니다."; + return false; } } + +/// +/// Requires the double value to be within an inclusive [Min, Max] range. +/// Either bound can be null to represent an unbounded side. +/// +internal class RangeDoubleLimit : DoubleLimit +{ + // ReSharper disable MemberCanBePrivate.Global + public double? Min { get; } + public double? Max { get; } + // ReSharper restore MemberCanBePrivate.Global + + /// Inclusive lower bound, or null for no lower bound. + /// Inclusive upper bound, or null for no upper bound. + public RangeDoubleLimit(double? min, double? max) + { + Min = min; + Max = max; + } + + public override bool Validate(double value, out string errorMessage) + { + if (value < Min) + { + errorMessage = Max.HasValue + ? $"값은 {Min.Value}에서 {Max.Value} 사이의 숫자여야 합니다." + : $"값은 {Min.Value} 이상의 숫자여야 합니다."; + return false; + } + + if (value > Max) + { + errorMessage = Min.HasValue + ? $"값은 {Min.Value}에서 {Max.Value} 사이의 숫자여야 합니다." + : $"값은 {Max.Value} 이하의 숫자여야 합니다."; + return false; + } + + errorMessage = null!; + return true; + } +} \ No newline at end of file diff --git a/Data/Input/DoubleInputValidator.cs b/Data/Input/DoubleInputValidator.cs index 26eeaa2..65de249 100644 --- a/Data/Input/DoubleInputValidator.cs +++ b/Data/Input/DoubleInputValidator.cs @@ -3,75 +3,74 @@ using System.Linq; using MelonLoader; using UnityEngine.UIElements; -namespace ADOFAIRenderer.Data.Input +namespace ADOFAIRenderer.Data.Input; + +internal class DoubleInputValidator { - internal class DoubleInputValidator + private struct FieldEntry { - private struct FieldEntry - { - public List Limits; - } + public List Limits; + } - private readonly VisualElement _rootVisualElement; - private readonly Dictionary _fields = new(); + private readonly VisualElement _rootVisualElement; + private readonly Dictionary _fields = new(); - public DoubleInputValidator(VisualElement rootVisualElement) - { - _rootVisualElement = rootVisualElement; - } + public DoubleInputValidator(VisualElement rootVisualElement) + { + _rootVisualElement = rootVisualElement; + } - /// - /// Registers one or more s for the field with the given name. - /// Multiple limits can be added to the same field — they are all checked in order. - /// - /// The name of the DoubleField in the UXML. - /// The limits to apply. - public DoubleInputValidator AddLimits(string fieldName, params DoubleLimit[] limits) + /// + /// Registers one or more s for the field with the given name. + /// Multiple limits can be added to the same field — they are all checked in order. + /// + /// The name of the DoubleField in the UXML. + /// The limits to apply. + public DoubleInputValidator AddLimits(string fieldName, params DoubleLimit[] limits) + { + if (!_fields.TryGetValue(fieldName, out var entry)) { - if (!_fields.TryGetValue(fieldName, out var entry)) + entry = new FieldEntry { - entry = new FieldEntry - { - Limits = new List() - }; + Limits = new List() + }; + } + + entry.Limits.AddRange(limits); + _fields[fieldName] = entry; + return this; + } + + /// + /// Queries all registered fields from the root VisualElement and + /// registers value-changed callbacks that rollback invalid values. + /// + public void SetupValidation() + { + foreach (var (fieldName, entry) in _fields) + { + var field = _rootVisualElement.Q(fieldName); + if (field == null) + { + Melon.Logger.Warning( + $"DoubleField '{fieldName}'을(를) 찾을 수 없습니다. 유효성 검사를 건너뜁니다."); + continue; } - entry.Limits.AddRange(limits); - _fields[fieldName] = entry; - return this; - } + var limits = entry.Limits; - /// - /// Queries all registered fields from the root VisualElement and - /// registers value-changed callbacks that rollback invalid values. - /// - public void SetupValidation() - { - foreach (var (fieldName, entry) in _fields) + field.RegisterValueChangedCallback(evt => { - var field = _rootVisualElement.Q(fieldName); - if (field == null) + if (string.IsNullOrEmpty(field.text)) { - Melon.Logger.Warning( - $"DoubleField '{fieldName}'을(를) 찾을 수 없습니다. 유효성 검사를 건너뜁니다."); - continue; + return; } - var limits = entry.Limits; - - field.RegisterValueChangedCallback(evt => + if (limits.Any(limit => !limit.Validate(evt.newValue, out _))) { - if (string.IsNullOrEmpty(field.text)) - { - return; - } - - if (limits.Any(limit => !limit.Validate(evt.newValue, out _))) - { - field.SetValueWithoutNotify(evt.previousValue); - } - }); - } + field.SetValueWithoutNotify(evt.previousValue); + } + }); } } -} +} \ No newline at end of file diff --git a/Data/Input/EnumDropdownBinder.cs b/Data/Input/EnumDropdownBinder.cs index 470be45..6e0e88d 100644 --- a/Data/Input/EnumDropdownBinder.cs +++ b/Data/Input/EnumDropdownBinder.cs @@ -1,86 +1,87 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using MelonLoader; using UnityEngine.UIElements; -namespace ADOFAIRenderer.Data.Input +namespace ADOFAIRenderer.Data.Input; + +/// +/// Binds a UI Toolkit to an enum type. +/// Populates choices from enum names and provides typed access to the selected value. +/// +/// The enum type to bind. +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +internal class EnumDropdownBinder where T : struct, Enum { + private readonly DropdownField? _dropdown; + private readonly List _values; + private Action? _onValueChanged; + /// - /// Binds a UI Toolkit to an enum type. - /// Populates choices from enum names and provides typed access to the selected value. + /// The currently selected enum value. /// - /// The enum type to bind. - internal class EnumDropdownBinder where T : struct, Enum + public T Value => _dropdown is { index: >= 0 } && _dropdown.index < _values.Count + ? _values[_dropdown.index] + : default; + + /// + /// Queries a by name from the root VisualElement, + /// populates it with enum names, and selects the default value. + /// + /// The root VisualElement loaded from the UXML bundle. + /// The name of the DropdownField in the UXML. + /// The initial selected value. Defaults to the first enum member. + public EnumDropdownBinder(VisualElement rootVisualElement, string fieldName, T? defaultValue = null) { - private readonly DropdownField? _dropdown; - private readonly List _values; - private Action? _onValueChanged; + _values = Enum.GetValues(typeof(T)).Cast().ToList(); - /// - /// The currently selected enum value. - /// - public T Value => _dropdown is { index: >= 0 } && _dropdown.index < _values.Count - ? _values[_dropdown.index] - : default; - - /// - /// Queries a by name from the root VisualElement, - /// populates it with enum names, and selects the default value. - /// - /// The root VisualElement loaded from the UXML bundle. - /// The name of the DropdownField in the UXML. - /// The initial selected value. Defaults to the first enum member. - public EnumDropdownBinder(VisualElement rootVisualElement, string fieldName, T? defaultValue = null) + _dropdown = rootVisualElement.Q(fieldName); + if (_dropdown == null) { - _values = Enum.GetValues(typeof(T)).Cast().ToList(); - - _dropdown = rootVisualElement.Q(fieldName); - if (_dropdown == null) - { - Melon.Logger.Error( - $"DropdownField '{fieldName}'을(를) 찾을 수 없습니다."); - return; - } - - _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); + Melon.Logger.Error( + $"DropdownField '{fieldName}'을(를) 찾을 수 없습니다."); + return; } - /// - /// Registers a callback that fires when the selection changes, providing the new enum value. - /// - public void OnValueChanged(Action callback) - { - _onValueChanged = callback; + _dropdown.choices = _values.Select(v => v.ToString()).ToList(); - // Fire immediately with current value so the caller gets the initial state - if (_dropdown != null) - { - callback(Value); - } - } - - /// - /// Sets the selected enum value and updates the dropdown index. - /// - /// The enum value to set. - public void SetValue(T value) + // Register the callback BEFORE setting index so the initial selection fires it too + _dropdown.RegisterValueChangedCallback(_ => { - if (_dropdown == null) return; - int idx = _values.IndexOf(value); - if (idx >= 0) - { - _dropdown.index = idx; - } + _onValueChanged?.Invoke(Value); + }); + + var initial = defaultValue ?? _values[0]; + _dropdown.index = _values.IndexOf(initial); + } + + /// + /// Registers a callback that fires when the selection changes, providing the new enum value. + /// + public void OnValueChanged(Action callback) + { + _onValueChanged = callback; + + // Fire immediately with current value so the caller gets the initial state + if (_dropdown != null) + { + callback(Value); } } -} + + /// + /// Sets the selected enum value and updates the dropdown index. + /// + /// The enum value to set. + public void SetValue(T value) + { + if (_dropdown == null) return; + int idx = _values.IndexOf(value); + if (idx >= 0) + { + _dropdown.index = idx; + } + } +} \ No newline at end of file diff --git a/Data/Input/IntegerInputValidator.cs b/Data/Input/IntegerInputValidator.cs index bfb378a..f19bd68 100644 --- a/Data/Input/IntegerInputValidator.cs +++ b/Data/Input/IntegerInputValidator.cs @@ -3,75 +3,74 @@ using System.Linq; using MelonLoader; using UnityEngine.UIElements; -namespace ADOFAIRenderer.Data.Input +namespace ADOFAIRenderer.Data.Input; + +internal class IntegerInputValidator { - internal class IntegerInputValidator + private struct FieldEntry { - private struct FieldEntry - { - public List Limits; - } + public List Limits; + } - private readonly VisualElement _rootVisualElement; - private readonly Dictionary _fields = new(); + private readonly VisualElement _rootVisualElement; + private readonly Dictionary _fields = new(); - public IntegerInputValidator(VisualElement rootVisualElement) - { - _rootVisualElement = rootVisualElement; - } + public IntegerInputValidator(VisualElement rootVisualElement) + { + _rootVisualElement = rootVisualElement; + } - /// - /// Registers one or more s for the field with the given name. - /// Multiple limits can be added to the same field — they are all checked in order. - /// - /// The name of the IntegerField in the UXML. - /// The limits to apply. - public IntegerInputValidator AddLimits(string fieldName, params IntegerLimit[] limits) + /// + /// Registers one or more s for the field with the given name. + /// Multiple limits can be added to the same field — they are all checked in order. + /// + /// The name of the IntegerField in the UXML. + /// The limits to apply. + public IntegerInputValidator AddLimits(string fieldName, params IntegerLimit[] limits) + { + if (!_fields.TryGetValue(fieldName, out var entry)) { - if (!_fields.TryGetValue(fieldName, out var entry)) + entry = new FieldEntry { - entry = new FieldEntry - { - Limits = new List() - }; + Limits = new List() + }; + } + + entry.Limits.AddRange(limits); + _fields[fieldName] = entry; + return this; + } + + /// + /// Queries all registered fields from the root VisualElement and + /// registers value-changed callbacks that rollback invalid values. + /// + public void SetupValidation() + { + foreach (var (fieldName, entry) in _fields) + { + var field = _rootVisualElement.Q(fieldName); + if (field == null) + { + Melon.Logger.Warning( + $"IntegerField '{fieldName}'을(를) 찾을 수 없습니다. 유효성 검사를 건너뜁니다."); + continue; } - entry.Limits.AddRange(limits); - _fields[fieldName] = entry; - return this; - } + var limits = entry.Limits; - /// - /// Queries all registered fields from the root VisualElement and - /// registers value-changed callbacks that rollback invalid values. - /// - public void SetupValidation() - { - foreach (var (fieldName, entry) in _fields) + field.RegisterValueChangedCallback(evt => { - var field = _rootVisualElement.Q(fieldName); - if (field == null) + if (string.IsNullOrEmpty(field.text)) { - Melon.Logger.Warning( - $"IntegerField '{fieldName}'을(를) 찾을 수 없습니다. 유효성 검사를 건너뜁니다."); - continue; + return; } - var limits = entry.Limits; - - field.RegisterValueChangedCallback(evt => + if (limits.Any(limit => !limit.Validate(evt.newValue, out _))) { - if (string.IsNullOrEmpty(field.text)) - { - return; - } - - if (limits.Any(limit => !limit.Validate(evt.newValue, out _))) - { - field.SetValueWithoutNotify(evt.previousValue); - } - }); - } + field.SetValueWithoutNotify(evt.previousValue); + } + }); } } } \ No newline at end of file diff --git a/Data/IntegerLimit.cs b/Data/IntegerLimit.cs index 285e4c6..01496b4 100644 --- a/Data/IntegerLimit.cs +++ b/Data/IntegerLimit.cs @@ -1,78 +1,79 @@ -namespace ADOFAIRenderer.Data +namespace ADOFAIRenderer.Data; + +/// +/// Base class for integer input validation limits. +/// Multiple limits can be composed on a single input field — all are checked in order. +/// +internal abstract class IntegerLimit { /// - /// Base class for integer input validation limits. - /// Multiple limits can be composed on a single input field — all are checked in order. + /// Validates the given integer value against this limit. /// - internal abstract class IntegerLimit + /// The parsed integer value to validate. + /// + /// When validation fails, set to a human-readable error message. + /// When validation succeeds, set to null. + /// + /// true if the value satisfies this limit; false otherwise. + public abstract bool Validate(int value, out string errorMessage); +} + +/// +/// Requires the integer value to be strictly positive (> 0). +/// +internal class PositiveOnlyIntegerLimit : IntegerLimit +{ + public override bool Validate(int value, out string errorMessage) { - /// - /// Validates the given integer value against this limit. - /// - /// The parsed integer value to validate. - /// - /// When validation fails, set to a human-readable error message. - /// When validation succeeds, set to null. - /// - /// true if the value satisfies this limit; false otherwise. - public abstract bool Validate(int value, out string errorMessage); - } - - /// - /// Requires the integer value to be strictly positive (> 0). - /// - internal class PositiveOnlyIntegerLimit : IntegerLimit - { - public override bool Validate(int value, out string errorMessage) + if (value > 0) { - if (value > 0) - { - errorMessage = null!; - return true; - } - - errorMessage = "값은 0보다 큰 양의 정수여야 합니다."; - return false; - } - } - - /// - /// Requires the integer value to be within an inclusive [Min, Max] range. - /// Either bound can be null to represent an unbounded side. - /// - internal class RangeIntegerLimit : IntegerLimit - { - public int? Min { get; } - public int? Max { get; } - - /// Inclusive lower bound, or null for no lower bound. - /// Inclusive upper bound, or null for no upper bound. - public RangeIntegerLimit(int? min, int? max) - { - Min = min; - Max = max; - } - - public override bool Validate(int value, out string errorMessage) - { - if (Min.HasValue && value < Min.Value) - { - errorMessage = Max.HasValue - ? $"값은 {Min.Value}에서 {Max.Value} 사이의 정수여야 합니다." - : $"값은 {Min.Value} 이상의 정수여야 합니다."; - return false; - } - - if (Max.HasValue && value > Max.Value) - { - errorMessage = Min.HasValue - ? $"값은 {Min.Value}에서 {Max.Value} 사이의 정수여야 합니다." - : $"값은 {Max.Value} 이하의 정수여야 합니다."; - return false; - } - errorMessage = null!; return true; } + + errorMessage = "값은 0보다 큰 양의 정수여야 합니다."; + return false; } } + +/// +/// Requires the integer value to be within an inclusive [Min, Max] range. +/// Either bound can be null to represent an unbounded side. +/// +internal class RangeIntegerLimit : IntegerLimit +{ + // ReSharper disable MemberCanBePrivate.Global + public int? Min { get; } + public int? Max { get; } + // ReSharper restore MemberCanBePrivate.Global + + /// Inclusive lower bound, or null for no lower bound. + /// Inclusive upper bound, or null for no upper bound. + public RangeIntegerLimit(int? min, int? max) + { + Min = min; + Max = max; + } + + public override bool Validate(int value, out string errorMessage) + { + if (value < Min) + { + errorMessage = Max.HasValue + ? $"값은 {Min.Value}에서 {Max.Value} 사이의 정수여야 합니다." + : $"값은 {Min.Value} 이상의 정수여야 합니다."; + return false; + } + + if (value > Max) + { + errorMessage = Min.HasValue + ? $"값은 {Min.Value}에서 {Max.Value} 사이의 정수여야 합니다." + : $"값은 {Max.Value} 이하의 정수여야 합니다."; + return false; + } + + errorMessage = null!; + return true; + } +} \ No newline at end of file diff --git a/Data/PresetManager.cs b/Data/PresetManager.cs index 344213a..d4393d3 100644 --- a/Data/PresetManager.cs +++ b/Data/PresetManager.cs @@ -7,184 +7,183 @@ using MelonLoader.Utils; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace ADOFAIRenderer.Data +namespace ADOFAIRenderer.Data; + +internal class RenderPreset { - internal class RenderPreset + public string Name { get; set; } = string.Empty; + public RenderConfig Config { get; set; } = null!; +} + +internal static class PresetManager +{ + private static string GetPresetsDirectory() { - public string Name { get; set; } = string.Empty; - public RenderConfig Config { get; set; } = null!; + return Path.Combine(MelonEnvironment.GameRootDirectory, "UserData", "ADOFAIRenderer", "Presets"); } - internal static class PresetManager + private static string GetSafeFileName(string name) { - private static string GetPresetsDirectory() + return Path.GetInvalidFileNameChars().Aggregate(name, (current, c) => current.Replace(c, '_')); + } + + public static void SavePreset(string name, RenderConfig config) + { + if (string.IsNullOrWhiteSpace(name)) { - return Path.Combine(MelonEnvironment.GameRootDirectory, "UserData", "ADOFAIRenderer", "Presets"); + throw new ArgumentException("프리셋 이름은 비어 있을 수 없습니다."); } - private static string GetSafeFileName(string name) + string dir = GetPresetsDirectory(); + if (!Directory.Exists(dir)) { - return Path.GetInvalidFileNameChars().Aggregate(name, (current, c) => current.Replace(c, '_')); + Directory.CreateDirectory(dir); } - public static void SavePreset(string name, RenderConfig config) + var preset = new RenderPreset { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentException("프리셋 이름은 비어 있을 수 없습니다."); - } + Name = name, + Config = config + }; - string dir = GetPresetsDirectory(); - if (!Directory.Exists(dir)) - { - Directory.CreateDirectory(dir); - } + string json = JsonConvert.SerializeObject(preset, Formatting.Indented); + string safeName = GetSafeFileName(name); + string filePath = Path.Combine(dir, safeName + ".json"); + File.WriteAllText(filePath, json); + } - var preset = new RenderPreset - { - Name = name, - Config = config - }; - - string json = JsonConvert.SerializeObject(preset, Formatting.Indented); - string safeName = GetSafeFileName(name); - string filePath = Path.Combine(dir, safeName + ".json"); - File.WriteAllText(filePath, json); - } - - public static List GetPresets() - { - var presets = new List(); - var dir = GetPresetsDirectory(); + public static List GetPresets() + { + var presets = new List(); + var dir = GetPresetsDirectory(); - if (!Directory.Exists(dir)) return presets; + if (!Directory.Exists(dir)) return presets; - try + try + { + var files = Directory.GetFiles(dir, "*.json"); + foreach (var file in files) { - var files = Directory.GetFiles(dir, "*.json"); - foreach (var file in files) + try { - try - { - var json = File.ReadAllText(file); - var jObject = JsonConvert.DeserializeObject(json); - if (jObject == null) continue; + 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"); + 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"); - } + // 1. legacy EncodingConfig -> VideoConfig + if (configObj["EncodingConfig"] is JObject oldEncodingConfigObj) + { + configObj["VideoConfig"] ??= oldEncodingConfigObj; + configObj.Remove("EncodingConfig"); } - - var preset = jObject.ToObject(); - if (preset != null && !string.IsNullOrEmpty(preset.Name)) + + // 2. legacy top-level Codec -> VideoConfig.Codec + var legacyCodecToken = configObj["Codec"]; + if (legacyCodecToken != null && legacyCodecToken.Type != JTokenType.Null) { - presets.Add(preset); + 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"); } } - catch (Exception ex) + + var preset = jObject.ToObject(); + if (preset != null && !string.IsNullOrEmpty(preset.Name)) { - MelonLogger.Warning($"프리셋 파일 '{Path.GetFileName(file)}' 로드 중 오류 발생: {ex.Message}"); + presets.Add(preset); } } + catch (Exception ex) + { + MelonLogger.Warning($"프리셋 파일 '{Path.GetFileName(file)}' 로드 중 오류 발생: {ex.Message}"); + } } - catch (Exception ex) - { - MelonLogger.Error($"프리셋 목록을 불러오는 중 오류 발생: {ex.Message}"); - } - - return presets; + } + catch (Exception ex) + { + MelonLogger.Error($"프리셋 목록을 불러오는 중 오류 발생: {ex.Message}"); } - private static void TryChangeKey(JObject configObj, string keyBefore, string targetKey) + 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) + { + string dir = GetPresetsDirectory(); + string safeName = GetSafeFileName(name); + string filePath = Path.Combine(dir, safeName + ".json"); + + if (File.Exists(filePath)) { - var legacyToken = configObj[keyBefore]; - if (legacyToken != null && legacyToken.Type != JTokenType.Null) - configObj[targetKey] = legacyToken; - - configObj.Remove(keyBefore); - } - - public static void DeletePreset(string name) - { - string dir = GetPresetsDirectory(); - string safeName = GetSafeFileName(name); - string filePath = Path.Combine(dir, safeName + ".json"); - - if (File.Exists(filePath)) - { - File.Delete(filePath); - } + File.Delete(filePath); } } -} +} \ No newline at end of file diff --git a/Data/RenderConfig.cs b/Data/RenderConfig.cs index 33235c1..b6629d8 100644 --- a/Data/RenderConfig.cs +++ b/Data/RenderConfig.cs @@ -1,71 +1,70 @@ using System.Diagnostics.CodeAnalysis; using ADOFAIRenderer.Util.FFmpeg; -namespace ADOFAIRenderer.Data +namespace ADOFAIRenderer.Data; + +[SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")] +public class RenderConfig { - [SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")] - public class RenderConfig - { - public double RenderFPS { get; set; } - public double VideoFPS { get; set; } + public double RenderFPS { get; set; } + public double VideoFPS { get; set; } - public int RenderWidth { get; set; } - public int RenderHeight { 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 VideoEncodingConfig VideoConfig { get; set; } = new(); + public AudioEncodingConfig AudioConfig { get; set; } = new(); + public AdvancedConfig AdvancedConfig { get; set; } = new(); - public double DelayAfterEnd { get; set; } - public bool ShowJudgementText { get; set; } = false; - } + public double DelayAfterEnd { get; set; } + 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"; - } +[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 class VideoEncodingConfig - { - public Codec Codec { get; set; } = Codec.H264; - public RateControlMode RateControlMode { get; set; } = RateControlMode.CBR; +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 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 = 44100; + private int _audioChannels = 2; + private int _audioBitrate = 256000; + + public bool RenderAudio { get; set; } = true; + + public int AudioSampleRate + { + get => _audioSampleRate <= 0 ? 44100 : _audioSampleRate; + set => _audioSampleRate = value <= 0 ? 44100 : value; } - public class AudioEncodingConfig + public int AudioChannels { - private int _audioSampleRate = 44100; - private int _audioChannels = 2; - private int _audioBitrate = 256000; + get => _audioChannels <= 0 ? 2 : _audioChannels; + set => _audioChannels = value <= 0 ? 2 : value; + } - public bool RenderAudio { get; set; } = true; - - public int AudioSampleRate - { - get => _audioSampleRate <= 0 ? 44100 : _audioSampleRate; - set => _audioSampleRate = value <= 0 ? 44100 : 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 int AudioBitrate + { + get => _audioBitrate <= 0 ? 256000 : _audioBitrate; + set => _audioBitrate = value <= 0 ? 256000 : value; } } \ No newline at end of file diff --git a/Patch/Engine/AsyncInputPatches.cs b/Patch/Engine/AsyncInputPatches.cs index 6b1dc2d..7b48d63 100644 --- a/Patch/Engine/AsyncInputPatches.cs +++ b/Patch/Engine/AsyncInputPatches.cs @@ -1,21 +1,20 @@ using ADOFAIRenderer.Core; using HarmonyLib; -namespace ADOFAIRenderer.Patch.Engine +namespace ADOFAIRenderer.Patch.Engine; + +[HarmonyPatch(typeof(AsyncInputUtils), "UpdateOffsetTime")] +// ReSharper disable once InconsistentNaming +public static class AsyncInputPatches { - [HarmonyPatch(typeof(AsyncInputUtils), "UpdateOffsetTime")] + [HarmonyPrefix] // ReSharper disable once InconsistentNaming - public static class AsyncInputPatches + public static bool Prefix() { - [HarmonyPrefix] - // ReSharper disable once InconsistentNaming - public static bool Prefix() - { - if (!RenderManager.IsActive) return true; - // Force offsetTick to match instantly without 1/100 damping division when rendering - AsyncInputManager.offsetTick = AsyncInputManager.currFrameTick - (ulong)(scrConductor.instance.dspTime * 10000000.0); - AsyncInputManager.offsetTickUpdated = true; - return false; // Skip the original damped offset calculation - } + if (!RenderManager.IsActive) return true; + // Force offsetTick to match instantly without 1/100 damping division when rendering + AsyncInputManager.offsetTick = AsyncInputManager.currFrameTick - (ulong)(scrConductor.instance.dspTime * 10000000.0); + AsyncInputManager.offsetTickUpdated = true; + return false; // Skip the original damped offset calculation } -} +} \ No newline at end of file diff --git a/Patch/Engine/AudioPatches.cs b/Patch/Engine/AudioPatches.cs index b6f3dc8..c79495c 100644 --- a/Patch/Engine/AudioPatches.cs +++ b/Patch/Engine/AudioPatches.cs @@ -4,110 +4,109 @@ using HarmonyLib; using UnityEngine; using UnityEngine.Audio; -namespace ADOFAIRenderer.Patch.Engine +namespace ADOFAIRenderer.Patch.Engine; + +[HarmonyPatch(typeof(AudioManager), "_Play")] +public static class AudioManagerPlayPatch { - [HarmonyPatch(typeof(AudioManager), "_Play")] - public static class AudioManagerPlayPatch + [HarmonyPrefix] + public static bool Prefix(string snd, double time, AudioMixerGroup group, float volume, int priority) { - [HarmonyPrefix] - public static bool Prefix(string snd, double time, AudioMixerGroup group, float volume, int priority) + if (RenderManager.IsCapturingActive && RenderManager.Instance.Configuration.AudioConfig.RenderAudio) { - if (RenderManager.IsCapturingActive && RenderManager.Instance.Configuration.AudioConfig.RenderAudio) + // Cache the clip on the main thread + RenderManager.Instance.AudioMixer?.CacheClip(snd); + } + return true; // Let the original method execute to prevent NullReferenceException + } + + [HarmonyPostfix] + // ReSharper disable once InconsistentNaming + public static void Postfix(AudioSource __result, string snd, double time, AudioMixerGroup group, float volume, int priority) + { + if (RenderManager.IsCapturingActive && RenderManager.Instance.Configuration.AudioConfig.RenderAudio && __result != null) + { + // Convert absolute DSP time to relative time in seconds from render start + double relativeStartTime = time - RenderManager.Instance.dspTimeBase; + + // Register event to the offline mixer (default pitch = 1.0f) and map it to the AudioSource + RenderManager.Instance.AudioMixer?.RegisterSoundEvent(__result, snd, relativeStartTime, -1.0, volume, 1.0f); + } + } +} + +[HarmonyPatch(typeof(scrConductor), "PlayWithEndTime")] +public static class PlayWithEndTimePatch +{ + [HarmonyPrefix] + public static bool Prefix(string snd, double time, double endTime, float volume, int priority) + { + if (RenderManager.IsCapturingActive && RenderManager.Instance.Configuration.AudioConfig.RenderAudio) + { + var mixer = RenderManager.Instance.AudioMixer; + if (mixer != null) { // Cache the clip on the main thread - RenderManager.Instance.AudioMixer?.CacheClip(snd); - } - return true; // Let the original method execute to prevent NullReferenceException - } + mixer.CacheClip(snd); - [HarmonyPostfix] - // ReSharper disable once InconsistentNaming - public static void Postfix(AudioSource __result, string snd, double time, AudioMixerGroup group, float volume, int priority) - { - if (RenderManager.IsCapturingActive && RenderManager.Instance.Configuration.AudioConfig.RenderAudio && __result != null) - { - // Convert absolute DSP time to relative time in seconds from render start + // Convert absolute DSP times to relative times in seconds from render start double relativeStartTime = time - RenderManager.Instance.dspTimeBase; + double relativeEndTime = endTime - RenderManager.Instance.dspTimeBase; - // Register event to the offline mixer (default pitch = 1.0f) and map it to the AudioSource - RenderManager.Instance.AudioMixer?.RegisterSoundEvent(__result, snd, relativeStartTime, -1.0, volume, 1.0f); + // Register looping sound event to the offline mixer (default pitch = 1.0f) + mixer.RegisterSoundEvent(snd, relativeStartTime, relativeEndTime, volume, 1.0f); } } + return true; // Let the original method execute } +} - [HarmonyPatch(typeof(scrConductor), "PlayWithEndTime")] - public static class PlayWithEndTimePatch +[HarmonyPatch(typeof(AudioSource), "pitch", MethodType.Setter)] +public static class AudioSourcePitchPatch +{ + [HarmonyPostfix] + // ReSharper disable once InconsistentNaming + public static void Postfix(AudioSource __instance, float value) { - [HarmonyPrefix] - public static bool Prefix(string snd, double time, double endTime, float volume, int priority) + if (RenderManager.IsCapturingActive && RenderManager.Instance.Configuration.AudioConfig.RenderAudio) { - if (RenderManager.IsCapturingActive && RenderManager.Instance.Configuration.AudioConfig.RenderAudio) + var mixer = RenderManager.Instance.AudioMixer; + if (mixer != null && mixer.TryGetSoundEvent(__instance, out var soundEvent)) { - var mixer = RenderManager.Instance.AudioMixer; - if (mixer != null) - { - // Cache the clip on the main thread - mixer.CacheClip(snd); - - // Convert absolute DSP times to relative times in seconds from render start - double relativeStartTime = time - RenderManager.Instance.dspTimeBase; - double relativeEndTime = endTime - RenderManager.Instance.dspTimeBase; - - // Register looping sound event to the offline mixer (default pitch = 1.0f) - mixer.RegisterSoundEvent(snd, relativeStartTime, relativeEndTime, volume, 1.0f); - } - } - return true; // Let the original method execute - } - } - - [HarmonyPatch(typeof(AudioSource), "pitch", MethodType.Setter)] - public static class AudioSourcePitchPatch - { - [HarmonyPostfix] - // ReSharper disable once InconsistentNaming - public static void Postfix(AudioSource __instance, float value) - { - if (RenderManager.IsCapturingActive && RenderManager.Instance.Configuration.AudioConfig.RenderAudio) - { - var mixer = RenderManager.Instance.AudioMixer; - if (mixer != null && mixer.TryGetSoundEvent(__instance, out var soundEvent)) - { - soundEvent.Pitch = value; - } - } - } - } - - [HarmonyPatch(typeof(AudioSource), "SetScheduledEndTime", typeof(double))] - public static class AudioSourceSetScheduledEndTimePatch - { - [HarmonyPostfix] - // ReSharper disable once InconsistentNaming - public static void Postfix(AudioSource __instance, double time) - { - if (RenderManager.IsCapturingActive && RenderManager.Instance.Configuration.AudioConfig.RenderAudio) - { - var mixer = RenderManager.Instance.AudioMixer; - if (mixer != null && mixer.TryGetSoundEvent(__instance, out var soundEvent)) - { - soundEvent.EndTime = time - RenderManager.Instance.dspTimeBase; - } - } - } - } - - [HarmonyPatch(typeof(AudioSource), "Stop", new Type[0])] - public static class AudioSourceStopPatch - { - [HarmonyPostfix] - // ReSharper disable once InconsistentNaming - public static void Postfix(AudioSource __instance) - { - if (RenderManager.IsCapturingActive && RenderManager.Instance.Configuration.AudioConfig.RenderAudio) - { - RenderManager.Instance.AudioMixer?.RemoveSourceMapping(__instance); + soundEvent.Pitch = value; } } } } + +[HarmonyPatch(typeof(AudioSource), "SetScheduledEndTime", typeof(double))] +public static class AudioSourceSetScheduledEndTimePatch +{ + [HarmonyPostfix] + // ReSharper disable once InconsistentNaming + public static void Postfix(AudioSource __instance, double time) + { + if (RenderManager.IsCapturingActive && RenderManager.Instance.Configuration.AudioConfig.RenderAudio) + { + var mixer = RenderManager.Instance.AudioMixer; + if (mixer != null && mixer.TryGetSoundEvent(__instance, out var soundEvent)) + { + soundEvent.EndTime = time - RenderManager.Instance.dspTimeBase; + } + } + } +} + +[HarmonyPatch(typeof(AudioSource), "Stop", new Type[0])] +public static class AudioSourceStopPatch +{ + [HarmonyPostfix] + // ReSharper disable once InconsistentNaming + public static void Postfix(AudioSource __instance) + { + if (RenderManager.IsCapturingActive && RenderManager.Instance.Configuration.AudioConfig.RenderAudio) + { + RenderManager.Instance.AudioMixer?.RemoveSourceMapping(__instance); + } + } +} \ No newline at end of file diff --git a/Patch/Engine/RenderEnvironmentPatches.cs b/Patch/Engine/RenderEnvironmentPatches.cs index 5f26017..726aad0 100644 --- a/Patch/Engine/RenderEnvironmentPatches.cs +++ b/Patch/Engine/RenderEnvironmentPatches.cs @@ -1,168 +1,165 @@ using System.Diagnostics.CodeAnalysis; using ADOFAIRenderer.Core; -using ADOFAIRenderer.Util; using ADOFAIRenderer.Util.Audio; using HarmonyLib; using UnityEngine; -namespace ADOFAIRenderer.Patch.Engine +namespace ADOFAIRenderer.Patch.Engine; + +public static class RenderEnvironmentPatches { - public static class RenderEnvironmentPatches + private static double TimeSpent => RenderManager.Instance.frameCount / RenderManager.Instance.Configuration.RenderFPS; + + [HarmonyPatch(typeof(Time), "time", MethodType.Getter)] + public static class TimePatch { - 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 + [SuppressMessage("ReSharper", "InconsistentNaming")] + public static bool Prefix(ref float __result) { - [SuppressMessage("ReSharper", "InconsistentNaming")] - public static bool Prefix(ref float __result) - { - if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true; - __result = (float)(TimeSpent + RenderManager.Instance.timeBase); - return false; - } - } - - [HarmonyPatch(typeof(Time), "timeAsDouble", MethodType.Getter)] - public static class TimeAsDoublePatch - { - [SuppressMessage("ReSharper", "InconsistentNaming")] - public static bool Prefix(ref double __result) - { - if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true; - __result = TimeSpent + RenderManager.Instance.timeBase; - return false; - } - } - - [HarmonyPatch(typeof(Time), "unscaledTime", MethodType.Getter)] - public static class UnscaledTimePatch - { - [SuppressMessage("ReSharper", "InconsistentNaming")] - public static bool Prefix(ref float __result) - { - if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true; - __result = (float)(TimeSpent + RenderManager.Instance.unscaledTimeBase); - return false; - } - } - - [HarmonyPatch(typeof(Time), "unscaledTimeAsDouble", MethodType.Getter)] - public static class UnscaledTimeAsDoublePatch - { - [SuppressMessage("ReSharper", "InconsistentNaming")] - public static bool Prefix(ref double __result) - { - if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true; - __result = TimeSpent + RenderManager.Instance.unscaledTimeBase; - return false; - } - } - - [HarmonyPatch(typeof(AudioSettings), "dspTime", MethodType.Getter)] - public static class DspTimePatch - { - [SuppressMessage("ReSharper", "InconsistentNaming")] - public static bool Prefix(ref double __result) - { - if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true; - __result = TimeSpent + RenderManager.Instance.dspTimeBase; - return false; - } - } - - [HarmonyPatch(typeof(AudioSource), "PlayScheduled")] - public static class PlayScheduledPatch - { - // ReSharper disable once InconsistentNaming - public static bool Prefix(AudioSource __instance, double time) - { - return !RenderManager.IsActive; - } - } - - [HarmonyPatch(typeof(Time), "frameCount", MethodType.Getter)] - public static class FrameCountPatch - { - [SuppressMessage("ReSharper", "InconsistentNaming")] - public static bool Prefix(ref int __result) - { - if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true; - __result = (int)(RenderManager.Instance.frameCount + RenderManager.Instance.frameCountBase); - return false; - } - } - - [HarmonyPatch(typeof(Time), "deltaTime", MethodType.Getter)] - public static class DeltaTimePatch - { - [SuppressMessage("ReSharper", "InconsistentNaming")] - public static bool Prefix(ref float __result) - { - if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true; - __result = (float)(1 / RenderManager.Instance.Configuration.RenderFPS); - return false; - } - } - - [HarmonyPatch(typeof(Time), "unscaledDeltaTime", MethodType.Getter)] - public static class UnscaledDeltaTimePatch - { - [SuppressMessage("ReSharper", "InconsistentNaming")] - public static bool Prefix(ref float __result) - { - if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true; - __result = (float)(1 / RenderManager.Instance.Configuration.RenderFPS); - return false; - } - } - - [HarmonyPatch(typeof(Screen), "width", MethodType.Getter)] - public static class ScreenWidthPatch - { - [SuppressMessage("ReSharper", "InconsistentNaming")] - public static bool Prefix(ref int __result) - { - if (!RenderManager.IsActive || !RenderContext.ScreenPatch || !RenderManager.Instance) return true; - __result = RenderManager.Instance.Configuration.RenderWidth; - return false; - } - } - - [HarmonyPatch(typeof(Screen), "height", MethodType.Getter)] - public static class ScreenHeightPatch - { - [SuppressMessage("ReSharper", "InconsistentNaming")] - public static bool Prefix(ref int __result) - { - if (!RenderManager.IsActive || !RenderContext.ScreenPatch || !RenderManager.Instance) return true; - __result = RenderManager.Instance.Configuration.RenderHeight; - return false; - } - } - - [HarmonyPatch(typeof(AudioSource), "GetSpectrumData", typeof(float[]), typeof(int), typeof(FFTWindow))] - public static class GetSpectrumDataPatch - { - [SuppressMessage("ReSharper", "InconsistentNaming")] - public static bool Prefix(AudioSource __instance, float[] samples, int channel, FFTWindow window) - { - if (!RenderManager.IsCapturingActive || !RenderManager.Instance.Configuration.AdvancedConfig.ProcessAudio) return true; - AudioSpectrumGenerator.GetSpectrumData(samples, channel, window); - return false; - } - } - - [HarmonyPatch(typeof(AudioManager), "FindOrLoadAudioClipExternal", typeof(string), typeof(bool), typeof(float), typeof(bool))] - public static class FindOrLoadAudioClipExternalPatch - { - [SuppressMessage("ReSharper", "InconsistentNaming")] - public static void Prefix(ref bool stream) - { - if (RenderManager.Instance && RenderManager.Instance.OverrideLoading && RenderManager.Instance.Configuration.AdvancedConfig.ProcessAudio) - stream = false; - } + if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true; + __result = (float)(TimeSpent + RenderManager.Instance.timeBase); + return false; } } -} + + [HarmonyPatch(typeof(Time), "timeAsDouble", MethodType.Getter)] + public static class TimeAsDoublePatch + { + [SuppressMessage("ReSharper", "InconsistentNaming")] + public static bool Prefix(ref double __result) + { + if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true; + __result = TimeSpent + RenderManager.Instance.timeBase; + return false; + } + } + + [HarmonyPatch(typeof(Time), "unscaledTime", MethodType.Getter)] + public static class UnscaledTimePatch + { + [SuppressMessage("ReSharper", "InconsistentNaming")] + public static bool Prefix(ref float __result) + { + if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true; + __result = (float)(TimeSpent + RenderManager.Instance.unscaledTimeBase); + return false; + } + } + + [HarmonyPatch(typeof(Time), "unscaledTimeAsDouble", MethodType.Getter)] + public static class UnscaledTimeAsDoublePatch + { + [SuppressMessage("ReSharper", "InconsistentNaming")] + public static bool Prefix(ref double __result) + { + if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true; + __result = TimeSpent + RenderManager.Instance.unscaledTimeBase; + return false; + } + } + + [HarmonyPatch(typeof(AudioSettings), "dspTime", MethodType.Getter)] + public static class DspTimePatch + { + [SuppressMessage("ReSharper", "InconsistentNaming")] + public static bool Prefix(ref double __result) + { + if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true; + __result = TimeSpent + RenderManager.Instance.dspTimeBase; + return false; + } + } + + [HarmonyPatch(typeof(AudioSource), "PlayScheduled")] + public static class PlayScheduledPatch + { + // ReSharper disable once InconsistentNaming + public static bool Prefix(AudioSource __instance, double time) + { + return !RenderManager.IsActive; + } + } + + [HarmonyPatch(typeof(Time), "frameCount", MethodType.Getter)] + public static class FrameCountPatch + { + [SuppressMessage("ReSharper", "InconsistentNaming")] + public static bool Prefix(ref int __result) + { + if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true; + __result = (int)(RenderManager.Instance.frameCount + RenderManager.Instance.frameCountBase); + return false; + } + } + + [HarmonyPatch(typeof(Time), "deltaTime", MethodType.Getter)] + public static class DeltaTimePatch + { + [SuppressMessage("ReSharper", "InconsistentNaming")] + public static bool Prefix(ref float __result) + { + if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true; + __result = (float)(1 / RenderManager.Instance.Configuration.RenderFPS); + return false; + } + } + + [HarmonyPatch(typeof(Time), "unscaledDeltaTime", MethodType.Getter)] + public static class UnscaledDeltaTimePatch + { + [SuppressMessage("ReSharper", "InconsistentNaming")] + public static bool Prefix(ref float __result) + { + if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true; + __result = (float)(1 / RenderManager.Instance.Configuration.RenderFPS); + return false; + } + } + + [HarmonyPatch(typeof(Screen), "width", MethodType.Getter)] + public static class ScreenWidthPatch + { + [SuppressMessage("ReSharper", "InconsistentNaming")] + public static bool Prefix(ref int __result) + { + if (!RenderManager.IsActive || !RenderContext.ScreenPatch || !RenderManager.Instance) return true; + __result = RenderManager.Instance.Configuration.RenderWidth; + return false; + } + } + + [HarmonyPatch(typeof(Screen), "height", MethodType.Getter)] + public static class ScreenHeightPatch + { + [SuppressMessage("ReSharper", "InconsistentNaming")] + public static bool Prefix(ref int __result) + { + if (!RenderManager.IsActive || !RenderContext.ScreenPatch || !RenderManager.Instance) return true; + __result = RenderManager.Instance.Configuration.RenderHeight; + return false; + } + } + + [HarmonyPatch(typeof(AudioSource), "GetSpectrumData", typeof(float[]), typeof(int), typeof(FFTWindow))] + public static class GetSpectrumDataPatch + { + [SuppressMessage("ReSharper", "InconsistentNaming")] + public static bool Prefix(AudioSource __instance, float[] samples, int channel, FFTWindow window) + { + if (!RenderManager.IsCapturingActive || !RenderManager.Instance.Configuration.AdvancedConfig.ProcessAudio) return true; + AudioSpectrumGenerator.GetSpectrumData(samples, channel, window); + return false; + } + } + + [HarmonyPatch(typeof(AudioManager), "FindOrLoadAudioClipExternal", typeof(string), typeof(bool), typeof(float), typeof(bool))] + public static class FindOrLoadAudioClipExternalPatch + { + [SuppressMessage("ReSharper", "InconsistentNaming")] + public static void Prefix(ref bool stream) + { + if (RenderManager.Instance && RenderManager.Instance.OverrideLoading && RenderManager.Instance.Configuration.AdvancedConfig.ProcessAudio) + stream = false; + } + } +} \ No newline at end of file diff --git a/Patch/Game/EndscreenLanternsPatch.cs b/Patch/Game/EndscreenLanternsPatch.cs index a2a8acd..5ddb24d 100644 --- a/Patch/Game/EndscreenLanternsPatch.cs +++ b/Patch/Game/EndscreenLanternsPatch.cs @@ -2,73 +2,72 @@ using System.Diagnostics.CodeAnalysis; using ADOFAIRenderer.Core; using HarmonyLib; -namespace ADOFAIRenderer.Patch.Game +namespace ADOFAIRenderer.Patch.Game; + +[HarmonyPatch(typeof(EndscreenLanterns))] +// ReSharper disable once InconsistentNaming +public static class EndscreenLanternsPatch { - [HarmonyPatch(typeof(EndscreenLanterns))] - // ReSharper disable once InconsistentNaming - public static class EndscreenLanternsPatch + [HarmonyPrefix] + [HarmonyPatch("Show")] + [SuppressMessage("ReSharper", "InconsistentNaming")] + public static bool ForceHide(EndscreenLanterns __instance, ref int __result) { - [HarmonyPrefix] - [HarmonyPatch("Show")] - [SuppressMessage("ReSharper", "InconsistentNaming")] - public static bool ForceHide(EndscreenLanterns __instance, ref int __result) + if (!RenderManager.IsActive) return true; + if (__instance != null) { - if (!RenderManager.IsActive) return true; - if (__instance != null) - { - __instance.Hide(); - } - __result = 0; // Return 0 lanterns shown - return false; // Skip original Show method - } - - [HarmonyPrefix] - [HarmonyPatch("Hide")] - [SuppressMessage("ReSharper", "InconsistentNaming")] - public static void PrefixHide(EndscreenLanterns __instance) - { - if (__instance == null) return; - if (!RenderManager.IsActive) return; - - var traverse = Traverse.Create(__instance); - - // Kill lampSequences (private Sequence[] lampSequences) via reflection - var lampSequencesField = traverse.Field("lampSequences"); - var lampSequencesObj = lampSequencesField.GetValue(); - if (lampSequencesObj is System.Array lampSequencesArray) - { - for (var i = 0; i < lampSequencesArray.Length; i++) - { - var item = lampSequencesArray.GetValue(i); - if (item == null) continue; - try - { - var killMethod = item.GetType().GetMethod("Kill", new[] { typeof(bool) }); - killMethod?.Invoke(item, new object[] { false }); - } - catch - { - // Ignore reflection errors - } - lampSequencesArray.SetValue(null, i); - } - } - - // Kill chainSequence (private Sequence chainSequence) via reflection - var chainSequenceField = traverse.Field("chainSequence"); - var chainSequenceObj = chainSequenceField.GetValue(); - if (chainSequenceObj == null) return; - - try - { - var killMethod = chainSequenceObj.GetType().GetMethod("Kill", new[] { typeof(bool) }); - killMethod?.Invoke(chainSequenceObj, new object[] { false }); - } - catch - { - // Ignore reflection errors - } - chainSequenceField.SetValue(null); + __instance.Hide(); } + __result = 0; // Return 0 lanterns shown + return false; // Skip original Show method } -} + + [HarmonyPrefix] + [HarmonyPatch("Hide")] + [SuppressMessage("ReSharper", "InconsistentNaming")] + public static void PrefixHide(EndscreenLanterns __instance) + { + if (__instance == null) return; + if (!RenderManager.IsActive) return; + + var traverse = Traverse.Create(__instance); + + // Kill lampSequences (private Sequence[] lampSequences) via reflection + var lampSequencesField = traverse.Field("lampSequences"); + var lampSequencesObj = lampSequencesField.GetValue(); + if (lampSequencesObj is System.Array lampSequencesArray) + { + for (var i = 0; i < lampSequencesArray.Length; i++) + { + var item = lampSequencesArray.GetValue(i); + if (item == null) continue; + try + { + var killMethod = item.GetType().GetMethod("Kill", new[] { typeof(bool) }); + killMethod?.Invoke(item, new object[] { false }); + } + catch + { + // Ignore reflection errors + } + lampSequencesArray.SetValue(null, i); + } + } + + // Kill chainSequence (private Sequence chainSequence) via reflection + var chainSequenceField = traverse.Field("chainSequence"); + var chainSequenceObj = chainSequenceField.GetValue(); + if (chainSequenceObj == null) return; + + try + { + var killMethod = chainSequenceObj.GetType().GetMethod("Kill", new[] { typeof(bool) }); + killMethod?.Invoke(chainSequenceObj, new object[] { false }); + } + catch + { + // Ignore reflection errors + } + chainSequenceField.SetValue(null); + } +} \ No newline at end of file diff --git a/Patch/Game/PersistencePatch.cs b/Patch/Game/PersistencePatch.cs index f7a1e39..72106d5 100644 --- a/Patch/Game/PersistencePatch.cs +++ b/Patch/Game/PersistencePatch.cs @@ -1,22 +1,21 @@ using ADOFAIRenderer.Core; using HarmonyLib; -namespace ADOFAIRenderer.Patch.Game +namespace ADOFAIRenderer.Patch.Game; + +[HarmonyPatch(typeof(Persistence))] +public static class PersistencePatch { - [HarmonyPatch(typeof(Persistence))] - public static class PersistencePatch + [HarmonyPostfix] + [HarmonyPatch("skipIntroBehavior", MethodType.Getter)] + // ReSharper disable once InconsistentNaming + public static void LockSkipIntroBehavior(ref SkipIntroBehavior __result) { - [HarmonyPostfix] - [HarmonyPatch("skipIntroBehavior", MethodType.Getter)] - // ReSharper disable once InconsistentNaming - public static void LockSkipIntroBehavior(ref SkipIntroBehavior __result) + if (RenderManager.IsActive && + scrConductor.instance != null && + !scrConductor.instance.hasSongStarted) { - if (RenderManager.IsActive && - scrConductor.instance != null && - !scrConductor.instance.hasSongStarted) - { - __result = SkipIntroBehavior.Off; - } + __result = SkipIntroBehavior.Off; } } -} +} \ No newline at end of file diff --git a/Patch/Game/scnEditorPatch.cs b/Patch/Game/scnEditorPatch.cs index 263f782..ab904af 100644 --- a/Patch/Game/scnEditorPatch.cs +++ b/Patch/Game/scnEditorPatch.cs @@ -4,48 +4,47 @@ using MelonLoader; using UnityEngine; using UnityEngine.UI; -namespace ADOFAIRenderer.Patch.Game -{ - [HarmonyPatch(typeof(scnEditor))] - // ReSharper disable once InconsistentNaming - public static class scnEditorPatch - { - [HarmonyPostfix] - [HarmonyPatch("Start")] - // ReSharper disable once InconsistentNaming - public static void AddButtonRender(scnEditor __instance) - { - if (RenderMod.IsFeatureDisabled) return; - if (!__instance || !__instance.buttonExit) - { - Melon.Logger.Msg("scnEditor에 렌더 버튼을 만들 수 없습니다."); - return; - } +namespace ADOFAIRenderer.Patch.Game; - var cloneTarget = __instance.buttonExit!.gameObject; - var buttonContainer = cloneTarget.transform.parent!; - - var buttonObj = Object.Instantiate(cloneTarget, buttonContainer)!; - buttonObj.name = "buttonRender"; - - var button = buttonObj.GetComponent