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