SHA256
forked from ADOFAIEditorForum/ADOFAIRenderer
Refactor Every namespace Keyword Usage
This commit is contained in:
1 parent
4e67cb9b09
commit
27be2141f7
44 files changed
+4545
-4471
No files matched your search
@@ -110,4 +110,68 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FFmpeg.AutoGen" Version="8.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Build 후 XML 문서에서 ADOFAIRenderer.API 네임스페이스만 남기고 나머지 제거 -->
|
||||
<UsingTask TaskName="FilterXmlDoc" TaskFactory="RoslynCodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
|
||||
<ParameterGroup>
|
||||
<XmlFilePath ParameterType="System.String" Required="true" />
|
||||
<TargetNamespace ParameterType="System.String" Required="true" />
|
||||
</ParameterGroup>
|
||||
<Task>
|
||||
<Using Namespace="System" />
|
||||
<Using Namespace="System.IO" />
|
||||
<Using Namespace="System.Collections.Generic" />
|
||||
<Using Namespace="System.Xml.Linq" />
|
||||
<Code Type="Fragment" Language="cs">
|
||||
<![CDATA[
|
||||
if (!File.Exists(XmlFilePath)) return true;
|
||||
try
|
||||
{
|
||||
var doc = XDocument.Load(XmlFilePath);
|
||||
var members = doc.Root?.Element("members");
|
||||
if (members != null)
|
||||
{
|
||||
var prefix = TargetNamespace + ".";
|
||||
var toRemove = new List<XElement>();
|
||||
|
||||
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}");
|
||||
}
|
||||
]]>
|
||||
</Code>
|
||||
</Task>
|
||||
</UsingTask>
|
||||
|
||||
<Target Name="FilterDocumentationFile" AfterTargets="Build" Condition="'$(GenerateDocumentationFile)' == 'true'">
|
||||
<PropertyGroup>
|
||||
<_XmlDocPath Condition="'$(_XmlDocPath)' == '' and Exists('$(TargetDir)$(TargetName).xml')">$(TargetDir)$(TargetName).xml</_XmlDocPath>
|
||||
<_XmlDocPath Condition="'$(_XmlDocPath)' == '' and Exists('$(DocumentationFile)')">$(DocumentationFile)</_XmlDocPath>
|
||||
</PropertyGroup>
|
||||
<FilterXmlDoc Condition="'$(_XmlDocPath)' != ''" XmlFilePath="$(_XmlDocPath)" TargetNamespace="ADOFAIRenderer.API" />
|
||||
</Target>
|
||||
</Project>
|
||||
+35
-4
@@ -5,9 +5,10 @@ using System.IO;
|
||||
namespace ADOFAIRenderer.API;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result and metadata of a completed or cancelled render session.
|
||||
/// Represents the result and metadata of a completed or canceled render session.
|
||||
/// </summary>
|
||||
[SuppressMessage("ReSharper", "UnusedType.Global")]
|
||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
|
||||
public class RenderResult
|
||||
{
|
||||
/// <summary>
|
||||
@@ -41,10 +42,20 @@ public class RenderResult
|
||||
public long FileSizeBytes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message or reason if rendering failed or was cancelled; <c>null</c> on success.
|
||||
/// Error message or reason if rendering failed or was canceled; <c>null</c> on success.
|
||||
/// </summary>
|
||||
public string? ErrorMessage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RenderResult"/> class.
|
||||
/// </summary>
|
||||
/// <param name="success">Whether the render session completed successfully.</param>
|
||||
/// <param name="outputPath">The absolute output path of the rendered video file.</param>
|
||||
/// <param name="totalFrames">Total number of video frames sent to the encoder.</param>
|
||||
/// <param name="videoDuration">Calculated video duration in seconds.</param>
|
||||
/// <param name="renderDuration">Total wall-clock time spent rendering.</param>
|
||||
/// <param name="fileSizeBytes">File size of the output video in bytes.</param>
|
||||
/// <param name="errorMessage">Error message or reason if failed or canceled; otherwise, <c>null</c>.</param>
|
||||
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)
|
||||
/// <summary>
|
||||
/// Creates a successful <see cref="RenderResult"/> instance from the specified render metrics.
|
||||
/// </summary>
|
||||
/// <param name="outputPath">The path to the rendered video file.</param>
|
||||
/// <param name="totalFrames">Total number of video frames sent to the encoder.</param>
|
||||
/// <param name="fps">The effective frames per second used to compute video duration.</param>
|
||||
/// <param name="renderDuration">Total wall-clock time spent rendering.</param>
|
||||
/// <returns>A new <see cref="RenderResult"/> representing a successful render session.</returns>
|
||||
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)
|
||||
/// <summary>
|
||||
/// Creates a failed or cancelled <see cref="RenderResult"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="errorMessage">Error message or reason for failure/cancellation.</param>
|
||||
/// <param name="totalFrames">Total number of video frames sent to the encoder prior to termination.</param>
|
||||
/// <param name="fps">The effective frames per second used to compute video duration.</param>
|
||||
/// <param name="renderDuration">Total wall-clock time spent rendering prior to termination.</param>
|
||||
/// <returns>A new <see cref="RenderResult"/> representing a failed or canceled render session.</returns>
|
||||
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.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string summarizing the render result metrics or failure reason.
|
||||
/// </summary>
|
||||
/// <returns>A string representation of the <see cref="RenderResult"/>.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return Success
|
||||
|
||||
@@ -92,12 +92,22 @@ public class RenderSession
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets whether desktop notifications should be displayed when rendering completes.
|
||||
/// </summary>
|
||||
/// <param name="enabled"><c>true</c> to enable desktop notifications; otherwise, <c>false</c>.</param>
|
||||
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
|
||||
public RenderSession WithNotification(bool enabled)
|
||||
{
|
||||
_notification = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a callback action to be invoked when the render session completes or fails.
|
||||
/// </summary>
|
||||
/// <param name="callback">The callback action that receives the resulting <see cref="RenderResult"/>.</param>
|
||||
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
|
||||
public RenderSession OnComplete(Action<RenderResult> callback)
|
||||
{
|
||||
_onComplete += callback;
|
||||
@@ -109,6 +119,7 @@ public class RenderSession
|
||||
/// </summary>
|
||||
/// <param name="renderConfig">The configuration settings for the render session.</param>
|
||||
/// <param name="levelPath">The file path of the level to render.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown when the output directory cannot be determined from the output path.</exception>
|
||||
public void Start(RenderConfig renderConfig, string levelPath)
|
||||
{
|
||||
RenderContext.Auto = _auto;
|
||||
|
||||
+49
-50
@@ -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");
|
||||
_checkedNvidiaDriver = true;
|
||||
CheckNvidiaDriverWarning();
|
||||
}
|
||||
|
||||
CommonWarningDialogUI.ShowDialog(
|
||||
"NVIDIA 그래픽 드라이버 업데이트 권장",
|
||||
$"현재 감지된 NVIDIA 그래픽 드라이버 버전: <b><color=#ff5555>{currentVersion}</color></b>\n\n하드웨어 가속(NVENC)을 사용하여 영상을 렌더링하려면 드라이버 버전 <b><color=#55ff55>610.00 이상</color></b>이 필요합니다.\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("이번만 그냥 진행", () =>
|
||||
{
|
||||
// 아무 동작 안 함
|
||||
})
|
||||
);
|
||||
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 그래픽 드라이버 버전: <b><color=#ff5555>{currentVersion}</color></b>\n\n하드웨어 가속(NVENC)을 사용하여 영상을 렌더링하려면 드라이버 버전 <b><color=#55ff55>610.00 이상</color></b>이 필요합니다.\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("이번만 그냥 진행", () =>
|
||||
{
|
||||
// 아무 동작 안 함
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
+21
-22
@@ -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;
|
||||
_camQuadMeshField = AccessTools.Field(typeof(scrCamera), "camQuadMesh");
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_camQuadMeshField = AccessTools.Field(typeof(scrCamera), "camQuadMesh");
|
||||
}
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (RenderManager.Instance)
|
||||
RenderManager.Instance.OnLastUpdate();
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (RenderManager.Instance)
|
||||
RenderManager.Instance.OnLastUpdate();
|
||||
StartCoroutine(PostFrameCoroutine());
|
||||
}
|
||||
|
||||
StartCoroutine(PostFrameCoroutine());
|
||||
}
|
||||
private IEnumerator PostFrameCoroutine()
|
||||
{
|
||||
yield return new WaitForEndOfFrame();
|
||||
|
||||
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);
|
||||
}
|
||||
var val = (MeshRenderer)_camQuadMeshField!.GetValue(scrCamera.instance);
|
||||
if (RenderManager.Instance)
|
||||
RenderManager.Instance.OnPostFrame(val.sharedMaterial.mainTexture as RenderTexture);
|
||||
}
|
||||
}
|
||||
+69
-68
@@ -1,78 +1,79 @@
|
||||
namespace ADOFAIRenderer.Data
|
||||
namespace ADOFAIRenderer.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for double input validation limits.
|
||||
/// Multiple limits can be composed on a single input field — all are checked in order.
|
||||
/// </summary>
|
||||
internal abstract class DoubleLimit
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal abstract class DoubleLimit
|
||||
/// <param name="value">The parsed double value to validate.</param>
|
||||
/// <param name="errorMessage">
|
||||
/// When validation fails, set to a human-readable error message.
|
||||
/// When validation succeeds, set to null.
|
||||
/// </param>
|
||||
/// <returns>true if the value satisfies this limit; false otherwise.</returns>
|
||||
public abstract bool Validate(double value, out string errorMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requires the double value to be strictly positive (> 0).
|
||||
/// </summary>
|
||||
internal class PositiveOnlyDoubleLimit : DoubleLimit
|
||||
{
|
||||
public override bool Validate(double value, out string errorMessage)
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates the given double value against this limit.
|
||||
/// </summary>
|
||||
/// <param name="value">The parsed double value to validate.</param>
|
||||
/// <param name="errorMessage">
|
||||
/// When validation fails, set to a human-readable error message.
|
||||
/// When validation succeeds, set to null.
|
||||
/// </param>
|
||||
/// <returns>true if the value satisfies this limit; false otherwise.</returns>
|
||||
public abstract bool Validate(double value, out string errorMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requires the double value to be strictly positive (> 0).
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requires the double value to be within an inclusive [Min, Max] range.
|
||||
/// Either bound can be null to represent an unbounded side.
|
||||
/// </summary>
|
||||
internal class RangeDoubleLimit : DoubleLimit
|
||||
{
|
||||
public double? Min { get; }
|
||||
public double? Max { get; }
|
||||
|
||||
/// <param name="min">Inclusive lower bound, or null for no lower bound.</param>
|
||||
/// <param name="max">Inclusive upper bound, or null for no upper bound.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requires the double value to be within an inclusive [Min, Max] range.
|
||||
/// Either bound can be null to represent an unbounded side.
|
||||
/// </summary>
|
||||
internal class RangeDoubleLimit : DoubleLimit
|
||||
{
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
public double? Min { get; }
|
||||
public double? Max { get; }
|
||||
// ReSharper restore MemberCanBePrivate.Global
|
||||
|
||||
/// <param name="min">Inclusive lower bound, or null for no lower bound.</param>
|
||||
/// <param name="max">Inclusive upper bound, or null for no upper bound.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<DoubleLimit> Limits;
|
||||
}
|
||||
public List<DoubleLimit> Limits;
|
||||
}
|
||||
|
||||
private readonly VisualElement _rootVisualElement;
|
||||
private readonly Dictionary<string, FieldEntry> _fields = new();
|
||||
private readonly VisualElement _rootVisualElement;
|
||||
private readonly Dictionary<string, FieldEntry> _fields = new();
|
||||
|
||||
public DoubleInputValidator(VisualElement rootVisualElement)
|
||||
{
|
||||
_rootVisualElement = rootVisualElement;
|
||||
}
|
||||
public DoubleInputValidator(VisualElement rootVisualElement)
|
||||
{
|
||||
_rootVisualElement = rootVisualElement;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers one or more <see cref="DoubleLimit"/>s for the field with the given name.
|
||||
/// Multiple limits can be added to the same field — they are all checked in order.
|
||||
/// </summary>
|
||||
/// <param name="fieldName">The name of the DoubleField in the UXML.</param>
|
||||
/// <param name="limits">The limits to apply.</param>
|
||||
public DoubleInputValidator AddLimits(string fieldName, params DoubleLimit[] limits)
|
||||
/// <summary>
|
||||
/// Registers one or more <see cref="DoubleLimit"/>s for the field with the given name.
|
||||
/// Multiple limits can be added to the same field — they are all checked in order.
|
||||
/// </summary>
|
||||
/// <param name="fieldName">The name of the DoubleField in the UXML.</param>
|
||||
/// <param name="limits">The limits to apply.</param>
|
||||
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<DoubleLimit>()
|
||||
};
|
||||
Limits = new List<DoubleLimit>()
|
||||
};
|
||||
}
|
||||
|
||||
entry.Limits.AddRange(limits);
|
||||
_fields[fieldName] = entry;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queries all registered fields from the root VisualElement and
|
||||
/// registers value-changed callbacks that rollback invalid values.
|
||||
/// </summary>
|
||||
public void SetupValidation()
|
||||
{
|
||||
foreach (var (fieldName, entry) in _fields)
|
||||
{
|
||||
var field = _rootVisualElement.Q<DoubleField>(fieldName);
|
||||
if (field == null)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Warning(
|
||||
$"DoubleField '{fieldName}'을(를) 찾을 수 없습니다. 유효성 검사를 건너뜁니다.");
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.Limits.AddRange(limits);
|
||||
_fields[fieldName] = entry;
|
||||
return this;
|
||||
}
|
||||
var limits = entry.Limits;
|
||||
|
||||
/// <summary>
|
||||
/// Queries all registered fields from the root VisualElement and
|
||||
/// registers value-changed callbacks that rollback invalid values.
|
||||
/// </summary>
|
||||
public void SetupValidation()
|
||||
{
|
||||
foreach (var (fieldName, entry) in _fields)
|
||||
field.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
var field = _rootVisualElement.Q<DoubleField>(fieldName);
|
||||
if (field == null)
|
||||
if (string.IsNullOrEmpty(field.text))
|
||||
{
|
||||
Melon<RenderMod>.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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Binds a UI Toolkit <see cref="DropdownField"/> to an enum type.
|
||||
/// Populates choices from enum names and provides typed access to the selected value.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The enum type to bind.</typeparam>
|
||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
|
||||
internal class EnumDropdownBinder<T> where T : struct, Enum
|
||||
{
|
||||
private readonly DropdownField? _dropdown;
|
||||
private readonly List<T> _values;
|
||||
private Action<T>? _onValueChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Binds a UI Toolkit <see cref="DropdownField"/> to an enum type.
|
||||
/// Populates choices from enum names and provides typed access to the selected value.
|
||||
/// The currently selected enum value.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The enum type to bind.</typeparam>
|
||||
internal class EnumDropdownBinder<T> where T : struct, Enum
|
||||
public T Value => _dropdown is { index: >= 0 } && _dropdown.index < _values.Count
|
||||
? _values[_dropdown.index]
|
||||
: default;
|
||||
|
||||
/// <summary>
|
||||
/// Queries a <see cref="DropdownField"/> by name from the root VisualElement,
|
||||
/// populates it with enum names, and selects the default value.
|
||||
/// </summary>
|
||||
/// <param name="rootVisualElement">The root VisualElement loaded from the UXML bundle.</param>
|
||||
/// <param name="fieldName">The name of the DropdownField in the UXML.</param>
|
||||
/// <param name="defaultValue">The initial selected value. Defaults to the first enum member.</param>
|
||||
public EnumDropdownBinder(VisualElement rootVisualElement, string fieldName, T? defaultValue = null)
|
||||
{
|
||||
private readonly DropdownField? _dropdown;
|
||||
private readonly List<T> _values;
|
||||
private Action<T>? _onValueChanged;
|
||||
_values = Enum.GetValues(typeof(T)).Cast<T>().ToList();
|
||||
|
||||
/// <summary>
|
||||
/// The currently selected enum value.
|
||||
/// </summary>
|
||||
public T Value => _dropdown is { index: >= 0 } && _dropdown.index < _values.Count
|
||||
? _values[_dropdown.index]
|
||||
: default;
|
||||
|
||||
/// <summary>
|
||||
/// Queries a <see cref="DropdownField"/> by name from the root VisualElement,
|
||||
/// populates it with enum names, and selects the default value.
|
||||
/// </summary>
|
||||
/// <param name="rootVisualElement">The root VisualElement loaded from the UXML bundle.</param>
|
||||
/// <param name="fieldName">The name of the DropdownField in the UXML.</param>
|
||||
/// <param name="defaultValue">The initial selected value. Defaults to the first enum member.</param>
|
||||
public EnumDropdownBinder(VisualElement rootVisualElement, string fieldName, T? defaultValue = null)
|
||||
_dropdown = rootVisualElement.Q<DropdownField>(fieldName);
|
||||
if (_dropdown == null)
|
||||
{
|
||||
_values = Enum.GetValues(typeof(T)).Cast<T>().ToList();
|
||||
|
||||
_dropdown = rootVisualElement.Q<DropdownField>(fieldName);
|
||||
if (_dropdown == null)
|
||||
{
|
||||
Melon<RenderMod>.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<RenderMod>.Logger.Error(
|
||||
$"DropdownField '{fieldName}'을(를) 찾을 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a callback that fires when the selection changes, providing the new enum value.
|
||||
/// </summary>
|
||||
public void OnValueChanged(Action<T> 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);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a callback that fires when the selection changes, providing the new enum value.
|
||||
/// </summary>
|
||||
public void OnValueChanged(Action<T> callback)
|
||||
{
|
||||
_onValueChanged = callback;
|
||||
|
||||
// Fire immediately with current value so the caller gets the initial state
|
||||
if (_dropdown != null)
|
||||
{
|
||||
callback(Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the selected enum value and updates the dropdown index.
|
||||
/// </summary>
|
||||
/// <param name="value">The enum value to set.</param>
|
||||
public void SetValue(T value)
|
||||
/// <summary>
|
||||
/// Sets the selected enum value and updates the dropdown index.
|
||||
/// </summary>
|
||||
/// <param name="value">The enum value to set.</param>
|
||||
public void SetValue(T value)
|
||||
{
|
||||
if (_dropdown == null) return;
|
||||
int idx = _values.IndexOf(value);
|
||||
if (idx >= 0)
|
||||
{
|
||||
if (_dropdown == null) return;
|
||||
int idx = _values.IndexOf(value);
|
||||
if (idx >= 0)
|
||||
{
|
||||
_dropdown.index = idx;
|
||||
}
|
||||
_dropdown.index = idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<IntegerLimit> Limits;
|
||||
}
|
||||
public List<IntegerLimit> Limits;
|
||||
}
|
||||
|
||||
private readonly VisualElement _rootVisualElement;
|
||||
private readonly Dictionary<string, FieldEntry> _fields = new();
|
||||
private readonly VisualElement _rootVisualElement;
|
||||
private readonly Dictionary<string, FieldEntry> _fields = new();
|
||||
|
||||
public IntegerInputValidator(VisualElement rootVisualElement)
|
||||
{
|
||||
_rootVisualElement = rootVisualElement;
|
||||
}
|
||||
public IntegerInputValidator(VisualElement rootVisualElement)
|
||||
{
|
||||
_rootVisualElement = rootVisualElement;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers one or more <see cref="IntegerLimit"/>s for the field with the given name.
|
||||
/// Multiple limits can be added to the same field — they are all checked in order.
|
||||
/// </summary>
|
||||
/// <param name="fieldName">The name of the IntegerField in the UXML.</param>
|
||||
/// <param name="limits">The limits to apply.</param>
|
||||
public IntegerInputValidator AddLimits(string fieldName, params IntegerLimit[] limits)
|
||||
/// <summary>
|
||||
/// Registers one or more <see cref="IntegerLimit"/>s for the field with the given name.
|
||||
/// Multiple limits can be added to the same field — they are all checked in order.
|
||||
/// </summary>
|
||||
/// <param name="fieldName">The name of the IntegerField in the UXML.</param>
|
||||
/// <param name="limits">The limits to apply.</param>
|
||||
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<IntegerLimit>()
|
||||
};
|
||||
Limits = new List<IntegerLimit>()
|
||||
};
|
||||
}
|
||||
|
||||
entry.Limits.AddRange(limits);
|
||||
_fields[fieldName] = entry;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queries all registered fields from the root VisualElement and
|
||||
/// registers value-changed callbacks that rollback invalid values.
|
||||
/// </summary>
|
||||
public void SetupValidation()
|
||||
{
|
||||
foreach (var (fieldName, entry) in _fields)
|
||||
{
|
||||
var field = _rootVisualElement.Q<IntegerField>(fieldName);
|
||||
if (field == null)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Warning(
|
||||
$"IntegerField '{fieldName}'을(를) 찾을 수 없습니다. 유효성 검사를 건너뜁니다.");
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.Limits.AddRange(limits);
|
||||
_fields[fieldName] = entry;
|
||||
return this;
|
||||
}
|
||||
var limits = entry.Limits;
|
||||
|
||||
/// <summary>
|
||||
/// Queries all registered fields from the root VisualElement and
|
||||
/// registers value-changed callbacks that rollback invalid values.
|
||||
/// </summary>
|
||||
public void SetupValidation()
|
||||
{
|
||||
foreach (var (fieldName, entry) in _fields)
|
||||
field.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
var field = _rootVisualElement.Q<IntegerField>(fieldName);
|
||||
if (field == null)
|
||||
if (string.IsNullOrEmpty(field.text))
|
||||
{
|
||||
Melon<RenderMod>.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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+69
-68
@@ -1,78 +1,79 @@
|
||||
namespace ADOFAIRenderer.Data
|
||||
namespace ADOFAIRenderer.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for integer input validation limits.
|
||||
/// Multiple limits can be composed on a single input field — all are checked in order.
|
||||
/// </summary>
|
||||
internal abstract class IntegerLimit
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal abstract class IntegerLimit
|
||||
/// <param name="value">The parsed integer value to validate.</param>
|
||||
/// <param name="errorMessage">
|
||||
/// When validation fails, set to a human-readable error message.
|
||||
/// When validation succeeds, set to null.
|
||||
/// </param>
|
||||
/// <returns>true if the value satisfies this limit; false otherwise.</returns>
|
||||
public abstract bool Validate(int value, out string errorMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requires the integer value to be strictly positive (> 0).
|
||||
/// </summary>
|
||||
internal class PositiveOnlyIntegerLimit : IntegerLimit
|
||||
{
|
||||
public override bool Validate(int value, out string errorMessage)
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates the given integer value against this limit.
|
||||
/// </summary>
|
||||
/// <param name="value">The parsed integer value to validate.</param>
|
||||
/// <param name="errorMessage">
|
||||
/// When validation fails, set to a human-readable error message.
|
||||
/// When validation succeeds, set to null.
|
||||
/// </param>
|
||||
/// <returns>true if the value satisfies this limit; false otherwise.</returns>
|
||||
public abstract bool Validate(int value, out string errorMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requires the integer value to be strictly positive (> 0).
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requires the integer value to be within an inclusive [Min, Max] range.
|
||||
/// Either bound can be null to represent an unbounded side.
|
||||
/// </summary>
|
||||
internal class RangeIntegerLimit : IntegerLimit
|
||||
{
|
||||
public int? Min { get; }
|
||||
public int? Max { get; }
|
||||
|
||||
/// <param name="min">Inclusive lower bound, or null for no lower bound.</param>
|
||||
/// <param name="max">Inclusive upper bound, or null for no upper bound.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requires the integer value to be within an inclusive [Min, Max] range.
|
||||
/// Either bound can be null to represent an unbounded side.
|
||||
/// </summary>
|
||||
internal class RangeIntegerLimit : IntegerLimit
|
||||
{
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
public int? Min { get; }
|
||||
public int? Max { get; }
|
||||
// ReSharper restore MemberCanBePrivate.Global
|
||||
|
||||
/// <param name="min">Inclusive lower bound, or null for no lower bound.</param>
|
||||
/// <param name="max">Inclusive upper bound, or null for no upper bound.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
+143
-144
@@ -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
|
||||
};
|
||||
public static List<RenderPreset> GetPresets()
|
||||
{
|
||||
var presets = new List<RenderPreset>();
|
||||
var dir = GetPresetsDirectory();
|
||||
|
||||
string json = JsonConvert.SerializeObject(preset, Formatting.Indented);
|
||||
string safeName = GetSafeFileName(name);
|
||||
string filePath = Path.Combine(dir, safeName + ".json");
|
||||
File.WriteAllText(filePath, json);
|
||||
}
|
||||
if (!Directory.Exists(dir)) return presets;
|
||||
|
||||
public static List<RenderPreset> GetPresets()
|
||||
try
|
||||
{
|
||||
var presets = new List<RenderPreset>();
|
||||
var dir = GetPresetsDirectory();
|
||||
|
||||
if (!Directory.Exists(dir)) return presets;
|
||||
|
||||
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<JObject>(json);
|
||||
if (jObject == null) continue;
|
||||
|
||||
var configToken = jObject["Config"];
|
||||
if (configToken is JObject configObj)
|
||||
{
|
||||
var json = File.ReadAllText(file);
|
||||
var jObject = JsonConvert.DeserializeObject<JObject>(json);
|
||||
if (jObject == null) continue;
|
||||
TryChangeKey(configObj, "DelayAfterLevelEnd", "DelayAfterEnd");
|
||||
|
||||
var configToken = jObject["Config"];
|
||||
if (configToken is JObject configObj)
|
||||
// 1. legacy EncodingConfig -> VideoConfig
|
||||
if (configObj["EncodingConfig"] is JObject oldEncodingConfigObj)
|
||||
{
|
||||
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");
|
||||
}
|
||||
configObj["VideoConfig"] ??= oldEncodingConfigObj;
|
||||
configObj.Remove("EncodingConfig");
|
||||
}
|
||||
|
||||
var preset = jObject.ToObject<RenderPreset>();
|
||||
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<RenderPreset>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+57
-58
@@ -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 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; }
|
||||
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 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 = 44100;
|
||||
private int _audioChannels = 2;
|
||||
private int _audioBitrate = 256000;
|
||||
|
||||
public bool RenderAudio { get; set; } = true;
|
||||
|
||||
public int AudioSampleRate
|
||||
{
|
||||
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; }
|
||||
public bool ShowJudgementText { get; set; } = false;
|
||||
get => _audioSampleRate <= 0 ? 44100 : _audioSampleRate;
|
||||
set => _audioSampleRate = value <= 0 ? 44100 : value;
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")]
|
||||
public class AdvancedConfig
|
||||
public int AudioChannels
|
||||
{
|
||||
public int? RandomSeed { get; set; } = null;
|
||||
public bool ProcessAudio { get; set; } = true;
|
||||
public bool NonTile { get; set; } = false;
|
||||
public string FileExtension { get; set; } = "mkv";
|
||||
get => _audioChannels <= 0 ? 2 : _audioChannels;
|
||||
set => _audioChannels = value <= 0 ? 2 : value;
|
||||
}
|
||||
|
||||
public class VideoEncodingConfig
|
||||
public int AudioBitrate
|
||||
{
|
||||
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 = 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 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;
|
||||
}
|
||||
get => _audioBitrate <= 0 ? 256000 : _audioBitrate;
|
||||
set => _audioBitrate = value <= 0 ? 256000 : value;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
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
|
||||
[HarmonyPatch(typeof(Time), "timeAsDouble", MethodType.Getter)]
|
||||
public static class TimeAsDoublePatch
|
||||
{
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
public static bool Prefix(ref double __result)
|
||||
{
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
public static bool Prefix(ref double __result)
|
||||
{
|
||||
if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true;
|
||||
__result = TimeSpent + RenderManager.Instance.timeBase;
|
||||
return false;
|
||||
}
|
||||
if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true;
|
||||
__result = TimeSpent + RenderManager.Instance.timeBase;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Time), "unscaledTime", MethodType.Getter)]
|
||||
public static class UnscaledTimePatch
|
||||
[HarmonyPatch(typeof(Time), "unscaledTime", MethodType.Getter)]
|
||||
public static class UnscaledTimePatch
|
||||
{
|
||||
[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.unscaledTimeBase);
|
||||
return false;
|
||||
}
|
||||
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
|
||||
[HarmonyPatch(typeof(Time), "unscaledTimeAsDouble", MethodType.Getter)]
|
||||
public static class UnscaledTimeAsDoublePatch
|
||||
{
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
public static bool Prefix(ref double __result)
|
||||
{
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
public static bool Prefix(ref double __result)
|
||||
{
|
||||
if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true;
|
||||
__result = TimeSpent + RenderManager.Instance.unscaledTimeBase;
|
||||
return false;
|
||||
}
|
||||
if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true;
|
||||
__result = TimeSpent + RenderManager.Instance.unscaledTimeBase;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(AudioSettings), "dspTime", MethodType.Getter)]
|
||||
public static class DspTimePatch
|
||||
[HarmonyPatch(typeof(AudioSettings), "dspTime", MethodType.Getter)]
|
||||
public static class DspTimePatch
|
||||
{
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
public static bool Prefix(ref double __result)
|
||||
{
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
public static bool Prefix(ref double __result)
|
||||
{
|
||||
if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true;
|
||||
__result = TimeSpent + RenderManager.Instance.dspTimeBase;
|
||||
return false;
|
||||
}
|
||||
if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true;
|
||||
__result = TimeSpent + RenderManager.Instance.dspTimeBase;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(AudioSource), "PlayScheduled")]
|
||||
public static class PlayScheduledPatch
|
||||
[HarmonyPatch(typeof(AudioSource), "PlayScheduled")]
|
||||
public static class PlayScheduledPatch
|
||||
{
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static bool Prefix(AudioSource __instance, double time)
|
||||
{
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static bool Prefix(AudioSource __instance, double time)
|
||||
{
|
||||
return !RenderManager.IsActive;
|
||||
}
|
||||
return !RenderManager.IsActive;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Time), "frameCount", MethodType.Getter)]
|
||||
public static class FrameCountPatch
|
||||
[HarmonyPatch(typeof(Time), "frameCount", MethodType.Getter)]
|
||||
public static class FrameCountPatch
|
||||
{
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
public static bool Prefix(ref int __result)
|
||||
{
|
||||
[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;
|
||||
}
|
||||
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
|
||||
[HarmonyPatch(typeof(Time), "deltaTime", MethodType.Getter)]
|
||||
public static class DeltaTimePatch
|
||||
{
|
||||
[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)(1 / RenderManager.Instance.Configuration.RenderFPS);
|
||||
return false;
|
||||
}
|
||||
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
|
||||
[HarmonyPatch(typeof(Time), "unscaledDeltaTime", MethodType.Getter)]
|
||||
public static class UnscaledDeltaTimePatch
|
||||
{
|
||||
[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)(1 / RenderManager.Instance.Configuration.RenderFPS);
|
||||
return false;
|
||||
}
|
||||
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
|
||||
[HarmonyPatch(typeof(Screen), "width", MethodType.Getter)]
|
||||
public static class ScreenWidthPatch
|
||||
{
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
public static bool Prefix(ref int __result)
|
||||
{
|
||||
[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;
|
||||
}
|
||||
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
|
||||
[HarmonyPatch(typeof(Screen), "height", MethodType.Getter)]
|
||||
public static class ScreenHeightPatch
|
||||
{
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
public static bool Prefix(ref int __result)
|
||||
{
|
||||
[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;
|
||||
}
|
||||
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
|
||||
[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)
|
||||
{
|
||||
[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;
|
||||
}
|
||||
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
|
||||
[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)
|
||||
{
|
||||
[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.Instance && RenderManager.Instance.OverrideLoading && RenderManager.Instance.Configuration.AdvancedConfig.ProcessAudio)
|
||||
stream = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
__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)
|
||||
[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)
|
||||
{
|
||||
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++)
|
||||
{
|
||||
for (var i = 0; i < lampSequencesArray.Length; i++)
|
||||
var item = lampSequencesArray.GetValue(i);
|
||||
if (item == null) continue;
|
||||
try
|
||||
{
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,48 +4,47 @@ using MelonLoader;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace ADOFAIRenderer.Patch.Game
|
||||
namespace ADOFAIRenderer.Patch.Game;
|
||||
|
||||
[HarmonyPatch(typeof(scnEditor))]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static class scnEditorPatch
|
||||
{
|
||||
[HarmonyPatch(typeof(scnEditor))]
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch("Start")]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static class scnEditorPatch
|
||||
public static void AddButtonRender(scnEditor __instance)
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch("Start")]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static void AddButtonRender(scnEditor __instance)
|
||||
if (RenderMod.IsFeatureDisabled) return;
|
||||
if (!__instance || !__instance.buttonExit)
|
||||
{
|
||||
if (RenderMod.IsFeatureDisabled) return;
|
||||
if (!__instance || !__instance.buttonExit)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Msg("scnEditor에 렌더 버튼을 만들 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
var cloneTarget = __instance.buttonExit!.gameObject;
|
||||
var buttonContainer = cloneTarget.transform.parent!;
|
||||
|
||||
var buttonObj = Object.Instantiate(cloneTarget, buttonContainer)!;
|
||||
buttonObj.name = "buttonRender";
|
||||
|
||||
var button = buttonObj.GetComponent<Button>()!;
|
||||
// Clear persistent and non-persistent listeners from the original button
|
||||
button.onClick = new Button.ButtonClickedEvent();
|
||||
button.onClick.AddListener(RenderManager.StartRender);
|
||||
|
||||
// The button root has an Image component for the background (rounded box).
|
||||
// We want to change the icon sprite, which is on a child GameObject.
|
||||
var images = buttonObj.GetComponentsInChildren<Image>();
|
||||
if (images.Length <= 1) return;
|
||||
|
||||
// ReSharper disable once Unity.UnknownResource
|
||||
// images[0] is the background on the root, images[1] is the icon on the child
|
||||
var sprite = Resources.Load<Sprite>("LevelEditor/LevelEvents/MoveCamera");
|
||||
if (sprite == null) return;
|
||||
|
||||
var iconImage = images[1];
|
||||
iconImage.sprite = sprite;
|
||||
iconImage.rectTransform.sizeDelta = new Vector2(24, 24);
|
||||
Melon<RenderMod>.Logger.Msg("scnEditor에 렌더 버튼을 만들 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
var cloneTarget = __instance.buttonExit!.gameObject;
|
||||
var buttonContainer = cloneTarget.transform.parent!;
|
||||
|
||||
var buttonObj = Object.Instantiate(cloneTarget, buttonContainer)!;
|
||||
buttonObj.name = "buttonRender";
|
||||
|
||||
var button = buttonObj.GetComponent<Button>()!;
|
||||
// Clear persistent and non-persistent listeners from the original button
|
||||
button.onClick = new Button.ButtonClickedEvent();
|
||||
button.onClick.AddListener(RenderManager.StartRender);
|
||||
|
||||
// The button root has an Image component for the background (rounded box).
|
||||
// We want to change the icon sprite, which is on a child GameObject.
|
||||
var images = buttonObj.GetComponentsInChildren<Image>();
|
||||
if (images.Length <= 1) return;
|
||||
|
||||
// ReSharper disable once Unity.UnknownResource
|
||||
// images[0] is the background on the root, images[1] is the icon on the child
|
||||
var sprite = Resources.Load<Sprite>("LevelEditor/LevelEvents/MoveCamera");
|
||||
if (sprite == null) return;
|
||||
|
||||
var iconImage = images[1];
|
||||
iconImage.sprite = sprite;
|
||||
iconImage.rectTransform.sizeDelta = new Vector2(24, 24);
|
||||
}
|
||||
}
|
||||
+120
-121
@@ -4,139 +4,138 @@ using ADOFAIRenderer.Util;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ADOFAIRenderer.Patch.Game
|
||||
namespace ADOFAIRenderer.Patch.Game;
|
||||
|
||||
[HarmonyPatch(typeof(scnGame))]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static class scnGamePatch
|
||||
{
|
||||
[HarmonyPatch(typeof(scnGame))]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static class scnGamePatch
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch("Play")]
|
||||
public static void ProcessFloors()
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch("Play")]
|
||||
public static void ProcessFloors()
|
||||
var nonTile = RenderManager.Instance.OverrideLoading &&
|
||||
RenderManager.Instance.Configuration.AdvancedConfig.NonTile;
|
||||
|
||||
if (!nonTile) return;
|
||||
foreach (var floor in scrLevelMaker.instance.listFloors)
|
||||
{
|
||||
var nonTile = RenderManager.Instance.OverrideLoading &&
|
||||
RenderManager.Instance.Configuration.AdvancedConfig.NonTile;
|
||||
DisableRenderers(
|
||||
floor.floorRenderer.renderer, floor.legacyFloorSpriteRenderer,
|
||||
floor.iconsprite, floor.outlineSprite,
|
||||
floor.topGlow, floor.bottomGlow,
|
||||
floor.multiplanetLine
|
||||
);
|
||||
|
||||
if (!nonTile) return;
|
||||
foreach (var floor in scrLevelMaker.instance.listFloors)
|
||||
{
|
||||
DisableRenderers(
|
||||
floor.floorRenderer.renderer, floor.legacyFloorSpriteRenderer,
|
||||
floor.iconsprite, floor.outlineSprite,
|
||||
floor.topGlow, floor.bottomGlow,
|
||||
floor.multiplanetLine
|
||||
);
|
||||
if (floor.holdRenderer) DisableRenderer(floor.holdRenderer.m_meshRenderer);
|
||||
|
||||
if (floor.holdRenderer) DisableRenderer(floor.holdRenderer.m_meshRenderer);
|
||||
|
||||
if (floor.dummyPlanets == null) continue;
|
||||
foreach (var planet in floor.dummyPlanets)
|
||||
DisablePlanetRenderer(planet);
|
||||
}
|
||||
|
||||
var players = scrPlayerManager.instance.players;
|
||||
if (players == null) return;
|
||||
|
||||
foreach (var player in players)
|
||||
{
|
||||
var planets = player.planetarySystem?.planetList;
|
||||
if (planets == null) continue;
|
||||
|
||||
foreach (var planet in planets)
|
||||
DisablePlanetRenderer(planet.planetRenderer);
|
||||
}
|
||||
return;
|
||||
|
||||
void DisablePlanetRenderer(PlanetRenderer planet)
|
||||
{
|
||||
if (planet.sprite) DisableRenderer(planet.sprite.meshRenderer);
|
||||
DisableRenderers(
|
||||
planet.coreParticles.GetRenderer(), planet.tailParticles.GetRenderer(),
|
||||
planet.tailParticlesCoop.GetRenderer(), planet.sparks.GetRenderer(), planet.ring, planet.glow,
|
||||
planet.faceSprite, planet.faceDetails, planet.samuraiSprite
|
||||
);
|
||||
}
|
||||
|
||||
void DisableRenderers(params Renderer?[] renderers)
|
||||
{
|
||||
foreach (var renderer in renderers) DisableRenderer(renderer);
|
||||
}
|
||||
|
||||
void DisableRenderer(Renderer? renderer)
|
||||
{
|
||||
if (renderer) renderer.enabled = false;
|
||||
}
|
||||
if (floor.dummyPlanets == null) continue;
|
||||
foreach (var planet in floor.dummyPlanets)
|
||||
DisablePlanetRenderer(planet);
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("UpdateVideo")]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static bool ReplaceVideo(scnGame __instance)
|
||||
var players = scrPlayerManager.instance.players;
|
||||
if (players == null) return;
|
||||
|
||||
foreach (var player in players)
|
||||
{
|
||||
// Only replace when rendering is active
|
||||
if (!RenderManager.IsActive)
|
||||
{
|
||||
// Remove FFmpeg player if it was attached previously
|
||||
var oldPlayer = __instance.videoBG.gameObject.GetComponent<FFmpegVideoBGPlayer>();
|
||||
if (oldPlayer != null)
|
||||
{
|
||||
Object.Destroy(oldPlayer);
|
||||
}
|
||||
// Re-enable native player component
|
||||
__instance.videoBG.enabled = true;
|
||||
return true; // Execute original
|
||||
}
|
||||
var planets = player.planetarySystem?.planetList;
|
||||
if (planets == null) continue;
|
||||
|
||||
string text = __instance.levelData.miscSettings.GetString("bgVideo");
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
__instance.videoBG.gameObject.SetActive(false);
|
||||
return false; // Skip original
|
||||
}
|
||||
foreach (var planet in planets)
|
||||
DisablePlanetRenderer(planet.planetRenderer);
|
||||
}
|
||||
return;
|
||||
|
||||
__instance.videoBG.gameObject.SetActive(true);
|
||||
void DisablePlanetRenderer(PlanetRenderer planet)
|
||||
{
|
||||
if (planet.sprite) DisableRenderer(planet.sprite.meshRenderer);
|
||||
DisableRenderers(
|
||||
planet.coreParticles.GetRenderer(), planet.tailParticles.GetRenderer(),
|
||||
planet.tailParticlesCoop.GetRenderer(), planet.sparks.GetRenderer(), planet.ring, planet.glow,
|
||||
planet.faceSprite, planet.faceDetails, planet.samuraiSprite
|
||||
);
|
||||
}
|
||||
|
||||
// We can only play external video files via FFmpegNativeDecoder.
|
||||
// Internal/Bundle level background videos are built-in VideoClips, so we fallback to native Unity VideoPlayer.
|
||||
if (ADOBase.isInternalLevel || ADOBase.isBundleLevel)
|
||||
{
|
||||
__instance.videoBG.enabled = true;
|
||||
return true; // Execute original
|
||||
}
|
||||
void DisableRenderers(params Renderer?[] renderers)
|
||||
{
|
||||
foreach (var renderer in renderers) DisableRenderer(renderer);
|
||||
}
|
||||
|
||||
// Custom level video path
|
||||
var videoPath = Path.Combine(Path.GetDirectoryName(ADOBase.levelPath) ?? "", text);
|
||||
if (!File.Exists(videoPath))
|
||||
{
|
||||
return true; // Let the original print error
|
||||
}
|
||||
|
||||
// Disable native VideoPlayer component (to prevent it from attempting real-time playback and sync errors)
|
||||
var targetCamera = __instance.videoBG.targetCamera;
|
||||
if (!targetCamera)
|
||||
{
|
||||
var camGo = GameObject.Find("BGStaticCam");
|
||||
if (camGo != null)
|
||||
{
|
||||
targetCamera = camGo.GetComponent<Camera>();
|
||||
}
|
||||
}
|
||||
|
||||
__instance.videoBG.enabled = false;
|
||||
|
||||
// Add or retrieve our custom FFmpeg video background player
|
||||
var player = __instance.videoBG.gameObject.GetComponent<FFmpegVideoBGPlayer>();
|
||||
if (player == null)
|
||||
{
|
||||
player = __instance.videoBG.gameObject.AddComponent<FFmpegVideoBGPlayer>();
|
||||
}
|
||||
|
||||
var isLooping = __instance.levelData.miscSettings.GetBool("loopVideo");
|
||||
|
||||
// Initialize player. If targetTexture is null (CameraFarPlane mode), it will build a quad in front of targetCamera.
|
||||
player.Init(videoPath, __instance.videoBG.targetTexture, isLooping, targetCamera, __instance.videoBG.gameObject);
|
||||
|
||||
return false; // Skip original UpdateVideo
|
||||
void DisableRenderer(Renderer? renderer)
|
||||
{
|
||||
if (renderer) renderer.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("UpdateVideo")]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static bool ReplaceVideo(scnGame __instance)
|
||||
{
|
||||
// Only replace when rendering is active
|
||||
if (!RenderManager.IsActive)
|
||||
{
|
||||
// Remove FFmpeg player if it was attached previously
|
||||
var oldPlayer = __instance.videoBG.gameObject.GetComponent<FFmpegVideoBGPlayer>();
|
||||
if (oldPlayer != null)
|
||||
{
|
||||
Object.Destroy(oldPlayer);
|
||||
}
|
||||
// Re-enable native player component
|
||||
__instance.videoBG.enabled = true;
|
||||
return true; // Execute original
|
||||
}
|
||||
|
||||
string text = __instance.levelData.miscSettings.GetString("bgVideo");
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
__instance.videoBG.gameObject.SetActive(false);
|
||||
return false; // Skip original
|
||||
}
|
||||
|
||||
__instance.videoBG.gameObject.SetActive(true);
|
||||
|
||||
// We can only play external video files via FFmpegNativeDecoder.
|
||||
// Internal/Bundle level background videos are built-in VideoClips, so we fall back to native Unity VideoPlayer.
|
||||
if (ADOBase.isInternalLevel || ADOBase.isBundleLevel)
|
||||
{
|
||||
__instance.videoBG.enabled = true;
|
||||
return true; // Execute original
|
||||
}
|
||||
|
||||
// Custom level video path
|
||||
var videoPath = Path.Combine(Path.GetDirectoryName(ADOBase.levelPath) ?? "", text);
|
||||
if (!File.Exists(videoPath))
|
||||
{
|
||||
return true; // Let the original print error
|
||||
}
|
||||
|
||||
// Disable native VideoPlayer component (to prevent it from attempting real-time playback and sync errors)
|
||||
var targetCamera = __instance.videoBG.targetCamera;
|
||||
if (!targetCamera)
|
||||
{
|
||||
var camGo = GameObject.Find("BGStaticCam");
|
||||
if (camGo != null)
|
||||
{
|
||||
targetCamera = camGo.GetComponent<Camera>();
|
||||
}
|
||||
}
|
||||
|
||||
__instance.videoBG.enabled = false;
|
||||
|
||||
// Add or retrieve our custom FFmpeg video background player
|
||||
var player = __instance.videoBG.gameObject.GetComponent<FFmpegVideoBGPlayer>();
|
||||
if (player == null)
|
||||
{
|
||||
player = __instance.videoBG.gameObject.AddComponent<FFmpegVideoBGPlayer>();
|
||||
}
|
||||
|
||||
var isLooping = __instance.levelData.miscSettings.GetBool("loopVideo");
|
||||
|
||||
// Initialize player. If targetTexture is null (CameraFarPlane mode), it will build a quad in front of targetCamera.
|
||||
player.Init(videoPath, __instance.videoBG.targetTexture, isLooping, targetCamera, __instance.videoBG.gameObject);
|
||||
|
||||
return false; // Skip original UpdateVideo
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,22 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using HarmonyLib;
|
||||
|
||||
namespace ADOFAIRenderer.Patch.Game
|
||||
{
|
||||
[HarmonyPatch(typeof(scrCamera))]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public class scrCameraPatch
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("SetCustomFrameRate")]
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
private static bool FixCamRT(scrCamera __instance, bool enable, float frame)
|
||||
{
|
||||
// There is an issue that `mainTexture.Release` triggers even if `mainTexture` is `camRT`, so mod tries to fix it
|
||||
if (enable || __instance.enableCustomFPS != enable) return true;
|
||||
namespace ADOFAIRenderer.Patch.Game;
|
||||
|
||||
__instance.enableCustomFPS = enable;
|
||||
__instance.frameRate = frame;
|
||||
return false;
|
||||
}
|
||||
[HarmonyPatch(typeof(scrCamera))]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public class scrCameraPatch
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("SetCustomFrameRate")]
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
private static bool FixCamRT(scrCamera __instance, bool enable, float frame)
|
||||
{
|
||||
// There is an issue that `mainTexture.Release` triggers even if `mainTexture` is `camRT`, so mod tries to fix it
|
||||
if (enable || __instance.enableCustomFPS != enable) return true;
|
||||
|
||||
__instance.enableCustomFPS = enable;
|
||||
__instance.frameRate = frame;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,44 @@
|
||||
using ADOFAIRenderer.Core;
|
||||
using HarmonyLib;
|
||||
|
||||
namespace ADOFAIRenderer.Patch.Game
|
||||
namespace ADOFAIRenderer.Patch.Game;
|
||||
|
||||
[HarmonyPatch(typeof(scrController))]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static class scrControllerPatch
|
||||
{
|
||||
[HarmonyPatch(typeof(scrController))]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static class scrControllerPatch
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("TogglePauseGame")]
|
||||
public static bool PreventPause()
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("TogglePauseGame")]
|
||||
public static bool PreventPause()
|
||||
{
|
||||
return !RenderManager.IsActive;
|
||||
}
|
||||
return !RenderManager.IsActive;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("PortalTravelAction")]
|
||||
public static bool PreventPortalTravel()
|
||||
{
|
||||
return !RenderManager.IsActive;
|
||||
}
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("PortalTravelAction")]
|
||||
public static bool PreventPortalTravel()
|
||||
{
|
||||
return !RenderManager.IsActive;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("FailAction")]
|
||||
public static bool PreventFailAction()
|
||||
{
|
||||
return !RenderManager.IsActive;
|
||||
}
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("FailAction")]
|
||||
public static bool PreventFailAction()
|
||||
{
|
||||
return !RenderManager.IsActive;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("Fail2Action")]
|
||||
public static bool PreventFail2Action()
|
||||
{
|
||||
return !RenderManager.IsActive;
|
||||
}
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("Fail2Action")]
|
||||
public static bool PreventFail2Action()
|
||||
{
|
||||
return !RenderManager.IsActive;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("OnPlayerDied")]
|
||||
public static bool PreventPlayerDied()
|
||||
{
|
||||
return !RenderManager.IsActive;
|
||||
}
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("OnPlayerDied")]
|
||||
public static bool PreventPlayerDied()
|
||||
{
|
||||
return !RenderManager.IsActive;
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1,40 @@
|
||||
using HarmonyLib;
|
||||
|
||||
namespace ADOFAIRenderer.Patch.Game
|
||||
namespace ADOFAIRenderer.Patch.Game;
|
||||
|
||||
[HarmonyPatch(typeof(scrDecoration))]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static class scrDecorationPatch
|
||||
{
|
||||
[HarmonyPatch(typeof(scrDecoration))]
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("SetRotation")]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static class scrDecorationPatch
|
||||
public static bool SetRotationPrefix(scrDecoration __instance)
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("SetRotation")]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static bool SetRotationPrefix(scrDecoration __instance)
|
||||
{
|
||||
return __instance != null && __instance && __instance.pivotTrans != null;
|
||||
}
|
||||
return __instance != null && __instance && __instance.pivotTrans != null;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("SetScale")]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static bool SetScalePrefix(scrDecoration __instance)
|
||||
{
|
||||
return __instance != null && __instance && __instance.pivotTrans != null;
|
||||
}
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("SetScale")]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static bool SetScalePrefix(scrDecoration __instance)
|
||||
{
|
||||
return __instance != null && __instance && __instance.pivotTrans != null;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("SetOpacity")]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static bool SetOpacityPrefix(scrDecoration __instance)
|
||||
{
|
||||
return __instance != null && __instance;
|
||||
}
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("SetOpacity")]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static bool SetOpacityPrefix(scrDecoration __instance)
|
||||
{
|
||||
return __instance != null && __instance;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("UpdatePosition")]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static bool UpdatePositionPrefix(scrDecoration __instance)
|
||||
{
|
||||
return __instance != null && __instance && __instance.pivotTrans != null;
|
||||
}
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("UpdatePosition")]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static bool UpdatePositionPrefix(scrDecoration __instance)
|
||||
{
|
||||
return __instance != null && __instance && __instance.pivotTrans != null;
|
||||
}
|
||||
}
|
||||
+15
-16
@@ -1,22 +1,21 @@
|
||||
using ADOFAIRenderer.Core;
|
||||
using HarmonyLib;
|
||||
|
||||
namespace ADOFAIRenderer.Patch.Game
|
||||
{
|
||||
[HarmonyPatch(typeof(scrFloor))]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public class scrFloorPatch
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("SpawnPortalParticles")]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static bool SkipPortalSpawn(scrFloor __instance)
|
||||
{
|
||||
if (!scrLevelMaker.instance.listFloors.Contains(__instance)) return true;
|
||||
namespace ADOFAIRenderer.Patch.Game;
|
||||
|
||||
var nonTile = RenderManager.Instance.OverrideLoading &&
|
||||
RenderManager.Instance.Configuration.AdvancedConfig.NonTile;
|
||||
return !nonTile;
|
||||
}
|
||||
[HarmonyPatch(typeof(scrFloor))]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public class scrFloorPatch
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("SpawnPortalParticles")]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static bool SkipPortalSpawn(scrFloor __instance)
|
||||
{
|
||||
if (!scrLevelMaker.instance.listFloors.Contains(__instance)) return true;
|
||||
|
||||
var nonTile = RenderManager.Instance.OverrideLoading &&
|
||||
RenderManager.Instance.Configuration.AdvancedConfig.NonTile;
|
||||
return !nonTile;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
using ADOFAIRenderer.Core;
|
||||
using HarmonyLib;
|
||||
|
||||
namespace ADOFAIRenderer.Patch.Game
|
||||
namespace ADOFAIRenderer.Patch.Game;
|
||||
|
||||
[HarmonyPatch(typeof(scrHitTextManager), "ShowHitText")]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public class scrHitTextManagerPatch
|
||||
{
|
||||
[HarmonyPatch(typeof(scrHitTextManager), "ShowHitText")]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public class scrHitTextManagerPatch
|
||||
public static bool Prefix()
|
||||
{
|
||||
public static bool Prefix()
|
||||
{
|
||||
return !RenderManager.IsActive || RenderManager.Instance is { Configuration: { ShowJudgementText: true } };
|
||||
}
|
||||
return !RenderManager.IsActive || RenderManager.Instance is { Configuration: { ShowJudgementText: true } };
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,16 @@
|
||||
using ADOFAIRenderer.Core;
|
||||
using HarmonyLib;
|
||||
|
||||
namespace ADOFAIRenderer.Patch.Game
|
||||
namespace ADOFAIRenderer.Patch.Game;
|
||||
|
||||
[HarmonyPatch(typeof(scrMistakesManager))]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static class scrMistakesManagerPatch
|
||||
{
|
||||
[HarmonyPatch(typeof(scrMistakesManager))]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static class scrMistakesManagerPatch
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("SaveCustom")]
|
||||
public static bool PreventSaveCustom()
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("SaveCustom")]
|
||||
public static bool PreventSaveCustom()
|
||||
{
|
||||
return !RenderManager.IsActive;
|
||||
}
|
||||
return !RenderManager.IsActive;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,20 @@
|
||||
using ADOFAIRenderer.Core;
|
||||
using HarmonyLib;
|
||||
|
||||
namespace ADOFAIRenderer.Patch.Game
|
||||
{
|
||||
[HarmonyPatch(typeof(scrPlayerManager))]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static class scrPlayerManagerPatch
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("AnyValidInputWasTriggered")]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private static bool IgnoreInput(ref bool __result)
|
||||
{
|
||||
if (!RenderManager.IsActive) return true;
|
||||
namespace ADOFAIRenderer.Patch.Game;
|
||||
|
||||
__result = RenderManager.Instance && RenderManager.Instance.frameCount == 0 && RenderManager.Instance.countdown == 0;
|
||||
return false;
|
||||
}
|
||||
[HarmonyPatch(typeof(scrPlayerManager))]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static class scrPlayerManagerPatch
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("AnyValidInputWasTriggered")]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private static bool IgnoreInput(ref bool __result)
|
||||
{
|
||||
if (!RenderManager.IsActive) return true;
|
||||
|
||||
__result = RenderManager.Instance && RenderManager.Instance.frameCount == 0 && RenderManager.Instance.countdown == 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -2,28 +2,27 @@ using ADOFAIRenderer.Core;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ADOFAIRenderer.Patch.Game
|
||||
namespace ADOFAIRenderer.Patch.Game;
|
||||
|
||||
[HarmonyPatch(typeof(scrPortalParticles))]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static class scrPortalParticlesPatch
|
||||
{
|
||||
[HarmonyPatch(typeof(scrPortalParticles))]
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch("Start")]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static class scrPortalParticlesPatch
|
||||
public static void ForceScaledTime(scrPortalParticles __instance)
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch("Start")]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static void ForceScaledTime(scrPortalParticles __instance)
|
||||
if (!RenderManager.Instance || !RenderManager.Instance.OverrideLoading) return;
|
||||
if (__instance == null) return;
|
||||
|
||||
var particleSystems = __instance.GetComponentsInChildren<ParticleSystem>(true);
|
||||
foreach (var ps in particleSystems)
|
||||
{
|
||||
if (!RenderManager.Instance || !RenderManager.Instance.OverrideLoading) return;
|
||||
if (__instance == null) return;
|
||||
if (ps == null) continue;
|
||||
|
||||
var particleSystems = __instance.GetComponentsInChildren<ParticleSystem>(true);
|
||||
foreach (var ps in particleSystems)
|
||||
{
|
||||
if (ps == null) continue;
|
||||
|
||||
var main = ps.main;
|
||||
main.useUnscaledTime = false;
|
||||
}
|
||||
var main = ps.main;
|
||||
main.useUnscaledTime = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,19 @@
|
||||
using ADOFAIRenderer.Core;
|
||||
using HarmonyLib;
|
||||
|
||||
namespace ADOFAIRenderer.Patch.Game
|
||||
{
|
||||
[HarmonyPatch(typeof(scrPressToStart))]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public class scrPressToStartPatch
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("ShowText")]
|
||||
private static bool PreventShowText()
|
||||
{
|
||||
if (!RenderManager.Instance || !RenderManager.Instance.skipPressToStart) return true;
|
||||
namespace ADOFAIRenderer.Patch.Game;
|
||||
|
||||
RenderManager.Instance.skipPressToStart = false;
|
||||
return false;
|
||||
}
|
||||
[HarmonyPatch(typeof(scrPressToStart))]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public class scrPressToStartPatch
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch("ShowText")]
|
||||
private static bool PreventShowText()
|
||||
{
|
||||
if (!RenderManager.Instance || !RenderManager.Instance.skipPressToStart) return true;
|
||||
|
||||
RenderManager.Instance.skipPressToStart = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -4,34 +4,33 @@ using HarmonyLib;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace ADOFAIRenderer.Patch.Game
|
||||
namespace ADOFAIRenderer.Patch.Game;
|
||||
|
||||
[HarmonyPatch(typeof(scrShowIfDebug))]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static class scrShowIfDebugPatch
|
||||
{
|
||||
[HarmonyPatch(typeof(scrShowIfDebug))]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static class scrShowIfDebugPatch
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch("Update")]
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
private static void IgnoreInput(Text ___txt, Color ___defaultColor)
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch("Update")]
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
private static void IgnoreInput(Text ___txt, Color ___defaultColor)
|
||||
if (!RenderManager.IsActive) return;
|
||||
|
||||
___txt.text = "Render";
|
||||
if (RenderManager.Instance)
|
||||
{
|
||||
if (!RenderManager.IsActive) return;
|
||||
if (RenderManager.Instance.countdown > 0) ___txt.text = $"Render in {RenderManager.Instance.countdown}s..";
|
||||
|
||||
___txt.text = "Render";
|
||||
if (RenderManager.Instance)
|
||||
if (RenderManager.Instance.wonFrame != -1)
|
||||
{
|
||||
if (RenderManager.Instance.countdown > 0) ___txt.text = $"Render in {RenderManager.Instance.countdown}s..";
|
||||
|
||||
if (RenderManager.Instance.wonFrame != -1)
|
||||
{
|
||||
var elapsedFrames = RenderManager.Instance.frameCount - RenderManager.Instance.wonFrame;
|
||||
var remainingTime = RenderManager.Instance.Configuration.DelayAfterEnd - elapsedFrames / RenderManager.Instance.Configuration.RenderFPS;
|
||||
if (remainingTime < 0) remainingTime = 0;
|
||||
___txt.text += $"\nWaiting for {remainingTime:F1}s..";
|
||||
}
|
||||
var elapsedFrames = RenderManager.Instance.frameCount - RenderManager.Instance.wonFrame;
|
||||
var remainingTime = RenderManager.Instance.Configuration.DelayAfterEnd - elapsedFrames / RenderManager.Instance.Configuration.RenderFPS;
|
||||
if (remainingTime < 0) remainingTime = 0;
|
||||
___txt.text += $"\nWaiting for {remainingTime:F1}s..";
|
||||
}
|
||||
|
||||
___txt.color = ___defaultColor;
|
||||
}
|
||||
|
||||
___txt.color = ___defaultColor;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,20 @@
|
||||
using ADOFAIRenderer.Core;
|
||||
using HarmonyLib;
|
||||
|
||||
namespace ADOFAIRenderer.Patch.Game
|
||||
namespace ADOFAIRenderer.Patch.Game;
|
||||
|
||||
[HarmonyPatch(typeof(scrUIController))]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public class scrUIControllerPatch
|
||||
{
|
||||
[HarmonyPatch(typeof(scrUIController))]
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch("ShowEndscreenLanterns")]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public class scrUIControllerPatch
|
||||
public static void PreventEndscreenLanternsPostfix(scrUIController __instance)
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch("ShowEndscreenLanterns")]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static void PreventEndscreenLanternsPostfix(scrUIController __instance)
|
||||
if (RenderManager.IsActive && __instance != null)
|
||||
{
|
||||
if (RenderManager.IsActive && __instance != null)
|
||||
{
|
||||
__instance.HideEndscreenLanterns();
|
||||
}
|
||||
__instance.HideEndscreenLanterns();
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -7,7 +7,7 @@ using MelonLoader.Utils;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
[assembly: MelonInfo(typeof(RenderMod), "ADOFAI Renderer", "0.1.1-INDEV", "ADOFAI Editor Forum")]
|
||||
[assembly: MelonInfo(typeof(RenderMod), "ADOFAI Renderer", "0.1.2-INDEV", "ADOFAI Editor Forum")]
|
||||
[assembly: MelonGame("7th Beat Games", "A Dance of Fire and Ice")]
|
||||
|
||||
namespace ADOFAIRenderer
|
||||
|
||||
+311
-310
@@ -7,317 +7,318 @@ using UnityEngine.UIElements;
|
||||
using Button = UnityEngine.UI.Button;
|
||||
using Image = UnityEngine.UI.Image;
|
||||
|
||||
namespace ADOFAIRenderer.UI
|
||||
namespace ADOFAIRenderer.UI;
|
||||
|
||||
internal class DialogButton
|
||||
{
|
||||
internal class DialogButton
|
||||
// ReSharper disable FieldCanBeMadeReadOnly.Global
|
||||
public string Text;
|
||||
public Action OnClick;
|
||||
public Color? BackgroundColor;
|
||||
// ReSharper restore FieldCanBeMadeReadOnly.Global
|
||||
|
||||
public DialogButton(string text, Action onClick, Color? backgroundColor = null)
|
||||
{
|
||||
public string Text;
|
||||
public Action OnClick;
|
||||
public Color? BackgroundColor;
|
||||
|
||||
public DialogButton(string text, Action onClick, Color? backgroundColor = null)
|
||||
{
|
||||
Text = text;
|
||||
OnClick = onClick;
|
||||
BackgroundColor = backgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
internal class CommonWarningDialogUI : MonoBehaviour
|
||||
{
|
||||
public static CommonWarningDialogUI? Instance { get; private set; }
|
||||
private readonly List<VisualElement> _disabledElements = new();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
foreach (var elem in _disabledElements)
|
||||
{
|
||||
if (elem != null)
|
||||
{
|
||||
elem.SetEnabled(true);
|
||||
}
|
||||
}
|
||||
_disabledElements.Clear();
|
||||
|
||||
if (Instance == this)
|
||||
{
|
||||
Instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static TMP_FontAsset GetNanumSquareNeoBdFontAsset()
|
||||
{
|
||||
TMP_FontAsset? font = null;
|
||||
if (RenderMod.FontBundle)
|
||||
{
|
||||
try
|
||||
{
|
||||
font = RenderMod.FontBundle.LoadAsset<TMP_FontAsset>("NanumSquareNeoOTF-Bd SDF");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 로딩 실패
|
||||
}
|
||||
}
|
||||
|
||||
// 폴백 처리
|
||||
if (!font)
|
||||
{
|
||||
try
|
||||
{
|
||||
font = RDString.fontData.fontTMP;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
if (!font)
|
||||
{
|
||||
// ReSharper disable once Unity.UnknownResource
|
||||
font = Resources.Load<TMP_FontAsset>("Fonts & Materials/LiberationSans SDF");
|
||||
}
|
||||
|
||||
if (font) return font;
|
||||
var fonts = Resources.FindObjectsOfTypeAll<TMP_FontAsset>();
|
||||
if (fonts.Length > 0) font = fonts[0];
|
||||
|
||||
return font!;
|
||||
}
|
||||
|
||||
public static void ShowDialog(string title, string description, params DialogButton[] buttons)
|
||||
{
|
||||
if (buttons.Length == 0) return;
|
||||
|
||||
var obj = new GameObject("CommonWarningDialogUI");
|
||||
DontDestroyOnLoad(obj);
|
||||
var dialog = obj.AddComponent<CommonWarningDialogUI>();
|
||||
dialog.Initialize(title, description, buttons);
|
||||
}
|
||||
|
||||
private void Initialize(string title, string description, DialogButton[] buttons)
|
||||
{
|
||||
if (!UnityEngine.EventSystems.EventSystem.current)
|
||||
{
|
||||
var eventSystemObj = new GameObject("EventSystem");
|
||||
eventSystemObj.transform.SetParent(transform);
|
||||
eventSystemObj.AddComponent<UnityEngine.EventSystems.EventSystem>();
|
||||
eventSystemObj.AddComponent<UnityEngine.EventSystems.StandaloneInputModule>();
|
||||
}
|
||||
|
||||
foreach (var doc in FindObjectsByType<UIDocument>(FindObjectsSortMode.None))
|
||||
{
|
||||
if (doc && doc.rootVisualElement != null && doc.rootVisualElement.enabledSelf)
|
||||
{
|
||||
doc.rootVisualElement.SetEnabled(false);
|
||||
_disabledElements.Add(doc.rootVisualElement);
|
||||
}
|
||||
}
|
||||
|
||||
// UI 레이어(5) 설정
|
||||
gameObject.layer = 5;
|
||||
|
||||
// Canvas 설정
|
||||
var canvas = gameObject.AddComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = 32766; // FFmpegDownloadUI(32767) 바로 아래
|
||||
|
||||
// 해상도 대응 캔버스 스케일러 설정
|
||||
var scaler = gameObject.AddComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920, 1080);
|
||||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
||||
scaler.matchWidthOrHeight = 0.5f;
|
||||
|
||||
gameObject.AddComponent<GraphicRaycaster>();
|
||||
|
||||
// 1. 전체 화면 오버레이 배경 생성
|
||||
var bgPanelObj = new GameObject("BackgroundPanel");
|
||||
bgPanelObj.transform.SetParent(transform, false);
|
||||
bgPanelObj.layer = 5;
|
||||
|
||||
var bgImage = bgPanelObj.AddComponent<Image>();
|
||||
bgImage.color = new Color(0f, 0f, 0f, 0.65f); // 어두운 투명 오버레이
|
||||
bgImage.raycastTarget = true;
|
||||
|
||||
var bgRect = bgPanelObj.GetComponent<RectTransform>();
|
||||
bgRect.anchorMin = Vector2.zero;
|
||||
bgRect.anchorMax = Vector2.one;
|
||||
bgRect.offsetMin = Vector2.zero;
|
||||
bgRect.offsetMax = Vector2.zero;
|
||||
|
||||
var font = GetNanumSquareNeoBdFontAsset();
|
||||
|
||||
// 2. 모달 컨테이너 생성
|
||||
var modalContainerObj = new GameObject("ModalContainer");
|
||||
modalContainerObj.transform.SetParent(bgPanelObj.transform, false);
|
||||
modalContainerObj.layer = 5;
|
||||
|
||||
var modalImage = modalContainerObj.AddComponent<Image>();
|
||||
modalImage.color = new Color(0.08f, 0.08f, 0.1f, 0.88f); // 기존 UI의 배경 톤과 동일
|
||||
|
||||
var modalOutline = modalContainerObj.AddComponent<Outline>();
|
||||
modalOutline.effectColor = Color.white;
|
||||
modalOutline.effectDistance = new Vector2(3.0f, -3.0f); // 기존 UI 외곽선 스타일 일관성
|
||||
|
||||
var modalRect = modalContainerObj.GetComponent<RectTransform>();
|
||||
modalRect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
modalRect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
modalRect.pivot = new Vector2(0.5f, 0.5f);
|
||||
modalRect.sizeDelta = new Vector2(650, 360);
|
||||
|
||||
// 3. 타이틀 텍스트 생성
|
||||
var titleObj = new GameObject("TitleText");
|
||||
titleObj.transform.SetParent(modalContainerObj.transform, false);
|
||||
titleObj.layer = 5;
|
||||
|
||||
var titleText = titleObj.AddComponent<TextMeshProUGUI>();
|
||||
titleText.text = title;
|
||||
titleText.fontSize = 24;
|
||||
titleText.font = font;
|
||||
titleText.color = new Color(1.0f, 0.8f, 0.2f, 1.0f); // 경고 테마에 맞는 주황/황색 계열
|
||||
titleText.alignment = TextAlignmentOptions.Center;
|
||||
|
||||
var titleRect = titleObj.GetComponent<RectTransform>();
|
||||
titleRect.anchorMin = new Vector2(0, 1);
|
||||
titleRect.anchorMax = new Vector2(1, 1);
|
||||
titleRect.pivot = new Vector2(0.5f, 1);
|
||||
titleRect.anchoredPosition = new Vector2(0, -35);
|
||||
titleRect.sizeDelta = new Vector2(0, 40);
|
||||
|
||||
// 4. 본문 설명 텍스트 생성
|
||||
var descObj = new GameObject("DescriptionText");
|
||||
descObj.transform.SetParent(modalContainerObj.transform, false);
|
||||
descObj.layer = 5;
|
||||
|
||||
var descText = descObj.AddComponent<TextMeshProUGUI>();
|
||||
descText.text = description;
|
||||
descText.fontSize = 20;
|
||||
descText.font = font;
|
||||
descText.color = new Color(0.85f, 0.85f, 0.85f, 1f);
|
||||
descText.alignment = TextAlignmentOptions.Center;
|
||||
descText.lineSpacing = 8;
|
||||
|
||||
var descRect = descObj.GetComponent<RectTransform>();
|
||||
descRect.anchorMin = new Vector2(0, 0.5f);
|
||||
descRect.anchorMax = new Vector2(1, 1);
|
||||
descRect.pivot = new Vector2(0.5f, 0.5f);
|
||||
descRect.anchoredPosition = new Vector2(0, -65);
|
||||
descRect.sizeDelta = new Vector2(-40, 120);
|
||||
|
||||
// 5. 버튼 컨테이너 생성
|
||||
var buttonContainerObj = new GameObject("ButtonContainer", typeof(RectTransform));
|
||||
buttonContainerObj.transform.SetParent(modalContainerObj.transform, false);
|
||||
buttonContainerObj.layer = 5;
|
||||
|
||||
var btnContainerRect = buttonContainerObj.GetComponent<RectTransform>();
|
||||
btnContainerRect.anchorMin = new Vector2(0, 0);
|
||||
btnContainerRect.anchorMax = new Vector2(1, 0);
|
||||
btnContainerRect.pivot = new Vector2(0.5f, 0);
|
||||
btnContainerRect.anchoredPosition = new Vector2(0, 35);
|
||||
btnContainerRect.sizeDelta = new Vector2(0, 50);
|
||||
|
||||
// 6. 가변 버튼 동적 배치 및 스타일링
|
||||
float totalWidth = 560f;
|
||||
float spacing = 15f;
|
||||
float buttonHeight = 45f;
|
||||
int numButtons = buttons.Length;
|
||||
|
||||
float buttonWidth = numButtons == 1
|
||||
? 200f
|
||||
: (totalWidth - (spacing * (numButtons - 1))) / numButtons;
|
||||
|
||||
float startX = numButtons == 1
|
||||
? 0
|
||||
: (-totalWidth / 2f) + (buttonWidth / 2f);
|
||||
|
||||
for (int i = 0; i < numButtons; i++)
|
||||
{
|
||||
var btnData = buttons[i];
|
||||
var btnObj = new GameObject($"Button_{i}");
|
||||
btnObj.transform.SetParent(buttonContainerObj.transform, false);
|
||||
btnObj.layer = 5;
|
||||
|
||||
// 기본 색상 정의
|
||||
Color baseColor = btnData.BackgroundColor ?? new Color(0.2f, 0.2f, 0.23f, 1f); // 커스텀 색상 또는 회색톤 기본값
|
||||
Color hoverColor = ApplyColorBrightness(baseColor, 1.2f);
|
||||
Color pressedColor = ApplyColorBrightness(baseColor, 0.8f);
|
||||
|
||||
var btnImage = btnObj.AddComponent<Image>();
|
||||
btnImage.color = baseColor;
|
||||
btnImage.raycastTarget = true;
|
||||
|
||||
var btn = btnObj.AddComponent<Button>();
|
||||
SetupButtonStyle(btn, baseColor, hoverColor, pressedColor);
|
||||
|
||||
// 클릭 콜백 등록
|
||||
btn.onClick.AddListener(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
btnData.OnClick();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MelonLoader.Melon<RenderMod>.Logger.Error($"Dialog Button Click Event Error: {ex}");
|
||||
}
|
||||
Destroy(gameObject);
|
||||
});
|
||||
|
||||
var btnRect = btnObj.GetComponent<RectTransform>();
|
||||
btnRect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
btnRect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
btnRect.pivot = new Vector2(0.5f, 0.5f);
|
||||
|
||||
float posX = numButtons == 1
|
||||
? 0
|
||||
: startX + i * (buttonWidth + spacing);
|
||||
|
||||
btnRect.anchoredPosition = new Vector2(posX, 0);
|
||||
btnRect.sizeDelta = new Vector2(buttonWidth, buttonHeight);
|
||||
|
||||
// 텍스트 라벨 추가
|
||||
var txtObj = new GameObject("Text");
|
||||
txtObj.transform.SetParent(btnObj.transform, false);
|
||||
txtObj.layer = 5;
|
||||
|
||||
var text = txtObj.AddComponent<TextMeshProUGUI>();
|
||||
text.text = btnData.Text;
|
||||
text.fontSize = 20;
|
||||
text.font = font;
|
||||
text.color = Color.white;
|
||||
text.alignment = TextAlignmentOptions.Center;
|
||||
|
||||
var txtRect = txtObj.GetComponent<RectTransform>();
|
||||
txtRect.anchorMin = Vector2.zero;
|
||||
txtRect.anchorMax = Vector2.one;
|
||||
txtRect.offsetMin = Vector2.zero;
|
||||
txtRect.offsetMax = Vector2.zero;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupButtonStyle(Button button, Color normalColor, Color hoverColor, Color pressedColor)
|
||||
{
|
||||
button.transition = Selectable.Transition.ColorTint;
|
||||
var colors = button.colors;
|
||||
colors.normalColor = normalColor;
|
||||
colors.highlightedColor = hoverColor;
|
||||
colors.pressedColor = pressedColor;
|
||||
colors.disabledColor = new Color(0.3f, 0.3f, 0.3f, 1f);
|
||||
button.colors = colors;
|
||||
}
|
||||
|
||||
private Color ApplyColorBrightness(Color color, float factor)
|
||||
{
|
||||
float r = Mathf.Clamp01(color.r * factor);
|
||||
float g = Mathf.Clamp01(color.g * factor);
|
||||
float b = Mathf.Clamp01(color.b * factor);
|
||||
return new Color(r, g, b, color.a);
|
||||
}
|
||||
Text = text;
|
||||
OnClick = onClick;
|
||||
BackgroundColor = backgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
internal class CommonWarningDialogUI : MonoBehaviour
|
||||
{
|
||||
public static CommonWarningDialogUI? Instance { get; private set; }
|
||||
private readonly List<VisualElement> _disabledElements = new();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
foreach (var elem in _disabledElements)
|
||||
{
|
||||
if (elem != null)
|
||||
{
|
||||
elem.SetEnabled(true);
|
||||
}
|
||||
}
|
||||
_disabledElements.Clear();
|
||||
|
||||
if (Instance == this)
|
||||
{
|
||||
Instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static TMP_FontAsset GetNanumSquareNeoBdFontAsset()
|
||||
{
|
||||
TMP_FontAsset? font = null;
|
||||
if (RenderMod.FontBundle)
|
||||
{
|
||||
try
|
||||
{
|
||||
font = RenderMod.FontBundle.LoadAsset<TMP_FontAsset>("NanumSquareNeoOTF-Bd SDF");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 로딩 실패
|
||||
}
|
||||
}
|
||||
|
||||
// 폴백 처리
|
||||
if (!font)
|
||||
{
|
||||
try
|
||||
{
|
||||
font = RDString.fontData.fontTMP;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
if (!font)
|
||||
{
|
||||
// ReSharper disable once Unity.UnknownResource
|
||||
font = Resources.Load<TMP_FontAsset>("Fonts & Materials/LiberationSans SDF");
|
||||
}
|
||||
|
||||
if (font) return font;
|
||||
var fonts = Resources.FindObjectsOfTypeAll<TMP_FontAsset>();
|
||||
if (fonts.Length > 0) font = fonts[0];
|
||||
|
||||
return font!;
|
||||
}
|
||||
|
||||
public static void ShowDialog(string title, string description, params DialogButton[] buttons)
|
||||
{
|
||||
if (buttons.Length == 0) return;
|
||||
|
||||
var obj = new GameObject("CommonWarningDialogUI");
|
||||
DontDestroyOnLoad(obj);
|
||||
var dialog = obj.AddComponent<CommonWarningDialogUI>();
|
||||
dialog.Initialize(title, description, buttons);
|
||||
}
|
||||
|
||||
private void Initialize(string title, string description, DialogButton[] buttons)
|
||||
{
|
||||
if (!UnityEngine.EventSystems.EventSystem.current)
|
||||
{
|
||||
var eventSystemObj = new GameObject("EventSystem");
|
||||
eventSystemObj.transform.SetParent(transform);
|
||||
eventSystemObj.AddComponent<UnityEngine.EventSystems.EventSystem>();
|
||||
eventSystemObj.AddComponent<UnityEngine.EventSystems.StandaloneInputModule>();
|
||||
}
|
||||
|
||||
foreach (var doc in FindObjectsByType<UIDocument>(FindObjectsSortMode.None))
|
||||
{
|
||||
if (doc && doc.rootVisualElement is { enabledSelf: true })
|
||||
{
|
||||
doc.rootVisualElement.SetEnabled(false);
|
||||
_disabledElements.Add(doc.rootVisualElement);
|
||||
}
|
||||
}
|
||||
|
||||
// UI 레이어(5) 설정
|
||||
gameObject.layer = 5;
|
||||
|
||||
// Canvas 설정
|
||||
var canvas = gameObject.AddComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = 32766; // FFmpegDownloadUI(32767) 바로 아래
|
||||
|
||||
// 해상도 대응 캔버스 스케일러 설정
|
||||
var scaler = gameObject.AddComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920, 1080);
|
||||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
||||
scaler.matchWidthOrHeight = 0.5f;
|
||||
|
||||
gameObject.AddComponent<GraphicRaycaster>();
|
||||
|
||||
// 1. 전체 화면 오버레이 배경 생성
|
||||
var bgPanelObj = new GameObject("BackgroundPanel");
|
||||
bgPanelObj.transform.SetParent(transform, false);
|
||||
bgPanelObj.layer = 5;
|
||||
|
||||
var bgImage = bgPanelObj.AddComponent<Image>();
|
||||
bgImage.color = new Color(0f, 0f, 0f, 0.65f); // 어두운 투명 오버레이
|
||||
bgImage.raycastTarget = true;
|
||||
|
||||
var bgRect = bgPanelObj.GetComponent<RectTransform>();
|
||||
bgRect.anchorMin = Vector2.zero;
|
||||
bgRect.anchorMax = Vector2.one;
|
||||
bgRect.offsetMin = Vector2.zero;
|
||||
bgRect.offsetMax = Vector2.zero;
|
||||
|
||||
var font = GetNanumSquareNeoBdFontAsset();
|
||||
|
||||
// 2. 모달 컨테이너 생성
|
||||
var modalContainerObj = new GameObject("ModalContainer");
|
||||
modalContainerObj.transform.SetParent(bgPanelObj.transform, false);
|
||||
modalContainerObj.layer = 5;
|
||||
|
||||
var modalImage = modalContainerObj.AddComponent<Image>();
|
||||
modalImage.color = new Color(0.08f, 0.08f, 0.1f, 0.88f); // 기존 UI의 배경 톤과 동일
|
||||
|
||||
var modalOutline = modalContainerObj.AddComponent<Outline>();
|
||||
modalOutline.effectColor = Color.white;
|
||||
modalOutline.effectDistance = new Vector2(3.0f, -3.0f); // 기존 UI 외곽선 스타일 일관성
|
||||
|
||||
var modalRect = modalContainerObj.GetComponent<RectTransform>();
|
||||
modalRect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
modalRect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
modalRect.pivot = new Vector2(0.5f, 0.5f);
|
||||
modalRect.sizeDelta = new Vector2(650, 360);
|
||||
|
||||
// 3. 타이틀 텍스트 생성
|
||||
var titleObj = new GameObject("TitleText");
|
||||
titleObj.transform.SetParent(modalContainerObj.transform, false);
|
||||
titleObj.layer = 5;
|
||||
|
||||
var titleText = titleObj.AddComponent<TextMeshProUGUI>();
|
||||
titleText.text = title;
|
||||
titleText.fontSize = 24;
|
||||
titleText.font = font;
|
||||
titleText.color = new Color(1.0f, 0.8f, 0.2f, 1.0f); // 경고 테마에 맞는 주황/황색 계열
|
||||
titleText.alignment = TextAlignmentOptions.Center;
|
||||
|
||||
var titleRect = titleObj.GetComponent<RectTransform>();
|
||||
titleRect.anchorMin = new Vector2(0, 1);
|
||||
titleRect.anchorMax = new Vector2(1, 1);
|
||||
titleRect.pivot = new Vector2(0.5f, 1);
|
||||
titleRect.anchoredPosition = new Vector2(0, -35);
|
||||
titleRect.sizeDelta = new Vector2(0, 40);
|
||||
|
||||
// 4. 본문 설명 텍스트 생성
|
||||
var descObj = new GameObject("DescriptionText");
|
||||
descObj.transform.SetParent(modalContainerObj.transform, false);
|
||||
descObj.layer = 5;
|
||||
|
||||
var descText = descObj.AddComponent<TextMeshProUGUI>();
|
||||
descText.text = description;
|
||||
descText.fontSize = 20;
|
||||
descText.font = font;
|
||||
descText.color = new Color(0.85f, 0.85f, 0.85f, 1f);
|
||||
descText.alignment = TextAlignmentOptions.Center;
|
||||
descText.lineSpacing = 8;
|
||||
|
||||
var descRect = descObj.GetComponent<RectTransform>();
|
||||
descRect.anchorMin = new Vector2(0, 0.5f);
|
||||
descRect.anchorMax = new Vector2(1, 1);
|
||||
descRect.pivot = new Vector2(0.5f, 0.5f);
|
||||
descRect.anchoredPosition = new Vector2(0, -65);
|
||||
descRect.sizeDelta = new Vector2(-40, 120);
|
||||
|
||||
// 5. 버튼 컨테이너 생성
|
||||
var buttonContainerObj = new GameObject("ButtonContainer", typeof(RectTransform));
|
||||
buttonContainerObj.transform.SetParent(modalContainerObj.transform, false);
|
||||
buttonContainerObj.layer = 5;
|
||||
|
||||
var btnContainerRect = buttonContainerObj.GetComponent<RectTransform>();
|
||||
btnContainerRect.anchorMin = new Vector2(0, 0);
|
||||
btnContainerRect.anchorMax = new Vector2(1, 0);
|
||||
btnContainerRect.pivot = new Vector2(0.5f, 0);
|
||||
btnContainerRect.anchoredPosition = new Vector2(0, 35);
|
||||
btnContainerRect.sizeDelta = new Vector2(0, 50);
|
||||
|
||||
// 6. 가변 버튼 동적 배치 및 스타일링
|
||||
float totalWidth = 560f;
|
||||
float spacing = 15f;
|
||||
float buttonHeight = 45f;
|
||||
int numButtons = buttons.Length;
|
||||
|
||||
float buttonWidth = numButtons == 1
|
||||
? 200f
|
||||
: (totalWidth - (spacing * (numButtons - 1))) / numButtons;
|
||||
|
||||
float startX = numButtons == 1
|
||||
? 0
|
||||
: (-totalWidth / 2f) + (buttonWidth / 2f);
|
||||
|
||||
for (int i = 0; i < numButtons; i++)
|
||||
{
|
||||
var btnData = buttons[i];
|
||||
var btnObj = new GameObject($"Button_{i}");
|
||||
btnObj.transform.SetParent(buttonContainerObj.transform, false);
|
||||
btnObj.layer = 5;
|
||||
|
||||
// 기본 색상 정의
|
||||
Color baseColor = btnData.BackgroundColor ?? new Color(0.2f, 0.2f, 0.23f, 1f); // 커스텀 색상 또는 회색톤 기본값
|
||||
Color hoverColor = ApplyColorBrightness(baseColor, 1.2f);
|
||||
Color pressedColor = ApplyColorBrightness(baseColor, 0.8f);
|
||||
|
||||
var btnImage = btnObj.AddComponent<Image>();
|
||||
btnImage.color = baseColor;
|
||||
btnImage.raycastTarget = true;
|
||||
|
||||
var btn = btnObj.AddComponent<Button>();
|
||||
SetupButtonStyle(btn, baseColor, hoverColor, pressedColor);
|
||||
|
||||
// 클릭 콜백 등록
|
||||
btn.onClick.AddListener(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
btnData.OnClick();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MelonLoader.Melon<RenderMod>.Logger.Error($"Dialog Button Click Event Error: {ex}");
|
||||
}
|
||||
Destroy(gameObject);
|
||||
});
|
||||
|
||||
var btnRect = btnObj.GetComponent<RectTransform>();
|
||||
btnRect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
btnRect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
btnRect.pivot = new Vector2(0.5f, 0.5f);
|
||||
|
||||
float posX = numButtons == 1
|
||||
? 0
|
||||
: startX + i * (buttonWidth + spacing);
|
||||
|
||||
btnRect.anchoredPosition = new Vector2(posX, 0);
|
||||
btnRect.sizeDelta = new Vector2(buttonWidth, buttonHeight);
|
||||
|
||||
// 텍스트 라벨 추가
|
||||
var txtObj = new GameObject("Text");
|
||||
txtObj.transform.SetParent(btnObj.transform, false);
|
||||
txtObj.layer = 5;
|
||||
|
||||
var text = txtObj.AddComponent<TextMeshProUGUI>();
|
||||
text.text = btnData.Text;
|
||||
text.fontSize = 20;
|
||||
text.font = font;
|
||||
text.color = Color.white;
|
||||
text.alignment = TextAlignmentOptions.Center;
|
||||
|
||||
var txtRect = txtObj.GetComponent<RectTransform>();
|
||||
txtRect.anchorMin = Vector2.zero;
|
||||
txtRect.anchorMax = Vector2.one;
|
||||
txtRect.offsetMin = Vector2.zero;
|
||||
txtRect.offsetMax = Vector2.zero;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupButtonStyle(Button button, Color normalColor, Color hoverColor, Color pressedColor)
|
||||
{
|
||||
button.transition = Selectable.Transition.ColorTint;
|
||||
var colors = button.colors;
|
||||
colors.normalColor = normalColor;
|
||||
colors.highlightedColor = hoverColor;
|
||||
colors.pressedColor = pressedColor;
|
||||
colors.disabledColor = new Color(0.3f, 0.3f, 0.3f, 1f);
|
||||
button.colors = colors;
|
||||
}
|
||||
|
||||
private Color ApplyColorBrightness(Color color, float factor)
|
||||
{
|
||||
float r = Mathf.Clamp01(color.r * factor);
|
||||
float g = Mathf.Clamp01(color.g * factor);
|
||||
float b = Mathf.Clamp01(color.b * factor);
|
||||
return new Color(r, g, b, color.a);
|
||||
}
|
||||
}
|
||||
+207
-208
@@ -6,229 +6,228 @@ using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace ADOFAIRenderer.UI
|
||||
namespace ADOFAIRenderer.UI;
|
||||
|
||||
internal class DebugMonitorUI : MonoBehaviour
|
||||
{
|
||||
internal class DebugMonitorUI : MonoBehaviour
|
||||
// --- Progress Bar UI Configuration Constants ---
|
||||
private const float BarWidth = 800f;
|
||||
private const float BarHeight = 28f;
|
||||
private const float BarPosX = 0f;
|
||||
private const float BarPosY = -420f; // Y position relative to screen center (Reference: 1920x1080)
|
||||
private const int BarTextFontSize = 16; // 눈에 띄게 증가된 텍스트 크기
|
||||
private static readonly Color BarBackgroundColor = new(0.08f, 0.08f, 0.1f, 1.0f);
|
||||
private static readonly Color BarFillColor = new(0.0f, 0.8f, 1.0f, 1.0f); // Sleek cyan/blue
|
||||
private static readonly Color BarTextColor = new(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
|
||||
// --- Progress Bar Outline Configuration Constants ---
|
||||
private static readonly Color BarOutlineColor = Color.white; // 대비되는 흰색 윤곽선
|
||||
private static readonly Vector2 BarOutlineDistance = new(3.0f, -3.0f); // 3px의 굵고 직관적인 윤곽선 두께
|
||||
|
||||
private TextMeshProUGUI _text = null!;
|
||||
private GameObject _progressBarObj = null!;
|
||||
private GameObject _progressBarFillObj = null!;
|
||||
private RectTransform _fillRect = null!;
|
||||
private TextMeshProUGUI _progressText = null!;
|
||||
|
||||
private static TMP_FontAsset GetNanumSquareNeoBdFontAsset()
|
||||
{
|
||||
// --- Progress Bar UI Configuration Constants ---
|
||||
private const float BarWidth = 800f;
|
||||
private const float BarHeight = 28f;
|
||||
private const float BarPosX = 0f;
|
||||
private const float BarPosY = -420f; // Y position relative to screen center (Reference: 1920x1080)
|
||||
private const int BarTextFontSize = 16; // 눈에 띄게 증가된 텍스트 크기
|
||||
private static readonly Color BarBackgroundColor = new(0.08f, 0.08f, 0.1f, 1.0f);
|
||||
private static readonly Color BarFillColor = new(0.0f, 0.8f, 1.0f, 1.0f); // Sleek cyan/blue
|
||||
private static readonly Color BarTextColor = new(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
|
||||
// --- Progress Bar Outline Configuration Constants ---
|
||||
private static readonly Color BarOutlineColor = Color.white; // 대비되는 흰색 윤곽선
|
||||
private static readonly Vector2 BarOutlineDistance = new(3.0f, -3.0f); // 3px의 굵고 직관적인 윤곽선 두께
|
||||
|
||||
private TextMeshProUGUI _text = null!;
|
||||
private GameObject _progressBarObj = null!;
|
||||
private GameObject _progressBarFillObj = null!;
|
||||
private RectTransform _fillRect = null!;
|
||||
private TextMeshProUGUI _progressText = null!;
|
||||
|
||||
private static TMP_FontAsset GetNanumSquareNeoBdFontAsset()
|
||||
TMP_FontAsset? font = null;
|
||||
if (RenderMod.FontBundle != null)
|
||||
{
|
||||
TMP_FontAsset? font = null;
|
||||
if (RenderMod.FontBundle != null)
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
font = RenderMod.FontBundle.LoadAsset<TMP_FontAsset>("NanumSquareNeoOTF-Bd SDF");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 로딩 실패
|
||||
}
|
||||
font = RenderMod.FontBundle.LoadAsset<TMP_FontAsset>("NanumSquareNeoOTF-Bd SDF");
|
||||
}
|
||||
|
||||
// 폴백 처리: 에셋 번들에서 실패 시 게임 자체 NanumSquareNeo 또는 내장 TMP SDF 폰트 사용
|
||||
if (font == null)
|
||||
catch
|
||||
{
|
||||
font = RDString.fontData.fontTMP;
|
||||
// 로딩 실패
|
||||
}
|
||||
if (font == null)
|
||||
{
|
||||
// ReSharper disable once Unity.UnknownResource
|
||||
font = Resources.Load<TMP_FontAsset>("Fonts & Materials/LiberationSans SDF");
|
||||
}
|
||||
|
||||
if (font != null) return font;
|
||||
var fonts = Resources.FindObjectsOfTypeAll<TMP_FontAsset>();
|
||||
if (fonts.Length > 0) font = fonts[0];
|
||||
|
||||
return font!;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
// 폴백 처리: 에셋 번들에서 실패 시 게임 자체 NanumSquareNeo 또는 내장 TMP SDF 폰트 사용
|
||||
if (font == null)
|
||||
{
|
||||
// UI 레이어(5) 설정
|
||||
gameObject.layer = 5;
|
||||
|
||||
// Canvas 설정
|
||||
var canvas = gameObject.AddComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = 9999;
|
||||
|
||||
// 해상도 다이내믹 스케일링 설정 (Reference: 1920x1080)
|
||||
var scaler = gameObject.AddComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920, 1080);
|
||||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
||||
scaler.matchWidthOrHeight = 0.5f;
|
||||
|
||||
gameObject.AddComponent<GraphicRaycaster>();
|
||||
|
||||
// 배경 패널 생성
|
||||
var panelObj = new GameObject("BackgroundPanel");
|
||||
panelObj.transform.SetParent(transform, false);
|
||||
panelObj.layer = 5;
|
||||
|
||||
var image = panelObj.AddComponent<Image>();
|
||||
image.color = new Color(0.08f, 0.08f, 0.1f, 0.88f); // 약간 더 고급스러운 다크톤 아크릴 감성
|
||||
|
||||
var panelRect = panelObj.GetComponent<RectTransform>();
|
||||
panelRect.anchorMin = new Vector2(1, 1);
|
||||
panelRect.anchorMax = new Vector2(1, 1);
|
||||
panelRect.pivot = new Vector2(1, 1);
|
||||
panelRect.anchoredPosition = new Vector2(-30, -30);
|
||||
panelRect.sizeDelta = new Vector2(360, 220);
|
||||
|
||||
// 텍스트 생성
|
||||
var textObj = new GameObject("MonitorText");
|
||||
textObj.transform.SetParent(panelObj.transform, false);
|
||||
textObj.layer = 5;
|
||||
|
||||
_text = textObj.AddComponent<TextMeshProUGUI>();
|
||||
_text.richText = true;
|
||||
_text.fontSize = 16; // 13에서 16으로 증가
|
||||
_text.alignment = TextAlignmentOptions.TopLeft;
|
||||
|
||||
// TMPro 텍스트 잘림 현상 예방
|
||||
_text.textWrappingMode = TextWrappingModes.Normal;
|
||||
_text.overflowMode = TextOverflowModes.Overflow;
|
||||
|
||||
// NanumSquareNeo-Bd SDF 폰트 로드 및 적용
|
||||
var font = GetNanumSquareNeoBdFontAsset();
|
||||
_text.font = font;
|
||||
|
||||
var textRect = textObj.GetComponent<RectTransform>();
|
||||
textRect.anchorMin = Vector2.zero;
|
||||
textRect.anchorMax = Vector2.one;
|
||||
textRect.offsetMin = new Vector2(20, 20); // 좀 더 넉넉하고 패딩 여백
|
||||
textRect.offsetMax = new Vector2(-20, -20);
|
||||
|
||||
// --- Progress Bar UI Setup ---
|
||||
// 1. Progress Bar Container
|
||||
_progressBarObj = new GameObject("ProgressBar");
|
||||
_progressBarObj.transform.SetParent(transform, false);
|
||||
_progressBarObj.layer = 5;
|
||||
|
||||
var bgImage = _progressBarObj.AddComponent<Image>();
|
||||
bgImage.color = BarBackgroundColor;
|
||||
|
||||
// 프로그레스 바 흰색 윤곽선 컴포넌트 추가
|
||||
var bgOutline = _progressBarObj.AddComponent<Outline>();
|
||||
bgOutline.effectColor = BarOutlineColor;
|
||||
bgOutline.effectDistance = BarOutlineDistance;
|
||||
|
||||
var barRect = _progressBarObj.GetComponent<RectTransform>();
|
||||
barRect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
barRect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
barRect.pivot = new Vector2(0.5f, 0.5f);
|
||||
barRect.anchoredPosition = new Vector2(BarPosX, BarPosY);
|
||||
barRect.sizeDelta = new Vector2(BarWidth, BarHeight);
|
||||
|
||||
// 2. Progress Bar Fill
|
||||
_progressBarFillObj = new GameObject("ProgressBarFill");
|
||||
_progressBarFillObj.transform.SetParent(_progressBarObj.transform, false);
|
||||
_progressBarFillObj.layer = 5;
|
||||
|
||||
var fillImage = _progressBarFillObj.AddComponent<Image>();
|
||||
fillImage.color = BarFillColor;
|
||||
|
||||
_fillRect = _progressBarFillObj.GetComponent<RectTransform>();
|
||||
_fillRect.anchorMin = new Vector2(0, 0);
|
||||
_fillRect.anchorMax = new Vector2(0, 1);
|
||||
_fillRect.pivot = new Vector2(0, 0.5f);
|
||||
_fillRect.anchoredPosition = Vector2.zero;
|
||||
_fillRect.sizeDelta = new Vector2(0, 0); // width will be updated dynamically in Update()
|
||||
|
||||
// 3. Progress Bar Text
|
||||
var progressTextObj = new GameObject("ProgressBarText");
|
||||
progressTextObj.transform.SetParent(_progressBarObj.transform, false);
|
||||
progressTextObj.layer = 5;
|
||||
|
||||
_progressText = progressTextObj.AddComponent<TextMeshProUGUI>();
|
||||
_progressText.richText = true;
|
||||
_progressText.fontSize = BarTextFontSize; // 14에서 18로 증가
|
||||
_progressText.alignment = TextAlignmentOptions.Center;
|
||||
_progressText.font = font;
|
||||
_progressText.color = BarTextColor;
|
||||
|
||||
// 프로그레스 바 내부 텍스트 아웃라인 추가 (진행율 가독성 확보)
|
||||
var textOutline = progressTextObj.AddComponent<Outline>();
|
||||
textOutline.effectColor = new Color(0f, 0f, 0f, 0.9f);
|
||||
textOutline.effectDistance = new Vector2(1.5f, -1.5f);
|
||||
|
||||
var textRectBar = progressTextObj.GetComponent<RectTransform>();
|
||||
textRectBar.anchorMin = Vector2.zero;
|
||||
textRectBar.anchorMax = Vector2.one;
|
||||
textRectBar.offsetMin = Vector2.zero;
|
||||
textRectBar.offsetMax = Vector2.zero;
|
||||
font = RDString.fontData.fontTMP;
|
||||
}
|
||||
if (font == null)
|
||||
{
|
||||
// ReSharper disable once Unity.UnknownResource
|
||||
font = Resources.Load<TMP_FontAsset>("Fonts & Materials/LiberationSans SDF");
|
||||
}
|
||||
|
||||
private void Update()
|
||||
if (font != null) return font;
|
||||
var fonts = Resources.FindObjectsOfTypeAll<TMP_FontAsset>();
|
||||
if (fonts.Length > 0) font = fonts[0];
|
||||
|
||||
return font!;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// UI 레이어(5) 설정
|
||||
gameObject.layer = 5;
|
||||
|
||||
// Canvas 설정
|
||||
var canvas = gameObject.AddComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = 9999;
|
||||
|
||||
// 해상도 다이내믹 스케일링 설정 (Reference: 1920x1080)
|
||||
var scaler = gameObject.AddComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920, 1080);
|
||||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
||||
scaler.matchWidthOrHeight = 0.5f;
|
||||
|
||||
gameObject.AddComponent<GraphicRaycaster>();
|
||||
|
||||
// 배경 패널 생성
|
||||
var panelObj = new GameObject("BackgroundPanel");
|
||||
panelObj.transform.SetParent(transform, false);
|
||||
panelObj.layer = 5;
|
||||
|
||||
var image = panelObj.AddComponent<Image>();
|
||||
image.color = new Color(0.08f, 0.08f, 0.1f, 0.88f); // 약간 더 고급스러운 다크톤 아크릴 감성
|
||||
|
||||
var panelRect = panelObj.GetComponent<RectTransform>();
|
||||
panelRect.anchorMin = new Vector2(1, 1);
|
||||
panelRect.anchorMax = new Vector2(1, 1);
|
||||
panelRect.pivot = new Vector2(1, 1);
|
||||
panelRect.anchoredPosition = new Vector2(-30, -30);
|
||||
panelRect.sizeDelta = new Vector2(360, 220);
|
||||
|
||||
// 텍스트 생성
|
||||
var textObj = new GameObject("MonitorText");
|
||||
textObj.transform.SetParent(panelObj.transform, false);
|
||||
textObj.layer = 5;
|
||||
|
||||
_text = textObj.AddComponent<TextMeshProUGUI>();
|
||||
_text.richText = true;
|
||||
_text.fontSize = 16; // 13에서 16으로 증가
|
||||
_text.alignment = TextAlignmentOptions.TopLeft;
|
||||
|
||||
// TMPro 텍스트 잘림 현상 예방
|
||||
_text.textWrappingMode = TextWrappingModes.Normal;
|
||||
_text.overflowMode = TextOverflowModes.Overflow;
|
||||
|
||||
// NanumSquareNeo-Bd SDF 폰트 로드 및 적용
|
||||
var font = GetNanumSquareNeoBdFontAsset();
|
||||
_text.font = font;
|
||||
|
||||
var textRect = textObj.GetComponent<RectTransform>();
|
||||
textRect.anchorMin = Vector2.zero;
|
||||
textRect.anchorMax = Vector2.one;
|
||||
textRect.offsetMin = new Vector2(20, 20); // 좀 더 넉넉하고 패딩 여백
|
||||
textRect.offsetMax = new Vector2(-20, -20);
|
||||
|
||||
// --- Progress Bar UI Setup ---
|
||||
// 1. Progress Bar Container
|
||||
_progressBarObj = new GameObject("ProgressBar");
|
||||
_progressBarObj.transform.SetParent(transform, false);
|
||||
_progressBarObj.layer = 5;
|
||||
|
||||
var bgImage = _progressBarObj.AddComponent<Image>();
|
||||
bgImage.color = BarBackgroundColor;
|
||||
|
||||
// 프로그레스 바 흰색 윤곽선 컴포넌트 추가
|
||||
var bgOutline = _progressBarObj.AddComponent<Outline>();
|
||||
bgOutline.effectColor = BarOutlineColor;
|
||||
bgOutline.effectDistance = BarOutlineDistance;
|
||||
|
||||
var barRect = _progressBarObj.GetComponent<RectTransform>();
|
||||
barRect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
barRect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
barRect.pivot = new Vector2(0.5f, 0.5f);
|
||||
barRect.anchoredPosition = new Vector2(BarPosX, BarPosY);
|
||||
barRect.sizeDelta = new Vector2(BarWidth, BarHeight);
|
||||
|
||||
// 2. Progress Bar Fill
|
||||
_progressBarFillObj = new GameObject("ProgressBarFill");
|
||||
_progressBarFillObj.transform.SetParent(_progressBarObj.transform, false);
|
||||
_progressBarFillObj.layer = 5;
|
||||
|
||||
var fillImage = _progressBarFillObj.AddComponent<Image>();
|
||||
fillImage.color = BarFillColor;
|
||||
|
||||
_fillRect = _progressBarFillObj.GetComponent<RectTransform>();
|
||||
_fillRect.anchorMin = new Vector2(0, 0);
|
||||
_fillRect.anchorMax = new Vector2(0, 1);
|
||||
_fillRect.pivot = new Vector2(0, 0.5f);
|
||||
_fillRect.anchoredPosition = Vector2.zero;
|
||||
_fillRect.sizeDelta = new Vector2(0, 0); // width will be updated dynamically in Update()
|
||||
|
||||
// 3. Progress Bar Text
|
||||
var progressTextObj = new GameObject("ProgressBarText");
|
||||
progressTextObj.transform.SetParent(_progressBarObj.transform, false);
|
||||
progressTextObj.layer = 5;
|
||||
|
||||
_progressText = progressTextObj.AddComponent<TextMeshProUGUI>();
|
||||
_progressText.richText = true;
|
||||
_progressText.fontSize = BarTextFontSize; // 14에서 18로 증가
|
||||
_progressText.alignment = TextAlignmentOptions.Center;
|
||||
_progressText.font = font;
|
||||
_progressText.color = BarTextColor;
|
||||
|
||||
// 프로그레스 바 내부 텍스트 아웃라인 추가 (진행율 가독성 확보)
|
||||
var textOutline = progressTextObj.AddComponent<Outline>();
|
||||
textOutline.effectColor = new Color(0f, 0f, 0f, 0.9f);
|
||||
textOutline.effectDistance = new Vector2(1.5f, -1.5f);
|
||||
|
||||
var textRectBar = progressTextObj.GetComponent<RectTransform>();
|
||||
textRectBar.anchorMin = Vector2.zero;
|
||||
textRectBar.anchorMax = Vector2.one;
|
||||
textRectBar.offsetMin = Vector2.zero;
|
||||
textRectBar.offsetMax = Vector2.zero;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!_text) return;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("<b><color=#00ccff>ADOFAI Renderer Realtime Monitor</color></b>");
|
||||
sb.AppendLine();
|
||||
|
||||
var queueCount = 0;
|
||||
if (RenderManager.Instance)
|
||||
{
|
||||
if (!_text) return;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("<b><color=#00ccff>ADOFAI Renderer Realtime Monitor</color></b>");
|
||||
sb.AppendLine();
|
||||
|
||||
var queueCount = 0;
|
||||
if (RenderManager.Instance)
|
||||
lock (RenderManager.Instance.ImageQueue)
|
||||
{
|
||||
lock (RenderManager.Instance.ImageQueue)
|
||||
{
|
||||
queueCount = RenderManager.Instance.ImageQueue.Count;
|
||||
}
|
||||
queueCount = RenderManager.Instance.ImageQueue.Count;
|
||||
}
|
||||
|
||||
sb.AppendLine($"• Queue: <b>{queueCount}</b> / {RenderManager.MaxQueuedFrames} frames");
|
||||
sb.AppendLine($"• Rendered Frames: <b>{(RenderManager.Instance ? RenderManager.Instance.frameCount : 0)}</b>");
|
||||
sb.AppendLine($"• Frames Sent: <b>{(RenderManager.Instance ? Interlocked.Read(ref RenderManager.Instance.framesSent) : 0)}</b>");
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"• Throttling Controls: {(RenderManager.Instance ? RenderManager.Instance.throttleCount : 0)} times");
|
||||
sb.AppendLine($"• Readback Hard Failures: <color=#ff3333>{(RenderManager.Instance ? RenderManager.Instance.readbackFailureCount : 0)}</color> times");
|
||||
sb.AppendLine($"• Render Skip-Retry: {(RenderManager.Instance ? RenderManager.Instance.skipCount : 0)} / {RenderManager.MaxRenderRetry}");
|
||||
|
||||
var memoryMb = GC.GetTotalMemory(false) / (1024.0 * 1024.0);
|
||||
sb.AppendLine($"• Mono Heap Memory: {memoryMb:F2} MB");
|
||||
|
||||
_text.text = sb.ToString();
|
||||
|
||||
// Update Progress Bar
|
||||
var progress = 0f;
|
||||
var currentFloor = 0;
|
||||
var totalFloors = 0;
|
||||
|
||||
if (scrController.instance && ADOBase.lm && ADOBase.lm.listFloors != null)
|
||||
{
|
||||
currentFloor = scrController.instance.currFloor ? scrController.instance.currFloor.seqID : 0;
|
||||
totalFloors = ADOBase.lm.listFloors.Count - 1;
|
||||
if (totalFloors > 0)
|
||||
{
|
||||
progress = Mathf.Clamp01((float)currentFloor / totalFloors);
|
||||
}
|
||||
}
|
||||
|
||||
if (_fillRect) _fillRect.sizeDelta = new Vector2(BarWidth * progress, 0);
|
||||
if (_progressText) _progressText.text = $"{progress * 100f:F1}% ({currentFloor}/{totalFloors})";
|
||||
}
|
||||
|
||||
sb.AppendLine($"• Queue: <b>{queueCount}</b> / {RenderManager.MaxQueuedFrames} frames");
|
||||
sb.AppendLine($"• Rendered Frames: <b>{(RenderManager.Instance ? RenderManager.Instance.frameCount : 0)}</b>");
|
||||
sb.AppendLine($"• Frames Sent: <b>{(RenderManager.Instance ? Interlocked.Read(ref RenderManager.Instance.framesSent) : 0)}</b>");
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"• Throttling Controls: {(RenderManager.Instance ? RenderManager.Instance.throttleCount : 0)} times");
|
||||
sb.AppendLine($"• Readback Hard Failures: <color=#ff3333>{(RenderManager.Instance ? RenderManager.Instance.readbackFailureCount : 0)}</color> times");
|
||||
sb.AppendLine($"• Render Skip-Retry: {(RenderManager.Instance ? RenderManager.Instance.skipCount : 0)} / {RenderManager.MaxRenderRetry}");
|
||||
|
||||
var memoryMb = GC.GetTotalMemory(false) / (1024.0 * 1024.0);
|
||||
sb.AppendLine($"• Mono Heap Memory: {memoryMb:F2} MB");
|
||||
|
||||
_text.text = sb.ToString();
|
||||
|
||||
// Update Progress Bar
|
||||
var progress = 0f;
|
||||
var currentFloor = 0;
|
||||
var totalFloors = 0;
|
||||
|
||||
if (scrController.instance && ADOBase.lm && ADOBase.lm.listFloors != null)
|
||||
{
|
||||
currentFloor = scrController.instance.currFloor ? scrController.instance.currFloor.seqID : 0;
|
||||
totalFloors = ADOBase.lm.listFloors.Count - 1;
|
||||
if (totalFloors > 0)
|
||||
{
|
||||
progress = Mathf.Clamp01((float)currentFloor / totalFloors);
|
||||
}
|
||||
}
|
||||
|
||||
if (_fillRect) _fillRect.sizeDelta = new Vector2(BarWidth * progress, 0);
|
||||
if (_progressText) _progressText.text = $"{progress * 100f:F1}% ({currentFloor}/{totalFloors})";
|
||||
}
|
||||
}
|
||||
+105
-106
@@ -4,128 +4,127 @@ using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace ADOFAIRenderer.UI
|
||||
namespace ADOFAIRenderer.UI;
|
||||
|
||||
internal class EscapeHandlerUI : MonoBehaviour
|
||||
{
|
||||
internal class EscapeHandlerUI : MonoBehaviour
|
||||
private GameObject _containerObj = null!;
|
||||
private TextMeshProUGUI _textComponent = null!;
|
||||
private Stopwatch? _escapeStopwatch;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
private GameObject _containerObj = null!;
|
||||
private TextMeshProUGUI _textComponent = null!;
|
||||
private Stopwatch? _escapeStopwatch;
|
||||
// UI 레이어(5) 설정
|
||||
gameObject.layer = 5;
|
||||
|
||||
private void Start()
|
||||
// Canvas 설정
|
||||
var canvas = gameObject.AddComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = 10000; // 디버그 모니터보다 위에 오도록 설정
|
||||
|
||||
// 해상도 대응 캔버스 스케일러 설정
|
||||
var scaler = gameObject.AddComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920, 1080);
|
||||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
||||
scaler.matchWidthOrHeight = 0.5f;
|
||||
|
||||
gameObject.AddComponent<GraphicRaycaster>();
|
||||
|
||||
// 1. 컨테이너 및 배경 패널 오브젝트 생성
|
||||
_containerObj = new GameObject("EscapeContainer");
|
||||
_containerObj.transform.SetParent(transform, false);
|
||||
_containerObj.layer = 5;
|
||||
|
||||
var bgImage = _containerObj.AddComponent<Image>();
|
||||
// 고급스러운 다크톤 아크릴 감성
|
||||
bgImage.color = new Color(0.05f, 0.05f, 0.07f, 0.85f);
|
||||
|
||||
var containerRect = _containerObj.GetComponent<RectTransform>();
|
||||
containerRect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
containerRect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
containerRect.pivot = new Vector2(0.5f, 0.5f);
|
||||
containerRect.anchoredPosition = Vector2.zero; // 화면 중앙
|
||||
containerRect.sizeDelta = new Vector2(460, 160);
|
||||
|
||||
// 2. 텍스트 오브젝트 생성
|
||||
var textObj = new GameObject("EscapeText");
|
||||
textObj.transform.SetParent(_containerObj.transform, false);
|
||||
textObj.layer = 5;
|
||||
|
||||
_textComponent = textObj.AddComponent<TextMeshProUGUI>();
|
||||
_textComponent.alignment = TextAlignmentOptions.Center;
|
||||
_textComponent.fontSize = 60;
|
||||
_textComponent.textWrappingMode = TextWrappingModes.NoWrap;
|
||||
_textComponent.richText = true;
|
||||
|
||||
// 게임의 기본 폰트 적용 (RDString.fontData.fontTMP)
|
||||
if (RDString.fontData.fontTMP != null)
|
||||
_textComponent.font = RDString.fontData.fontTMP;
|
||||
|
||||
var textRect = textObj.GetComponent<RectTransform>();
|
||||
textRect.anchorMin = Vector2.zero;
|
||||
textRect.anchorMax = Vector2.one;
|
||||
textRect.offsetMin = new Vector2(10, 10);
|
||||
textRect.offsetMax = new Vector2(-10, -10);
|
||||
|
||||
// 초기 상태는 비활성화
|
||||
_containerObj.SetActive(false);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// 렌더링이 더 이상 활성화되어 있지 않으면 자가 파괴
|
||||
if (!RenderManager.IsActive)
|
||||
{
|
||||
// UI 레이어(5) 설정
|
||||
gameObject.layer = 5;
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
// Canvas 설정
|
||||
var canvas = gameObject.AddComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = 10000; // 디버그 모니터보다 위에 오도록 설정
|
||||
if (Input.GetKey(KeyCode.Escape))
|
||||
{
|
||||
if (_escapeStopwatch == null)
|
||||
{
|
||||
_escapeStopwatch = new Stopwatch();
|
||||
_escapeStopwatch.Start();
|
||||
_containerObj.SetActive(true);
|
||||
}
|
||||
|
||||
// 해상도 대응 캔버스 스케일러 설정
|
||||
var scaler = gameObject.AddComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920, 1080);
|
||||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
||||
scaler.matchWidthOrHeight = 0.5f;
|
||||
var elapsed = (float)_escapeStopwatch.Elapsed.TotalSeconds;
|
||||
var remaining = Mathf.Max(0f, 3f - elapsed);
|
||||
|
||||
gameObject.AddComponent<GraphicRaycaster>();
|
||||
// 남은 시간 텍스트 업데이트
|
||||
_textComponent.text = $"<color=#FF5252>탈출까지 {remaining:F1}초..</color>\n<size=40><color=#DDDDDD>Esc 키를 계속 누르고 계세요</color></size>";
|
||||
|
||||
// 1. 컨테이너 및 배경 패널 오브젝트 생성
|
||||
_containerObj = new GameObject("EscapeContainer");
|
||||
_containerObj.transform.SetParent(transform, false);
|
||||
_containerObj.layer = 5;
|
||||
// 마이크로 펄스 애니메이션 적용 (누르는 동안 컨테이너 전체 크기가 부드럽게 커짐)
|
||||
var scale = 1.0f + elapsed / 3.0f * 0.15f;
|
||||
_containerObj.transform.localScale = new Vector3(scale, scale, 1f);
|
||||
|
||||
var bgImage = _containerObj.AddComponent<Image>();
|
||||
// 고급스러운 다크톤 아크릴 감성
|
||||
bgImage.color = new Color(0.05f, 0.05f, 0.07f, 0.85f);
|
||||
if (elapsed < 3f) return;
|
||||
|
||||
var containerRect = _containerObj.GetComponent<RectTransform>();
|
||||
containerRect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
containerRect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
containerRect.pivot = new Vector2(0.5f, 0.5f);
|
||||
containerRect.anchoredPosition = Vector2.zero; // 화면 중앙
|
||||
containerRect.sizeDelta = new Vector2(460, 160);
|
||||
|
||||
// 2. 텍스트 오브젝트 생성
|
||||
var textObj = new GameObject("EscapeText");
|
||||
textObj.transform.SetParent(_containerObj.transform, false);
|
||||
textObj.layer = 5;
|
||||
|
||||
_textComponent = textObj.AddComponent<TextMeshProUGUI>();
|
||||
_textComponent.alignment = TextAlignmentOptions.Center;
|
||||
_textComponent.fontSize = 60;
|
||||
_textComponent.textWrappingMode = TextWrappingModes.NoWrap;
|
||||
_textComponent.richText = true;
|
||||
|
||||
// 게임의 기본 폰트 적용 (RDString.fontData.fontTMP)
|
||||
if (RDString.fontData.fontTMP != null)
|
||||
_textComponent.font = RDString.fontData.fontTMP;
|
||||
|
||||
var textRect = textObj.GetComponent<RectTransform>();
|
||||
textRect.anchorMin = Vector2.zero;
|
||||
textRect.anchorMax = Vector2.one;
|
||||
textRect.offsetMin = new Vector2(10, 10);
|
||||
textRect.offsetMax = new Vector2(-10, -10);
|
||||
|
||||
// 초기 상태는 비활성화
|
||||
_escapeStopwatch.Stop();
|
||||
_escapeStopwatch = null;
|
||||
_containerObj.transform.localScale = Vector3.one; // 스케일 초기화
|
||||
_containerObj.SetActive(false);
|
||||
|
||||
// 렌더링 취소 및 씬 이탈
|
||||
if (RenderManager.Instance)
|
||||
RenderManager.Instance.CancelRender();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// 렌더링이 더 이상 활성화되어 있지 않으면 자가 파괴
|
||||
if (!RenderManager.IsActive)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Input.GetKey(KeyCode.Escape))
|
||||
{
|
||||
if (_escapeStopwatch == null)
|
||||
{
|
||||
_escapeStopwatch = new Stopwatch();
|
||||
_escapeStopwatch.Start();
|
||||
_containerObj.SetActive(true);
|
||||
}
|
||||
|
||||
var elapsed = (float)_escapeStopwatch.Elapsed.TotalSeconds;
|
||||
var remaining = Mathf.Max(0f, 3f - elapsed);
|
||||
|
||||
// 남은 시간 텍스트 업데이트
|
||||
_textComponent.text = $"<color=#FF5252>탈출까지 {remaining:F1}초..</color>\n<size=40><color=#DDDDDD>Esc 키를 계속 누르고 계세요</color></size>";
|
||||
|
||||
// 마이크로 펄스 애니메이션 적용 (누르는 동안 컨테이너 전체 크기가 부드럽게 커짐)
|
||||
var scale = 1.0f + elapsed / 3.0f * 0.15f;
|
||||
_containerObj.transform.localScale = new Vector3(scale, scale, 1f);
|
||||
|
||||
if (elapsed < 3f) return;
|
||||
|
||||
_escapeStopwatch.Stop();
|
||||
_escapeStopwatch = null;
|
||||
_containerObj.transform.localScale = Vector3.one; // 스케일 초기화
|
||||
_containerObj.SetActive(false);
|
||||
|
||||
// 렌더링 취소 및 씬 이탈
|
||||
if (RenderManager.Instance)
|
||||
RenderManager.Instance.CancelRender();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_escapeStopwatch == null) return;
|
||||
_escapeStopwatch.Stop();
|
||||
_escapeStopwatch = null;
|
||||
_containerObj.transform.localScale = Vector3.one; // 스케일 초기화
|
||||
_containerObj.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
else
|
||||
{
|
||||
if (_escapeStopwatch == null) return;
|
||||
_escapeStopwatch.Stop();
|
||||
_escapeStopwatch = null;
|
||||
_containerObj.transform.localScale = Vector3.one; // 스케일 초기화
|
||||
_containerObj.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_escapeStopwatch == null) return;
|
||||
_escapeStopwatch.Stop();
|
||||
_escapeStopwatch = null;
|
||||
}
|
||||
}
|
||||
+653
-654
File diff suppressed because it is too large.
Load diff
+1034
-1035
File diff suppressed because it is too large.
Load diff
@@ -1,101 +1,100 @@
|
||||
using System;
|
||||
|
||||
namespace ADOFAIRenderer.Util.Audio
|
||||
namespace ADOFAIRenderer.Util.Audio;
|
||||
|
||||
internal static class AudioResampler
|
||||
{
|
||||
internal static class AudioResampler
|
||||
public static float[] Resample(float[] inputSamples, int inputChannels, int inputFrequency, int targetChannels, int targetFrequency)
|
||||
{
|
||||
public static float[] Resample(float[] inputSamples, int inputChannels, int inputFrequency, int targetChannels, int targetFrequency)
|
||||
// ── Safety Validation ──
|
||||
if (inputSamples.Length == 0 ||
|
||||
inputChannels <= 0 || targetChannels <= 0 ||
|
||||
inputFrequency <= 0 || targetFrequency <= 0)
|
||||
{
|
||||
// ── Safety Validation ──
|
||||
if (inputSamples.Length == 0 ||
|
||||
inputChannels <= 0 || targetChannels <= 0 ||
|
||||
inputFrequency <= 0 || targetFrequency <= 0)
|
||||
return Array.Empty<float>();
|
||||
}
|
||||
|
||||
// If format is identical, return original samples
|
||||
if (inputFrequency == targetFrequency && inputChannels == targetChannels)
|
||||
return inputSamples;
|
||||
|
||||
int inputLength = inputSamples.Length / inputChannels;
|
||||
int targetLength = (int)Math.Floor((double)inputLength * targetFrequency / inputFrequency);
|
||||
float[] outputSamples = new float[targetLength * targetChannels];
|
||||
|
||||
for (int i = 0; i < targetLength; i++)
|
||||
{
|
||||
double srcIndex = (double)i * inputFrequency / targetFrequency;
|
||||
long idxLow = (long)Math.Floor(srcIndex);
|
||||
long idxHigh = idxLow + 1;
|
||||
float t = (float)(srcIndex - idxLow);
|
||||
|
||||
if (idxHigh >= inputLength) idxHigh = inputLength - 1;
|
||||
if (idxLow >= inputLength) idxLow = inputLength - 1;
|
||||
|
||||
if (targetChannels == 1) // 1. Mono Output
|
||||
{
|
||||
return Array.Empty<float>();
|
||||
float valLow, valHigh;
|
||||
if (inputChannels == 1) // Mono -> Mono
|
||||
{
|
||||
valLow = inputSamples[idxLow];
|
||||
valHigh = inputSamples[idxHigh];
|
||||
}
|
||||
else // Multi -> Mono (Downmix: Average all channels)
|
||||
{
|
||||
float sumLow = 0f;
|
||||
float sumHigh = 0f;
|
||||
for (int c = 0; c < inputChannels; c++)
|
||||
{
|
||||
sumLow += inputSamples[idxLow * inputChannels + c];
|
||||
sumHigh += inputSamples[idxHigh * inputChannels + c];
|
||||
}
|
||||
valLow = sumLow / inputChannels;
|
||||
valHigh = sumHigh / inputChannels;
|
||||
}
|
||||
outputSamples[i] = valLow + t * (valHigh - valLow);
|
||||
}
|
||||
|
||||
// If format is identical, return original samples
|
||||
if (inputFrequency == targetFrequency && inputChannels == targetChannels)
|
||||
return inputSamples;
|
||||
|
||||
int inputLength = inputSamples.Length / inputChannels;
|
||||
int targetLength = (int)Math.Floor((double)inputLength * targetFrequency / inputFrequency);
|
||||
float[] outputSamples = new float[targetLength * targetChannels];
|
||||
|
||||
for (int i = 0; i < targetLength; i++)
|
||||
else if (targetChannels == 2) // 2. Stereo Output
|
||||
{
|
||||
double srcIndex = (double)i * inputFrequency / targetFrequency;
|
||||
long idxLow = (long)Math.Floor(srcIndex);
|
||||
long idxHigh = idxLow + 1;
|
||||
float t = (float)(srcIndex - idxLow);
|
||||
if (inputChannels == 1) // Mono -> Stereo (Duplicate to Left/Right)
|
||||
{
|
||||
float valLow = inputSamples[idxLow];
|
||||
float valHigh = inputSamples[idxHigh];
|
||||
float val = valLow + t * (valHigh - valLow);
|
||||
outputSamples[i * 2] = val;
|
||||
outputSamples[i * 2 + 1] = val;
|
||||
}
|
||||
else // Stereo/Multi -> Stereo (Map Left/Right, ignore extra channels)
|
||||
{
|
||||
float valLowL = inputSamples[idxLow * inputChannels];
|
||||
float valHighL = inputSamples[idxHigh * inputChannels];
|
||||
outputSamples[i * 2] = valLowL + t * (valHighL - valLowL);
|
||||
|
||||
if (idxHigh >= inputLength) idxHigh = inputLength - 1;
|
||||
if (idxLow >= inputLength) idxLow = inputLength - 1;
|
||||
|
||||
if (targetChannels == 1) // 1. Mono Output
|
||||
float valLowR = inputSamples[idxLow * inputChannels + 1];
|
||||
float valHighR = inputSamples[idxHigh * inputChannels + 1];
|
||||
outputSamples[i * 2 + 1] = valLowR + t * (valHighR - valLowR);
|
||||
}
|
||||
}
|
||||
else // 3. Multi-channel Output (e.g., 5.1 Surround)
|
||||
{
|
||||
for (int c = 0; c < targetChannels; c++)
|
||||
{
|
||||
float valLow, valHigh;
|
||||
if (inputChannels == 1) // Mono -> Mono
|
||||
if (c < inputChannels)
|
||||
{
|
||||
valLow = inputSamples[idxLow];
|
||||
valHigh = inputSamples[idxHigh];
|
||||
valLow = inputSamples[idxLow * inputChannels + c];
|
||||
valHigh = inputSamples[idxHigh * inputChannels + c];
|
||||
}
|
||||
else // Multi -> Mono (Downmix: Average all channels)
|
||||
else
|
||||
{
|
||||
float sumLow = 0f;
|
||||
float sumHigh = 0f;
|
||||
for (int c = 0; c < inputChannels; c++)
|
||||
{
|
||||
sumLow += inputSamples[idxLow * inputChannels + c];
|
||||
sumHigh += inputSamples[idxHigh * inputChannels + c];
|
||||
}
|
||||
valLow = sumLow / inputChannels;
|
||||
valHigh = sumHigh / inputChannels;
|
||||
}
|
||||
outputSamples[i] = valLow + t * (valHigh - valLow);
|
||||
}
|
||||
else if (targetChannels == 2) // 2. Stereo Output
|
||||
{
|
||||
if (inputChannels == 1) // Mono -> Stereo (Duplicate to Left/Right)
|
||||
{
|
||||
float valLow = inputSamples[idxLow];
|
||||
float valHigh = inputSamples[idxHigh];
|
||||
float val = valLow + t * (valHigh - valLow);
|
||||
outputSamples[i * 2] = val;
|
||||
outputSamples[i * 2 + 1] = val;
|
||||
}
|
||||
else // Stereo/Multi -> Stereo (Map Left/Right, ignore extra channels)
|
||||
{
|
||||
float valLowL = inputSamples[idxLow * inputChannels];
|
||||
float valHighL = inputSamples[idxHigh * inputChannels];
|
||||
outputSamples[i * 2] = valLowL + t * (valHighL - valLowL);
|
||||
|
||||
float valLowR = inputSamples[idxLow * inputChannels + 1];
|
||||
float valHighR = inputSamples[idxHigh * inputChannels + 1];
|
||||
outputSamples[i * 2 + 1] = valLowR + t * (valHighR - valLowR);
|
||||
}
|
||||
}
|
||||
else // 3. Multi-channel Output (e.g., 5.1 Surround)
|
||||
{
|
||||
for (int c = 0; c < targetChannels; c++)
|
||||
{
|
||||
float valLow, valHigh;
|
||||
if (c < inputChannels)
|
||||
{
|
||||
valLow = inputSamples[idxLow * inputChannels + c];
|
||||
valHigh = inputSamples[idxHigh * inputChannels + c];
|
||||
}
|
||||
else
|
||||
{
|
||||
valLow = inputSamples[idxLow * inputChannels];
|
||||
valHigh = inputSamples[idxHigh * inputChannels];
|
||||
}
|
||||
outputSamples[i * targetChannels + c] = valLow + t * (valHigh - valLow);
|
||||
valLow = inputSamples[idxLow * inputChannels];
|
||||
valHigh = inputSamples[idxHigh * inputChannels];
|
||||
}
|
||||
outputSamples[i * targetChannels + c] = valLow + t * (valHigh - valLow);
|
||||
}
|
||||
}
|
||||
|
||||
return outputSamples;
|
||||
}
|
||||
|
||||
return outputSamples;
|
||||
}
|
||||
}
|
||||
@@ -3,238 +3,239 @@ using System.Collections.Generic;
|
||||
using ADOFAIRenderer.Core;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ADOFAIRenderer.Util.Audio
|
||||
namespace ADOFAIRenderer.Util.Audio;
|
||||
|
||||
internal static class AudioSpectrumGenerator
|
||||
{
|
||||
internal static class AudioSpectrumGenerator
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
public static float[]? AudioClipData { get; private set; }
|
||||
public static int Channels { get; private set; }
|
||||
public static int Frequency { get; private set; }
|
||||
public static int TotalSamples { get; private set; }
|
||||
// ReSharper restore MemberCanBePrivate.Global
|
||||
|
||||
private static long _cachedFrame = -1;
|
||||
private static readonly Dictionary<int, float[]> CachedSpectrums = new();
|
||||
|
||||
public static void Initialize(AudioClip? clip)
|
||||
{
|
||||
public static float[]? AudioClipData { get; private set; }
|
||||
public static int Channels { get; private set; }
|
||||
public static int Frequency { get; private set; }
|
||||
public static int TotalSamples { get; private set; }
|
||||
|
||||
private static long _cachedFrame = -1;
|
||||
private static readonly Dictionary<int, float[]> CachedSpectrums = new();
|
||||
|
||||
public static void Initialize(AudioClip? clip)
|
||||
if (!clip)
|
||||
{
|
||||
if (!clip)
|
||||
{
|
||||
Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Channels = clip.channels;
|
||||
Frequency = clip.frequency;
|
||||
int samplesCount = clip.samples;
|
||||
|
||||
float[] rawSamples = new float[samplesCount * Channels];
|
||||
clip.GetData(rawSamples, 0);
|
||||
|
||||
if (Channels == 1)
|
||||
{
|
||||
AudioClipData = rawSamples;
|
||||
TotalSamples = samplesCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
AudioClipData = new float[samplesCount];
|
||||
TotalSamples = samplesCount;
|
||||
for (int i = 0; i < samplesCount; i++)
|
||||
{
|
||||
float sum = 0f;
|
||||
for (int c = 0; c < Channels; c++)
|
||||
{
|
||||
sum += rawSamples[i * Channels + c];
|
||||
}
|
||||
AudioClipData[i] = sum / Channels;
|
||||
}
|
||||
}
|
||||
MelonLoader.MelonLogger.Msg($"[AudioSpectrumGenerator] Cached audio clip: {clip.name}, Samples: {TotalSamples}, Frequency: {Frequency}Hz, Channels: {Channels}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
MelonLoader.MelonLogger.Error($"[AudioSpectrumGenerator] Failed to initialize audio cache: {e}");
|
||||
Clear();
|
||||
}
|
||||
Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
public static void Clear()
|
||||
try
|
||||
{
|
||||
AudioClipData = null;
|
||||
TotalSamples = 0;
|
||||
Channels = 0;
|
||||
Frequency = 0;
|
||||
lock (CachedSpectrums)
|
||||
Channels = clip.channels;
|
||||
Frequency = clip.frequency;
|
||||
int samplesCount = clip.samples;
|
||||
|
||||
float[] rawSamples = new float[samplesCount * Channels];
|
||||
clip.GetData(rawSamples, 0);
|
||||
|
||||
if (Channels == 1)
|
||||
{
|
||||
_cachedFrame = -1;
|
||||
AudioClipData = rawSamples;
|
||||
TotalSamples = samplesCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
AudioClipData = new float[samplesCount];
|
||||
TotalSamples = samplesCount;
|
||||
for (int i = 0; i < samplesCount; i++)
|
||||
{
|
||||
float sum = 0f;
|
||||
for (int c = 0; c < Channels; c++)
|
||||
{
|
||||
sum += rawSamples[i * Channels + c];
|
||||
}
|
||||
AudioClipData[i] = sum / Channels;
|
||||
}
|
||||
}
|
||||
MelonLoader.MelonLogger.Msg($"[AudioSpectrumGenerator] Cached audio clip: {clip.name}, Samples: {TotalSamples}, Frequency: {Frequency}Hz, Channels: {Channels}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
MelonLoader.MelonLogger.Error($"[AudioSpectrumGenerator] Failed to initialize audio cache: {e}");
|
||||
Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public static void Clear()
|
||||
{
|
||||
AudioClipData = null;
|
||||
TotalSamples = 0;
|
||||
Channels = 0;
|
||||
Frequency = 0;
|
||||
lock (CachedSpectrums)
|
||||
{
|
||||
_cachedFrame = -1;
|
||||
CachedSpectrums.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public static void GetSpectrumData(float[] samples, int channel, FFTWindow window)
|
||||
{
|
||||
if (AudioClipData == null || AudioClipData.Length == 0 || !scrConductor.instance || !scrConductor.instance.hasSongStarted || !scrConductor.instance.song)
|
||||
{
|
||||
Array.Clear(samples, 0, samples.Length);
|
||||
return;
|
||||
}
|
||||
|
||||
var conductor = scrConductor.instance;
|
||||
var pitch = conductor.song.pitch;
|
||||
var separateCountdownOffset = conductor.separateCountdownTime ?
|
||||
conductor.crotchetAtStart * conductor.adjustedCountdownTicks : 0.0;
|
||||
|
||||
// Calculate the actual scheduled start time of the song
|
||||
var songScheduledStartTime = conductor.skipOffset ?
|
||||
conductor.dspTimeSong + conductor.addoffset / pitch :
|
||||
conductor.dspTimeSong + separateCountdownOffset / pitch;
|
||||
|
||||
if (conductor.dspTime < songScheduledStartTime)
|
||||
{
|
||||
Array.Clear(samples, 0, samples.Length);
|
||||
return;
|
||||
}
|
||||
|
||||
var songTime = (conductor.dspTime - conductor.dspTimeSong) * pitch - separateCountdownOffset;
|
||||
|
||||
var n = samples.Length;
|
||||
if (n == 0 || (n & (n - 1)) != 0) // Ensure it's a power of 2
|
||||
{
|
||||
Array.Clear(samples, 0, samples.Length);
|
||||
return;
|
||||
}
|
||||
|
||||
var currentFrame = (RenderManager.Instance != null) ? RenderManager.Instance.frameCount : -1;
|
||||
|
||||
lock (CachedSpectrums)
|
||||
{
|
||||
if (currentFrame != _cachedFrame)
|
||||
{
|
||||
_cachedFrame = currentFrame;
|
||||
CachedSpectrums.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public static void GetSpectrumData(float[] samples, int channel, FFTWindow window)
|
||||
{
|
||||
if (AudioClipData == null || AudioClipData.Length == 0 || !scrConductor.instance || !scrConductor.instance.hasSongStarted || !scrConductor.instance.song)
|
||||
if (CachedSpectrums.TryGetValue(n, out var cached))
|
||||
{
|
||||
Array.Clear(samples, 0, samples.Length);
|
||||
Array.Copy(cached, samples, n);
|
||||
return;
|
||||
}
|
||||
|
||||
var conductor = scrConductor.instance;
|
||||
var pitch = conductor.song.pitch;
|
||||
var separateCountdownOffset = conductor.separateCountdownTime ?
|
||||
conductor.crotchetAtStart * conductor.adjustedCountdownTicks : 0.0;
|
||||
|
||||
// Calculate the actual scheduled start time of the song
|
||||
var songScheduledStartTime = conductor.skipOffset ?
|
||||
conductor.dspTimeSong + conductor.addoffset / pitch :
|
||||
conductor.dspTimeSong + separateCountdownOffset / pitch;
|
||||
|
||||
if (conductor.dspTime < songScheduledStartTime)
|
||||
{
|
||||
Array.Clear(samples, 0, samples.Length);
|
||||
return;
|
||||
}
|
||||
|
||||
var songTime = (conductor.dspTime - conductor.dspTimeSong) * pitch - separateCountdownOffset;
|
||||
|
||||
var n = samples.Length;
|
||||
if (n == 0 || (n & (n - 1)) != 0) // Ensure it's a power of 2
|
||||
{
|
||||
Array.Clear(samples, 0, samples.Length);
|
||||
return;
|
||||
}
|
||||
|
||||
var currentFrame = (RenderManager.Instance != null) ? RenderManager.Instance.frameCount : -1;
|
||||
|
||||
lock (CachedSpectrums)
|
||||
{
|
||||
if (currentFrame != _cachedFrame)
|
||||
{
|
||||
_cachedFrame = currentFrame;
|
||||
CachedSpectrums.Clear();
|
||||
}
|
||||
|
||||
if (CachedSpectrums.TryGetValue(n, out var cached))
|
||||
{
|
||||
Array.Copy(cached, samples, n);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var fftSize = 2 * n;
|
||||
var frequency = Frequency;
|
||||
var totalSamples = TotalSamples;
|
||||
var currentSampleIndex = (long)(songTime * frequency);
|
||||
|
||||
var windowSamples = new float[fftSize];
|
||||
for (var i = 0; i < fftSize; i++)
|
||||
{
|
||||
var idx = currentSampleIndex - fftSize / 2 + i;
|
||||
if (idx >= 0 && idx < totalSamples)
|
||||
{
|
||||
windowSamples[i] = AudioClipData[idx];
|
||||
}
|
||||
else
|
||||
{
|
||||
windowSamples[i] = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
var spectrum = new float[n];
|
||||
CalculateFFT(windowSamples, spectrum);
|
||||
|
||||
lock (CachedSpectrums)
|
||||
{
|
||||
CachedSpectrums[n] = spectrum;
|
||||
Array.Copy(spectrum, samples, n);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CalculateFFT(float[] samples, float[] spectrum)
|
||||
var fftSize = 2 * n;
|
||||
var frequency = Frequency;
|
||||
var totalSamples = TotalSamples;
|
||||
var currentSampleIndex = (long)(songTime * frequency);
|
||||
|
||||
var windowSamples = new float[fftSize];
|
||||
for (var i = 0; i < fftSize; i++)
|
||||
{
|
||||
var n = samples.Length;
|
||||
|
||||
// Apply Blackman-Harris window
|
||||
const float a0 = 0.35875f;
|
||||
const float a1 = 0.48829f;
|
||||
const float a2 = 0.14128f;
|
||||
const float a3 = 0.01168f;
|
||||
|
||||
var re = new float[n];
|
||||
var im = new float[n];
|
||||
|
||||
for (var i = 0; i < n; i++)
|
||||
var idx = currentSampleIndex - fftSize / 2 + i;
|
||||
if (idx >= 0 && idx < totalSamples)
|
||||
{
|
||||
var angle = 2.0 * Math.PI * i / (n - 1);
|
||||
var w = (float)(a0 - a1 * Math.Cos(angle) + a2 * Math.Cos(2.0 * angle) - a3 * Math.Cos(3.0 * angle));
|
||||
re[i] = samples[i] * w;
|
||||
im[i] = 0f;
|
||||
windowSamples[i] = AudioClipData[idx];
|
||||
}
|
||||
|
||||
// Cooley-Tukey Radix-2 FFT
|
||||
var m = (int)Math.Round(Math.Log(n, 2));
|
||||
|
||||
// Bit-reversal
|
||||
var j = 0;
|
||||
for (var i = 0; i < n - 1; i++)
|
||||
else
|
||||
{
|
||||
if (i < j)
|
||||
windowSamples[i] = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
var spectrum = new float[n];
|
||||
CalculateFFT(windowSamples, spectrum);
|
||||
|
||||
lock (CachedSpectrums)
|
||||
{
|
||||
CachedSpectrums[n] = spectrum;
|
||||
Array.Copy(spectrum, samples, n);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CalculateFFT(float[] samples, float[] spectrum)
|
||||
{
|
||||
var n = samples.Length;
|
||||
|
||||
// Apply Blackman-Harris window
|
||||
const float a0 = 0.35875f;
|
||||
const float a1 = 0.48829f;
|
||||
const float a2 = 0.14128f;
|
||||
const float a3 = 0.01168f;
|
||||
|
||||
var re = new float[n];
|
||||
var im = new float[n];
|
||||
|
||||
for (var i = 0; i < n; i++)
|
||||
{
|
||||
var angle = 2.0 * Math.PI * i / (n - 1);
|
||||
var w = (float)(a0 - a1 * Math.Cos(angle) + a2 * Math.Cos(2.0 * angle) - a3 * Math.Cos(3.0 * angle));
|
||||
re[i] = samples[i] * w;
|
||||
im[i] = 0f;
|
||||
}
|
||||
|
||||
// Cooley-Tukey Radix-2 FFT
|
||||
var m = (int)Math.Round(Math.Log(n, 2));
|
||||
|
||||
// Bit-reversal
|
||||
var j = 0;
|
||||
for (var i = 0; i < n - 1; i++)
|
||||
{
|
||||
if (i < j)
|
||||
{
|
||||
var temp = re[i];
|
||||
re[i] = re[j];
|
||||
re[j] = temp;
|
||||
|
||||
temp = im[i];
|
||||
im[i] = im[j];
|
||||
im[j] = temp;
|
||||
}
|
||||
var k = n / 2;
|
||||
while (k <= j)
|
||||
{
|
||||
j -= k;
|
||||
k /= 2;
|
||||
}
|
||||
j += k;
|
||||
}
|
||||
|
||||
// FFT stages
|
||||
for (var stage = 1; stage <= m; stage++)
|
||||
{
|
||||
var le = 1 << stage;
|
||||
var le2 = le / 2;
|
||||
var ur = 1f;
|
||||
var ui = 0f;
|
||||
var wr = (float)Math.Cos(Math.PI / le2);
|
||||
var wi = -(float)Math.Sin(Math.PI / le2);
|
||||
|
||||
for (var sub = 0; sub < le2; sub++)
|
||||
{
|
||||
for (var item = sub; item < n; item += le)
|
||||
{
|
||||
var temp = re[i];
|
||||
re[i] = re[j];
|
||||
re[j] = temp;
|
||||
var ip = item + le2;
|
||||
var tr = re[ip] * ur - im[ip] * ui;
|
||||
var ti = re[ip] * ui + im[ip] * ur;
|
||||
|
||||
temp = im[i];
|
||||
im[i] = im[j];
|
||||
im[j] = temp;
|
||||
re[ip] = re[item] - tr;
|
||||
im[ip] = im[item] - ti;
|
||||
re[item] += tr;
|
||||
im[item] += ti;
|
||||
}
|
||||
var k = n / 2;
|
||||
while (k <= j)
|
||||
{
|
||||
j -= k;
|
||||
k /= 2;
|
||||
}
|
||||
j += k;
|
||||
var tempUr = ur;
|
||||
ur = tempUr * wr - ui * wi;
|
||||
ui = tempUr * wi + ui * wr;
|
||||
}
|
||||
}
|
||||
|
||||
// FFT stages
|
||||
for (var stage = 1; stage <= m; stage++)
|
||||
{
|
||||
var le = 1 << stage;
|
||||
var le2 = le / 2;
|
||||
var ur = 1f;
|
||||
var ui = 0f;
|
||||
var wr = (float)Math.Cos(Math.PI / le2);
|
||||
var wi = -(float)Math.Sin(Math.PI / le2);
|
||||
|
||||
for (var sub = 0; sub < le2; sub++)
|
||||
{
|
||||
for (var item = sub; item < n; item += le)
|
||||
{
|
||||
var ip = item + le2;
|
||||
var tr = re[ip] * ur - im[ip] * ui;
|
||||
var ti = re[ip] * ui + im[ip] * ur;
|
||||
|
||||
re[ip] = re[item] - tr;
|
||||
im[ip] = im[item] - ti;
|
||||
re[item] += tr;
|
||||
im[item] += ti;
|
||||
}
|
||||
var tempUr = ur;
|
||||
ur = tempUr * wr - ui * wi;
|
||||
ui = tempUr * wi + ui * wr;
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize and fill spectrum
|
||||
var factor = 2.0f / n;
|
||||
var outLen = Math.Min(spectrum.Length, n / 2);
|
||||
for (var i = 0; i < outLen; i++)
|
||||
{
|
||||
spectrum[i] = (float)Math.Sqrt(re[i] * re[i] + im[i] * im[i]) * factor;
|
||||
}
|
||||
// Normalize and fill spectrum
|
||||
var factor = 2.0f / n;
|
||||
var outLen = Math.Min(spectrum.Length, n / 2);
|
||||
for (var i = 0; i < outLen; i++)
|
||||
{
|
||||
spectrum[i] = (float)Math.Sqrt(re[i] * re[i] + im[i] * im[i]) * factor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1,40 @@
|
||||
using System;
|
||||
|
||||
namespace ADOFAIRenderer.Util.Audio
|
||||
namespace ADOFAIRenderer.Util.Audio;
|
||||
|
||||
internal class AudioSyncCalculator
|
||||
{
|
||||
internal class AudioSyncCalculator
|
||||
private readonly long _sampleRate;
|
||||
private readonly (long Numerator, long Denominator) _fpsRational;
|
||||
|
||||
// ReSharper disable once NotAccessedField.Local
|
||||
private long _currentSample;
|
||||
private long _remainder;
|
||||
|
||||
public AudioSyncCalculator(long sampleRate, double fps)
|
||||
{
|
||||
private readonly long _sampleRate;
|
||||
private readonly (long Numerator, long Denominator) _fpsRational;
|
||||
_sampleRate = sampleRate;
|
||||
_fpsRational = RationalUtil.Double2MaxPrecisionInt(fps);
|
||||
|
||||
// ReSharper disable once NotAccessedField.Local
|
||||
private long _currentSample;
|
||||
private long _remainder;
|
||||
// NTSC
|
||||
if (Math.Abs(fps - 29.97) < 0.001) _fpsRational = (30000, 1001);
|
||||
if (Math.Abs(fps - 59.94) < 0.001) _fpsRational = (60000, 1001);
|
||||
if (Math.Abs(fps - 23.976) < 0.001 || Math.Abs(fps - 23.98) < 0.001) _fpsRational = (24000, 1001);
|
||||
}
|
||||
|
||||
public AudioSyncCalculator(long sampleRate, double fps)
|
||||
{
|
||||
_sampleRate = sampleRate;
|
||||
_fpsRational = RationalUtil.Double2MaxPrecisionInt(fps);
|
||||
public void Reset()
|
||||
{
|
||||
_currentSample = 0;
|
||||
_remainder = 0;
|
||||
}
|
||||
|
||||
// NTSC
|
||||
if (Math.Abs(fps - 29.97) < 0.001) _fpsRational = (30000, 1001);
|
||||
if (Math.Abs(fps - 59.94) < 0.001) _fpsRational = (60000, 1001);
|
||||
if (Math.Abs(fps - 23.976) < 0.001 || Math.Abs(fps - 23.98) < 0.001) _fpsRational = (24000, 1001);
|
||||
}
|
||||
public int GetNextFrameSamples()
|
||||
{
|
||||
var numerator = _sampleRate * _fpsRational.Denominator + _remainder;
|
||||
var samplesNeeded = numerator / _fpsRational.Numerator;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_currentSample = 0;
|
||||
_remainder = 0;
|
||||
}
|
||||
|
||||
public int GetNextFrameSamples()
|
||||
{
|
||||
var numerator = _sampleRate * _fpsRational.Denominator + _remainder;
|
||||
var samplesNeeded = numerator / _fpsRational.Numerator;
|
||||
|
||||
_remainder = numerator % _fpsRational.Numerator;
|
||||
_currentSample += samplesNeeded;
|
||||
return (int)samplesNeeded;
|
||||
}
|
||||
_remainder = numerator % _fpsRational.Numerator;
|
||||
_currentSample += samplesNeeded;
|
||||
return (int)samplesNeeded;
|
||||
}
|
||||
}
|
||||
+17
-18
@@ -1,24 +1,23 @@
|
||||
using System;
|
||||
|
||||
namespace ADOFAIRenderer.Util.Audio.Filter
|
||||
namespace ADOFAIRenderer.Util.Audio.Filter;
|
||||
|
||||
internal class Gain : IAudioFilter
|
||||
{
|
||||
internal class Gain : IAudioFilter
|
||||
public float Level = 0f;
|
||||
|
||||
public void Initialize(int sampleRate, int channels) { }
|
||||
|
||||
public float ProcessSample(float sample, int channel) =>
|
||||
sample * (float)Math.Pow(10, Level / 20);
|
||||
|
||||
public void ProcessBlock(Span<float> buffer)
|
||||
{
|
||||
public float Level = 0f;
|
||||
|
||||
public void Initialize(int sampleRate, int channels) { }
|
||||
|
||||
public float ProcessSample(float sample, int channel) =>
|
||||
sample * (float)Math.Pow(10, Level / 20);
|
||||
|
||||
public void ProcessBlock(Span<float> buffer)
|
||||
{
|
||||
for (var i = 0; i < buffer.Length; i++)
|
||||
buffer[i] = ProcessSample(buffer[i], 0);
|
||||
}
|
||||
|
||||
public void Reset() { }
|
||||
|
||||
public double Latency => 0.0;
|
||||
for (var i = 0; i < buffer.Length; i++)
|
||||
buffer[i] = ProcessSample(buffer[i], 0);
|
||||
}
|
||||
|
||||
public void Reset() { }
|
||||
|
||||
public double Latency => 0.0;
|
||||
}
|
||||
@@ -1,33 +1,32 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace ADOFAIRenderer.Util.Audio.Filter
|
||||
namespace ADOFAIRenderer.Util.Audio.Filter;
|
||||
|
||||
internal class HardClip : IAudioFilter
|
||||
{
|
||||
internal class HardClip : IAudioFilter
|
||||
// ReSharper disable once MemberCanBePrivate.Global
|
||||
// ReSharper disable once FieldCanBeMadeReadOnly.Global
|
||||
// ReSharper disable once ConvertToConstant.Global
|
||||
public float Threshold = 1.0f;
|
||||
|
||||
public void Initialize(int sampleRate, int channels) { }
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public float ProcessSample(float sample, int channel)
|
||||
{
|
||||
// ReSharper disable once MemberCanBePrivate.Global
|
||||
// ReSharper disable once FieldCanBeMadeReadOnly.Global
|
||||
// ReSharper disable once ConvertToConstant.Global
|
||||
public float Threshold = 1.0f;
|
||||
|
||||
public void Initialize(int sampleRate, int channels) { }
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public float ProcessSample(float sample, int channel)
|
||||
{
|
||||
if (sample > Threshold) return Threshold;
|
||||
if (sample < -Threshold) return -Threshold;
|
||||
return sample;
|
||||
}
|
||||
|
||||
public void ProcessBlock(Span<float> buffer)
|
||||
{
|
||||
for (var i = 0; i < buffer.Length; i++)
|
||||
buffer[i] = ProcessSample(buffer[i], 0);
|
||||
}
|
||||
|
||||
public void Reset() { }
|
||||
|
||||
public double Latency => 0.0;
|
||||
if (sample > Threshold) return Threshold;
|
||||
if (sample < -Threshold) return -Threshold;
|
||||
return sample;
|
||||
}
|
||||
|
||||
public void ProcessBlock(Span<float> buffer)
|
||||
{
|
||||
for (var i = 0; i < buffer.Length; i++)
|
||||
buffer[i] = ProcessSample(buffer[i], 0);
|
||||
}
|
||||
|
||||
public void Reset() { }
|
||||
|
||||
public double Latency => 0.0;
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace ADOFAIRenderer.Util.Audio.Filter
|
||||
namespace ADOFAIRenderer.Util.Audio.Filter;
|
||||
|
||||
internal interface IAudioFilter
|
||||
{
|
||||
internal interface IAudioFilter
|
||||
{
|
||||
void Initialize(int sampleRate, int channels);
|
||||
void Initialize(int sampleRate, int channels);
|
||||
|
||||
float ProcessSample(float sample, int channel);
|
||||
void ProcessBlock(Span<float> buffer);
|
||||
float ProcessSample(float sample, int channel);
|
||||
void ProcessBlock(Span<float> buffer);
|
||||
|
||||
void Reset();
|
||||
void Reset();
|
||||
|
||||
double Latency { get; }
|
||||
}
|
||||
double Latency { get; }
|
||||
}
|
||||
+218
-219
@@ -1,229 +1,228 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace ADOFAIRenderer.Util.Audio.Filter
|
||||
namespace ADOFAIRenderer.Util.Audio.Filter;
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
internal class RMSCompressor : IAudioFilter
|
||||
{
|
||||
// ReSharper disable once InconsistentNaming
|
||||
internal class RMSCompressor : IAudioFilter
|
||||
private float _thresholdDb = -20f;
|
||||
private float _ratio = 4f;
|
||||
private float _attackTime = 0.010f; // in seconds
|
||||
private float _releaseTime = 0.100f; // in seconds
|
||||
private float _kneeDb = 5f; // in dB
|
||||
private float _rmsWindowTime = 0.030f; // in seconds
|
||||
private float _lookAheadTime; // in seconds
|
||||
private int _lookAheadSamples;
|
||||
|
||||
private int _sampleRate = 44100;
|
||||
private int _channels = 2;
|
||||
|
||||
private float _attackCoef;
|
||||
private float _releaseCoef;
|
||||
private float _rmsCoef;
|
||||
|
||||
// Per-channel state
|
||||
private float[] _rmsState = Array.Empty<float>(); // running mean-square (MS) state
|
||||
private float[] _gainState = Array.Empty<float>(); // smoothed gain reduction in dB
|
||||
private float[][] _delayBuffers = Array.Empty<float[]>(); // circular delay buffers
|
||||
private int[] _delayWriteIndices = Array.Empty<int>();
|
||||
|
||||
public float LookAheadTime
|
||||
{
|
||||
private float _thresholdDb = -20f;
|
||||
private float _ratio = 4f;
|
||||
private float _attackTime = 0.010f; // in seconds
|
||||
private float _releaseTime = 0.100f; // in seconds
|
||||
private float _kneeDb = 5f; // in dB
|
||||
private float _rmsWindowTime = 0.030f; // in seconds
|
||||
private float _lookAheadTime; // in seconds
|
||||
private int _lookAheadSamples;
|
||||
|
||||
private int _sampleRate = 44100;
|
||||
private int _channels = 2;
|
||||
|
||||
private float _attackCoef;
|
||||
private float _releaseCoef;
|
||||
private float _rmsCoef;
|
||||
|
||||
// Per-channel state
|
||||
private float[] _rmsState = Array.Empty<float>(); // running mean-square (MS) state
|
||||
private float[] _gainState = Array.Empty<float>(); // smoothed gain reduction in dB
|
||||
private float[][] _delayBuffers = Array.Empty<float[]>(); // circular delay buffers
|
||||
private int[] _delayWriteIndices = Array.Empty<int>();
|
||||
|
||||
public float LookAheadTime
|
||||
get => _lookAheadTime;
|
||||
set
|
||||
{
|
||||
get => _lookAheadTime;
|
||||
set
|
||||
{
|
||||
_lookAheadTime = Math.Max(0f, Math.Min(1.0f, value));
|
||||
UpdateLookAheadSettings();
|
||||
}
|
||||
}
|
||||
|
||||
public double Latency => _lookAheadTime;
|
||||
|
||||
public float ThresholdDb
|
||||
{
|
||||
get => _thresholdDb;
|
||||
set => _thresholdDb = value;
|
||||
}
|
||||
|
||||
public float Ratio
|
||||
{
|
||||
get => _ratio;
|
||||
set => _ratio = Math.Max(1f, value);
|
||||
}
|
||||
|
||||
public float AttackTime
|
||||
{
|
||||
get => _attackTime;
|
||||
set
|
||||
{
|
||||
_attackTime = Math.Max(0.0001f, value);
|
||||
UpdateAttackCoef();
|
||||
}
|
||||
}
|
||||
|
||||
public float ReleaseTime
|
||||
{
|
||||
get => _releaseTime;
|
||||
set
|
||||
{
|
||||
_releaseTime = Math.Max(0.0001f, value);
|
||||
UpdateReleaseCoef();
|
||||
}
|
||||
}
|
||||
|
||||
public float KneeDb
|
||||
{
|
||||
get => _kneeDb;
|
||||
set => _kneeDb = Math.Max(0f, value);
|
||||
}
|
||||
|
||||
public float RmsWindowTime
|
||||
{
|
||||
get => _rmsWindowTime;
|
||||
set
|
||||
{
|
||||
_rmsWindowTime = Math.Max(0.0001f, value);
|
||||
UpdateRmsCoef();
|
||||
}
|
||||
}
|
||||
|
||||
public void Initialize(int sampleRate, int channels)
|
||||
{
|
||||
_sampleRate = sampleRate;
|
||||
_channels = channels;
|
||||
|
||||
UpdateAttackCoef();
|
||||
UpdateReleaseCoef();
|
||||
UpdateRmsCoef();
|
||||
_lookAheadTime = Math.Max(0f, Math.Min(1.0f, value));
|
||||
UpdateLookAheadSettings();
|
||||
|
||||
_rmsState = new float[channels];
|
||||
_gainState = new float[channels];
|
||||
|
||||
Reset();
|
||||
}
|
||||
|
||||
private void UpdateLookAheadSettings()
|
||||
{
|
||||
_lookAheadSamples = (int)Math.Round(_sampleRate * _lookAheadTime);
|
||||
ReallocateDelayBuffers();
|
||||
}
|
||||
|
||||
private void ReallocateDelayBuffers()
|
||||
{
|
||||
if (_channels <= 0) return;
|
||||
|
||||
_delayBuffers = new float[_channels][];
|
||||
_delayWriteIndices = new int[_channels];
|
||||
|
||||
for (int i = 0; i < _channels; i++)
|
||||
{
|
||||
_delayBuffers[i] = _lookAheadSamples > 0 ? new float[_lookAheadSamples] : Array.Empty<float>();
|
||||
_delayWriteIndices[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAttackCoef()
|
||||
{
|
||||
_attackCoef = (float)Math.Exp(-1.0 / (_sampleRate * _attackTime));
|
||||
}
|
||||
|
||||
private void UpdateReleaseCoef()
|
||||
{
|
||||
_releaseCoef = (float)Math.Exp(-1.0 / (_sampleRate * _releaseTime));
|
||||
}
|
||||
|
||||
private void UpdateRmsCoef()
|
||||
{
|
||||
_rmsCoef = (float)Math.Exp(-1.0 / (_sampleRate * _rmsWindowTime));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public float ProcessSample(float sample, int channel)
|
||||
{
|
||||
if (channel < 0 || channel >= _channels)
|
||||
{
|
||||
return sample;
|
||||
}
|
||||
|
||||
// Calculate running Mean-Square (MS) level
|
||||
// v_c[n] = alpha * v_c[n-1] + (1 - alpha) * x^2[n]
|
||||
float sq = sample * sample;
|
||||
_rmsState[channel] = _rmsCoef * _rmsState[channel] + (1.0f - _rmsCoef) * sq;
|
||||
|
||||
float rmsSq = Math.Max(0f, _rmsState[channel]);
|
||||
// Convert to dB. 10 * log10(rmsSq) is equivalent to 20 * log10(sqrt(rmsSq))
|
||||
float rmsDb = 10.0f * (float)Math.Log10(rmsSq + 1e-12f);
|
||||
|
||||
// Calculate static envelope curve (gain reduction in dB)
|
||||
float gStatic = 0f;
|
||||
if (_kneeDb > 0f)
|
||||
{
|
||||
if (rmsDb > _thresholdDb + _kneeDb * 0.5f)
|
||||
{
|
||||
gStatic = (1f / _ratio - 1f) * (rmsDb - _thresholdDb);
|
||||
}
|
||||
else if (rmsDb >= _thresholdDb - _kneeDb * 0.5f)
|
||||
{
|
||||
float diff = rmsDb - _thresholdDb + _kneeDb * 0.5f;
|
||||
gStatic = (1f / _ratio - 1f) * diff * diff / (2f * _kneeDb);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rmsDb > _thresholdDb)
|
||||
{
|
||||
gStatic = (1f / _ratio - 1f) * (rmsDb - _thresholdDb);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply ballistics (attack and release) on the dB gain reduction
|
||||
float lastGain = _gainState[channel];
|
||||
float targetGain = gStatic;
|
||||
|
||||
// Note: Since targetGain and lastGain are <= 0 (gain reduction in dB),
|
||||
// targetGain < lastGain means target gain reduction is larger (more negative),
|
||||
// so we are in the attack phase.
|
||||
float coef = targetGain < lastGain ? _attackCoef : _releaseCoef;
|
||||
float currentGain = coef * lastGain + (1f - coef) * targetGain;
|
||||
_gainState[channel] = currentGain;
|
||||
|
||||
// Convert back to linear gain multiplier
|
||||
float linearGain = (float)Math.Pow(10.0, currentGain / 20.0);
|
||||
|
||||
if (_lookAheadSamples > 0 && _delayBuffers.Length > channel && _delayBuffers[channel].Length > 0)
|
||||
{
|
||||
var buffer = _delayBuffers[channel];
|
||||
int writeIdx = _delayWriteIndices[channel];
|
||||
float delayedSample = buffer[writeIdx];
|
||||
buffer[writeIdx] = sample;
|
||||
_delayWriteIndices[channel] = (writeIdx + 1) % _lookAheadSamples;
|
||||
return delayedSample * linearGain;
|
||||
}
|
||||
|
||||
return sample * linearGain;
|
||||
}
|
||||
|
||||
public void ProcessBlock(Span<float> buffer)
|
||||
{
|
||||
for (var i = 0; i < buffer.Length; i++)
|
||||
{
|
||||
int channel = i % _channels;
|
||||
buffer[i] = ProcessSample(buffer[i], channel);
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Array.Clear(_rmsState, 0, _rmsState.Length);
|
||||
Array.Clear(_gainState, 0, _gainState.Length);
|
||||
|
||||
for (int i = 0; i < _delayBuffers.Length; i++)
|
||||
{
|
||||
Array.Clear(_delayBuffers[i], 0, _delayBuffers[i].Length);
|
||||
}
|
||||
Array.Clear(_delayWriteIndices, 0, _delayWriteIndices.Length);
|
||||
}
|
||||
}
|
||||
|
||||
public double Latency => _lookAheadTime;
|
||||
|
||||
public float ThresholdDb
|
||||
{
|
||||
get => _thresholdDb;
|
||||
set => _thresholdDb = value;
|
||||
}
|
||||
|
||||
public float Ratio
|
||||
{
|
||||
get => _ratio;
|
||||
set => _ratio = Math.Max(1f, value);
|
||||
}
|
||||
|
||||
public float AttackTime
|
||||
{
|
||||
get => _attackTime;
|
||||
set
|
||||
{
|
||||
_attackTime = Math.Max(0.0001f, value);
|
||||
UpdateAttackCoef();
|
||||
}
|
||||
}
|
||||
|
||||
public float ReleaseTime
|
||||
{
|
||||
get => _releaseTime;
|
||||
set
|
||||
{
|
||||
_releaseTime = Math.Max(0.0001f, value);
|
||||
UpdateReleaseCoef();
|
||||
}
|
||||
}
|
||||
|
||||
public float KneeDb
|
||||
{
|
||||
get => _kneeDb;
|
||||
set => _kneeDb = Math.Max(0f, value);
|
||||
}
|
||||
|
||||
public float RmsWindowTime
|
||||
{
|
||||
get => _rmsWindowTime;
|
||||
set
|
||||
{
|
||||
_rmsWindowTime = Math.Max(0.0001f, value);
|
||||
UpdateRmsCoef();
|
||||
}
|
||||
}
|
||||
|
||||
public void Initialize(int sampleRate, int channels)
|
||||
{
|
||||
_sampleRate = sampleRate;
|
||||
_channels = channels;
|
||||
|
||||
UpdateAttackCoef();
|
||||
UpdateReleaseCoef();
|
||||
UpdateRmsCoef();
|
||||
UpdateLookAheadSettings();
|
||||
|
||||
_rmsState = new float[channels];
|
||||
_gainState = new float[channels];
|
||||
|
||||
Reset();
|
||||
}
|
||||
|
||||
private void UpdateLookAheadSettings()
|
||||
{
|
||||
_lookAheadSamples = (int)Math.Round(_sampleRate * _lookAheadTime);
|
||||
ReallocateDelayBuffers();
|
||||
}
|
||||
|
||||
private void ReallocateDelayBuffers()
|
||||
{
|
||||
if (_channels <= 0) return;
|
||||
|
||||
_delayBuffers = new float[_channels][];
|
||||
_delayWriteIndices = new int[_channels];
|
||||
|
||||
for (int i = 0; i < _channels; i++)
|
||||
{
|
||||
_delayBuffers[i] = _lookAheadSamples > 0 ? new float[_lookAheadSamples] : Array.Empty<float>();
|
||||
_delayWriteIndices[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAttackCoef()
|
||||
{
|
||||
_attackCoef = (float)Math.Exp(-1.0 / (_sampleRate * _attackTime));
|
||||
}
|
||||
|
||||
private void UpdateReleaseCoef()
|
||||
{
|
||||
_releaseCoef = (float)Math.Exp(-1.0 / (_sampleRate * _releaseTime));
|
||||
}
|
||||
|
||||
private void UpdateRmsCoef()
|
||||
{
|
||||
_rmsCoef = (float)Math.Exp(-1.0 / (_sampleRate * _rmsWindowTime));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public float ProcessSample(float sample, int channel)
|
||||
{
|
||||
if (channel < 0 || channel >= _channels)
|
||||
{
|
||||
return sample;
|
||||
}
|
||||
|
||||
// Calculate running Mean-Square (MS) level
|
||||
// v_c[n] = alpha * v_c[n-1] + (1 - alpha) * x^2[n]
|
||||
float sq = sample * sample;
|
||||
_rmsState[channel] = _rmsCoef * _rmsState[channel] + (1.0f - _rmsCoef) * sq;
|
||||
|
||||
float rmsSq = Math.Max(0f, _rmsState[channel]);
|
||||
// Convert to dB. 10 * log10(rmsSq) is equivalent to 20 * log10(sqrt(rmsSq))
|
||||
float rmsDb = 10.0f * (float)Math.Log10(rmsSq + 1e-12f);
|
||||
|
||||
// Calculate static envelope curve (gain reduction in dB)
|
||||
float gStatic = 0f;
|
||||
if (_kneeDb > 0f)
|
||||
{
|
||||
if (rmsDb > _thresholdDb + _kneeDb * 0.5f)
|
||||
{
|
||||
gStatic = (1f / _ratio - 1f) * (rmsDb - _thresholdDb);
|
||||
}
|
||||
else if (rmsDb >= _thresholdDb - _kneeDb * 0.5f)
|
||||
{
|
||||
float diff = rmsDb - _thresholdDb + _kneeDb * 0.5f;
|
||||
gStatic = (1f / _ratio - 1f) * diff * diff / (2f * _kneeDb);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rmsDb > _thresholdDb)
|
||||
{
|
||||
gStatic = (1f / _ratio - 1f) * (rmsDb - _thresholdDb);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply ballistics (attack and release) on the dB gain reduction
|
||||
float lastGain = _gainState[channel];
|
||||
float targetGain = gStatic;
|
||||
|
||||
// Note: Since targetGain and lastGain are <= 0 (gain reduction in dB),
|
||||
// targetGain < lastGain means target gain reduction is larger (more negative),
|
||||
// so we are in the attack phase.
|
||||
float coef = targetGain < lastGain ? _attackCoef : _releaseCoef;
|
||||
float currentGain = coef * lastGain + (1f - coef) * targetGain;
|
||||
_gainState[channel] = currentGain;
|
||||
|
||||
// Convert back to linear gain multiplier
|
||||
float linearGain = (float)Math.Pow(10.0, currentGain / 20.0);
|
||||
|
||||
if (_lookAheadSamples > 0 && _delayBuffers.Length > channel && _delayBuffers[channel].Length > 0)
|
||||
{
|
||||
var buffer = _delayBuffers[channel];
|
||||
int writeIdx = _delayWriteIndices[channel];
|
||||
float delayedSample = buffer[writeIdx];
|
||||
buffer[writeIdx] = sample;
|
||||
_delayWriteIndices[channel] = (writeIdx + 1) % _lookAheadSamples;
|
||||
return delayedSample * linearGain;
|
||||
}
|
||||
|
||||
return sample * linearGain;
|
||||
}
|
||||
|
||||
public void ProcessBlock(Span<float> buffer)
|
||||
{
|
||||
for (var i = 0; i < buffer.Length; i++)
|
||||
{
|
||||
int channel = i % _channels;
|
||||
buffer[i] = ProcessSample(buffer[i], channel);
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Array.Clear(_rmsState, 0, _rmsState.Length);
|
||||
Array.Clear(_gainState, 0, _gainState.Length);
|
||||
|
||||
foreach (var delayBuffer in _delayBuffers)
|
||||
{
|
||||
Array.Clear(delayBuffer, 0, delayBuffer.Length);
|
||||
}
|
||||
Array.Clear(_delayWriteIndices, 0, _delayWriteIndices.Length);
|
||||
}
|
||||
}
|
||||
+332
-333
@@ -5,413 +5,412 @@ using ADOFAIRenderer.Util.Audio.Filter;
|
||||
using MelonLoader;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ADOFAIRenderer.Util.Audio
|
||||
namespace ADOFAIRenderer.Util.Audio;
|
||||
|
||||
internal class AudioClipCache
|
||||
{
|
||||
internal class AudioClipCache
|
||||
private readonly Dictionary<string, CachedClipData> _cache = new();
|
||||
|
||||
public class CachedClipData
|
||||
{
|
||||
private readonly Dictionary<string, CachedClipData> _cache = new();
|
||||
public float[] Samples = null!; // Pre-resampled to target channels and sample rate
|
||||
public double Duration;
|
||||
}
|
||||
|
||||
public class CachedClipData
|
||||
public CachedClipData? GetOrCache(string clipName, int targetChannels, int targetSampleRate, bool allowLoad = true)
|
||||
{
|
||||
if (string.IsNullOrEmpty(clipName)) return null;
|
||||
|
||||
lock (_cache)
|
||||
{
|
||||
public float[] Samples = null!; // Pre-resampled to target channels and sample rate
|
||||
public double Duration;
|
||||
}
|
||||
if (_cache.TryGetValue(clipName, out var cached))
|
||||
return cached;
|
||||
|
||||
public CachedClipData? GetOrCache(string clipName, int targetChannels, int targetSampleRate, bool allowLoad = true)
|
||||
{
|
||||
if (string.IsNullOrEmpty(clipName)) return null;
|
||||
if (!allowLoad) return null;
|
||||
|
||||
lock (_cache)
|
||||
if (!AudioManager.Instance) return null;
|
||||
|
||||
var clip = AudioManager.Instance.FindOrLoadAudioClip(clipName);
|
||||
if (!clip) return null;
|
||||
|
||||
try
|
||||
{
|
||||
if (_cache.TryGetValue(clipName, out var cached))
|
||||
return cached;
|
||||
var totalSamples = clip.samples;
|
||||
var channels = clip.channels;
|
||||
var rawSamples = new float[totalSamples * channels];
|
||||
clip.GetData(rawSamples, 0);
|
||||
|
||||
if (!allowLoad) return null;
|
||||
// Auto-resample during cache generation
|
||||
var resampled = AudioResampler.Resample(rawSamples, channels, clip.frequency, targetChannels, targetSampleRate);
|
||||
|
||||
if (!AudioManager.Instance) return null;
|
||||
|
||||
var clip = AudioManager.Instance.FindOrLoadAudioClip(clipName);
|
||||
if (!clip) return null;
|
||||
|
||||
try
|
||||
var data = new CachedClipData
|
||||
{
|
||||
var totalSamples = clip.samples;
|
||||
var channels = clip.channels;
|
||||
var rawSamples = new float[totalSamples * channels];
|
||||
clip.GetData(rawSamples, 0);
|
||||
Samples = resampled,
|
||||
Duration = (double)resampled.Length / (targetChannels * targetSampleRate)
|
||||
};
|
||||
|
||||
// Auto-resample during cache generation
|
||||
var resampled = AudioResampler.Resample(rawSamples, channels, clip.frequency, targetChannels, targetSampleRate);
|
||||
|
||||
var data = new CachedClipData
|
||||
{
|
||||
Samples = resampled,
|
||||
Duration = (double)resampled.Length / (targetChannels * targetSampleRate)
|
||||
};
|
||||
|
||||
_cache[clipName] = data;
|
||||
return data;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
MelonLogger.Error($"[AudioClipCache] Failed to cache and resample clip {clipName}: {e}");
|
||||
return null;
|
||||
}
|
||||
_cache[clipName] = data;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
lock (_cache)
|
||||
catch (Exception e)
|
||||
{
|
||||
_cache.Clear();
|
||||
MelonLogger.Error($"[AudioClipCache] Failed to cache and resample clip {clipName}: {e}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class OfflineAudioMixer
|
||||
public void Clear()
|
||||
{
|
||||
public class SoundEvent
|
||||
lock (_cache)
|
||||
{
|
||||
public string ClipName = null!;
|
||||
public double StartTime; // In seconds relative to dspTimeBase
|
||||
public double EndTime; // In seconds relative to dspTimeBase (-1 if one-shot)
|
||||
public float Volume;
|
||||
public float Pitch;
|
||||
_cache.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<SoundEvent> _activeEvents = new();
|
||||
private ConditionalWeakTable<AudioSource, SoundEvent> _sourceMappings = new();
|
||||
private readonly object _lock = new();
|
||||
internal class OfflineAudioMixer
|
||||
{
|
||||
public class SoundEvent
|
||||
{
|
||||
public string ClipName = null!;
|
||||
public double StartTime; // In seconds relative to dspTimeBase
|
||||
public double EndTime; // In seconds relative to dspTimeBase (-1 if one-shot)
|
||||
public float Volume;
|
||||
public float Pitch;
|
||||
}
|
||||
|
||||
// Target Audio Settings
|
||||
private readonly int _targetChannels;
|
||||
private readonly int _targetSampleRate;
|
||||
private const float ClipVolume = .5f;
|
||||
private readonly List<SoundEvent> _activeEvents = new();
|
||||
private ConditionalWeakTable<AudioSource, SoundEvent> _sourceMappings = new();
|
||||
private readonly object _lock = new();
|
||||
|
||||
// Audio Filter Settings
|
||||
private readonly LinkedList<IAudioFilter> _filters = new();
|
||||
// Target Audio Settings
|
||||
private readonly int _targetChannels;
|
||||
private readonly int _targetSampleRate;
|
||||
private const float ClipVolume = .5f;
|
||||
|
||||
// BGM Cached Samples (Pre-resampled)
|
||||
private float[]? _bgmSamples;
|
||||
private double _bgmDuration;
|
||||
// Audio Filter Settings
|
||||
private readonly LinkedList<IAudioFilter> _filters = new();
|
||||
|
||||
// Clip Cache
|
||||
private readonly AudioClipCache _clipCache = new();
|
||||
private bool _isPrimed;
|
||||
// BGM Cached Samples (Pre-resampled)
|
||||
private float[]? _bgmSamples;
|
||||
private double _bgmDuration;
|
||||
|
||||
public OfflineAudioMixer(int targetChannels, int targetSampleRate)
|
||||
// Clip Cache
|
||||
private readonly AudioClipCache _clipCache = new();
|
||||
private bool _isPrimed;
|
||||
|
||||
public OfflineAudioMixer(int targetChannels, int targetSampleRate)
|
||||
{
|
||||
_targetChannels = targetChannels;
|
||||
_targetSampleRate = targetSampleRate;
|
||||
|
||||
var rmsCompressor = new RMSCompressor
|
||||
{
|
||||
_targetChannels = targetChannels;
|
||||
_targetSampleRate = targetSampleRate;
|
||||
ThresholdDb = -10, Ratio = 2,
|
||||
AttackTime = 0.1f, ReleaseTime = 0.1f,
|
||||
KneeDb = 10, RmsWindowTime = 0.02f
|
||||
};
|
||||
rmsCompressor.Initialize(_targetSampleRate, _targetChannels);
|
||||
_filters.AddLast(rmsCompressor);
|
||||
|
||||
var rmsCompressor = new RMSCompressor
|
||||
{
|
||||
ThresholdDb = -10, Ratio = 2,
|
||||
AttackTime = 0.1f, ReleaseTime = 0.1f,
|
||||
KneeDb = 10, RmsWindowTime = 0.02f
|
||||
};
|
||||
rmsCompressor.Initialize(_targetSampleRate, _targetChannels);
|
||||
_filters.AddLast(rmsCompressor);
|
||||
|
||||
var limiter = new RMSCompressor
|
||||
{
|
||||
ThresholdDb = 0, Ratio = float.PositiveInfinity,
|
||||
AttackTime = 0f, ReleaseTime = 0.05f,
|
||||
KneeDb = 0, RmsWindowTime = 0.02f,
|
||||
LookAheadTime = 0.01f
|
||||
};
|
||||
limiter.Initialize(_targetSampleRate, _targetChannels);
|
||||
_filters.AddLast(limiter);
|
||||
|
||||
var hardClip = new HardClip();
|
||||
hardClip.Initialize(_targetSampleRate, _targetChannels);
|
||||
_filters.AddLast(hardClip);
|
||||
|
||||
var truePeakSafetyGain = new Gain { Level = -1f };
|
||||
truePeakSafetyGain.Initialize(_targetSampleRate, _targetChannels);
|
||||
_filters.AddLast(truePeakSafetyGain);
|
||||
}
|
||||
|
||||
public void InitializeBGM(AudioClip? clip)
|
||||
var limiter = new RMSCompressor
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!clip)
|
||||
{
|
||||
_bgmSamples = null;
|
||||
_bgmDuration = 0;
|
||||
return;
|
||||
}
|
||||
ThresholdDb = 0, Ratio = float.PositiveInfinity,
|
||||
AttackTime = 0f, ReleaseTime = 0.05f,
|
||||
KneeDb = 0, RmsWindowTime = 0.02f,
|
||||
LookAheadTime = 0.01f
|
||||
};
|
||||
limiter.Initialize(_targetSampleRate, _targetChannels);
|
||||
_filters.AddLast(limiter);
|
||||
|
||||
try
|
||||
{
|
||||
int originalSamples = clip.samples;
|
||||
int originalChannels = clip.channels;
|
||||
float[] rawSamples = new float[originalSamples * originalChannels];
|
||||
clip.GetData(rawSamples, 0);
|
||||
var hardClip = new HardClip();
|
||||
hardClip.Initialize(_targetSampleRate, _targetChannels);
|
||||
_filters.AddLast(hardClip);
|
||||
|
||||
// Pre-resample the BGM to the target format
|
||||
_bgmSamples = AudioResampler.Resample(rawSamples, originalChannels, clip.frequency, _targetChannels, _targetSampleRate);
|
||||
_bgmDuration = (double)_bgmSamples.Length / (_targetChannels * _targetSampleRate);
|
||||
var truePeakSafetyGain = new Gain { Level = -1f };
|
||||
truePeakSafetyGain.Initialize(_targetSampleRate, _targetChannels);
|
||||
_filters.AddLast(truePeakSafetyGain);
|
||||
}
|
||||
|
||||
Melon<RenderMod>.Logger.Msg($"[OfflineAudioMixer] Initialized resampled BGM: {clip.name}, Samples: {_bgmSamples.Length / _targetChannels}, TargetFreq: {_targetSampleRate}Hz, TargetChannels: {_targetChannels}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Error($"[OfflineAudioMixer] Failed to initialize and resample BGM: {e}");
|
||||
_bgmSamples = null;
|
||||
_bgmDuration = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CacheClip(string clipName)
|
||||
public void InitializeBGM(AudioClip? clip)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_clipCache.GetOrCache(clipName, _targetChannels, _targetSampleRate, allowLoad: true);
|
||||
}
|
||||
|
||||
public void RegisterSoundEvent(AudioSource? source, string clipName, double startTime, double endTime, float volume, float pitch)
|
||||
{
|
||||
lock (_lock)
|
||||
if (!clip)
|
||||
{
|
||||
var ev = new SoundEvent
|
||||
{
|
||||
ClipName = clipName,
|
||||
StartTime = startTime,
|
||||
EndTime = endTime,
|
||||
Volume = volume,
|
||||
Pitch = pitch
|
||||
};
|
||||
_activeEvents.Add(ev);
|
||||
|
||||
if (!source) return;
|
||||
_sourceMappings.Remove(source);
|
||||
_sourceMappings.Add(source, ev);
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterSoundEvent(string clipName, double startTime, double endTime, float volume, float pitch)
|
||||
{
|
||||
RegisterSoundEvent(null, clipName, startTime, endTime, volume, pitch);
|
||||
}
|
||||
|
||||
public bool TryGetSoundEvent(AudioSource? source, out SoundEvent ev)
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
ev = null!;
|
||||
return false;
|
||||
}
|
||||
lock (_lock)
|
||||
{
|
||||
return _sourceMappings.TryGetValue(source, out ev);
|
||||
}
|
||||
}
|
||||
|
||||
public bool RemoveSourceMapping(AudioSource source)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _sourceMappings.Remove(source);
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_activeEvents.Clear();
|
||||
_sourceMappings = new ConditionalWeakTable<AudioSource, SoundEvent>();
|
||||
_bgmSamples = null;
|
||||
_bgmDuration = 0;
|
||||
_clipCache.Clear();
|
||||
_isPrimed = false;
|
||||
foreach (var filter in _filters)
|
||||
{
|
||||
filter.Reset();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void PrimeFilters(double totalLatency, double dspTimeBase)
|
||||
{
|
||||
int latencySamples = (int)Math.Round(totalLatency * _targetSampleRate);
|
||||
if (latencySamples <= 0) return;
|
||||
|
||||
// Rent temporary buffer to prime filters with initial audio
|
||||
float[] tempBuffer = System.Buffers.ArrayPool<float>.Shared.Rent(latencySamples * _targetChannels);
|
||||
try
|
||||
{
|
||||
Array.Clear(tempBuffer, 0, latencySamples * _targetChannels);
|
||||
int originalSamples = clip.samples;
|
||||
int originalChannels = clip.channels;
|
||||
float[] rawSamples = new float[originalSamples * originalChannels];
|
||||
clip.GetData(rawSamples, 0);
|
||||
|
||||
// Mix starting from -totalLatency to 0
|
||||
MixBGM(tempBuffer, latencySamples, -totalLatency, dspTimeBase, totalLatency);
|
||||
MixEffectSounds(tempBuffer, latencySamples, -totalLatency, totalLatency);
|
||||
// Pre-resample the BGM to the target format
|
||||
_bgmSamples = AudioResampler.Resample(rawSamples, originalChannels, clip.frequency, _targetChannels, _targetSampleRate);
|
||||
_bgmDuration = (double)_bgmSamples.Length / (_targetChannels * _targetSampleRate);
|
||||
|
||||
// Feed the buffer into filters to prime circular delay buffers
|
||||
foreach (var filter in _filters)
|
||||
{
|
||||
filter.ProcessBlock(tempBuffer.AsSpan(0, latencySamples * _targetChannels));
|
||||
}
|
||||
Melon<RenderMod>.Logger.Msg($"[OfflineAudioMixer] Initialized resampled BGM: {clip.name}, Samples: {_bgmSamples.Length / _targetChannels}, TargetFreq: {_targetSampleRate}Hz, TargetChannels: {_targetChannels}");
|
||||
}
|
||||
finally
|
||||
catch (Exception e)
|
||||
{
|
||||
System.Buffers.ArrayPool<float>.Shared.Return(tempBuffer);
|
||||
Melon<RenderMod>.Logger.Error($"[OfflineAudioMixer] Failed to initialize and resample BGM: {e}");
|
||||
_bgmSamples = null;
|
||||
_bgmDuration = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void GenerateAudioForFrame(float[] mixedBuffer, int samplesNeeded, double frameStartTime, double dspTimeBase)
|
||||
public void CacheClip(string clipName)
|
||||
{
|
||||
_clipCache.GetOrCache(clipName, _targetChannels, _targetSampleRate, allowLoad: true);
|
||||
}
|
||||
|
||||
public void RegisterSoundEvent(AudioSource? source, string clipName, double startTime, double endTime, float volume, float pitch)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
// mixedBuffer size must be at least samplesNeeded * _targetChannels
|
||||
Array.Clear(mixedBuffer, 0, samplesNeeded * _targetChannels);
|
||||
|
||||
lock (_lock)
|
||||
var ev = new SoundEvent
|
||||
{
|
||||
double totalLatency = 0;
|
||||
foreach (var filter in _filters)
|
||||
ClipName = clipName,
|
||||
StartTime = startTime,
|
||||
EndTime = endTime,
|
||||
Volume = volume,
|
||||
Pitch = pitch
|
||||
};
|
||||
_activeEvents.Add(ev);
|
||||
|
||||
if (!source) return;
|
||||
_sourceMappings.Remove(source);
|
||||
_sourceMappings.Add(source, ev);
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterSoundEvent(string clipName, double startTime, double endTime, float volume, float pitch)
|
||||
{
|
||||
RegisterSoundEvent(null, clipName, startTime, endTime, volume, pitch);
|
||||
}
|
||||
|
||||
public bool TryGetSoundEvent(AudioSource? source, out SoundEvent ev)
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
ev = null!;
|
||||
return false;
|
||||
}
|
||||
lock (_lock)
|
||||
{
|
||||
return _sourceMappings.TryGetValue(source, out ev);
|
||||
}
|
||||
}
|
||||
|
||||
public bool RemoveSourceMapping(AudioSource source)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _sourceMappings.Remove(source);
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_activeEvents.Clear();
|
||||
_sourceMappings = new ConditionalWeakTable<AudioSource, SoundEvent>();
|
||||
_bgmSamples = null;
|
||||
_bgmDuration = 0;
|
||||
_clipCache.Clear();
|
||||
_isPrimed = false;
|
||||
foreach (var filter in _filters)
|
||||
{
|
||||
filter.Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PrimeFilters(double totalLatency, double dspTimeBase)
|
||||
{
|
||||
int latencySamples = (int)Math.Round(totalLatency * _targetSampleRate);
|
||||
if (latencySamples <= 0) return;
|
||||
|
||||
// Rent temporary buffer to prime filters with initial audio
|
||||
float[] tempBuffer = System.Buffers.ArrayPool<float>.Shared.Rent(latencySamples * _targetChannels);
|
||||
try
|
||||
{
|
||||
Array.Clear(tempBuffer, 0, latencySamples * _targetChannels);
|
||||
|
||||
// Mix starting from -totalLatency to 0
|
||||
MixBGM(tempBuffer, latencySamples, -totalLatency, dspTimeBase, totalLatency);
|
||||
MixEffectSounds(tempBuffer, latencySamples, -totalLatency, totalLatency);
|
||||
|
||||
// Feed the buffer into filters to prime circular delay buffers
|
||||
foreach (var filter in _filters)
|
||||
{
|
||||
filter.ProcessBlock(tempBuffer.AsSpan(0, latencySamples * _targetChannels));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
System.Buffers.ArrayPool<float>.Shared.Return(tempBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
public void GenerateAudioForFrame(float[] mixedBuffer, int samplesNeeded, double frameStartTime, double dspTimeBase)
|
||||
{
|
||||
// mixedBuffer size must be at least samplesNeeded * _targetChannels
|
||||
Array.Clear(mixedBuffer, 0, samplesNeeded * _targetChannels);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
double totalLatency = 0;
|
||||
foreach (var filter in _filters)
|
||||
{
|
||||
totalLatency += filter.Latency;
|
||||
}
|
||||
|
||||
// Prime filters on first frame execution if latency exists
|
||||
if (!_isPrimed && totalLatency > 0)
|
||||
{
|
||||
PrimeFilters(totalLatency, dspTimeBase);
|
||||
_isPrimed = true;
|
||||
}
|
||||
|
||||
// 1. Mix BGM
|
||||
MixBGM(mixedBuffer, samplesNeeded, frameStartTime, dspTimeBase, totalLatency);
|
||||
|
||||
// 2. Mix Hitsounds/Effect Sounds
|
||||
MixEffectSounds(mixedBuffer, samplesNeeded, frameStartTime, totalLatency);
|
||||
|
||||
foreach (var filter in _filters)
|
||||
filter.ProcessBlock(mixedBuffer.AsSpan(0, samplesNeeded * _targetChannels));
|
||||
|
||||
for (var i = 0; i < samplesNeeded * _targetChannels; i++)
|
||||
{
|
||||
if (Math.Abs(mixedBuffer[i]) > 1) Melon<RenderMod>.Logger.Msg("Warning Why Audio Sample Is Larger Than 1");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void MixBGM(float[] mixedBuffer, int samplesNeeded, double frameStartTime, double dspTimeBase, double totalLatency)
|
||||
{
|
||||
if (_bgmSamples == null || _bgmSamples.Length == 0) return;
|
||||
|
||||
var conductor = scrConductor.instance;
|
||||
if (!conductor) return;
|
||||
|
||||
double pitch = conductor.song.pitch;
|
||||
var dspTimeSong = conductor.dspTimeSong;
|
||||
var separateCountdownOffset = conductor.separateCountdownTime
|
||||
? conductor.crotchetAtStart / pitch * conductor.adjustedCountdownTicks
|
||||
: 0.0;
|
||||
|
||||
// BGM Playback start time relative to dspTimeBase
|
||||
var time1 = dspTimeSong + separateCountdownOffset;
|
||||
var relativeTime1 = time1 - dspTimeBase;
|
||||
var adjustedRelativeTime1 = relativeTime1 - totalLatency;
|
||||
|
||||
var totalSamples = _bgmSamples.Length / _targetChannels;
|
||||
|
||||
for (int s = 0; s < samplesNeeded; s++)
|
||||
{
|
||||
double sampleTime = frameStartTime + (double)s / _targetSampleRate;
|
||||
double clipTime = (sampleTime - adjustedRelativeTime1) * pitch;
|
||||
|
||||
if (clipTime >= 0 && clipTime < _bgmDuration)
|
||||
{
|
||||
double sampleIdxDouble = clipTime * _targetSampleRate;
|
||||
long idxLow = (long)Math.Floor(sampleIdxDouble);
|
||||
long idxHigh = idxLow + 1;
|
||||
float t = (float)(sampleIdxDouble - idxLow);
|
||||
|
||||
if (idxHigh >= totalSamples) idxHigh = totalSamples - 1;
|
||||
if (idxLow >= totalSamples) idxLow = totalSamples - 1;
|
||||
|
||||
for (var c = 0; c < _targetChannels; c++)
|
||||
{
|
||||
totalLatency += filter.Latency;
|
||||
}
|
||||
|
||||
// Prime filters on first frame execution if latency exists
|
||||
if (!_isPrimed && totalLatency > 0)
|
||||
{
|
||||
PrimeFilters(totalLatency, dspTimeBase);
|
||||
_isPrimed = true;
|
||||
}
|
||||
|
||||
// 1. Mix BGM
|
||||
MixBGM(mixedBuffer, samplesNeeded, frameStartTime, dspTimeBase, totalLatency);
|
||||
|
||||
// 2. Mix Hitsounds/Effect Sounds
|
||||
MixEffectSounds(mixedBuffer, samplesNeeded, frameStartTime, totalLatency);
|
||||
|
||||
foreach (var filter in _filters)
|
||||
filter.ProcessBlock(mixedBuffer.AsSpan(0, samplesNeeded * _targetChannels));
|
||||
|
||||
for (var i = 0; i < samplesNeeded * _targetChannels; i++)
|
||||
{
|
||||
if (Math.Abs(mixedBuffer[i]) > 1) Melon<RenderMod>.Logger.Msg("Warning Why Audio Sample Is Larger Than 1");
|
||||
float valLow = _bgmSamples[idxLow * _targetChannels + c];
|
||||
float valHigh = _bgmSamples[idxHigh * _targetChannels + c];
|
||||
mixedBuffer[s * _targetChannels + c] += (valLow + t * (valHigh - valLow)) * (scnGame.instance.levelData.volume * 0.01f)
|
||||
* ClipVolume;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void MixBGM(float[] mixedBuffer, int samplesNeeded, double frameStartTime, double dspTimeBase, double totalLatency)
|
||||
private void MixEffectSounds(float[] mixedBuffer, int samplesNeeded, double frameStartTime, double totalLatency)
|
||||
{
|
||||
for (var i = _activeEvents.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (_bgmSamples == null || _bgmSamples.Length == 0) return;
|
||||
|
||||
var conductor = scrConductor.instance;
|
||||
if (!conductor) return;
|
||||
|
||||
double pitch = conductor.song.pitch;
|
||||
var dspTimeSong = conductor.dspTimeSong;
|
||||
var separateCountdownOffset = conductor.separateCountdownTime
|
||||
? conductor.crotchetAtStart / pitch * conductor.adjustedCountdownTicks
|
||||
: 0.0;
|
||||
|
||||
// BGM Playback start time relative to dspTimeBase
|
||||
var time1 = dspTimeSong + separateCountdownOffset;
|
||||
var relativeTime1 = time1 - dspTimeBase;
|
||||
var adjustedRelativeTime1 = relativeTime1 - totalLatency;
|
||||
|
||||
var totalSamples = _bgmSamples.Length / _targetChannels;
|
||||
|
||||
for (int s = 0; s < samplesNeeded; s++)
|
||||
var ev = _activeEvents[i];
|
||||
var clipData = _clipCache.GetOrCache(ev.ClipName, _targetChannels, _targetSampleRate, allowLoad: false);
|
||||
if (clipData == null)
|
||||
{
|
||||
double sampleTime = frameStartTime + (double)s / _targetSampleRate;
|
||||
double clipTime = (sampleTime - adjustedRelativeTime1) * pitch;
|
||||
|
||||
if (clipTime >= 0 && clipTime < _bgmDuration)
|
||||
{
|
||||
double sampleIdxDouble = clipTime * _targetSampleRate;
|
||||
long idxLow = (long)Math.Floor(sampleIdxDouble);
|
||||
long idxHigh = idxLow + 1;
|
||||
float t = (float)(sampleIdxDouble - idxLow);
|
||||
|
||||
if (idxHigh >= totalSamples) idxHigh = totalSamples - 1;
|
||||
if (idxLow >= totalSamples) idxLow = totalSamples - 1;
|
||||
|
||||
for (var c = 0; c < _targetChannels; c++)
|
||||
{
|
||||
float valLow = _bgmSamples[idxLow * _targetChannels + c];
|
||||
float valHigh = _bgmSamples[idxHigh * _targetChannels + c];
|
||||
mixedBuffer[s * _targetChannels + c] += (valLow + t * (valHigh - valLow)) * (scnGame.instance.levelData.volume * 0.01f)
|
||||
* ClipVolume;
|
||||
}
|
||||
}
|
||||
_activeEvents.RemoveAt(i);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
private void MixEffectSounds(float[] mixedBuffer, int samplesNeeded, double frameStartTime, double totalLatency)
|
||||
{
|
||||
for (var i = _activeEvents.Count - 1; i >= 0; i--)
|
||||
var isSoundFinished = false;
|
||||
var totalSamples = clipData.Samples.Length / _targetChannels;
|
||||
double adjustedStartTime = ev.StartTime - totalLatency;
|
||||
double adjustedEndTime = ev.EndTime >= 0 ? ev.EndTime - totalLatency : -1.0;
|
||||
|
||||
for (var s = 0; s < samplesNeeded; s++)
|
||||
{
|
||||
var ev = _activeEvents[i];
|
||||
var clipData = _clipCache.GetOrCache(ev.ClipName, _targetChannels, _targetSampleRate, allowLoad: false);
|
||||
if (clipData == null)
|
||||
var sampleTime = frameStartTime + (double)s / _targetSampleRate;
|
||||
|
||||
// If it is a looping sound and has reached or exceeded its end time, stop playing.
|
||||
if (adjustedEndTime >= 0 && sampleTime >= adjustedEndTime)
|
||||
{
|
||||
_activeEvents.RemoveAt(i);
|
||||
continue;
|
||||
isSoundFinished = true;
|
||||
break;
|
||||
}
|
||||
|
||||
var isSoundFinished = false;
|
||||
var totalSamples = clipData.Samples.Length / _targetChannels;
|
||||
double adjustedStartTime = ev.StartTime - totalLatency;
|
||||
double adjustedEndTime = ev.EndTime >= 0 ? ev.EndTime - totalLatency : -1.0;
|
||||
var elapsed = sampleTime - adjustedStartTime;
|
||||
if (elapsed < 0) continue; // Sound hasn't started yet
|
||||
|
||||
for (var s = 0; s < samplesNeeded; s++)
|
||||
var soundTime = elapsed * ev.Pitch;
|
||||
|
||||
if (soundTime >= clipData.Duration)
|
||||
{
|
||||
var sampleTime = frameStartTime + (double)s / _targetSampleRate;
|
||||
|
||||
// If it is a looping sound and has reached or exceeded its end time, stop playing.
|
||||
if (adjustedEndTime >= 0 && sampleTime >= adjustedEndTime)
|
||||
if (ev.EndTime < 0) // One-shot
|
||||
{
|
||||
isSoundFinished = true;
|
||||
break;
|
||||
}
|
||||
|
||||
var elapsed = sampleTime - adjustedStartTime;
|
||||
if (elapsed < 0) continue; // Sound hasn't started yet
|
||||
|
||||
var soundTime = elapsed * ev.Pitch;
|
||||
|
||||
if (soundTime >= clipData.Duration)
|
||||
{
|
||||
if (ev.EndTime < 0) // One-shot
|
||||
{
|
||||
isSoundFinished = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Looping
|
||||
soundTime %= clipData.Duration;
|
||||
}
|
||||
|
||||
// Linear interpolation for pitch shifting in the mixer loop
|
||||
double sampleIdxDouble = soundTime * _targetSampleRate;
|
||||
long idxLow = (long)Math.Floor(sampleIdxDouble);
|
||||
long idxHigh = idxLow + 1;
|
||||
float t = (float)(sampleIdxDouble - idxLow);
|
||||
|
||||
if (idxHigh >= totalSamples) idxHigh = totalSamples - 1;
|
||||
if (idxLow >= totalSamples) idxLow = totalSamples - 1;
|
||||
|
||||
for (var c = 0; c < _targetChannels; c++)
|
||||
{
|
||||
float valLow = clipData.Samples[idxLow * _targetChannels + c];
|
||||
float valHigh = clipData.Samples[idxHigh * _targetChannels + c];
|
||||
mixedBuffer[s * _targetChannels + c] += (valLow + t * (valHigh - valLow)) * ev.Volume
|
||||
* ClipVolume * 2 / 3;
|
||||
}
|
||||
// Looping
|
||||
soundTime %= clipData.Duration;
|
||||
}
|
||||
|
||||
// Clean up finished sound events (both one-shot and looping)
|
||||
if (isSoundFinished)
|
||||
// Linear interpolation for pitch shifting in the mixer loop
|
||||
double sampleIdxDouble = soundTime * _targetSampleRate;
|
||||
long idxLow = (long)Math.Floor(sampleIdxDouble);
|
||||
long idxHigh = idxLow + 1;
|
||||
float t = (float)(sampleIdxDouble - idxLow);
|
||||
|
||||
if (idxHigh >= totalSamples) idxHigh = totalSamples - 1;
|
||||
if (idxLow >= totalSamples) idxLow = totalSamples - 1;
|
||||
|
||||
for (var c = 0; c < _targetChannels; c++)
|
||||
{
|
||||
_activeEvents.RemoveAt(i);
|
||||
float valLow = clipData.Samples[idxLow * _targetChannels + c];
|
||||
float valHigh = clipData.Samples[idxHigh * _targetChannels + c];
|
||||
mixedBuffer[s * _targetChannels + c] += (valLow + t * (valHigh - valLow)) * ev.Volume
|
||||
* ClipVolume * 2 / 3;
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up finished sound events (both one-shot and looping)
|
||||
if (isSoundFinished)
|
||||
{
|
||||
_activeEvents.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user