Files
ADOFAIRenderer/API/RenderSession.cs
T
2026-10-15 18:29:22 +09:00

709 lines
32 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using ADOFAIRenderer.Core;
using ADOFAIRenderer.Data;
using ADOFAIRenderer.Util;
using UnityEngine;
namespace ADOFAIRenderer.API;
/// <summary>
/// Fluent builder for configuring and starting a render session, and the registry of global frame hooks.
/// </summary>
/// <remarks>
/// <para>
/// <b>Usage:</b> create an instance, configure it with the <c>With…</c>, <c>On…</c> and <c>AddFrameHook</c> methods,
/// then call <see cref="Start"/>. The builder's values are copied into the render when <see cref="Start"/> is called;
/// changing the builder afterwards does not affect the running render. A builder can be reused for later renders.
/// </para>
/// <para>
/// <b>Lifecycle of a render:</b> <see cref="Start"/> loads the game scene of the level. When the scene has loaded,
/// a 3-second countdown runs, then capture starts and the level plays with the game clock driven by the render
/// frame rate (offline, not real time). The render ends successfully <see cref="RenderConfig.DelayAfterEnd"/> seconds
/// (render time) after the end condition first holds, or is cancelled by the user (ESC), or fails. In every case
/// the completion callback receives a <see cref="RenderResult"/> and the game returns to the level editor.
/// </para>
/// <para>
/// <b>Threading:</b> every member must be called on the main Unity thread, except the global hook members, which are
/// thread-safe (they lock an internal object). All callbacks (end condition, completion, last update, frame hooks)
/// are invoked on the main Unity thread.
/// </para>
/// <para>
/// <b>Frame hooks</b> run on each captured video frame before it is encoded, in this order: session hooks in the order
/// they were added, then global hooks in registration order.
/// </para>
/// </remarks>
[SuppressMessage("ReSharper", "UnusedType.Global")]
[SuppressMessage("ReSharper", "UnusedMember.Global")]
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public class RenderSession
{
/// <summary>
/// Handle returned by AddGlobalFrameHook. Key is the object the caller registered (delegate, material or IFrameHook)
/// and is used by RemoveGlobalFrameHook; Action is the delegate the pipeline invokes.
/// Dispose removes this exact registration once; later calls do nothing.
/// </summary>
private sealed class GlobalHookRegistration : IDisposable
{
public object Key { get; }
public Action<FrameHookContext> Action { get; }
private bool _isDisposed;
public GlobalHookRegistration(object key, Action<FrameHookContext> action)
{
Key = key;
Action = action;
}
public void Dispose()
{
if (_isDisposed) return;
_isDisposed = true;
RemoveGlobalRegistration(this);
}
}
/// <summary>
/// Session hook added to this builder. Key is the object the caller passed (delegate, material or IFrameHook) and
/// is used by RemoveFrameHook; Action is the delegate copied to the render by Start.
/// </summary>
private sealed class SessionHookEntry
{
public object Key { get; }
public Action<FrameHookContext> Action { get; }
public SessionHookEntry(object key, Action<FrameHookContext> action)
{
Key = key;
Action = action;
}
}
/// <summary>Global hooks in registration order. Guarded by GlobalHooksLock.</summary>
private static readonly List<GlobalHookRegistration> GlobalFrameHooks = new();
/// <summary>Lock for GlobalFrameHooks and _globalHooksVersion (global hooks may be changed from any thread).</summary>
private static readonly object GlobalHooksLock = new();
/// <summary>
/// Incremented on every change of GlobalFrameHooks. RenderManager compares it each captured frame to rebuild its
/// cached hook list only after a change. Guarded by GlobalHooksLock.
/// </summary>
private static int _globalHooksVersion;
/// <summary>Session hooks of this builder in the order they were added.</summary>
private readonly List<SessionHookEntry> _frameHooks = new();
/// <summary>Autoplay setting copied to the render by Start (default true).</summary>
private bool _auto = true;
/// <summary>
/// Specifies the types of patches that can be configured for a render session.
/// </summary>
public enum PatchType
{
/// <summary>
/// Time-related patch controlling frame timing and clock synchronization.
/// While capturing, <c>Time.time</c>, <c>Time.timeAsDouble</c>, <c>Time.unscaledTime</c>,
/// <c>Time.unscaledTimeAsDouble</c>, <c>Time.deltaTime</c>, <c>Time.unscaledDeltaTime</c>, <c>Time.frameCount</c>
/// and <c>AudioSettings.dspTime</c> return values of the render clock (one frame = 1 / RenderFPS seconds),
/// so the game advances exactly one render frame per Unity frame regardless of how long a frame takes.
/// Disabling it leaves these values to Unity.
/// </summary>
Time,
/// <summary>
/// Screen-related patch controlling screen resolution.
/// While the render is active (from the countdown on), <c>Screen.width</c> and <c>Screen.height</c> return
/// <see cref="RenderConfig.RenderWidth"/> and <see cref="RenderConfig.RenderHeight"/>, so the game creates its
/// camera RenderTexture at the render resolution.
/// </summary>
Screen
}
/// <summary>Time patch setting copied to the render by Start (default true).</summary>
private bool _timePatch = true;
/// <summary>Screen patch setting copied to the render by Start (default true).</summary>
private bool _screenPatch = true;
/// <summary>Custom output file path, or null for the default output directory.</summary>
private string? _outputPath;
/// <summary>Notification setting copied to the render by Start (default true).</summary>
private bool _notification = true;
/// <summary>Custom end condition, or null to keep the default (level cleared).</summary>
private Func<bool>? _endCondition;
/// <summary>Combined completion callbacks, or null to keep the default (notification).</summary>
private Action<RenderResult>? _onComplete;
/// <summary>Combined last-update callbacks, or null for none.</summary>
private Action? _onLastUpdate;
/// <summary>
/// Sets whether auto mode (the game's autoplay, <c>RDC.auto</c>) is enabled during the render.
/// With auto mode off, the level is not played automatically; the render still enables the game's no-fail
/// settings (<c>scrController.noFail</c>, <c>scrController.noFailInfiniteMargin</c>).
/// </summary>
/// <param name="enabled"><c>true</c> to enable auto mode; otherwise, <c>false</c>. Default is <c>true</c>.</param>
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
public RenderSession WithAuto(bool enabled = true)
{
_auto = enabled;
return this;
}
/// <summary>
/// Sets whether a specific patch type is enabled for the render session.
/// </summary>
/// <param name="patchType">The <see cref="PatchType"/> to configure.</param>
/// <param name="enabled"><c>true</c> to enable the specified patch; otherwise, <c>false</c>. Default is <c>true</c>.</param>
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when an unsupported <see cref="PatchType"/> is provided.</exception>
public RenderSession WithPatch(PatchType patchType, bool enabled = true)
{
switch (patchType)
{
case PatchType.Time: _timePatch = enabled; break;
case PatchType.Screen: _screenPatch = enabled; break;
default: throw new ArgumentOutOfRangeException(nameof(patchType), patchType, null);
}
return this;
}
/// <summary>
/// Sets the custom condition function that determines when the render session should end.
/// </summary>
/// <remarks>
/// The condition is polled on the main Unity thread once per frame from the start of the countdown until the render ends.
/// When it first returns <c>true</c>, the render continues for <see cref="RenderConfig.DelayAfterEnd"/> seconds of
/// render time and then finishes successfully; it is not required to stay <c>true</c>. The default condition is
/// that the level is cleared (<c>scrController.instance.state == States.Won</c>). Calling this method again
/// replaces the previous condition. Exceptions thrown by the condition are not caught.
/// </remarks>
/// <param name="endCondition">A delegate returning <c>true</c> when the session end condition is met.</param>
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
public RenderSession WithEndCondition(Func<bool> endCondition)
{
_endCondition = endCondition;
return this;
}
/// <summary>
/// Sets the custom output path for the rendered file.
/// </summary>
/// <remarks>
/// The directory of the path is created by <see cref="Start"/> when it does not exist. An existing file at the path
/// is overwritten. The container format follows the file extension. With <c>null</c>, the file is named
/// <c>yyyy-MM-dd HH-mm-ss.&lt;ext&gt;</c> (local start time, extension from <see cref="AdvancedConfig.FileExtension"/>)
/// in the output directory chosen in the mod settings. When the render is cancelled or fails, the unfinished file
/// is deleted.
/// </remarks>
/// <param name="outputPath">The file path where the output will be saved, or <c>null</c> to use the default capture directory.</param>
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
public RenderSession WithOutputPath(string? outputPath)
{
_outputPath = outputPath;
return this;
}
/// <summary>
/// Sets whether desktop notifications should be displayed when rendering completes.
/// </summary>
/// <remarks>
/// The notification is shown by the default completion callback only. When a callback is added with
/// <see cref="OnComplete"/>, it replaces the default callback and no notification is shown. Default is <c>true</c>.
/// </remarks>
/// <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)
{
_notification = enabled;
return this;
}
/// <summary>
/// Adds a callback action to be invoked when the render session completes or fails.
/// </summary>
/// <remarks>
/// Called once per render on the main Unity thread: on success after the output file has been finished, on cancel or
/// failure after the unfinished file has been deleted, and before the game returns to the level editor.
/// It is not called when the render stops because the game quits. Multiple calls combine the callbacks (invoked in
/// the order added). The combined callback replaces the default one, which shows the desktop notification.
/// An exception thrown by a callback is logged and stops the callbacks added after it.
/// </remarks>
/// <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)
{
_onComplete += callback;
return this;
}
/// <summary>
/// Adds a callback action to be invoked on the last update of each frame in the render session.
/// </summary>
/// <remarks>
/// Called on the main Unity thread every Unity frame after all other <c>LateUpdate</c> calls, from the moment the game scene
/// of the render has loaded until the render ends (including the countdown, before capture starts).
/// Multiple calls combine the callbacks. An exception thrown by a callback is logged.
/// </remarks>
/// <param name="callback">The callback action to execute.</param>
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
public RenderSession OnLastUpdate(Action callback)
{
_onLastUpdate += callback;
return this;
}
/// <summary>
/// Adds a custom frame modification hook to be executed for each encoded video frame.
/// The hook can modify the rendered frame using GPU ping-pong double buffering.
/// </summary>
/// <remarks>
/// <para>
/// <b>Execution Frequency Note:</b> This hook is invoked only for frames that are captured for the output video.
/// When <c>RenderFPS</c> is higher than <c>VideoFPS</c>, intermediate game render frames are skipped,
/// so this hook will <b>not</b> be called on every game frame.
/// </para>
/// <para>
/// The hook is called on the main Unity thread at the end of the frame, after the game has rendered it and before
/// the GPU readback for encoding is requested. Write the result into <see cref="FrameHookContext.Destination"/> and
/// swap (or use <see cref="FrameHookContext.Blit(Material, int)"/>, which swaps); a hook that does not swap leaves
/// the frame unchanged. An exception thrown by the hook is logged and the next hook still runs.
/// Session hooks apply only to the render started by <see cref="Start"/> and are discarded when it ends.
/// The same delegate can be added more than once; it then runs once per addition.
/// </para>
/// </remarks>
/// <param name="hook">The callback action that receives a <see cref="FrameHookContext"/>.</param>
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="hook"/> is null.</exception>
public RenderSession AddFrameHook(Action<FrameHookContext> hook)
{
if (hook == null) throw new ArgumentNullException(nameof(hook));
_frameHooks.Add(new SessionHookEntry(hook, hook));
return this;
}
/// <summary>
/// Adds a material-based post-processing pass to be executed for each encoded video frame.
/// Automatically performs a GPU blit from source to destination and swaps buffers.
/// </summary>
/// <remarks>
/// Equivalent to a hook calling <see cref="FrameHookContext.Blit(Material, int)"/>: the current frame is bound as the
/// material's <c>_MainTex</c> by <c>Graphics.Blit</c>. The material is referenced, not copied, so property changes
/// made to it later apply to the following frames. Execution rules are the same as
/// <see cref="AddFrameHook(Action{FrameHookContext})"/>.
/// Reference: https://docs.unity3d.com/ScriptReference/Graphics.Blit.html
/// </remarks>
/// <param name="material">The material containing the shader to apply.</param>
/// <param name="pass">The shader pass index to use, or -1 to execute all passes.</param>
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="material"/> is null.</exception>
public RenderSession AddFrameHook(Material material, int pass = -1)
{
if (material is null) throw new ArgumentNullException(nameof(material));
_frameHooks.Add(new SessionHookEntry(material, context => context.Blit(material, pass)));
return this;
}
/// <summary>
/// Adds a custom frame modification hook implementing <see cref="IFrameHook"/>.
/// </summary>
/// <remarks>
/// <see cref="IFrameHook.OnRenderFrame"/> is invoked with the same rules as
/// <see cref="AddFrameHook(Action{FrameHookContext})"/>. Remove it with <see cref="RemoveFrameHook(IFrameHook)"/>.
/// </remarks>
/// <param name="hook">The hook instance to execute.</param>
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="hook"/> is null.</exception>
public RenderSession AddFrameHook(IFrameHook hook)
{
if (hook == null) throw new ArgumentNullException(nameof(hook));
_frameHooks.Add(new SessionHookEntry(hook, hook.OnRenderFrame));
return this;
}
/// <summary>
/// Alias for <see cref="AddFrameHook(Action{FrameHookContext})"/>.
/// </summary>
/// <param name="hook">The callback action that receives a <see cref="FrameHookContext"/>.</param>
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
public RenderSession WithFrameHook(Action<FrameHookContext> hook) => AddFrameHook(hook);
/// <summary>
/// Alias for <see cref="AddFrameHook(Material, int)"/>.
/// </summary>
/// <param name="material">The material containing the shader to apply.</param>
/// <param name="pass">The shader pass index to use, or -1 to execute all passes.</param>
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
public RenderSession WithFrameHook(Material material, int pass = -1) => AddFrameHook(material, pass);
/// <summary>
/// Alias for <see cref="AddFrameHook(IFrameHook)"/>.
/// </summary>
/// <param name="hook">The hook instance to execute.</param>
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
public RenderSession WithFrameHook(IFrameHook hook) => AddFrameHook(hook);
/// <summary>
/// Removes a previously added frame modification hook callback from this session.
/// </summary>
/// <remarks>
/// Removes the first entry (in the order added) whose registered object is <paramref name="hook"/> or whose delegate
/// equals it. When the same delegate was added several times, one call removes one addition. Does nothing when no
/// entry matches. Affects only renders started after this call.
/// </remarks>
/// <param name="hook">The hook action to remove.</param>
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
public RenderSession RemoveFrameHook(Action<FrameHookContext> hook)
{
var idx = _frameHooks.FindIndex(e => ReferenceEquals(e.Key, hook) || Equals(e.Action, hook));
if (idx >= 0) _frameHooks.RemoveAt(idx);
return this;
}
/// <summary>
/// Removes a previously added material-based post-processing pass from this session.
/// </summary>
/// <remarks>
/// Removes every pass added with <paramref name="material"/> (any pass index). Does nothing when the material is
/// null or destroyed. Affects only renders started after this call.
/// </remarks>
/// <param name="material">The material to remove.</param>
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
public RenderSession RemoveFrameHook(Material material)
{
if (material)
{
for (var i = _frameHooks.Count - 1; i >= 0; i--)
{
if (ReferenceEquals(_frameHooks[i].Key, material))
{
_frameHooks.RemoveAt(i);
}
}
}
return this;
}
/// <summary>
/// Removes a previously added <see cref="IFrameHook"/> from this session.
/// </summary>
/// <remarks>
/// Removes the first entry added with this instance (reference equality). Affects only renders started after this call.
/// </remarks>
/// <param name="hook">The hook instance to remove.</param>
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
public RenderSession RemoveFrameHook(IFrameHook hook)
{
var idx = _frameHooks.FindIndex(e => ReferenceEquals(e.Key, hook));
if (idx >= 0) _frameHooks.RemoveAt(idx);
return this;
}
/// <summary>
/// Clears all frame modification hooks configured for this render session.
/// </summary>
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
public RenderSession ClearFrameHooks()
{
_frameHooks.Clear();
return this;
}
/// <summary>
/// Registers a global frame modification hook that will execute on every render session.
/// </summary>
/// <remarks>
/// <para>
/// <b>Execution Frequency Note:</b> Global frame hooks are executed only for frames that are captured for the output video.
/// When <c>RenderFPS</c> is higher than <c>VideoFPS</c>, intermediate game render frames are skipped,
/// so this hook will <b>not</b> be called on every game frame.
/// </para>
/// <para>
/// The registration stays active for every following render (and a render already running, from its next
/// captured frame) until the returned handle is disposed or the hook is removed with
/// <see cref="RemoveGlobalFrameHook(Action{FrameHookContext})"/> or <see cref="ClearGlobalFrameHooks"/>.
/// Global hooks run after the session hooks, in registration order, on the main Unity thread; an exception thrown
/// by the hook is logged and the next hook still runs. This method is thread-safe. Registering the same delegate
/// twice creates two registrations.
/// </para>
/// </remarks>
/// <param name="hook">The callback action that receives a <see cref="FrameHookContext"/>.</param>
/// <returns>An <see cref="IDisposable"/> registration handle that unregisters the hook when disposed.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="hook"/> is null.</exception>
public static IDisposable AddGlobalFrameHook(Action<FrameHookContext> hook)
{
if (hook == null) throw new ArgumentNullException(nameof(hook));
var reg = new GlobalHookRegistration(hook, hook);
lock (GlobalHooksLock)
{
GlobalFrameHooks.Add(reg);
_globalHooksVersion++;
}
return reg;
}
/// <summary>
/// Registers a global material-based post-processing pass that will execute on every render session.
/// </summary>
/// <remarks>
/// Same lifetime and ordering as <see cref="AddGlobalFrameHook(Action{FrameHookContext})"/>; the pass is applied as
/// <see cref="FrameHookContext.Blit(Material, int)"/>. Thread-safe.
/// </remarks>
/// <param name="material">The material containing the shader to apply.</param>
/// <param name="pass">The shader pass index to use, or -1 to execute all passes.</param>
/// <returns>An <see cref="IDisposable"/> registration handle that unregisters the hook when disposed.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="material"/> is null.</exception>
public static IDisposable AddGlobalFrameHook(Material material, int pass = -1)
{
if (material is null) throw new ArgumentNullException(nameof(material));
var reg = new GlobalHookRegistration(material, context => context.Blit(material, pass));
lock (GlobalHooksLock)
{
GlobalFrameHooks.Add(reg);
_globalHooksVersion++;
}
return reg;
}
/// <summary>
/// Registers a global frame modification hook implementing <see cref="IFrameHook"/>.
/// </summary>
/// <remarks>
/// Same lifetime and ordering as <see cref="AddGlobalFrameHook(Action{FrameHookContext})"/>. Thread-safe.
/// </remarks>
/// <param name="hook">The hook instance to execute.</param>
/// <returns>An <see cref="IDisposable"/> registration handle that unregisters the hook when disposed.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="hook"/> is null.</exception>
public static IDisposable AddGlobalFrameHook(IFrameHook hook)
{
if (hook == null) throw new ArgumentNullException(nameof(hook));
var reg = new GlobalHookRegistration(hook, hook.OnRenderFrame);
lock (GlobalHooksLock)
{
GlobalFrameHooks.Add(reg);
_globalHooksVersion++;
}
return reg;
}
/// <summary>
/// Unregisters a previously registered global frame modification hook.
/// </summary>
/// <remarks>
/// Removes the first registration (in registration order) whose registered object is <paramref name="hook"/> or whose
/// delegate equals it. Thread-safe; a running render stops calling the hook from its next captured frame.
/// </remarks>
/// <param name="hook">The hook action to remove.</param>
/// <returns><c>true</c> if the hook was successfully removed; otherwise, <c>false</c>.</returns>
public static bool RemoveGlobalFrameHook(Action<FrameHookContext> hook)
{
lock (GlobalHooksLock)
{
var idx = GlobalFrameHooks.FindIndex(r => ReferenceEquals(r.Key, hook) || Equals(r.Action, hook));
if (idx >= 0)
{
GlobalFrameHooks.RemoveAt(idx);
_globalHooksVersion++;
return true;
}
return false;
}
}
/// <summary>
/// Unregisters a previously registered global material-based post-processing pass.
/// </summary>
/// <remarks>Removes every registration made with <paramref name="material"/> (any pass index). Thread-safe.</remarks>
/// <param name="material">The material to remove.</param>
/// <returns><c>true</c> if any hooks matching <paramref name="material"/> were removed; otherwise, <c>false</c>.</returns>
public static bool RemoveGlobalFrameHook(Material material)
{
lock (GlobalHooksLock)
{
var removed = false;
for (var i = GlobalFrameHooks.Count - 1; i >= 0; i--)
{
if (ReferenceEquals(GlobalFrameHooks[i].Key, material))
{
GlobalFrameHooks.RemoveAt(i);
removed = true;
}
}
if (removed)
{
_globalHooksVersion++;
}
return removed;
}
}
/// <summary>
/// Unregisters a previously registered global frame modification hook implementing <see cref="IFrameHook"/>.
/// </summary>
/// <remarks>Removes the first registration made with this instance (reference equality). Thread-safe.</remarks>
/// <param name="hook">The hook instance to remove.</param>
/// <returns><c>true</c> if the hook was successfully removed; otherwise, <c>false</c>.</returns>
public static bool RemoveGlobalFrameHook(IFrameHook hook)
{
lock (GlobalHooksLock)
{
var idx = GlobalFrameHooks.FindIndex(r => ReferenceEquals(r.Key, hook));
if (idx >= 0)
{
GlobalFrameHooks.RemoveAt(idx);
_globalHooksVersion++;
return true;
}
return false;
}
}
/// <summary>Removes one registration (called by its Dispose). Does nothing when it was already removed.</summary>
private static void RemoveGlobalRegistration(GlobalHookRegistration registration)
{
lock (GlobalHooksLock)
{
if (GlobalFrameHooks.Remove(registration))
{
_globalHooksVersion++;
}
}
}
/// <summary>
/// Clears all registered global frame modification hooks.
/// </summary>
/// <remarks>
/// Removes the registrations of every mod, not only the caller's. Handles returned earlier stay valid; disposing them
/// does nothing. Thread-safe.
/// </remarks>
public static void ClearGlobalFrameHooks()
{
lock (GlobalHooksLock)
{
if (GlobalFrameHooks.Count > 0)
{
GlobalFrameHooks.Clear();
_globalHooksVersion++;
}
}
}
/// <summary>
/// Indicates whether any global frame modification hooks are currently registered (by any mod). Thread-safe.
/// </summary>
public static bool HasGlobalFrameHooks
{
get
{
lock (GlobalHooksLock)
{
return GlobalFrameHooks.Count > 0;
}
}
}
/// <summary>
/// The number of currently registered global frame modification hooks (by any mod). Thread-safe.
/// </summary>
public static int GlobalFrameHookCount
{
get
{
lock (GlobalHooksLock)
{
return GlobalFrameHooks.Count;
}
}
}
/// <summary>Change counter of the global hook list (see _globalHooksVersion). Thread-safe.</summary>
internal static int GlobalHooksVersion
{
get
{
lock (GlobalHooksLock)
{
return _globalHooksVersion;
}
}
}
/// <summary>Appends the delegates of every global hook, in registration order, to <paramref name="destination"/>. Thread-safe.</summary>
internal static void CopyGlobalFrameHooksTo(List<Action<FrameHookContext>> destination)
{
lock (GlobalHooksLock)
{
for (var i = 0; i < GlobalFrameHooks.Count; i++)
{
destination.Add(GlobalFrameHooks[i].Action);
}
}
}
/// <summary>
/// Applies the configured flags and initiates the render session.
/// </summary>
/// <remarks>
/// <para>
/// Must be called on the main Unity thread while no render is running; this method does not check for a running
/// render. It copies the builder's options and hooks into the render, copies <paramref name="renderConfig"/>
/// (later changes to the caller's object do not affect the render), creates the output directory, and starts
/// loading the game scene (<c>scnGame</c>) with the level. It returns immediately; the render runs over the following
/// frames and reports its end through the <see cref="OnComplete"/> callback.
/// </para>
/// <para>
/// Options that were not set keep their defaults: auto mode on, both patches on, default output path, notification
/// on, end condition "level cleared", default completion callback (notification), no last-update callback.
/// </para>
/// </remarks>
/// <param name="renderConfig">The configuration settings for the render session.</param>
/// <param name="levelPath">The file path of the level to render (the .adofai file).</param>
/// <exception cref="InvalidOperationException">Thrown when the output directory cannot be determined from the output path.</exception>
/// <exception cref="ArgumentException">
/// Thrown by the directory creation when the output path has no directory part (for example a bare file name).
/// </exception>
public void Start(RenderConfig renderConfig, string levelPath)
{
// Callbacks not set on this builder keep the values RenderContext holds; RenderContext.Reset at the end of every
// render restores the defaults.
RenderContext.Auto = _auto;
RenderContext.TimePatch = _timePatch;
RenderContext.ScreenPatch = _screenPatch;
RenderContext.OutputPath = _outputPath;
RenderContext.Notification = _notification;
if (_endCondition is not null) RenderContext.EndCondition = _endCondition;
if (_onComplete is not null) RenderContext.OnComplete = _onComplete;
if (_onLastUpdate is not null) RenderContext.OnLastUpdate = _onLastUpdate;
RenderContext.SessionFrameHooks.Clear();
if (_frameHooks.Count > 0)
{
foreach (var t in _frameHooks)
{
RenderContext.SessionFrameHooks.Add(t.Action);
}
}
RenderManager.Instance.levelPath = levelPath;
// The render uses its own copy of the configuration: values adjusted during the render
// (e.g. the audio sample rate in SetupPlay) stay in this copy and do not change the caller's object.
RenderManager.Instance.Configuration = renderConfig.Clone();
RenderManager.Instance.stage = RenderStage.Load;
if (_outputPath is null) RenderManager.Instance.SetupCaptureDirectory();
else FileUtil.CreateDirectory(Path.GetDirectoryName(_outputPath) ?? throw new InvalidOperationException());
RenderManager.Instance.SetupGameScene();
}
}