Add RenderSession.OnComplete

This commit is contained in:
copecone committed 2026-08-19 15:08:55 +09:00
1 parent 6840395041
commit 4e67cb9b09
4 files changed
+115 -27

No files matched your search

+1 -1
View File
@@ -8,7 +8,7 @@
<EnableSourceControlManagerQueries>false</EnableSourceControlManagerQueries> <EnableSourceControlManagerQueries>false</EnableSourceControlManagerQueries>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<LangVersion>10</LangVersion> <LangVersion>11</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
+20
View File
@@ -1,5 +1,6 @@
using System; using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO;
namespace ADOFAIRenderer.API; namespace ADOFAIRenderer.API;
@@ -62,6 +63,25 @@ public class RenderResult
ErrorMessage = errorMessage; ErrorMessage = errorMessage;
} }
public static RenderResult CreateSuccess(string outputPath, long totalFrames, double fps, TimeSpan renderDuration)
{
var fileSizeBytes = 0L;
if (!string.IsNullOrEmpty(outputPath) && File.Exists(outputPath))
{
try { fileSizeBytes = new FileInfo(outputPath).Length; }
catch { /* ignored */ }
}
var videoDuration = fps > 0 ? totalFrames / fps : 0.0;
return new RenderResult(true, outputPath, totalFrames, videoDuration, renderDuration, fileSizeBytes);
}
public static RenderResult CreateFailure(string? errorMessage, long totalFrames, double fps, TimeSpan renderDuration)
{
var videoDuration = fps > 0 ? totalFrames / fps : 0.0;
return new RenderResult(false, string.Empty, totalFrames, videoDuration, renderDuration, 0, errorMessage ?? "Rendering failed or was cancelled.");
}
public override string ToString() public override string ToString()
{ {
return Success return Success
+21 -3
View File
@@ -33,10 +33,13 @@ public class RenderSession
private bool _timePatch = true; private bool _timePatch = true;
private bool _screenPatch = true; private bool _screenPatch = true;
private Func<bool> _endCondition = () => scrController.instance.state == States.Won;
private string? _outputPath; private string? _outputPath;
private bool _notification = true;
private Func<bool>? _endCondition;
private Action<RenderResult>? _onComplete;
/// <summary> /// <summary>
/// Sets whether auto mode is enabled. /// Sets whether auto mode is enabled.
/// </summary> /// </summary>
@@ -89,6 +92,18 @@ public class RenderSession
return this; return this;
} }
public RenderSession WithNotification(bool enabled)
{
_notification = enabled;
return this;
}
public RenderSession OnComplete(Action<RenderResult> callback)
{
_onComplete += callback;
return this;
}
/// <summary> /// <summary>
/// Applies the configured flags and initiates the render session. /// Applies the configured flags and initiates the render session.
/// </summary> /// </summary>
@@ -99,8 +114,11 @@ public class RenderSession
RenderContext.Auto = _auto; RenderContext.Auto = _auto;
RenderContext.TimePatch = _timePatch; RenderContext.TimePatch = _timePatch;
RenderContext.ScreenPatch = _screenPatch; RenderContext.ScreenPatch = _screenPatch;
RenderContext.EndCondition = _endCondition;
RenderContext.OutputPath = _outputPath; RenderContext.OutputPath = _outputPath;
RenderContext.Notification = _notification;
if (_endCondition is not null) RenderContext.EndCondition = _endCondition;
if (_onComplete is not null) RenderContext.OnComplete = _onComplete;
RenderManager.Instance.levelPath = levelPath; RenderManager.Instance.levelPath = levelPath;
RenderManager.Instance.Configuration = renderConfig; RenderManager.Instance.Configuration = renderConfig;
+73 -23
View File
@@ -4,7 +4,9 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Buffers; using System.Buffers;
using System.Diagnostics;
using System.Linq; using System.Linq;
using ADOFAIRenderer.API;
using ADOFAIRenderer.Data; using ADOFAIRenderer.Data;
using ADOFAIRenderer.UI; using ADOFAIRenderer.UI;
using ADOFAIRenderer.Util; using ADOFAIRenderer.Util;
@@ -19,27 +21,48 @@ using UnityEngine.Rendering;
using Random = UnityEngine.Random; using Random = UnityEngine.Random;
using ThreadPriority = System.Threading.ThreadPriority; using ThreadPriority = System.Threading.ThreadPriority;
namespace ADOFAIRenderer.Core namespace ADOFAIRenderer.Core;
internal static class RenderContext
{ {
internal static class RenderContext
{
public static bool Auto = true; public static bool Auto = true;
public static bool TimePatch = true; public static bool TimePatch = true;
public static bool ScreenPatch = true; public static bool ScreenPatch = true;
public static Func<bool> EndCondition = () => scrController.instance.state == States.Won;
public static string? OutputPath; public static string? OutputPath;
public static bool Notification = true;
private static readonly Func<bool> EndConditionDefault = () => scrController.instance.state == States.Won;
public static Func<bool> EndCondition = EndConditionDefault;
private static readonly Action<RenderResult> OnCompleteDefault = result =>
{
if (result.Success && Notification)
{
var fileString = !string.IsNullOrEmpty(RenderManager.Instance.CurrentOutPath) ?
$"'{Path.GetFileName(RenderManager.Instance.CurrentOutPath)}'이(가)" : "동영상이";
var renderFolder = Path.GetDirectoryName(RenderManager.Instance.CurrentOutPath);
OSNotification.Show("렌더링 완료!", $"{fileString} 성공적으로 저장되었습니다.", renderFolder);
}
};
public static Action<RenderResult> OnComplete = OnCompleteDefault;
public static void Reset() public static void Reset()
{ {
Auto = true; Auto = true;
TimePatch = true; ScreenPatch = true; TimePatch = true; ScreenPatch = true;
EndCondition = () => scrController.instance.state == States.Won;
OutputPath = null; OutputPath = null;
}
}
internal class RenderManager : MonoBehaviour Notification = true;
{
EndCondition = EndConditionDefault;
OnComplete = OnCompleteDefault;
}
}
internal class RenderManager : MonoBehaviour
{
// Singleton Accessors // Singleton Accessors
public static RenderManager Instance { get; private set; } = null!; public static RenderManager Instance { get; private set; } = null!;
public static bool IsActive => Instance && Instance.IsRendering; public static bool IsActive => Instance && Instance.IsRendering;
@@ -50,6 +73,7 @@ namespace ADOFAIRenderer.Core
// Statistics // Statistics
public long framesSent; public long framesSent;
private readonly Stopwatch _renderStopwatch = new();
// Core Render Configuration and Path // Core Render Configuration and Path
public int countdown; public int countdown;
@@ -105,7 +129,7 @@ namespace ADOFAIRenderer.Core
private int _originalInputOffset; private int _originalInputOffset;
private GameObject? _debugMonitorObj; private GameObject? _debugMonitorObj;
private GameObject? _escapeHandlerObj; private GameObject? _escapeHandlerObj;
private string? _currentOutPath; internal string? CurrentOutPath;
private bool _renderSetup; private bool _renderSetup;
private void Awake() private void Awake()
@@ -411,8 +435,9 @@ namespace ADOFAIRenderer.Core
yield return new WaitForSecondsRealtime(1); yield return new WaitForSecondsRealtime(1);
} }
frameCount = 0; frameCount = 0; framesSent = 0;
framesSent = 0; _renderStopwatch.Restart();
timeBase = Time.timeAsDouble; timeBase = Time.timeAsDouble;
unscaledTimeBase = Time.unscaledTimeAsDouble; unscaledTimeBase = Time.unscaledTimeAsDouble;
dspTimeBase = AudioSettings.dspTime; dspTimeBase = AudioSettings.dspTime;
@@ -490,11 +515,11 @@ namespace ADOFAIRenderer.Core
var videoFPS = Configuration.VideoFPS; var videoFPS = Configuration.VideoFPS;
var startTime = DateTime.Now; var startTime = DateTime.Now;
if (RenderContext.OutputPath != null) _currentOutPath = RenderContext.OutputPath; if (RenderContext.OutputPath != null) CurrentOutPath = RenderContext.OutputPath;
else { else {
var fileName = startTime.ToString("yyyy-MM-dd HH-mm-ss") + var fileName = startTime.ToString("yyyy-MM-dd HH-mm-ss") +
$".{Configuration.AdvancedConfig.FileExtension.TrimStart('.').ToLower()}"; $".{Configuration.AdvancedConfig.FileExtension.TrimStart('.').ToLower()}";
_currentOutPath = Path.Combine(MelonEnvironment.GameRootDirectory, "Render", fileName); CurrentOutPath = Path.Combine(MelonEnvironment.GameRootDirectory, "Render", fileName);
} }
FFmpegNativeEncoder? videoFFmpeg = null; FFmpegNativeEncoder? videoFFmpeg = null;
@@ -531,7 +556,7 @@ namespace ADOFAIRenderer.Core
{ {
_allVideoFFmpegInstances.Add(videoFFmpeg); _allVideoFFmpegInstances.Add(videoFFmpeg);
} }
videoFFmpeg.Start(_currentOutPath); videoFFmpeg.Start(CurrentOutPath);
} }
catch (Exception) catch (Exception)
{ {
@@ -748,29 +773,32 @@ namespace ADOFAIRenderer.Core
} }
// 6. 취소된 미완성 임시 동영상 파일 즉각 삭제 // 6. 취소된 미완성 임시 동영상 파일 즉각 삭제
if (!string.IsNullOrEmpty(_currentOutPath) && File.Exists(_currentOutPath)) if (!string.IsNullOrEmpty(CurrentOutPath) && File.Exists(CurrentOutPath))
{ {
try try
{ {
File.Delete(_currentOutPath); File.Delete(CurrentOutPath);
Melon<RenderMod>.Logger.Msg($"[File Cleanup] 미완성 동영상 삭제 완료: {_currentOutPath}"); Melon<RenderMod>.Logger.Msg($"[File Cleanup] 미완성 동영상 삭제 완료: {CurrentOutPath}");
} }
catch (Exception ex) catch (Exception ex)
{ {
Melon<RenderMod>.Logger.Error($"[File Cleanup] 임시 동영상 삭제 실패: {ex.Message}"); Melon<RenderMod>.Logger.Error($"[File Cleanup] 임시 동영상 삭제 실패: {ex.Message}");
} }
_currentOutPath = null; CurrentOutPath = null;
} }
var result = BuildResult(false,
IsRenderCancelled ? "Rendering was cancelled by user." : "Rendering failed.");
TriggerOnComplete(result);
} }
} }
private IEnumerator StopRenderCoroutine() private IEnumerator StopRenderCoroutine()
{ {
yield return new WaitUntil(() => endSuccessful); yield return new WaitUntil(() => endSuccessful);
var fileString = !string.IsNullOrEmpty(_currentOutPath) ? $"'{Path.GetFileName(_currentOutPath)}'이(가)" : "동영상이";
var renderFolder = Path.Combine(MelonEnvironment.GameRootDirectory, "Render"); var result = BuildResult(success: true);
OSNotification.Show("렌더링 완료!", $"{fileString} 성공적으로 저장되었습니다.", renderFolder); TriggerOnComplete(result);
EndRender(); EndRender();
} }
@@ -817,6 +845,29 @@ namespace ADOFAIRenderer.Core
Cursor.visible = true; Cursor.visible = true;
} }
private RenderResult BuildResult(bool success, string? errorMessage = null)
{
_renderStopwatch.Stop();
var effectiveFps = Math.Min(Configuration.VideoFPS, Configuration.RenderFPS);
var sent = Interlocked.Read(ref framesSent);
return success
? RenderResult.CreateSuccess(CurrentOutPath ?? string.Empty, sent, effectiveFps, _renderStopwatch.Elapsed)
: RenderResult.CreateFailure(errorMessage, sent, effectiveFps, _renderStopwatch.Elapsed);
}
private static void TriggerOnComplete(RenderResult result)
{
try
{
RenderContext.OnComplete.Invoke(result);
}
catch (Exception err)
{
Melon<RenderMod>.Logger.Error($"OnComplete callback caused error: {err}");
}
}
private static readonly Dictionary<Codec, AVCodecID> AudioCodecMatch = new() private static readonly Dictionary<Codec, AVCodecID> AudioCodecMatch = new()
{ {
[Codec.H264] = AVCodecID.AV_CODEC_ID_AAC, [Codec.H264] = AVCodecID.AV_CODEC_ID_AAC,
@@ -825,5 +876,4 @@ namespace ADOFAIRenderer.Core
[Codec.AV1] = AVCodecID.AV_CODEC_ID_OPUS, [Codec.AV1] = AVCodecID.AV_CODEC_ID_OPUS,
[Codec.VVC] = AVCodecID.AV_CODEC_ID_OPUS [Codec.VVC] = AVCodecID.AV_CODEC_ID_OPUS
}; };
}
} }