SHA256
Add RenderSession.OnComplete
This commit is contained in:
1 parent
6840395041
commit
4e67cb9b09
4 files changed
+110
-22
No files matched your search
@@ -8,7 +8,7 @@
|
||||
<EnableSourceControlManagerQueries>false</EnableSourceControlManagerQueries>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<LangVersion>10</LangVersion>
|
||||
<LangVersion>11</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
+68
-18
@@ -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,22 +21,43 @@ using UnityEngine.Rendering;
|
||||
using Random = UnityEngine.Random;
|
||||
using ThreadPriority = System.Threading.ThreadPriority;
|
||||
|
||||
namespace ADOFAIRenderer.Core
|
||||
{
|
||||
namespace ADOFAIRenderer.Core;
|
||||
|
||||
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;
|
||||
|
||||
Notification = true;
|
||||
|
||||
EndCondition = EndConditionDefault;
|
||||
OnComplete = OnCompleteDefault;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -826,4 +877,3 @@ namespace ADOFAIRenderer.Core
|
||||
[Codec.VVC] = AVCodecID.AV_CODEC_ID_OPUS
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user