SHA256
Add RenderSession.OnComplete
This commit is contained in:
1 parent
6840395041
commit
4e67cb9b09
4 files changed
+115
-27
No files matched your search
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user