SHA256
forked from ADOFAIEditorForum/ADOFAIRenderer
Refactor Every namespace Keyword Usage
This commit is contained in:
1 parent
4e67cb9b09
commit
27be2141f7
44 files changed
+4619
-4545
No files matched your search
@@ -110,4 +110,68 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FFmpeg.AutoGen" Version="8.1.0" />
|
<PackageReference Include="FFmpeg.AutoGen" Version="8.1.0" />
|
||||||
</ItemGroup>
|
</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>
|
</Project>
|
||||||
+36
-5
@@ -5,9 +5,10 @@ using System.IO;
|
|||||||
namespace ADOFAIRenderer.API;
|
namespace ADOFAIRenderer.API;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
[SuppressMessage("ReSharper", "UnusedType.Global")]
|
[SuppressMessage("ReSharper", "UnusedType.Global")]
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
|
||||||
public class RenderResult
|
public class RenderResult
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -41,10 +42,20 @@ public class RenderResult
|
|||||||
public long FileSizeBytes { get; }
|
public long FileSizeBytes { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public string? ErrorMessage { get; }
|
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(
|
public RenderResult(
|
||||||
bool success,
|
bool success,
|
||||||
string outputPath,
|
string outputPath,
|
||||||
@@ -63,7 +74,15 @@ public class RenderResult
|
|||||||
ErrorMessage = errorMessage;
|
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;
|
var fileSizeBytes = 0L;
|
||||||
if (!string.IsNullOrEmpty(outputPath) && File.Exists(outputPath))
|
if (!string.IsNullOrEmpty(outputPath) && File.Exists(outputPath))
|
||||||
@@ -76,12 +95,24 @@ public class RenderResult
|
|||||||
return new RenderResult(true, outputPath, totalFrames, videoDuration, renderDuration, fileSizeBytes);
|
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;
|
var videoDuration = fps > 0 ? totalFrames / fps : 0.0;
|
||||||
return new RenderResult(false, string.Empty, totalFrames, videoDuration, renderDuration, 0, errorMessage ?? "Rendering failed or was cancelled.");
|
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()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return Success
|
return Success
|
||||||
|
|||||||
@@ -92,12 +92,22 @@ public class RenderSession
|
|||||||
return this;
|
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)
|
public RenderSession WithNotification(bool enabled)
|
||||||
{
|
{
|
||||||
_notification = enabled;
|
_notification = enabled;
|
||||||
return this;
|
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)
|
public RenderSession OnComplete(Action<RenderResult> callback)
|
||||||
{
|
{
|
||||||
_onComplete += callback;
|
_onComplete += callback;
|
||||||
@@ -109,6 +119,7 @@ public class RenderSession
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="renderConfig">The configuration settings for the render session.</param>
|
/// <param name="renderConfig">The configuration settings for the render session.</param>
|
||||||
/// <param name="levelPath">The file path of the level to render.</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)
|
public void Start(RenderConfig renderConfig, string levelPath)
|
||||||
{
|
{
|
||||||
RenderContext.Auto = _auto;
|
RenderContext.Auto = _auto;
|
||||||
|
|||||||
+50
-51
@@ -1,66 +1,65 @@
|
|||||||
using ADOFAIRenderer.UI;
|
using ADOFAIRenderer.UI;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace ADOFAIRenderer.Core
|
namespace ADOFAIRenderer.Core;
|
||||||
|
|
||||||
|
[DefaultExecutionOrder(int.MinValue)]
|
||||||
|
internal class FirstUpdateHook : MonoBehaviour
|
||||||
{
|
{
|
||||||
[DefaultExecutionOrder(int.MinValue)]
|
private bool _checkedFFmpeg;
|
||||||
internal class FirstUpdateHook : MonoBehaviour
|
private bool _checkedNvidiaDriver;
|
||||||
|
|
||||||
|
private void Update()
|
||||||
{
|
{
|
||||||
private bool _checkedFFmpeg;
|
if (!_checkedFFmpeg)
|
||||||
private bool _checkedNvidiaDriver;
|
|
||||||
|
|
||||||
private void Update()
|
|
||||||
{
|
{
|
||||||
if (!_checkedFFmpeg)
|
if (FFmpegDownloadUI.Instance) return;
|
||||||
|
|
||||||
|
if (!FFmpegDownloadUI.CheckFFmpegExists())
|
||||||
{
|
{
|
||||||
if (FFmpegDownloadUI.Instance) return;
|
FFmpegDownloadUI.ShowDownloadUI();
|
||||||
|
return;
|
||||||
if (!FFmpegDownloadUI.CheckFFmpegExists())
|
|
||||||
{
|
|
||||||
FFmpegDownloadUI.ShowDownloadUI();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_checkedFFmpeg = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_checkedNvidiaDriver)
|
|
||||||
{
|
|
||||||
if (CommonWarningDialogUI.Instance) return;
|
|
||||||
|
|
||||||
_checkedNvidiaDriver = true;
|
|
||||||
CheckNvidiaDriverWarning();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (RenderManager.Instance)
|
|
||||||
{
|
|
||||||
RenderManager.Instance.OnPreRenderUpdate();
|
|
||||||
}
|
}
|
||||||
|
_checkedFFmpeg = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CheckNvidiaDriverWarning()
|
if (!_checkedNvidiaDriver)
|
||||||
{
|
{
|
||||||
var driverVer = Util.GPUUtil.GetNvidiaDriverVersion();
|
if (CommonWarningDialogUI.Instance) return;
|
||||||
if (driverVer is null or >= 610.00f) 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) 인코딩으로 대체되어 렌더링이 매우 느려집니다. 드라이버 업데이트를 진행해 주시기 바랍니다.",
|
if (RenderManager.Instance)
|
||||||
new DialogButton("드라이버 업데이트", () =>
|
{
|
||||||
{
|
RenderManager.Instance.OnPreRenderUpdate();
|
||||||
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("이번만 그냥 진행", () =>
|
|
||||||
{
|
|
||||||
// 아무 동작 안 함
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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("이번만 그냥 진행", () =>
|
||||||
|
{
|
||||||
|
// 아무 동작 안 함
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+26
-27
@@ -3,33 +3,32 @@ using System.Reflection;
|
|||||||
using HarmonyLib;
|
using HarmonyLib;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace ADOFAIRenderer.Core
|
namespace ADOFAIRenderer.Core;
|
||||||
|
|
||||||
|
[DefaultExecutionOrder(int.MaxValue)]
|
||||||
|
internal class PostRenderHook : MonoBehaviour
|
||||||
{
|
{
|
||||||
[DefaultExecutionOrder(int.MaxValue)]
|
private FieldInfo? _camQuadMeshField;
|
||||||
internal class PostRenderHook : MonoBehaviour
|
|
||||||
|
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();
|
|
||||||
|
|
||||||
StartCoroutine(PostFrameCoroutine());
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerator PostFrameCoroutine()
|
|
||||||
{
|
|
||||||
yield return new WaitForEndOfFrame();
|
|
||||||
|
|
||||||
var val = (MeshRenderer)_camQuadMeshField!.GetValue(scrCamera.instance);
|
|
||||||
if (RenderManager.Instance)
|
|
||||||
RenderManager.Instance.OnPostFrame(val.sharedMaterial.mainTexture as RenderTexture);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private void LateUpdate()
|
||||||
|
{
|
||||||
|
if (RenderManager.Instance)
|
||||||
|
RenderManager.Instance.OnLastUpdate();
|
||||||
|
|
||||||
|
StartCoroutine(PostFrameCoroutine());
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerator PostFrameCoroutine()
|
||||||
|
{
|
||||||
|
yield return new WaitForEndOfFrame();
|
||||||
|
|
||||||
|
var val = (MeshRenderer)_camQuadMeshField!.GetValue(scrCamera.instance);
|
||||||
|
if (RenderManager.Instance)
|
||||||
|
RenderManager.Instance.OnPostFrame(val.sharedMaterial.mainTexture as RenderTexture);
|
||||||
|
}
|
||||||
|
}
|
||||||
+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>
|
/// <summary>
|
||||||
/// Base class for double input validation limits.
|
/// Validates the given double value against this limit.
|
||||||
/// Multiple limits can be composed on a single input field — all are checked in order.
|
|
||||||
/// </summary>
|
/// </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>
|
if (value > 0)
|
||||||
/// 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)
|
|
||||||
{
|
|
||||||
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!;
|
errorMessage = null!;
|
||||||
return true;
|
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 MelonLoader;
|
||||||
using UnityEngine.UIElements;
|
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 VisualElement _rootVisualElement;
|
||||||
private readonly Dictionary<string, FieldEntry> _fields = new();
|
private readonly Dictionary<string, FieldEntry> _fields = new();
|
||||||
|
|
||||||
public DoubleInputValidator(VisualElement rootVisualElement)
|
public DoubleInputValidator(VisualElement rootVisualElement)
|
||||||
{
|
{
|
||||||
_rootVisualElement = rootVisualElement;
|
_rootVisualElement = rootVisualElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registers one or more <see cref="DoubleLimit"/>s for the field with the given name.
|
/// 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.
|
/// Multiple limits can be added to the same field — they are all checked in order.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="fieldName">The name of the DoubleField in the UXML.</param>
|
/// <param name="fieldName">The name of the DoubleField in the UXML.</param>
|
||||||
/// <param name="limits">The limits to apply.</param>
|
/// <param name="limits">The limits to apply.</param>
|
||||||
public DoubleInputValidator AddLimits(string fieldName, params DoubleLimit[] limits)
|
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);
|
var limits = entry.Limits;
|
||||||
_fields[fieldName] = entry;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
field.RegisterValueChangedCallback(evt =>
|
||||||
/// 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 (string.IsNullOrEmpty(field.text))
|
||||||
if (field == null)
|
|
||||||
{
|
{
|
||||||
Melon<RenderMod>.Logger.Warning(
|
return;
|
||||||
$"DoubleField '{fieldName}'을(를) 찾을 수 없습니다. 유효성 검사를 건너뜁니다.");
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var limits = entry.Limits;
|
if (limits.Any(limit => !limit.Validate(evt.newValue, out _)))
|
||||||
|
|
||||||
field.RegisterValueChangedCallback(evt =>
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(field.text))
|
field.SetValueWithoutNotify(evt.previousValue);
|
||||||
{
|
}
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (limits.Any(limit => !limit.Validate(evt.newValue, out _)))
|
|
||||||
{
|
|
||||||
field.SetValueWithoutNotify(evt.previousValue);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,86 +1,87 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using MelonLoader;
|
using MelonLoader;
|
||||||
using UnityEngine.UIElements;
|
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>
|
/// <summary>
|
||||||
/// Binds a UI Toolkit <see cref="DropdownField"/> to an enum type.
|
/// The currently selected enum value.
|
||||||
/// Populates choices from enum names and provides typed access to the selected value.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">The enum type to bind.</typeparam>
|
public T Value => _dropdown is { index: >= 0 } && _dropdown.index < _values.Count
|
||||||
internal class EnumDropdownBinder<T> where T : struct, Enum
|
? _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;
|
_values = Enum.GetValues(typeof(T)).Cast<T>().ToList();
|
||||||
private readonly List<T> _values;
|
|
||||||
private Action<T>? _onValueChanged;
|
|
||||||
|
|
||||||
/// <summary>
|
_dropdown = rootVisualElement.Q<DropdownField>(fieldName);
|
||||||
/// The currently selected enum value.
|
if (_dropdown == null)
|
||||||
/// </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)
|
|
||||||
{
|
{
|
||||||
_values = Enum.GetValues(typeof(T)).Cast<T>().ToList();
|
Melon<RenderMod>.Logger.Error(
|
||||||
|
$"DropdownField '{fieldName}'을(를) 찾을 수 없습니다.");
|
||||||
_dropdown = rootVisualElement.Q<DropdownField>(fieldName);
|
return;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
_dropdown.choices = _values.Select(v => v.ToString()).ToList();
|
||||||
/// 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
|
// Register the callback BEFORE setting index so the initial selection fires it too
|
||||||
if (_dropdown != null)
|
_dropdown.RegisterValueChangedCallback(_ =>
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
if (_dropdown == null) return;
|
_onValueChanged?.Invoke(Value);
|
||||||
int idx = _values.IndexOf(value);
|
});
|
||||||
if (idx >= 0)
|
|
||||||
{
|
var initial = defaultValue ?? _values[0];
|
||||||
_dropdown.index = idx;
|
_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)
|
||||||
|
{
|
||||||
|
if (_dropdown == null) return;
|
||||||
|
int idx = _values.IndexOf(value);
|
||||||
|
if (idx >= 0)
|
||||||
|
{
|
||||||
|
_dropdown.index = idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,75 +3,74 @@ using System.Linq;
|
|||||||
using MelonLoader;
|
using MelonLoader;
|
||||||
using UnityEngine.UIElements;
|
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 VisualElement _rootVisualElement;
|
||||||
private readonly Dictionary<string, FieldEntry> _fields = new();
|
private readonly Dictionary<string, FieldEntry> _fields = new();
|
||||||
|
|
||||||
public IntegerInputValidator(VisualElement rootVisualElement)
|
public IntegerInputValidator(VisualElement rootVisualElement)
|
||||||
{
|
{
|
||||||
_rootVisualElement = rootVisualElement;
|
_rootVisualElement = rootVisualElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registers one or more <see cref="IntegerLimit"/>s for the field with the given name.
|
/// 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.
|
/// Multiple limits can be added to the same field — they are all checked in order.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="fieldName">The name of the IntegerField in the UXML.</param>
|
/// <param name="fieldName">The name of the IntegerField in the UXML.</param>
|
||||||
/// <param name="limits">The limits to apply.</param>
|
/// <param name="limits">The limits to apply.</param>
|
||||||
public IntegerInputValidator AddLimits(string fieldName, params IntegerLimit[] limits)
|
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);
|
var limits = entry.Limits;
|
||||||
_fields[fieldName] = entry;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
field.RegisterValueChangedCallback(evt =>
|
||||||
/// 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 (string.IsNullOrEmpty(field.text))
|
||||||
if (field == null)
|
|
||||||
{
|
{
|
||||||
Melon<RenderMod>.Logger.Warning(
|
return;
|
||||||
$"IntegerField '{fieldName}'을(를) 찾을 수 없습니다. 유효성 검사를 건너뜁니다.");
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var limits = entry.Limits;
|
if (limits.Any(limit => !limit.Validate(evt.newValue, out _)))
|
||||||
|
|
||||||
field.RegisterValueChangedCallback(evt =>
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(field.text))
|
field.SetValueWithoutNotify(evt.previousValue);
|
||||||
{
|
}
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (limits.Any(limit => !limit.Validate(evt.newValue, out _)))
|
|
||||||
{
|
|
||||||
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>
|
/// <summary>
|
||||||
/// Base class for integer input validation limits.
|
/// Validates the given integer value against this limit.
|
||||||
/// Multiple limits can be composed on a single input field — all are checked in order.
|
|
||||||
/// </summary>
|
/// </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>
|
if (value > 0)
|
||||||
/// 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)
|
|
||||||
{
|
|
||||||
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!;
|
errorMessage = null!;
|
||||||
return true;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+147
-148
@@ -7,184 +7,183 @@ using MelonLoader.Utils;
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
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;
|
return Path.Combine(MelonEnvironment.GameRootDirectory, "UserData", "ADOFAIRenderer", "Presets");
|
||||||
public RenderConfig Config { get; set; } = null!;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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))
|
Name = name,
|
||||||
{
|
Config = config
|
||||||
throw new ArgumentException("프리셋 이름은 비어 있을 수 없습니다.");
|
};
|
||||||
}
|
|
||||||
|
|
||||||
string dir = GetPresetsDirectory();
|
string json = JsonConvert.SerializeObject(preset, Formatting.Indented);
|
||||||
if (!Directory.Exists(dir))
|
string safeName = GetSafeFileName(name);
|
||||||
{
|
string filePath = Path.Combine(dir, safeName + ".json");
|
||||||
Directory.CreateDirectory(dir);
|
File.WriteAllText(filePath, json);
|
||||||
}
|
}
|
||||||
|
|
||||||
var preset = new RenderPreset
|
public static List<RenderPreset> GetPresets()
|
||||||
{
|
{
|
||||||
Name = name,
|
var presets = new List<RenderPreset>();
|
||||||
Config = config
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<RenderPreset> GetPresets()
|
|
||||||
{
|
|
||||||
var presets = new List<RenderPreset>();
|
|
||||||
var dir = GetPresetsDirectory();
|
|
||||||
|
|
||||||
if (!Directory.Exists(dir)) return presets;
|
if (!Directory.Exists(dir)) return presets;
|
||||||
|
|
||||||
try
|
try
|
||||||
|
{
|
||||||
|
var files = Directory.GetFiles(dir, "*.json");
|
||||||
|
foreach (var file in files)
|
||||||
{
|
{
|
||||||
var files = Directory.GetFiles(dir, "*.json");
|
try
|
||||||
foreach (var file in files)
|
|
||||||
{
|
{
|
||||||
try
|
var json = File.ReadAllText(file);
|
||||||
{
|
var jObject = JsonConvert.DeserializeObject<JObject>(json);
|
||||||
var json = File.ReadAllText(file);
|
if (jObject == null) continue;
|
||||||
var jObject = JsonConvert.DeserializeObject<JObject>(json);
|
|
||||||
if (jObject == null) continue;
|
|
||||||
|
|
||||||
var configToken = jObject["Config"];
|
var configToken = jObject["Config"];
|
||||||
if (configToken is JObject configObj)
|
if (configToken is JObject configObj)
|
||||||
{
|
{
|
||||||
TryChangeKey(configObj, "DelayAfterLevelEnd", "DelayAfterEnd");
|
TryChangeKey(configObj, "DelayAfterLevelEnd", "DelayAfterEnd");
|
||||||
|
|
||||||
// 1. legacy EncodingConfig -> VideoConfig
|
// 1. legacy EncodingConfig -> VideoConfig
|
||||||
if (configObj["EncodingConfig"] is JObject oldEncodingConfigObj)
|
if (configObj["EncodingConfig"] is JObject oldEncodingConfigObj)
|
||||||
{
|
{
|
||||||
configObj["VideoConfig"] ??= oldEncodingConfigObj;
|
configObj["VideoConfig"] ??= oldEncodingConfigObj;
|
||||||
configObj.Remove("EncodingConfig");
|
configObj.Remove("EncodingConfig");
|
||||||
}
|
|
||||||
|
|
||||||
// 2. legacy top-level Codec -> VideoConfig.Codec
|
|
||||||
var legacyCodecToken = configObj["Codec"];
|
|
||||||
if (legacyCodecToken != null && legacyCodecToken.Type != JTokenType.Null)
|
|
||||||
{
|
|
||||||
if (configObj["VideoConfig"] == null || configObj["VideoConfig"]!.Type == JTokenType.Null)
|
|
||||||
{
|
|
||||||
configObj["VideoConfig"] = new JObject();
|
|
||||||
}
|
|
||||||
|
|
||||||
var videoConfigObj = (JObject)configObj["VideoConfig"]!;
|
|
||||||
videoConfigObj["Codec"] ??= legacyCodecToken;
|
|
||||||
|
|
||||||
configObj.Remove("Codec");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. legacy top-level Audio properties -> AudioConfig
|
|
||||||
string[] legacyAudioProps = { "RenderAudio", "AudioSampleRate", "AudioChannels", "AudioBitrate" };
|
|
||||||
foreach (var prop in legacyAudioProps)
|
|
||||||
{
|
|
||||||
var legacyToken = configObj[prop];
|
|
||||||
if (legacyToken != null && legacyToken.Type != JTokenType.Null)
|
|
||||||
{
|
|
||||||
if (configObj["AudioConfig"] == null || configObj["AudioConfig"]!.Type == JTokenType.Null)
|
|
||||||
{
|
|
||||||
configObj["AudioConfig"] = new JObject();
|
|
||||||
}
|
|
||||||
|
|
||||||
var audioConfigObj = (JObject)configObj["AudioConfig"]!;
|
|
||||||
audioConfigObj[prop] ??= legacyToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
configObj.Remove(prop);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (configObj["AudioConfig"] != null && configObj["AudioConfig"]!.Type == JTokenType.Null)
|
|
||||||
{
|
|
||||||
configObj.Remove("AudioConfig");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. legacy top-level Advanced properties -> AdvancedConfig
|
|
||||||
string[] legacyAdvancedProps = { "RandomSeed", "ProcessAudio", "NonTile" };
|
|
||||||
foreach (var prop in legacyAdvancedProps)
|
|
||||||
{
|
|
||||||
var legacyToken = configObj[prop];
|
|
||||||
if (legacyToken != null && legacyToken.Type != JTokenType.Null)
|
|
||||||
{
|
|
||||||
if (configObj["AdvancedConfig"] == null || configObj["AdvancedConfig"]!.Type == JTokenType.Null)
|
|
||||||
{
|
|
||||||
configObj["AdvancedConfig"] = new JObject();
|
|
||||||
}
|
|
||||||
|
|
||||||
var advancedConfigObj = (JObject)configObj["AdvancedConfig"]!;
|
|
||||||
advancedConfigObj[prop] ??= legacyToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
configObj.Remove(prop);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (configObj["AdvancedConfig"] != null && configObj["AdvancedConfig"]!.Type == JTokenType.Null)
|
|
||||||
{
|
|
||||||
configObj.Remove("AdvancedConfig");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var preset = jObject.ToObject<RenderPreset>();
|
// 2. legacy top-level Codec -> VideoConfig.Codec
|
||||||
if (preset != null && !string.IsNullOrEmpty(preset.Name))
|
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)
|
}
|
||||||
{
|
catch (Exception ex)
|
||||||
MelonLogger.Error($"프리셋 목록을 불러오는 중 오류 발생: {ex.Message}");
|
{
|
||||||
}
|
MelonLogger.Error($"프리셋 목록을 불러오는 중 오류 발생: {ex.Message}");
|
||||||
|
|
||||||
return presets;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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];
|
File.Delete(filePath);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+54
-55
@@ -1,71 +1,70 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using ADOFAIRenderer.Util.FFmpeg;
|
using ADOFAIRenderer.Util.FFmpeg;
|
||||||
|
|
||||||
namespace ADOFAIRenderer.Data
|
namespace ADOFAIRenderer.Data;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")]
|
||||||
|
public class RenderConfig
|
||||||
{
|
{
|
||||||
[SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")]
|
public double RenderFPS { get; set; }
|
||||||
public class RenderConfig
|
public double VideoFPS { get; set; }
|
||||||
{
|
|
||||||
public double RenderFPS { get; set; }
|
|
||||||
public double VideoFPS { get; set; }
|
|
||||||
|
|
||||||
public int RenderWidth { get; set; }
|
public int RenderWidth { get; set; }
|
||||||
public int RenderHeight { get; set; }
|
public int RenderHeight { get; set; }
|
||||||
|
|
||||||
public VideoEncodingConfig VideoConfig { get; set; } = new();
|
public VideoEncodingConfig VideoConfig { get; set; } = new();
|
||||||
public AudioEncodingConfig AudioConfig { get; set; } = new();
|
public AudioEncodingConfig AudioConfig { get; set; } = new();
|
||||||
public AdvancedConfig AdvancedConfig { get; set; } = new();
|
public AdvancedConfig AdvancedConfig { get; set; } = new();
|
||||||
|
|
||||||
public double DelayAfterEnd { get; set; }
|
public double DelayAfterEnd { get; set; }
|
||||||
public bool ShowJudgementText { get; set; } = false;
|
public bool ShowJudgementText { get; set; } = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")]
|
[SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")]
|
||||||
public class AdvancedConfig
|
public class AdvancedConfig
|
||||||
{
|
{
|
||||||
public int? RandomSeed { get; set; } = null;
|
public int? RandomSeed { get; set; } = null;
|
||||||
public bool ProcessAudio { get; set; } = true;
|
public bool ProcessAudio { get; set; } = true;
|
||||||
public bool NonTile { get; set; } = false;
|
public bool NonTile { get; set; } = false;
|
||||||
public string FileExtension { get; set; } = "mkv";
|
public string FileExtension { get; set; } = "mkv";
|
||||||
}
|
}
|
||||||
|
|
||||||
public class VideoEncodingConfig
|
public class VideoEncodingConfig
|
||||||
{
|
{
|
||||||
public Codec Codec { get; set; } = Codec.H264;
|
public Codec Codec { get; set; } = Codec.H264;
|
||||||
public RateControlMode RateControlMode { get; set; } = RateControlMode.CBR;
|
public RateControlMode RateControlMode { get; set; } = RateControlMode.CBR;
|
||||||
|
|
||||||
public int Bitrate { get; set; } = 30000;
|
public int Bitrate { get; set; } = 30000;
|
||||||
public int MaxBitrate { get; set; } = 50000;
|
public int MaxBitrate { get; set; } = 50000;
|
||||||
// ReSharper disable once InconsistentNaming
|
// ReSharper disable once InconsistentNaming
|
||||||
public int QP { get; set; } = 18;
|
public int QP { get; set; } = 18;
|
||||||
public double KeyframeInterval { get; set; } = 2.0;
|
public double KeyframeInterval { get; set; } = 2.0;
|
||||||
public bool ForceSoftwareEncoder { get; set; }
|
public bool ForceSoftwareEncoder { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AudioEncodingConfig
|
||||||
|
{
|
||||||
|
private int _audioSampleRate = 44100;
|
||||||
|
private int _audioChannels = 2;
|
||||||
|
private int _audioBitrate = 256000;
|
||||||
|
|
||||||
|
public bool RenderAudio { get; set; } = true;
|
||||||
|
|
||||||
|
public int AudioSampleRate
|
||||||
|
{
|
||||||
|
get => _audioSampleRate <= 0 ? 44100 : _audioSampleRate;
|
||||||
|
set => _audioSampleRate = value <= 0 ? 44100 : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AudioEncodingConfig
|
public int AudioChannels
|
||||||
{
|
{
|
||||||
private int _audioSampleRate = 44100;
|
get => _audioChannels <= 0 ? 2 : _audioChannels;
|
||||||
private int _audioChannels = 2;
|
set => _audioChannels = value <= 0 ? 2 : value;
|
||||||
private int _audioBitrate = 256000;
|
}
|
||||||
|
|
||||||
public bool RenderAudio { get; set; } = true;
|
public int AudioBitrate
|
||||||
|
{
|
||||||
public int AudioSampleRate
|
get => _audioBitrate <= 0 ? 256000 : _audioBitrate;
|
||||||
{
|
set => _audioBitrate = value <= 0 ? 256000 : value;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,20 @@
|
|||||||
using ADOFAIRenderer.Core;
|
using ADOFAIRenderer.Core;
|
||||||
using HarmonyLib;
|
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
|
// ReSharper disable once InconsistentNaming
|
||||||
public static class AsyncInputPatches
|
public static bool Prefix()
|
||||||
{
|
{
|
||||||
[HarmonyPrefix]
|
if (!RenderManager.IsActive) return true;
|
||||||
// ReSharper disable once InconsistentNaming
|
// Force offsetTick to match instantly without 1/100 damping division when rendering
|
||||||
public static bool Prefix()
|
AsyncInputManager.offsetTick = AsyncInputManager.currFrameTick - (ulong)(scrConductor.instance.dspTime * 10000000.0);
|
||||||
{
|
AsyncInputManager.offsetTickUpdated = true;
|
||||||
if (!RenderManager.IsActive) return true;
|
return false; // Skip the original damped offset calculation
|
||||||
// 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;
|
||||||
using UnityEngine.Audio;
|
using UnityEngine.Audio;
|
||||||
|
|
||||||
namespace ADOFAIRenderer.Patch.Engine
|
namespace ADOFAIRenderer.Patch.Engine;
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(AudioManager), "_Play")]
|
||||||
|
public static class AudioManagerPlayPatch
|
||||||
{
|
{
|
||||||
[HarmonyPatch(typeof(AudioManager), "_Play")]
|
[HarmonyPrefix]
|
||||||
public static class AudioManagerPlayPatch
|
public static bool Prefix(string snd, double time, AudioMixerGroup group, float volume, int priority)
|
||||||
{
|
{
|
||||||
[HarmonyPrefix]
|
if (RenderManager.IsCapturingActive && RenderManager.Instance.Configuration.AudioConfig.RenderAudio)
|
||||||
public static bool Prefix(string snd, double time, AudioMixerGroup group, float volume, int priority)
|
|
||||||
{
|
{
|
||||||
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
|
// Cache the clip on the main thread
|
||||||
RenderManager.Instance.AudioMixer?.CacheClip(snd);
|
mixer.CacheClip(snd);
|
||||||
}
|
|
||||||
return true; // Let the original method execute to prevent NullReferenceException
|
|
||||||
}
|
|
||||||
|
|
||||||
[HarmonyPostfix]
|
// Convert absolute DSP times to relative times in seconds from render start
|
||||||
// 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;
|
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
|
// Register looping sound event to the offline mixer (default pitch = 1.0f)
|
||||||
RenderManager.Instance.AudioMixer?.RegisterSoundEvent(__result, snd, relativeStartTime, -1.0, volume, 1.0f);
|
mixer.RegisterSoundEvent(snd, relativeStartTime, relativeEndTime, volume, 1.0f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true; // Let the original method execute
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[HarmonyPatch(typeof(scrConductor), "PlayWithEndTime")]
|
[HarmonyPatch(typeof(AudioSource), "pitch", MethodType.Setter)]
|
||||||
public static class PlayWithEndTimePatch
|
public static class AudioSourcePitchPatch
|
||||||
|
{
|
||||||
|
[HarmonyPostfix]
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
public static void Postfix(AudioSource __instance, float value)
|
||||||
{
|
{
|
||||||
[HarmonyPrefix]
|
if (RenderManager.IsCapturingActive && RenderManager.Instance.Configuration.AudioConfig.RenderAudio)
|
||||||
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 && mixer.TryGetSoundEvent(__instance, out var soundEvent))
|
||||||
{
|
{
|
||||||
var mixer = RenderManager.Instance.AudioMixer;
|
soundEvent.Pitch = value;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[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 System.Diagnostics.CodeAnalysis;
|
||||||
using ADOFAIRenderer.Core;
|
using ADOFAIRenderer.Core;
|
||||||
using ADOFAIRenderer.Util;
|
|
||||||
using ADOFAIRenderer.Util.Audio;
|
using ADOFAIRenderer.Util.Audio;
|
||||||
using HarmonyLib;
|
using HarmonyLib;
|
||||||
using UnityEngine;
|
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;
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
private static double TimeSpent => RenderManager.Instance.frameCount / RenderManager.Instance.Configuration.RenderFPS;
|
public static bool Prefix(ref float __result)
|
||||||
|
|
||||||
[HarmonyPatch(typeof(Time), "time", MethodType.Getter)]
|
|
||||||
public static class TimePatch
|
|
||||||
{
|
{
|
||||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true;
|
||||||
public static bool Prefix(ref float __result)
|
__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
|
|
||||||
{
|
|
||||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
|
||||||
public static bool Prefix(ref double __result)
|
|
||||||
{
|
|
||||||
if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true;
|
|
||||||
__result = TimeSpent + RenderManager.Instance.timeBase;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HarmonyPatch(typeof(Time), "unscaledTime", MethodType.Getter)]
|
|
||||||
public static class UnscaledTimePatch
|
|
||||||
{
|
|
||||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
|
||||||
public static bool Prefix(ref float __result)
|
|
||||||
{
|
|
||||||
if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true;
|
|
||||||
__result = (float)(TimeSpent + RenderManager.Instance.unscaledTimeBase);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HarmonyPatch(typeof(Time), "unscaledTimeAsDouble", MethodType.Getter)]
|
|
||||||
public static class UnscaledTimeAsDoublePatch
|
|
||||||
{
|
|
||||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
|
||||||
public static bool Prefix(ref double __result)
|
|
||||||
{
|
|
||||||
if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true;
|
|
||||||
__result = TimeSpent + RenderManager.Instance.unscaledTimeBase;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HarmonyPatch(typeof(AudioSettings), "dspTime", MethodType.Getter)]
|
|
||||||
public static class DspTimePatch
|
|
||||||
{
|
|
||||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
|
||||||
public static bool Prefix(ref double __result)
|
|
||||||
{
|
|
||||||
if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true;
|
|
||||||
__result = TimeSpent + RenderManager.Instance.dspTimeBase;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HarmonyPatch(typeof(AudioSource), "PlayScheduled")]
|
|
||||||
public static class PlayScheduledPatch
|
|
||||||
{
|
|
||||||
// ReSharper disable once InconsistentNaming
|
|
||||||
public static bool Prefix(AudioSource __instance, double time)
|
|
||||||
{
|
|
||||||
return !RenderManager.IsActive;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HarmonyPatch(typeof(Time), "frameCount", MethodType.Getter)]
|
|
||||||
public static class FrameCountPatch
|
|
||||||
{
|
|
||||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
|
||||||
public static bool Prefix(ref int __result)
|
|
||||||
{
|
|
||||||
if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true;
|
|
||||||
__result = (int)(RenderManager.Instance.frameCount + RenderManager.Instance.frameCountBase);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HarmonyPatch(typeof(Time), "deltaTime", MethodType.Getter)]
|
|
||||||
public static class DeltaTimePatch
|
|
||||||
{
|
|
||||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
|
||||||
public static bool Prefix(ref float __result)
|
|
||||||
{
|
|
||||||
if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true;
|
|
||||||
__result = (float)(1 / RenderManager.Instance.Configuration.RenderFPS);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HarmonyPatch(typeof(Time), "unscaledDeltaTime", MethodType.Getter)]
|
|
||||||
public static class UnscaledDeltaTimePatch
|
|
||||||
{
|
|
||||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
|
||||||
public static bool Prefix(ref float __result)
|
|
||||||
{
|
|
||||||
if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true;
|
|
||||||
__result = (float)(1 / RenderManager.Instance.Configuration.RenderFPS);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HarmonyPatch(typeof(Screen), "width", MethodType.Getter)]
|
|
||||||
public static class ScreenWidthPatch
|
|
||||||
{
|
|
||||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
|
||||||
public static bool Prefix(ref int __result)
|
|
||||||
{
|
|
||||||
if (!RenderManager.IsActive || !RenderContext.ScreenPatch || !RenderManager.Instance) return true;
|
|
||||||
__result = RenderManager.Instance.Configuration.RenderWidth;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HarmonyPatch(typeof(Screen), "height", MethodType.Getter)]
|
|
||||||
public static class ScreenHeightPatch
|
|
||||||
{
|
|
||||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
|
||||||
public static bool Prefix(ref int __result)
|
|
||||||
{
|
|
||||||
if (!RenderManager.IsActive || !RenderContext.ScreenPatch || !RenderManager.Instance) return true;
|
|
||||||
__result = RenderManager.Instance.Configuration.RenderHeight;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HarmonyPatch(typeof(AudioSource), "GetSpectrumData", typeof(float[]), typeof(int), typeof(FFTWindow))]
|
|
||||||
public static class GetSpectrumDataPatch
|
|
||||||
{
|
|
||||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
|
||||||
public static bool Prefix(AudioSource __instance, float[] samples, int channel, FFTWindow window)
|
|
||||||
{
|
|
||||||
if (!RenderManager.IsCapturingActive || !RenderManager.Instance.Configuration.AdvancedConfig.ProcessAudio) return true;
|
|
||||||
AudioSpectrumGenerator.GetSpectrumData(samples, channel, window);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HarmonyPatch(typeof(AudioManager), "FindOrLoadAudioClipExternal", typeof(string), typeof(bool), typeof(float), typeof(bool))]
|
|
||||||
public static class FindOrLoadAudioClipExternalPatch
|
|
||||||
{
|
|
||||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
|
||||||
public static void Prefix(ref bool stream)
|
|
||||||
{
|
|
||||||
if (RenderManager.Instance && RenderManager.Instance.OverrideLoading && RenderManager.Instance.Configuration.AdvancedConfig.ProcessAudio)
|
|
||||||
stream = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
[HarmonyPatch(typeof(Time), "timeAsDouble", MethodType.Getter)]
|
||||||
|
public static class TimeAsDoublePatch
|
||||||
|
{
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
public static bool Prefix(ref double __result)
|
||||||
|
{
|
||||||
|
if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true;
|
||||||
|
__result = TimeSpent + RenderManager.Instance.timeBase;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(Time), "unscaledTime", MethodType.Getter)]
|
||||||
|
public static class UnscaledTimePatch
|
||||||
|
{
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
public static bool Prefix(ref float __result)
|
||||||
|
{
|
||||||
|
if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true;
|
||||||
|
__result = (float)(TimeSpent + RenderManager.Instance.unscaledTimeBase);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(Time), "unscaledTimeAsDouble", MethodType.Getter)]
|
||||||
|
public static class UnscaledTimeAsDoublePatch
|
||||||
|
{
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
public static bool Prefix(ref double __result)
|
||||||
|
{
|
||||||
|
if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true;
|
||||||
|
__result = TimeSpent + RenderManager.Instance.unscaledTimeBase;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(AudioSettings), "dspTime", MethodType.Getter)]
|
||||||
|
public static class DspTimePatch
|
||||||
|
{
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
public static bool Prefix(ref double __result)
|
||||||
|
{
|
||||||
|
if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true;
|
||||||
|
__result = TimeSpent + RenderManager.Instance.dspTimeBase;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(AudioSource), "PlayScheduled")]
|
||||||
|
public static class PlayScheduledPatch
|
||||||
|
{
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
public static bool Prefix(AudioSource __instance, double time)
|
||||||
|
{
|
||||||
|
return !RenderManager.IsActive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(Time), "frameCount", MethodType.Getter)]
|
||||||
|
public static class FrameCountPatch
|
||||||
|
{
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
public static bool Prefix(ref int __result)
|
||||||
|
{
|
||||||
|
if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true;
|
||||||
|
__result = (int)(RenderManager.Instance.frameCount + RenderManager.Instance.frameCountBase);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(Time), "deltaTime", MethodType.Getter)]
|
||||||
|
public static class DeltaTimePatch
|
||||||
|
{
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
public static bool Prefix(ref float __result)
|
||||||
|
{
|
||||||
|
if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true;
|
||||||
|
__result = (float)(1 / RenderManager.Instance.Configuration.RenderFPS);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(Time), "unscaledDeltaTime", MethodType.Getter)]
|
||||||
|
public static class UnscaledDeltaTimePatch
|
||||||
|
{
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
public static bool Prefix(ref float __result)
|
||||||
|
{
|
||||||
|
if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true;
|
||||||
|
__result = (float)(1 / RenderManager.Instance.Configuration.RenderFPS);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(Screen), "width", MethodType.Getter)]
|
||||||
|
public static class ScreenWidthPatch
|
||||||
|
{
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
public static bool Prefix(ref int __result)
|
||||||
|
{
|
||||||
|
if (!RenderManager.IsActive || !RenderContext.ScreenPatch || !RenderManager.Instance) return true;
|
||||||
|
__result = RenderManager.Instance.Configuration.RenderWidth;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(Screen), "height", MethodType.Getter)]
|
||||||
|
public static class ScreenHeightPatch
|
||||||
|
{
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
public static bool Prefix(ref int __result)
|
||||||
|
{
|
||||||
|
if (!RenderManager.IsActive || !RenderContext.ScreenPatch || !RenderManager.Instance) return true;
|
||||||
|
__result = RenderManager.Instance.Configuration.RenderHeight;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(AudioSource), "GetSpectrumData", typeof(float[]), typeof(int), typeof(FFTWindow))]
|
||||||
|
public static class GetSpectrumDataPatch
|
||||||
|
{
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
public static bool Prefix(AudioSource __instance, float[] samples, int channel, FFTWindow window)
|
||||||
|
{
|
||||||
|
if (!RenderManager.IsCapturingActive || !RenderManager.Instance.Configuration.AdvancedConfig.ProcessAudio) return true;
|
||||||
|
AudioSpectrumGenerator.GetSpectrumData(samples, channel, window);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(AudioManager), "FindOrLoadAudioClipExternal", typeof(string), typeof(bool), typeof(float), typeof(bool))]
|
||||||
|
public static class FindOrLoadAudioClipExternalPatch
|
||||||
|
{
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
public static void Prefix(ref bool stream)
|
||||||
|
{
|
||||||
|
if (RenderManager.Instance && RenderManager.Instance.OverrideLoading && RenderManager.Instance.Configuration.AdvancedConfig.ProcessAudio)
|
||||||
|
stream = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,73 +2,72 @@ using System.Diagnostics.CodeAnalysis;
|
|||||||
using ADOFAIRenderer.Core;
|
using ADOFAIRenderer.Core;
|
||||||
using HarmonyLib;
|
using HarmonyLib;
|
||||||
|
|
||||||
namespace ADOFAIRenderer.Patch.Game
|
namespace ADOFAIRenderer.Patch.Game;
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(EndscreenLanterns))]
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
public static class EndscreenLanternsPatch
|
||||||
{
|
{
|
||||||
[HarmonyPatch(typeof(EndscreenLanterns))]
|
[HarmonyPrefix]
|
||||||
// ReSharper disable once InconsistentNaming
|
[HarmonyPatch("Show")]
|
||||||
public static class EndscreenLanternsPatch
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
public static bool ForceHide(EndscreenLanterns __instance, ref int __result)
|
||||||
{
|
{
|
||||||
[HarmonyPrefix]
|
if (!RenderManager.IsActive) return true;
|
||||||
[HarmonyPatch("Show")]
|
if (__instance != null)
|
||||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
|
||||||
public static bool ForceHide(EndscreenLanterns __instance, ref int __result)
|
|
||||||
{
|
{
|
||||||
if (!RenderManager.IsActive) return true;
|
__instance.Hide();
|
||||||
if (__instance != null)
|
|
||||||
{
|
|
||||||
__instance.Hide();
|
|
||||||
}
|
|
||||||
__result = 0; // Return 0 lanterns shown
|
|
||||||
return false; // Skip original Show method
|
|
||||||
}
|
|
||||||
|
|
||||||
[HarmonyPrefix]
|
|
||||||
[HarmonyPatch("Hide")]
|
|
||||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
|
||||||
public static void PrefixHide(EndscreenLanterns __instance)
|
|
||||||
{
|
|
||||||
if (__instance == null) return;
|
|
||||||
if (!RenderManager.IsActive) return;
|
|
||||||
|
|
||||||
var traverse = Traverse.Create(__instance);
|
|
||||||
|
|
||||||
// Kill lampSequences (private Sequence[] lampSequences) via reflection
|
|
||||||
var lampSequencesField = traverse.Field("lampSequences");
|
|
||||||
var lampSequencesObj = lampSequencesField.GetValue();
|
|
||||||
if (lampSequencesObj is System.Array lampSequencesArray)
|
|
||||||
{
|
|
||||||
for (var i = 0; i < lampSequencesArray.Length; i++)
|
|
||||||
{
|
|
||||||
var item = lampSequencesArray.GetValue(i);
|
|
||||||
if (item == null) continue;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var killMethod = item.GetType().GetMethod("Kill", new[] { typeof(bool) });
|
|
||||||
killMethod?.Invoke(item, new object[] { false });
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Ignore reflection errors
|
|
||||||
}
|
|
||||||
lampSequencesArray.SetValue(null, i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kill chainSequence (private Sequence chainSequence) via reflection
|
|
||||||
var chainSequenceField = traverse.Field("chainSequence");
|
|
||||||
var chainSequenceObj = chainSequenceField.GetValue();
|
|
||||||
if (chainSequenceObj == null) return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var killMethod = chainSequenceObj.GetType().GetMethod("Kill", new[] { typeof(bool) });
|
|
||||||
killMethod?.Invoke(chainSequenceObj, new object[] { false });
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Ignore reflection errors
|
|
||||||
}
|
|
||||||
chainSequenceField.SetValue(null);
|
|
||||||
}
|
}
|
||||||
|
__result = 0; // Return 0 lanterns shown
|
||||||
|
return false; // Skip original Show method
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
[HarmonyPrefix]
|
||||||
|
[HarmonyPatch("Hide")]
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
public static void PrefixHide(EndscreenLanterns __instance)
|
||||||
|
{
|
||||||
|
if (__instance == null) return;
|
||||||
|
if (!RenderManager.IsActive) return;
|
||||||
|
|
||||||
|
var traverse = Traverse.Create(__instance);
|
||||||
|
|
||||||
|
// Kill lampSequences (private Sequence[] lampSequences) via reflection
|
||||||
|
var lampSequencesField = traverse.Field("lampSequences");
|
||||||
|
var lampSequencesObj = lampSequencesField.GetValue();
|
||||||
|
if (lampSequencesObj is System.Array lampSequencesArray)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < lampSequencesArray.Length; i++)
|
||||||
|
{
|
||||||
|
var item = lampSequencesArray.GetValue(i);
|
||||||
|
if (item == null) continue;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var killMethod = item.GetType().GetMethod("Kill", new[] { typeof(bool) });
|
||||||
|
killMethod?.Invoke(item, new object[] { false });
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore reflection errors
|
||||||
|
}
|
||||||
|
lampSequencesArray.SetValue(null, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kill chainSequence (private Sequence chainSequence) via reflection
|
||||||
|
var chainSequenceField = traverse.Field("chainSequence");
|
||||||
|
var chainSequenceObj = chainSequenceField.GetValue();
|
||||||
|
if (chainSequenceObj == null) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var killMethod = chainSequenceObj.GetType().GetMethod("Kill", new[] { typeof(bool) });
|
||||||
|
killMethod?.Invoke(chainSequenceObj, new object[] { false });
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore reflection errors
|
||||||
|
}
|
||||||
|
chainSequenceField.SetValue(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,21 @@
|
|||||||
using ADOFAIRenderer.Core;
|
using ADOFAIRenderer.Core;
|
||||||
using HarmonyLib;
|
using HarmonyLib;
|
||||||
|
|
||||||
namespace ADOFAIRenderer.Patch.Game
|
namespace ADOFAIRenderer.Patch.Game;
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(Persistence))]
|
||||||
|
public static class PersistencePatch
|
||||||
{
|
{
|
||||||
[HarmonyPatch(typeof(Persistence))]
|
[HarmonyPostfix]
|
||||||
public static class PersistencePatch
|
[HarmonyPatch("skipIntroBehavior", MethodType.Getter)]
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
public static void LockSkipIntroBehavior(ref SkipIntroBehavior __result)
|
||||||
{
|
{
|
||||||
[HarmonyPostfix]
|
if (RenderManager.IsActive &&
|
||||||
[HarmonyPatch("skipIntroBehavior", MethodType.Getter)]
|
scrConductor.instance != null &&
|
||||||
// ReSharper disable once InconsistentNaming
|
!scrConductor.instance.hasSongStarted)
|
||||||
public static void LockSkipIntroBehavior(ref SkipIntroBehavior __result)
|
|
||||||
{
|
{
|
||||||
if (RenderManager.IsActive &&
|
__result = SkipIntroBehavior.Off;
|
||||||
scrConductor.instance != null &&
|
|
||||||
!scrConductor.instance.hasSongStarted)
|
|
||||||
{
|
|
||||||
__result = SkipIntroBehavior.Off;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,48 +4,47 @@ using MelonLoader;
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.UI;
|
using UnityEngine.UI;
|
||||||
|
|
||||||
namespace ADOFAIRenderer.Patch.Game
|
namespace ADOFAIRenderer.Patch.Game;
|
||||||
{
|
|
||||||
[HarmonyPatch(typeof(scnEditor))]
|
|
||||||
// ReSharper disable once InconsistentNaming
|
|
||||||
public static class scnEditorPatch
|
|
||||||
{
|
|
||||||
[HarmonyPostfix]
|
|
||||||
[HarmonyPatch("Start")]
|
|
||||||
// ReSharper disable once InconsistentNaming
|
|
||||||
public static void AddButtonRender(scnEditor __instance)
|
|
||||||
{
|
|
||||||
if (RenderMod.IsFeatureDisabled) return;
|
|
||||||
if (!__instance || !__instance.buttonExit)
|
|
||||||
{
|
|
||||||
Melon<RenderMod>.Logger.Msg("scnEditor에 렌더 버튼을 만들 수 없습니다.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var cloneTarget = __instance.buttonExit!.gameObject;
|
[HarmonyPatch(typeof(scnEditor))]
|
||||||
var buttonContainer = cloneTarget.transform.parent!;
|
// ReSharper disable once InconsistentNaming
|
||||||
|
public static class scnEditorPatch
|
||||||
var buttonObj = Object.Instantiate(cloneTarget, buttonContainer)!;
|
{
|
||||||
buttonObj.name = "buttonRender";
|
[HarmonyPostfix]
|
||||||
|
[HarmonyPatch("Start")]
|
||||||
var button = buttonObj.GetComponent<Button>()!;
|
// ReSharper disable once InconsistentNaming
|
||||||
// Clear persistent and non-persistent listeners from the original button
|
public static void AddButtonRender(scnEditor __instance)
|
||||||
button.onClick = new Button.ButtonClickedEvent();
|
{
|
||||||
button.onClick.AddListener(RenderManager.StartRender);
|
if (RenderMod.IsFeatureDisabled) return;
|
||||||
|
if (!__instance || !__instance.buttonExit)
|
||||||
// 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.
|
Melon<RenderMod>.Logger.Msg("scnEditor에 렌더 버튼을 만들 수 없습니다.");
|
||||||
var images = buttonObj.GetComponentsInChildren<Image>();
|
return;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+124
-125
@@ -4,139 +4,138 @@ using ADOFAIRenderer.Util;
|
|||||||
using HarmonyLib;
|
using HarmonyLib;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace ADOFAIRenderer.Patch.Game
|
namespace ADOFAIRenderer.Patch.Game;
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(scnGame))]
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
public static class scnGamePatch
|
||||||
{
|
{
|
||||||
[HarmonyPatch(typeof(scnGame))]
|
[HarmonyPostfix]
|
||||||
// ReSharper disable once InconsistentNaming
|
[HarmonyPatch("Play")]
|
||||||
public static class scnGamePatch
|
public static void ProcessFloors()
|
||||||
{
|
{
|
||||||
[HarmonyPostfix]
|
var nonTile = RenderManager.Instance.OverrideLoading &&
|
||||||
[HarmonyPatch("Play")]
|
RenderManager.Instance.Configuration.AdvancedConfig.NonTile;
|
||||||
public static void ProcessFloors()
|
|
||||||
|
if (!nonTile) return;
|
||||||
|
foreach (var floor in scrLevelMaker.instance.listFloors)
|
||||||
{
|
{
|
||||||
var nonTile = RenderManager.Instance.OverrideLoading &&
|
DisableRenderers(
|
||||||
RenderManager.Instance.Configuration.AdvancedConfig.NonTile;
|
floor.floorRenderer.renderer, floor.legacyFloorSpriteRenderer,
|
||||||
|
floor.iconsprite, floor.outlineSprite,
|
||||||
if (!nonTile) return;
|
floor.topGlow, floor.bottomGlow,
|
||||||
foreach (var floor in scrLevelMaker.instance.listFloors)
|
floor.multiplanetLine
|
||||||
{
|
);
|
||||||
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;
|
if (floor.dummyPlanets == null) continue;
|
||||||
foreach (var planet in floor.dummyPlanets)
|
foreach (var planet in floor.dummyPlanets)
|
||||||
DisablePlanetRenderer(planet);
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[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");
|
var players = scrPlayerManager.instance.players;
|
||||||
if (string.IsNullOrEmpty(text))
|
if (players == null) return;
|
||||||
{
|
|
||||||
__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 fallback 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.
|
foreach (var player in players)
|
||||||
player.Init(videoPath, __instance.videoBG.targetTexture, isLooping, targetCamera, __instance.videoBG.gameObject);
|
{
|
||||||
|
var planets = player.planetarySystem?.planetList;
|
||||||
|
if (planets == null) continue;
|
||||||
|
|
||||||
return false; // Skip original UpdateVideo
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
[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 System.Diagnostics.CodeAnalysis;
|
||||||
using HarmonyLib;
|
using HarmonyLib;
|
||||||
|
|
||||||
namespace ADOFAIRenderer.Patch.Game
|
namespace ADOFAIRenderer.Patch.Game;
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(scrCamera))]
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
public class scrCameraPatch
|
||||||
{
|
{
|
||||||
[HarmonyPatch(typeof(scrCamera))]
|
[HarmonyPrefix]
|
||||||
// ReSharper disable once InconsistentNaming
|
[HarmonyPatch("SetCustomFrameRate")]
|
||||||
public class scrCameraPatch
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
private static bool FixCamRT(scrCamera __instance, bool enable, float frame)
|
||||||
{
|
{
|
||||||
[HarmonyPrefix]
|
// There is an issue that `mainTexture.Release` triggers even if `mainTexture` is `camRT`, so mod tries to fix it
|
||||||
[HarmonyPatch("SetCustomFrameRate")]
|
if (enable || __instance.enableCustomFPS != enable) return true;
|
||||||
[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.enableCustomFPS = enable;
|
||||||
__instance.frameRate = frame;
|
__instance.frameRate = frame;
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,45 +1,44 @@
|
|||||||
using ADOFAIRenderer.Core;
|
using ADOFAIRenderer.Core;
|
||||||
using HarmonyLib;
|
using HarmonyLib;
|
||||||
|
|
||||||
namespace ADOFAIRenderer.Patch.Game
|
namespace ADOFAIRenderer.Patch.Game;
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(scrController))]
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
public static class scrControllerPatch
|
||||||
{
|
{
|
||||||
[HarmonyPatch(typeof(scrController))]
|
[HarmonyPrefix]
|
||||||
// ReSharper disable once InconsistentNaming
|
[HarmonyPatch("TogglePauseGame")]
|
||||||
public static class scrControllerPatch
|
public static bool PreventPause()
|
||||||
{
|
{
|
||||||
[HarmonyPrefix]
|
return !RenderManager.IsActive;
|
||||||
[HarmonyPatch("TogglePauseGame")]
|
}
|
||||||
public static bool PreventPause()
|
|
||||||
{
|
|
||||||
return !RenderManager.IsActive;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HarmonyPrefix]
|
[HarmonyPrefix]
|
||||||
[HarmonyPatch("PortalTravelAction")]
|
[HarmonyPatch("PortalTravelAction")]
|
||||||
public static bool PreventPortalTravel()
|
public static bool PreventPortalTravel()
|
||||||
{
|
{
|
||||||
return !RenderManager.IsActive;
|
return !RenderManager.IsActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HarmonyPrefix]
|
[HarmonyPrefix]
|
||||||
[HarmonyPatch("FailAction")]
|
[HarmonyPatch("FailAction")]
|
||||||
public static bool PreventFailAction()
|
public static bool PreventFailAction()
|
||||||
{
|
{
|
||||||
return !RenderManager.IsActive;
|
return !RenderManager.IsActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HarmonyPrefix]
|
[HarmonyPrefix]
|
||||||
[HarmonyPatch("Fail2Action")]
|
[HarmonyPatch("Fail2Action")]
|
||||||
public static bool PreventFail2Action()
|
public static bool PreventFail2Action()
|
||||||
{
|
{
|
||||||
return !RenderManager.IsActive;
|
return !RenderManager.IsActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HarmonyPrefix]
|
[HarmonyPrefix]
|
||||||
[HarmonyPatch("OnPlayerDied")]
|
[HarmonyPatch("OnPlayerDied")]
|
||||||
public static bool PreventPlayerDied()
|
public static bool PreventPlayerDied()
|
||||||
{
|
{
|
||||||
return !RenderManager.IsActive;
|
return !RenderManager.IsActive;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,41 +1,40 @@
|
|||||||
using HarmonyLib;
|
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
|
// ReSharper disable once InconsistentNaming
|
||||||
public static class scrDecorationPatch
|
public static bool SetRotationPrefix(scrDecoration __instance)
|
||||||
{
|
{
|
||||||
[HarmonyPrefix]
|
return __instance != null && __instance && __instance.pivotTrans != null;
|
||||||
[HarmonyPatch("SetRotation")]
|
|
||||||
// ReSharper disable once InconsistentNaming
|
|
||||||
public static bool SetRotationPrefix(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("UpdatePosition")]
|
|
||||||
// ReSharper disable once InconsistentNaming
|
|
||||||
public static bool UpdatePositionPrefix(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("UpdatePosition")]
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
public static bool UpdatePositionPrefix(scrDecoration __instance)
|
||||||
|
{
|
||||||
|
return __instance != null && __instance && __instance.pivotTrans != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
-13
@@ -1,22 +1,21 @@
|
|||||||
using ADOFAIRenderer.Core;
|
using ADOFAIRenderer.Core;
|
||||||
using HarmonyLib;
|
using HarmonyLib;
|
||||||
|
|
||||||
namespace ADOFAIRenderer.Patch.Game
|
namespace ADOFAIRenderer.Patch.Game;
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(scrFloor))]
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
public class scrFloorPatch
|
||||||
{
|
{
|
||||||
[HarmonyPatch(typeof(scrFloor))]
|
[HarmonyPrefix]
|
||||||
|
[HarmonyPatch("SpawnPortalParticles")]
|
||||||
// ReSharper disable once InconsistentNaming
|
// ReSharper disable once InconsistentNaming
|
||||||
public class scrFloorPatch
|
public static bool SkipPortalSpawn(scrFloor __instance)
|
||||||
{
|
{
|
||||||
[HarmonyPrefix]
|
if (!scrLevelMaker.instance.listFloors.Contains(__instance)) return true;
|
||||||
[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 &&
|
var nonTile = RenderManager.Instance.OverrideLoading &&
|
||||||
RenderManager.Instance.Configuration.AdvancedConfig.NonTile;
|
RenderManager.Instance.Configuration.AdvancedConfig.NonTile;
|
||||||
return !nonTile;
|
return !nonTile;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
using ADOFAIRenderer.Core;
|
using ADOFAIRenderer.Core;
|
||||||
using HarmonyLib;
|
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")]
|
public static bool Prefix()
|
||||||
// ReSharper disable once InconsistentNaming
|
|
||||||
public class scrHitTextManagerPatch
|
|
||||||
{
|
{
|
||||||
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 ADOFAIRenderer.Core;
|
||||||
using HarmonyLib;
|
using HarmonyLib;
|
||||||
|
|
||||||
namespace ADOFAIRenderer.Patch.Game
|
namespace ADOFAIRenderer.Patch.Game;
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(scrMistakesManager))]
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
public static class scrMistakesManagerPatch
|
||||||
{
|
{
|
||||||
[HarmonyPatch(typeof(scrMistakesManager))]
|
[HarmonyPrefix]
|
||||||
// ReSharper disable once InconsistentNaming
|
[HarmonyPatch("SaveCustom")]
|
||||||
public static class scrMistakesManagerPatch
|
public static bool PreventSaveCustom()
|
||||||
{
|
{
|
||||||
[HarmonyPrefix]
|
return !RenderManager.IsActive;
|
||||||
[HarmonyPatch("SaveCustom")]
|
|
||||||
public static bool PreventSaveCustom()
|
|
||||||
{
|
|
||||||
return !RenderManager.IsActive;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,20 @@
|
|||||||
using ADOFAIRenderer.Core;
|
using ADOFAIRenderer.Core;
|
||||||
using HarmonyLib;
|
using HarmonyLib;
|
||||||
|
|
||||||
namespace ADOFAIRenderer.Patch.Game
|
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;
|
|
||||||
|
|
||||||
__result = RenderManager.Instance && RenderManager.Instance.frameCount == 0 && RenderManager.Instance.countdown == 0;
|
[HarmonyPatch(typeof(scrPlayerManager))]
|
||||||
return false;
|
// 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 HarmonyLib;
|
||||||
using UnityEngine;
|
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
|
// ReSharper disable once InconsistentNaming
|
||||||
public static class scrPortalParticlesPatch
|
public static void ForceScaledTime(scrPortalParticles __instance)
|
||||||
{
|
{
|
||||||
[HarmonyPostfix]
|
if (!RenderManager.Instance || !RenderManager.Instance.OverrideLoading) return;
|
||||||
[HarmonyPatch("Start")]
|
if (__instance == null) return;
|
||||||
// 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);
|
var particleSystems = __instance.GetComponentsInChildren<ParticleSystem>(true);
|
||||||
foreach (var ps in particleSystems)
|
foreach (var ps in particleSystems)
|
||||||
{
|
{
|
||||||
if (ps == null) continue;
|
if (ps == null) continue;
|
||||||
|
|
||||||
var main = ps.main;
|
var main = ps.main;
|
||||||
main.useUnscaledTime = false;
|
main.useUnscaledTime = false;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,19 @@
|
|||||||
using ADOFAIRenderer.Core;
|
using ADOFAIRenderer.Core;
|
||||||
using HarmonyLib;
|
using HarmonyLib;
|
||||||
|
|
||||||
namespace ADOFAIRenderer.Patch.Game
|
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;
|
|
||||||
|
|
||||||
RenderManager.Instance.skipPressToStart = false;
|
[HarmonyPatch(typeof(scrPressToStart))]
|
||||||
return false;
|
// 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;
|
||||||
using UnityEngine.UI;
|
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))]
|
[HarmonyPostfix]
|
||||||
// ReSharper disable once InconsistentNaming
|
[HarmonyPatch("Update")]
|
||||||
public static class scrShowIfDebugPatch
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
private static void IgnoreInput(Text ___txt, Color ___defaultColor)
|
||||||
{
|
{
|
||||||
[HarmonyPostfix]
|
if (!RenderManager.IsActive) return;
|
||||||
[HarmonyPatch("Update")]
|
|
||||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
___txt.text = "Render";
|
||||||
private static void IgnoreInput(Text ___txt, Color ___defaultColor)
|
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.wonFrame != -1)
|
||||||
if (RenderManager.Instance)
|
|
||||||
{
|
{
|
||||||
if (RenderManager.Instance.countdown > 0) ___txt.text = $"Render in {RenderManager.Instance.countdown}s..";
|
var elapsedFrames = RenderManager.Instance.frameCount - RenderManager.Instance.wonFrame;
|
||||||
|
var remainingTime = RenderManager.Instance.Configuration.DelayAfterEnd - elapsedFrames / RenderManager.Instance.Configuration.RenderFPS;
|
||||||
if (RenderManager.Instance.wonFrame != -1)
|
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 ADOFAIRenderer.Core;
|
||||||
using HarmonyLib;
|
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
|
// ReSharper disable once InconsistentNaming
|
||||||
public class scrUIControllerPatch
|
public static void PreventEndscreenLanternsPostfix(scrUIController __instance)
|
||||||
{
|
{
|
||||||
[HarmonyPostfix]
|
if (RenderManager.IsActive && __instance != null)
|
||||||
[HarmonyPatch("ShowEndscreenLanterns")]
|
|
||||||
// ReSharper disable once InconsistentNaming
|
|
||||||
public static void PreventEndscreenLanternsPostfix(scrUIController __instance)
|
|
||||||
{
|
{
|
||||||
if (RenderManager.IsActive && __instance != null)
|
__instance.HideEndscreenLanterns();
|
||||||
{
|
|
||||||
__instance.HideEndscreenLanterns();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -7,7 +7,7 @@ using MelonLoader.Utils;
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.EventSystems;
|
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")]
|
[assembly: MelonGame("7th Beat Games", "A Dance of Fire and Ice")]
|
||||||
|
|
||||||
namespace ADOFAIRenderer
|
namespace ADOFAIRenderer
|
||||||
|
|||||||
+311
-310
@@ -7,317 +7,318 @@ using UnityEngine.UIElements;
|
|||||||
using Button = UnityEngine.UI.Button;
|
using Button = UnityEngine.UI.Button;
|
||||||
using Image = UnityEngine.UI.Image;
|
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;
|
Text = text;
|
||||||
public Action OnClick;
|
OnClick = onClick;
|
||||||
public Color? BackgroundColor;
|
BackgroundColor = 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+211
-212
@@ -6,229 +6,228 @@ using TMPro;
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.UI;
|
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 ---
|
TMP_FontAsset? font = null;
|
||||||
private const float BarWidth = 800f;
|
if (RenderMod.FontBundle != null)
|
||||||
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;
|
try
|
||||||
if (RenderMod.FontBundle != null)
|
|
||||||
{
|
{
|
||||||
try
|
font = RenderMod.FontBundle.LoadAsset<TMP_FontAsset>("NanumSquareNeoOTF-Bd SDF");
|
||||||
{
|
|
||||||
font = RenderMod.FontBundle.LoadAsset<TMP_FontAsset>("NanumSquareNeoOTF-Bd SDF");
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// 로딩 실패
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
catch
|
||||||
// 폴백 처리: 에셋 번들에서 실패 시 게임 자체 NanumSquareNeo 또는 내장 TMP SDF 폰트 사용
|
|
||||||
if (font == null)
|
|
||||||
{
|
{
|
||||||
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) 설정
|
font = RDString.fontData.fontTMP;
|
||||||
gameObject.layer = 5;
|
}
|
||||||
|
if (font == null)
|
||||||
// Canvas 설정
|
{
|
||||||
var canvas = gameObject.AddComponent<Canvas>();
|
// ReSharper disable once Unity.UnknownResource
|
||||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
font = Resources.Load<TMP_FontAsset>("Fonts & Materials/LiberationSans SDF");
|
||||||
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 (font != null) return font;
|
||||||
{
|
var fonts = Resources.FindObjectsOfTypeAll<TMP_FontAsset>();
|
||||||
if (!_text) return;
|
if (fonts.Length > 0) font = fonts[0];
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
return font!;
|
||||||
sb.AppendLine("<b><color=#00ccff>ADOFAI Renderer Realtime Monitor</color></b>");
|
|
||||||
sb.AppendLine();
|
|
||||||
|
|
||||||
var queueCount = 0;
|
|
||||||
if (RenderManager.Instance)
|
|
||||||
{
|
|
||||||
lock (RenderManager.Instance.ImageQueue)
|
|
||||||
{
|
|
||||||
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})";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
lock (RenderManager.Instance.ImageQueue)
|
||||||
|
{
|
||||||
|
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})";
|
||||||
|
}
|
||||||
|
}
|
||||||
+102
-103
@@ -4,128 +4,127 @@ using TMPro;
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.UI;
|
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!;
|
// UI 레이어(5) 설정
|
||||||
private TextMeshProUGUI _textComponent = null!;
|
gameObject.layer = 5;
|
||||||
private Stopwatch? _escapeStopwatch;
|
|
||||||
|
|
||||||
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) 설정
|
Destroy(gameObject);
|
||||||
gameObject.layer = 5;
|
return;
|
||||||
|
|
||||||
// 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 (Input.GetKey(KeyCode.Escape))
|
||||||
{
|
{
|
||||||
// 렌더링이 더 이상 활성화되어 있지 않으면 자가 파괴
|
if (_escapeStopwatch == null)
|
||||||
if (!RenderManager.IsActive)
|
|
||||||
{
|
{
|
||||||
Destroy(gameObject);
|
_escapeStopwatch = new Stopwatch();
|
||||||
return;
|
_escapeStopwatch.Start();
|
||||||
|
_containerObj.SetActive(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Input.GetKey(KeyCode.Escape))
|
var elapsed = (float)_escapeStopwatch.Elapsed.TotalSeconds;
|
||||||
{
|
var remaining = Mathf.Max(0f, 3f - elapsed);
|
||||||
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>";
|
||||||
|
|
||||||
// 남은 시간 텍스트 업데이트
|
// 마이크로 펄스 애니메이션 적용 (누르는 동안 컨테이너 전체 크기가 부드럽게 커짐)
|
||||||
_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;
|
||||||
var scale = 1.0f + elapsed / 3.0f * 0.15f;
|
|
||||||
_containerObj.transform.localScale = new Vector3(scale, scale, 1f);
|
|
||||||
|
|
||||||
if (elapsed < 3f) return;
|
|
||||||
|
|
||||||
_escapeStopwatch.Stop();
|
_escapeStopwatch.Stop();
|
||||||
_escapeStopwatch = null;
|
_escapeStopwatch = null;
|
||||||
_containerObj.transform.localScale = Vector3.one; // 스케일 초기화
|
_containerObj.transform.localScale = Vector3.one; // 스케일 초기화
|
||||||
_containerObj.SetActive(false);
|
_containerObj.SetActive(false);
|
||||||
|
|
||||||
// 렌더링 취소 및 씬 이탈
|
// 렌더링 취소 및 씬 이탈
|
||||||
if (RenderManager.Instance)
|
if (RenderManager.Instance)
|
||||||
RenderManager.Instance.CancelRender();
|
RenderManager.Instance.CancelRender();
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (_escapeStopwatch == null) return;
|
|
||||||
_escapeStopwatch.Stop();
|
|
||||||
_escapeStopwatch = null;
|
|
||||||
_containerObj.transform.localScale = Vector3.one; // 스케일 초기화
|
|
||||||
_containerObj.SetActive(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
private void OnDestroy()
|
|
||||||
{
|
{
|
||||||
if (_escapeStopwatch == null) return;
|
if (_escapeStopwatch == null) return;
|
||||||
_escapeStopwatch.Stop();
|
_escapeStopwatch.Stop();
|
||||||
_escapeStopwatch = null;
|
_escapeStopwatch = null;
|
||||||
|
_containerObj.transform.localScale = Vector3.one; // 스케일 초기화
|
||||||
|
_containerObj.SetActive(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private void OnDestroy()
|
||||||
|
{
|
||||||
|
if (_escapeStopwatch == null) return;
|
||||||
|
_escapeStopwatch.Stop();
|
||||||
|
_escapeStopwatch = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+648
-649
File diff suppressed because it is too large.
Load diff
+1028
-1029
File diff suppressed because it is too large.
Load diff
@@ -1,101 +1,100 @@
|
|||||||
using System;
|
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 ──
|
return Array.Empty<float>();
|
||||||
if (inputSamples.Length == 0 ||
|
}
|
||||||
inputChannels <= 0 || targetChannels <= 0 ||
|
|
||||||
inputFrequency <= 0 || targetFrequency <= 0)
|
// 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);
|
||||||
}
|
}
|
||||||
|
else if (targetChannels == 2) // 2. Stereo Output
|
||||||
// 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;
|
if (inputChannels == 1) // Mono -> Stereo (Duplicate to Left/Right)
|
||||||
long idxLow = (long)Math.Floor(srcIndex);
|
{
|
||||||
long idxHigh = idxLow + 1;
|
float valLow = inputSamples[idxLow];
|
||||||
float t = (float)(srcIndex - 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;
|
float valLowR = inputSamples[idxLow * inputChannels + 1];
|
||||||
if (idxLow >= inputLength) idxLow = inputLength - 1;
|
float valHighR = inputSamples[idxHigh * inputChannels + 1];
|
||||||
|
outputSamples[i * 2 + 1] = valLowR + t * (valHighR - valLowR);
|
||||||
if (targetChannels == 1) // 1. Mono Output
|
}
|
||||||
|
}
|
||||||
|
else // 3. Multi-channel Output (e.g., 5.1 Surround)
|
||||||
|
{
|
||||||
|
for (int c = 0; c < targetChannels; c++)
|
||||||
{
|
{
|
||||||
float valLow, valHigh;
|
float valLow, valHigh;
|
||||||
if (inputChannels == 1) // Mono -> Mono
|
if (c < inputChannels)
|
||||||
{
|
{
|
||||||
valLow = inputSamples[idxLow];
|
valLow = inputSamples[idxLow * inputChannels + c];
|
||||||
valHigh = inputSamples[idxHigh];
|
valHigh = inputSamples[idxHigh * inputChannels + c];
|
||||||
}
|
}
|
||||||
else // Multi -> Mono (Downmix: Average all channels)
|
else
|
||||||
{
|
{
|
||||||
float sumLow = 0f;
|
valLow = inputSamples[idxLow * inputChannels];
|
||||||
float sumHigh = 0f;
|
valHigh = inputSamples[idxHigh * inputChannels];
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
outputSamples[i * targetChannels + c] = valLow + t * (valHigh - valLow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return outputSamples;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return outputSamples;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,238 +3,239 @@ using System.Collections.Generic;
|
|||||||
using ADOFAIRenderer.Core;
|
using ADOFAIRenderer.Core;
|
||||||
using UnityEngine;
|
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; }
|
if (!clip)
|
||||||
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)
|
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)
|
||||||
{
|
{
|
||||||
Clear();
|
AudioClipData = rawSamples;
|
||||||
return;
|
TotalSamples = samplesCount;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
try
|
|
||||||
{
|
{
|
||||||
Channels = clip.channels;
|
AudioClipData = new float[samplesCount];
|
||||||
Frequency = clip.frequency;
|
TotalSamples = samplesCount;
|
||||||
int samplesCount = clip.samples;
|
for (int i = 0; i < samplesCount; i++)
|
||||||
|
|
||||||
float[] rawSamples = new float[samplesCount * Channels];
|
|
||||||
clip.GetData(rawSamples, 0);
|
|
||||||
|
|
||||||
if (Channels == 1)
|
|
||||||
{
|
{
|
||||||
AudioClipData = rawSamples;
|
float sum = 0f;
|
||||||
TotalSamples = samplesCount;
|
for (int c = 0; c < Channels; c++)
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
AudioClipData = new float[samplesCount];
|
|
||||||
TotalSamples = samplesCount;
|
|
||||||
for (int i = 0; i < samplesCount; i++)
|
|
||||||
{
|
{
|
||||||
float sum = 0f;
|
sum += rawSamples[i * Channels + c];
|
||||||
for (int c = 0; c < Channels; c++)
|
|
||||||
{
|
|
||||||
sum += rawSamples[i * Channels + c];
|
|
||||||
}
|
|
||||||
AudioClipData[i] = sum / Channels;
|
|
||||||
}
|
}
|
||||||
|
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();
|
|
||||||
}
|
}
|
||||||
|
MelonLoader.MelonLogger.Msg($"[AudioSpectrumGenerator] Cached audio clip: {clip.name}, Samples: {TotalSamples}, Frequency: {Frequency}Hz, Channels: {Channels}");
|
||||||
}
|
}
|
||||||
|
catch (Exception e)
|
||||||
public static void Clear()
|
|
||||||
{
|
{
|
||||||
AudioClipData = null;
|
MelonLoader.MelonLogger.Error($"[AudioSpectrumGenerator] Failed to initialize audio cache: {e}");
|
||||||
TotalSamples = 0;
|
Clear();
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,41 +1,40 @@
|
|||||||
using System;
|
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;
|
||||||
private readonly long _sampleRate;
|
|
||||||
private readonly (long Numerator, long Denominator) _fpsRational;
|
|
||||||
|
|
||||||
// ReSharper disable once NotAccessedField.Local
|
// ReSharper disable once NotAccessedField.Local
|
||||||
private long _currentSample;
|
private long _currentSample;
|
||||||
private long _remainder;
|
private long _remainder;
|
||||||
|
|
||||||
public AudioSyncCalculator(long sampleRate, double fps)
|
public AudioSyncCalculator(long sampleRate, double fps)
|
||||||
{
|
{
|
||||||
_sampleRate = sampleRate;
|
_sampleRate = sampleRate;
|
||||||
_fpsRational = RationalUtil.Double2MaxPrecisionInt(fps);
|
_fpsRational = RationalUtil.Double2MaxPrecisionInt(fps);
|
||||||
|
|
||||||
// NTSC
|
// NTSC
|
||||||
if (Math.Abs(fps - 29.97) < 0.001) _fpsRational = (30000, 1001);
|
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 - 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);
|
if (Math.Abs(fps - 23.976) < 0.001 || Math.Abs(fps - 23.98) < 0.001) _fpsRational = (24000, 1001);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
_currentSample = 0;
|
_currentSample = 0;
|
||||||
_remainder = 0;
|
_remainder = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int GetNextFrameSamples()
|
public int GetNextFrameSamples()
|
||||||
{
|
{
|
||||||
var numerator = _sampleRate * _fpsRational.Denominator + _remainder;
|
var numerator = _sampleRate * _fpsRational.Denominator + _remainder;
|
||||||
var samplesNeeded = numerator / _fpsRational.Numerator;
|
var samplesNeeded = numerator / _fpsRational.Numerator;
|
||||||
|
|
||||||
_remainder = numerator % _fpsRational.Numerator;
|
_remainder = numerator % _fpsRational.Numerator;
|
||||||
_currentSample += samplesNeeded;
|
_currentSample += samplesNeeded;
|
||||||
return (int)samplesNeeded;
|
return (int)samplesNeeded;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+17
-18
@@ -1,24 +1,23 @@
|
|||||||
using System;
|
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;
|
for (var i = 0; i < buffer.Length; i++)
|
||||||
|
buffer[i] = ProcessSample(buffer[i], 0);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Reset() { }
|
||||||
|
|
||||||
|
public double Latency => 0.0;
|
||||||
}
|
}
|
||||||
@@ -1,33 +1,32 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Runtime.CompilerServices;
|
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
|
if (sample > Threshold) return Threshold;
|
||||||
// ReSharper disable once FieldCanBeMadeReadOnly.Global
|
if (sample < -Threshold) return -Threshold;
|
||||||
// ReSharper disable once ConvertToConstant.Global
|
return sample;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
using System;
|
||||||
|
|
||||||
namespace ADOFAIRenderer.Util.Audio.Filter
|
namespace ADOFAIRenderer.Util.Audio.Filter;
|
||||||
{
|
|
||||||
internal interface IAudioFilter
|
|
||||||
{
|
|
||||||
void Initialize(int sampleRate, int channels);
|
|
||||||
|
|
||||||
float ProcessSample(float sample, int channel);
|
|
||||||
void ProcessBlock(Span<float> buffer);
|
|
||||||
|
|
||||||
void Reset();
|
|
||||||
|
|
||||||
double Latency { get; }
|
internal interface IAudioFilter
|
||||||
}
|
{
|
||||||
|
void Initialize(int sampleRate, int channels);
|
||||||
|
|
||||||
|
float ProcessSample(float sample, int channel);
|
||||||
|
void ProcessBlock(Span<float> buffer);
|
||||||
|
|
||||||
|
void Reset();
|
||||||
|
|
||||||
|
double Latency { get; }
|
||||||
}
|
}
|
||||||
+219
-220
@@ -1,229 +1,228 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Runtime.CompilerServices;
|
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
|
private float _thresholdDb = -20f;
|
||||||
internal class RMSCompressor : IAudioFilter
|
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;
|
get => _lookAheadTime;
|
||||||
private float _ratio = 4f;
|
set
|
||||||
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;
|
_lookAheadTime = Math.Max(0f, Math.Min(1.0f, value));
|
||||||
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();
|
|
||||||
UpdateLookAheadSettings();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+334
-335
@@ -5,413 +5,412 @@ using ADOFAIRenderer.Util.Audio.Filter;
|
|||||||
using MelonLoader;
|
using MelonLoader;
|
||||||
using UnityEngine;
|
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
|
if (_cache.TryGetValue(clipName, out var cached))
|
||||||
public double Duration;
|
return cached;
|
||||||
}
|
|
||||||
|
|
||||||
public CachedClipData? GetOrCache(string clipName, int targetChannels, int targetSampleRate, bool allowLoad = true)
|
if (!allowLoad) return null;
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(clipName)) 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))
|
var totalSamples = clip.samples;
|
||||||
return cached;
|
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 data = new CachedClipData
|
||||||
|
|
||||||
var clip = AudioManager.Instance.FindOrLoadAudioClip(clipName);
|
|
||||||
if (!clip) return null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var totalSamples = clip.samples;
|
Samples = resampled,
|
||||||
var channels = clip.channels;
|
Duration = (double)resampled.Length / (targetChannels * targetSampleRate)
|
||||||
var rawSamples = new float[totalSamples * channels];
|
};
|
||||||
clip.GetData(rawSamples, 0);
|
|
||||||
|
|
||||||
// Auto-resample during cache generation
|
_cache[clipName] = data;
|
||||||
var resampled = AudioResampler.Resample(rawSamples, channels, clip.frequency, targetChannels, targetSampleRate);
|
return data;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
catch (Exception e)
|
||||||
|
|
||||||
public void Clear()
|
|
||||||
{
|
|
||||||
lock (_cache)
|
|
||||||
{
|
{
|
||||||
_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!;
|
_cache.Clear();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private readonly List<SoundEvent> _activeEvents = new();
|
internal class OfflineAudioMixer
|
||||||
private ConditionalWeakTable<AudioSource, SoundEvent> _sourceMappings = new();
|
{
|
||||||
private readonly object _lock = new();
|
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 List<SoundEvent> _activeEvents = new();
|
||||||
private readonly int _targetChannels;
|
private ConditionalWeakTable<AudioSource, SoundEvent> _sourceMappings = new();
|
||||||
private readonly int _targetSampleRate;
|
private readonly object _lock = new();
|
||||||
private const float ClipVolume = .5f;
|
|
||||||
|
// Target Audio Settings
|
||||||
|
private readonly int _targetChannels;
|
||||||
|
private readonly int _targetSampleRate;
|
||||||
|
private const float ClipVolume = .5f;
|
||||||
|
|
||||||
// Audio Filter Settings
|
// Audio Filter Settings
|
||||||
private readonly LinkedList<IAudioFilter> _filters = new();
|
private readonly LinkedList<IAudioFilter> _filters = new();
|
||||||
|
|
||||||
// BGM Cached Samples (Pre-resampled)
|
// BGM Cached Samples (Pre-resampled)
|
||||||
private float[]? _bgmSamples;
|
private float[]? _bgmSamples;
|
||||||
private double _bgmDuration;
|
private double _bgmDuration;
|
||||||
|
|
||||||
// Clip Cache
|
// Clip Cache
|
||||||
private readonly AudioClipCache _clipCache = new();
|
private readonly AudioClipCache _clipCache = new();
|
||||||
private bool _isPrimed;
|
private bool _isPrimed;
|
||||||
|
|
||||||
public OfflineAudioMixer(int targetChannels, int targetSampleRate)
|
public OfflineAudioMixer(int targetChannels, int targetSampleRate)
|
||||||
|
{
|
||||||
|
_targetChannels = targetChannels;
|
||||||
|
_targetSampleRate = targetSampleRate;
|
||||||
|
|
||||||
|
var rmsCompressor = new RMSCompressor
|
||||||
{
|
{
|
||||||
_targetChannels = targetChannels;
|
ThresholdDb = -10, Ratio = 2,
|
||||||
_targetSampleRate = targetSampleRate;
|
AttackTime = 0.1f, ReleaseTime = 0.1f,
|
||||||
|
KneeDb = 10, RmsWindowTime = 0.02f
|
||||||
var rmsCompressor = new RMSCompressor
|
};
|
||||||
{
|
rmsCompressor.Initialize(_targetSampleRate, _targetChannels);
|
||||||
ThresholdDb = -10, Ratio = 2,
|
_filters.AddLast(rmsCompressor);
|
||||||
AttackTime = 0.1f, ReleaseTime = 0.1f,
|
|
||||||
KneeDb = 10, RmsWindowTime = 0.02f
|
|
||||||
};
|
|
||||||
rmsCompressor.Initialize(_targetSampleRate, _targetChannels);
|
|
||||||
_filters.AddLast(rmsCompressor);
|
|
||||||
|
|
||||||
var limiter = new RMSCompressor
|
var limiter = new RMSCompressor
|
||||||
{
|
{
|
||||||
ThresholdDb = 0, Ratio = float.PositiveInfinity,
|
ThresholdDb = 0, Ratio = float.PositiveInfinity,
|
||||||
AttackTime = 0f, ReleaseTime = 0.05f,
|
AttackTime = 0f, ReleaseTime = 0.05f,
|
||||||
KneeDb = 0, RmsWindowTime = 0.02f,
|
KneeDb = 0, RmsWindowTime = 0.02f,
|
||||||
LookAheadTime = 0.01f
|
LookAheadTime = 0.01f
|
||||||
};
|
};
|
||||||
limiter.Initialize(_targetSampleRate, _targetChannels);
|
limiter.Initialize(_targetSampleRate, _targetChannels);
|
||||||
_filters.AddLast(limiter);
|
_filters.AddLast(limiter);
|
||||||
|
|
||||||
var hardClip = new HardClip();
|
var hardClip = new HardClip();
|
||||||
hardClip.Initialize(_targetSampleRate, _targetChannels);
|
hardClip.Initialize(_targetSampleRate, _targetChannels);
|
||||||
_filters.AddLast(hardClip);
|
_filters.AddLast(hardClip);
|
||||||
|
|
||||||
var truePeakSafetyGain = new Gain { Level = -1f };
|
var truePeakSafetyGain = new Gain { Level = -1f };
|
||||||
truePeakSafetyGain.Initialize(_targetSampleRate, _targetChannels);
|
truePeakSafetyGain.Initialize(_targetSampleRate, _targetChannels);
|
||||||
_filters.AddLast(truePeakSafetyGain);
|
_filters.AddLast(truePeakSafetyGain);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void InitializeBGM(AudioClip? clip)
|
public void InitializeBGM(AudioClip? clip)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
{
|
{
|
||||||
lock (_lock)
|
if (!clip)
|
||||||
{
|
{
|
||||||
if (!clip)
|
|
||||||
{
|
|
||||||
_bgmSamples = null;
|
|
||||||
_bgmDuration = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
int originalSamples = clip.samples;
|
|
||||||
int originalChannels = clip.channels;
|
|
||||||
float[] rawSamples = new float[originalSamples * originalChannels];
|
|
||||||
clip.GetData(rawSamples, 0);
|
|
||||||
|
|
||||||
// Pre-resample the BGM to the target format
|
|
||||||
_bgmSamples = AudioResampler.Resample(rawSamples, originalChannels, clip.frequency, _targetChannels, _targetSampleRate);
|
|
||||||
_bgmDuration = (double)_bgmSamples.Length / (_targetChannels * _targetSampleRate);
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
_clipCache.GetOrCache(clipName, _targetChannels, _targetSampleRate, allowLoad: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RegisterSoundEvent(AudioSource? source, string clipName, double startTime, double endTime, float volume, float pitch)
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
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;
|
_bgmSamples = null;
|
||||||
_bgmDuration = 0;
|
_bgmDuration = 0;
|
||||||
_clipCache.Clear();
|
return;
|
||||||
_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
|
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
|
// Pre-resample the BGM to the target format
|
||||||
MixBGM(tempBuffer, latencySamples, -totalLatency, dspTimeBase, totalLatency);
|
_bgmSamples = AudioResampler.Resample(rawSamples, originalChannels, clip.frequency, _targetChannels, _targetSampleRate);
|
||||||
MixEffectSounds(tempBuffer, latencySamples, -totalLatency, totalLatency);
|
_bgmDuration = (double)_bgmSamples.Length / (_targetChannels * _targetSampleRate);
|
||||||
|
|
||||||
// Feed the buffer into filters to prime circular delay buffers
|
Melon<RenderMod>.Logger.Msg($"[OfflineAudioMixer] Initialized resampled BGM: {clip.name}, Samples: {_bgmSamples.Length / _targetChannels}, TargetFreq: {_targetSampleRate}Hz, TargetChannels: {_targetChannels}");
|
||||||
foreach (var filter in _filters)
|
|
||||||
{
|
|
||||||
filter.ProcessBlock(tempBuffer.AsSpan(0, latencySamples * _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
|
var ev = new SoundEvent
|
||||||
Array.Clear(mixedBuffer, 0, samplesNeeded * _targetChannels);
|
|
||||||
|
|
||||||
lock (_lock)
|
|
||||||
{
|
{
|
||||||
double totalLatency = 0;
|
ClipName = clipName,
|
||||||
foreach (var filter in _filters)
|
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;
|
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)
|
||||||
// Prime filters on first frame execution if latency exists
|
* ClipVolume;
|
||||||
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)
|
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 ev = _activeEvents[i];
|
||||||
|
var clipData = _clipCache.GetOrCache(ev.ClipName, _targetChannels, _targetSampleRate, allowLoad: false);
|
||||||
var conductor = scrConductor.instance;
|
if (clipData == null)
|
||||||
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;
|
_activeEvents.RemoveAt(i);
|
||||||
double clipTime = (sampleTime - adjustedRelativeTime1) * pitch;
|
continue;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void MixEffectSounds(float[] mixedBuffer, int samplesNeeded, double frameStartTime, double totalLatency)
|
var isSoundFinished = false;
|
||||||
{
|
var totalSamples = clipData.Samples.Length / _targetChannels;
|
||||||
for (var i = _activeEvents.Count - 1; i >= 0; i--)
|
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 sampleTime = frameStartTime + (double)s / _targetSampleRate;
|
||||||
var clipData = _clipCache.GetOrCache(ev.ClipName, _targetChannels, _targetSampleRate, allowLoad: false);
|
|
||||||
if (clipData == null)
|
// If it is a looping sound and has reached or exceeded its end time, stop playing.
|
||||||
|
if (adjustedEndTime >= 0 && sampleTime >= adjustedEndTime)
|
||||||
{
|
{
|
||||||
_activeEvents.RemoveAt(i);
|
isSoundFinished = true;
|
||||||
continue;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
var isSoundFinished = false;
|
var elapsed = sampleTime - adjustedStartTime;
|
||||||
var totalSamples = clipData.Samples.Length / _targetChannels;
|
if (elapsed < 0) continue; // Sound hasn't started yet
|
||||||
double adjustedStartTime = ev.StartTime - totalLatency;
|
|
||||||
double adjustedEndTime = ev.EndTime >= 0 ? ev.EndTime - totalLatency : -1.0;
|
|
||||||
|
|
||||||
for (var s = 0; s < samplesNeeded; s++)
|
var soundTime = elapsed * ev.Pitch;
|
||||||
|
|
||||||
|
if (soundTime >= clipData.Duration)
|
||||||
{
|
{
|
||||||
var sampleTime = frameStartTime + (double)s / _targetSampleRate;
|
if (ev.EndTime < 0) // One-shot
|
||||||
|
|
||||||
// If it is a looping sound and has reached or exceeded its end time, stop playing.
|
|
||||||
if (adjustedEndTime >= 0 && sampleTime >= adjustedEndTime)
|
|
||||||
{
|
{
|
||||||
isSoundFinished = true;
|
isSoundFinished = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
var elapsed = sampleTime - adjustedStartTime;
|
// Looping
|
||||||
if (elapsed < 0) continue; // Sound hasn't started yet
|
soundTime %= clipData.Duration;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up finished sound events (both one-shot and looping)
|
// Linear interpolation for pitch shifting in the mixer loop
|
||||||
if (isSoundFinished)
|
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