SHA256
Compare commits
7
Commits
9e3941e3b5
...
5efe1d0dd2
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
5efe1d0dd2
|
||
|
|
71e86c0262
|
||
|
|
f72a8acbac | ||
|
|
d90d06c095 | ||
|
|
b66d774cb9 | ||
|
|
d1ba5d5c18 | ||
|
|
7c6210d2c4 |
No files matched your search
@@ -17,6 +17,11 @@ internal class GlobalConfig
|
|||||||
/// GlobalSettings.DefaultOutputDirectory. Ignored while the directory does not exist (see EffectiveOutputDirectory).
|
/// GlobalSettings.DefaultOutputDirectory. Ignored while the directory does not exist (see EffectiveOutputDirectory).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? OutputDirectory { get; set; }
|
public string? OutputDirectory { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// File name format template chosen in the render configuration window; null or empty selects
|
||||||
|
/// GlobalSettings.DefaultFileNameFormat ("{date} {time}").
|
||||||
|
/// </summary>
|
||||||
|
public string? FileNameFormat { get; set; }
|
||||||
/// <summary>Name of the preset last selected or saved in the render configuration window; selected again when it opens.</summary>
|
/// <summary>Name of the preset last selected or saved in the render configuration window; selected again when it opens.</summary>
|
||||||
public string? LastPresetName { get; set; }
|
public string? LastPresetName { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -72,6 +77,18 @@ internal static class GlobalSettings
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Default file name format template: "{date} {time}".</summary>
|
||||||
|
public const string DefaultFileNameFormat = "{date} {time}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// File name format used for renders without an explicit output path: Current.FileNameFormat when it is set,
|
||||||
|
/// otherwise <see cref="DefaultFileNameFormat"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static string EffectiveFileNameFormat =>
|
||||||
|
string.IsNullOrWhiteSpace(Current.FileNameFormat)
|
||||||
|
? DefaultFileNameFormat
|
||||||
|
: Current.FileNameFormat;
|
||||||
|
|
||||||
static GlobalSettings()
|
static GlobalSettings()
|
||||||
{
|
{
|
||||||
Load();
|
Load();
|
||||||
|
|||||||
+22
-3
@@ -940,6 +940,14 @@ internal class RenderManager : MonoBehaviour
|
|||||||
// Cancelled or failed during the countdown: do not start the encoder
|
// Cancelled or failed during the countdown: do not start the encoder
|
||||||
if (stage != RenderStage.OnScene) yield break;
|
if (stage != RenderStage.OnScene) yield break;
|
||||||
|
|
||||||
|
if (Configuration.AdvancedConfig.DisableTileCulling && scrLevelMaker.instance)
|
||||||
|
{
|
||||||
|
foreach (var floor in scrLevelMaker.instance.listFloors)
|
||||||
|
{
|
||||||
|
if (floor) floor.enabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
frameCount = 0; framesSent = 0;
|
frameCount = 0; framesSent = 0;
|
||||||
_renderStopwatch.Restart();
|
_renderStopwatch.Restart();
|
||||||
|
|
||||||
@@ -1045,9 +1053,20 @@ internal class RenderManager : MonoBehaviour
|
|||||||
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 format = GlobalSettings.EffectiveFileNameFormat;
|
||||||
$".{Configuration.AdvancedConfig.FileExtension.TrimStart('.').ToLower()}";
|
var ext = Configuration.AdvancedConfig.FileExtension;
|
||||||
CurrentOutPath = Path.Combine(GlobalSettings.EffectiveOutputDirectory, fileName);
|
var baseName = RenderFileNameUtil.FormatFileName(
|
||||||
|
format,
|
||||||
|
startTime,
|
||||||
|
Configuration,
|
||||||
|
GlobalSettings.Current.LastPresetName,
|
||||||
|
levelPath
|
||||||
|
);
|
||||||
|
CurrentOutPath = RenderFileNameUtil.GetUniqueFilePath(
|
||||||
|
GlobalSettings.EffectiveOutputDirectory,
|
||||||
|
baseName,
|
||||||
|
ext
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
FFmpegNativeEncoder? videoFFmpeg = null;
|
FFmpegNativeEncoder? videoFFmpeg = null;
|
||||||
|
|||||||
+272
-262
@@ -1,262 +1,272 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace ADOFAIRenderer.Data;
|
namespace ADOFAIRenderer.Data;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Settings of one render: resolution, frame rates, encoding and advanced options. Serialized to JSON in the preset
|
/// Settings of one render: resolution, frame rates, encoding and advanced options. Serialized to JSON in the preset
|
||||||
/// files (PresetManager). <see cref="ADOFAIRenderer.API.RenderSession.Start"/> renders with a copy (<see cref="Clone"/>).
|
/// files (PresetManager). <see cref="ADOFAIRenderer.API.RenderSession.Start"/> renders with a copy (<see cref="Clone"/>).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")]
|
[SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")]
|
||||||
public class RenderConfig
|
public class RenderConfig
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Game frames simulated per second of render time (the game clock advances 1 / RenderFPS seconds per frame).
|
/// Game frames simulated per second of render time (the game clock advances 1 / RenderFPS seconds per frame).
|
||||||
/// Values above <see cref="VideoFPS"/> render extra frames between video frames, which are not captured
|
/// Values above <see cref="VideoFPS"/> render extra frames between video frames, which are not captured
|
||||||
/// (the game state then advances in smaller time steps).
|
/// (the game state then advances in smaller time steps).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double RenderFPS { get; set; }
|
public double RenderFPS { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Frame rate of the output video. The effective rate is min(VideoFPS, RenderFPS).
|
/// Frame rate of the output video. The effective rate is min(VideoFPS, RenderFPS).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double VideoFPS { get; set; }
|
public double VideoFPS { get; set; }
|
||||||
|
|
||||||
/// <summary>Width of the rendered frames and the output video in pixels (also reported by the Screen.width patch).</summary>
|
/// <summary>Width of the rendered frames and the output video in pixels (also reported by the Screen.width patch).</summary>
|
||||||
public int RenderWidth { get; set; }
|
public int RenderWidth { get; set; }
|
||||||
|
|
||||||
/// <summary>Height of the rendered frames and the output video in pixels (also reported by the Screen.height patch).</summary>
|
/// <summary>Height of the rendered frames and the output video in pixels (also reported by the Screen.height patch).</summary>
|
||||||
public int RenderHeight { get; set; }
|
public int RenderHeight { get; set; }
|
||||||
|
|
||||||
/// <summary>Video encoder settings.</summary>
|
/// <summary>Video encoder settings.</summary>
|
||||||
public VideoEncodingConfig VideoConfig { get; set; } = new();
|
public VideoEncodingConfig VideoConfig { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>Audio rendering and encoder settings.</summary>
|
/// <summary>Audio rendering and encoder settings.</summary>
|
||||||
public AudioEncodingConfig AudioConfig { get; set; } = new();
|
public AudioEncodingConfig AudioConfig { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>Advanced options (random seed, spectrum processing, tile hiding, container, planet colors).</summary>
|
/// <summary>Advanced options (random seed, spectrum processing, tile hiding, container, planet colors).</summary>
|
||||||
public AdvancedConfig AdvancedConfig { get; set; } = new();
|
public AdvancedConfig AdvancedConfig { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>Seconds of render time recorded after the end condition first holds (default 15).</summary>
|
/// <summary>Seconds of render time recorded after the end condition first holds (default 15).</summary>
|
||||||
public double DelayAfterEnd { get; set; } = 15;
|
public double DelayAfterEnd { get; set; } = 15;
|
||||||
|
|
||||||
/// <summary>Whether the judgement texts (hit accuracy texts) are shown during the render (scrHitTextManagerPatch). Default false.</summary>
|
/// <summary>Whether the judgement texts (hit accuracy texts) are shown during the render (scrHitTextManagerPatch). Default false.</summary>
|
||||||
public bool ShowJudgementText { get; set; } = false;
|
public bool ShowJudgementText { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Copies every value of <paramref name="source"/> into this object; the nested configs are copied value by value
|
/// Copies every value of <paramref name="source"/> into this object; the nested configs are copied value by value
|
||||||
/// into the existing nested objects. Does nothing when <paramref name="source"/> is null.
|
/// into the existing nested objects. Does nothing when <paramref name="source"/> is null.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void CopyFrom(RenderConfig? source)
|
public void CopyFrom(RenderConfig? source)
|
||||||
{
|
{
|
||||||
if (source == null) return;
|
if (source == null) return;
|
||||||
RenderFPS = source.RenderFPS;
|
RenderFPS = source.RenderFPS;
|
||||||
VideoFPS = source.VideoFPS;
|
VideoFPS = source.VideoFPS;
|
||||||
RenderWidth = source.RenderWidth;
|
RenderWidth = source.RenderWidth;
|
||||||
RenderHeight = source.RenderHeight;
|
RenderHeight = source.RenderHeight;
|
||||||
DelayAfterEnd = source.DelayAfterEnd;
|
DelayAfterEnd = source.DelayAfterEnd;
|
||||||
ShowJudgementText = source.ShowJudgementText;
|
ShowJudgementText = source.ShowJudgementText;
|
||||||
|
|
||||||
VideoConfig.CopyFrom(source.VideoConfig);
|
VideoConfig.CopyFrom(source.VideoConfig);
|
||||||
AudioConfig.CopyFrom(source.AudioConfig);
|
AudioConfig.CopyFrom(source.AudioConfig);
|
||||||
AdvancedConfig.CopyFrom(source.AdvancedConfig);
|
AdvancedConfig.CopyFrom(source.AdvancedConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Returns a deep copy (nested configs are new objects).</summary>
|
/// <summary>Returns a deep copy (nested configs are new objects).</summary>
|
||||||
public RenderConfig Clone()
|
public RenderConfig Clone()
|
||||||
{
|
{
|
||||||
var clone = new RenderConfig();
|
var clone = new RenderConfig();
|
||||||
clone.CopyFrom(this);
|
clone.CopyFrom(this);
|
||||||
return clone;
|
return clone;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Advanced render options.</summary>
|
/// <summary>Advanced render options.</summary>
|
||||||
[SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")]
|
[SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")]
|
||||||
public class AdvancedConfig
|
public class AdvancedConfig
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Seed for UnityEngine.Random set before the game scene loads, so random visual effects repeat between renders;
|
/// Seed for UnityEngine.Random set before the game scene loads, so random visual effects repeat between renders;
|
||||||
/// null leaves the random state unchanged.
|
/// null leaves the random state unchanged.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? RandomSeed { get; set; } = null;
|
public int? RandomSeed { get; set; } = null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// When true, AudioSource.GetSpectrumData is answered from the offline song audio at the render time
|
/// When true, AudioSource.GetSpectrumData is answered from the offline song audio at the render time
|
||||||
/// (AudioSpectrumGenerator), so audio-reactive visuals follow the rendered audio. Default true.
|
/// (AudioSpectrumGenerator), so audio-reactive visuals follow the rendered audio. Default true.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool ProcessAudio { get; set; } = true;
|
public bool ProcessAudio { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// When true, the level's floor tiles (including hold meshes and the portal particles of the level's floors) and the
|
/// When true, the level's floor tiles (including hold meshes and the portal particles of the level's floors) and the
|
||||||
/// planets are hidden during the render, so only the background and decorations remain (scnGamePatch, scrFloorPatch,
|
/// planets are hidden during the render, so only the background and decorations remain (scnGamePatch, scrFloorPatch,
|
||||||
/// RenderManager.OnLastUpdate). Default false.
|
/// RenderManager.OnLastUpdate). Default false.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool NonTile { get; set; } = false;
|
public bool NonTile { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// When true, the game's bloom (<c>VideoBloom</c>) computes its blur size as if the frame were 720 pixels high, so the
|
/// When true, the game's bloom (<c>VideoBloom</c>) computes its blur size as if the frame were 720 pixels high, so the
|
||||||
/// blur passes on the 198-pixel-high bloom buffer are the same as in a 720p frame at any render height
|
/// blur passes on the 198-pixel-high bloom buffer are the same as in a 720p frame at any render height
|
||||||
/// (VideoBloomPatch). Default false.
|
/// (VideoBloomPatch). Default false.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool HighResBloomCorrection { get; set; } = false;
|
public bool HighResBloomCorrection { get; set; } = false;
|
||||||
|
|
||||||
// === [MARKER: FIX_SET_FRAMERATE_BUG START] ===
|
/// <summary>
|
||||||
/// <summary>
|
/// When true, fixes in-game and render bugs during SetFramerate events (where objects disappear on the first frame
|
||||||
/// When true, fixes in-game and render bugs during SetFramerate events (where objects disappear on the first frame
|
/// or parallax/tiles lag behind) by deferring manual camera rendering to the end of the frame after all LateUpdates.
|
||||||
/// or parallax/tiles lag behind) by deferring manual camera rendering to the end of the frame after all LateUpdates.
|
/// Default true.
|
||||||
/// Default true.
|
/// </summary>
|
||||||
/// </summary>
|
public bool FixCustomFrameRate { get; set; } = true;
|
||||||
public bool FixCustomFrameRate { get; set; } = true;
|
|
||||||
// === [MARKER: FIX_SET_FRAMERATE_BUG END] ===
|
/// <summary>
|
||||||
|
/// When true, prevents floor tiles from being disabled when outside the camera frustum (scrFloor.OnBecameInvisible),
|
||||||
/// <summary>File extension of the default output file name, without the dot (e.g. "mkv", "mp4"); selects the container.</summary>
|
/// eliminating the 1-frame update lag/glitch when tiles enter the screen. Default false.
|
||||||
public string FileExtension { get; set; } = "mkv";
|
/// </summary>
|
||||||
|
public bool DisableTileCulling { get; set; } = false;
|
||||||
/// <summary>Color of the fire (red) planet during the render; null keeps the user's setting. Serialized as "#RRGGBBAA".</summary>
|
|
||||||
[JsonConverter(typeof(ColorJsonConverter))]
|
/// <summary>
|
||||||
public Color? firePlanetColor = null;
|
/// When true, hides placeholder decorations (default tree-in-circle sprite) that failed to load or are empty.
|
||||||
|
/// Default false.
|
||||||
/// <summary>Color of the ice (blue) planet during the render; null keeps the user's setting. Serialized as "#RRGGBBAA".</summary>
|
/// </summary>
|
||||||
[JsonConverter(typeof(ColorJsonConverter))]
|
public bool HideFailedDecorations { get; set; } = false;
|
||||||
public Color? icePlanetColor = null;
|
|
||||||
|
/// <summary>File extension of the default output file name, without the dot (e.g. "mkv", "mp4"); selects the container.</summary>
|
||||||
/// <summary>
|
public string FileExtension { get; set; } = "mkv";
|
||||||
/// Color of the third (wind, planetGreen) planet during the render (PlanetarySystemPatch); null keeps the game's color.
|
|
||||||
/// Serialized as "#RRGGBBAA".
|
/// <summary>Color of the fire (red) planet during the render; null keeps the user's setting. Serialized as "#RRGGBBAA".</summary>
|
||||||
/// </summary>
|
[JsonConverter(typeof(ColorJsonConverter))]
|
||||||
[JsonConverter(typeof(ColorJsonConverter))]
|
public Color? firePlanetColor = null;
|
||||||
public Color? windPlanetColor = null;
|
|
||||||
|
/// <summary>Color of the ice (blue) planet during the render; null keeps the user's setting. Serialized as "#RRGGBBAA".</summary>
|
||||||
/// <summary>Copies every value of <paramref name="source"/>; does nothing when it is null.</summary>
|
[JsonConverter(typeof(ColorJsonConverter))]
|
||||||
public void CopyFrom(AdvancedConfig? source)
|
public Color? icePlanetColor = null;
|
||||||
{
|
|
||||||
if (source == null) return;
|
/// <summary>
|
||||||
RandomSeed = source.RandomSeed;
|
/// Color of the third (wind, planetGreen) planet during the render (PlanetarySystemPatch); null keeps the game's color.
|
||||||
ProcessAudio = source.ProcessAudio;
|
/// Serialized as "#RRGGBBAA".
|
||||||
NonTile = source.NonTile;
|
/// </summary>
|
||||||
HighResBloomCorrection = source.HighResBloomCorrection;
|
[JsonConverter(typeof(ColorJsonConverter))]
|
||||||
// === [MARKER: FIX_SET_FRAMERATE_BUG START] ===
|
public Color? windPlanetColor = null;
|
||||||
FixCustomFrameRate = source.FixCustomFrameRate;
|
|
||||||
// === [MARKER: FIX_SET_FRAMERATE_BUG END] ===
|
/// <summary>Copies every value of <paramref name="source"/>; does nothing when it is null.</summary>
|
||||||
FileExtension = source.FileExtension;
|
public void CopyFrom(AdvancedConfig? source)
|
||||||
firePlanetColor = source.firePlanetColor;
|
{
|
||||||
icePlanetColor = source.icePlanetColor;
|
if (source == null) return;
|
||||||
windPlanetColor = source.windPlanetColor;
|
RandomSeed = source.RandomSeed;
|
||||||
}
|
ProcessAudio = source.ProcessAudio;
|
||||||
|
NonTile = source.NonTile;
|
||||||
/// <summary>Returns a copy of this object.</summary>
|
HighResBloomCorrection = source.HighResBloomCorrection;
|
||||||
public AdvancedConfig Clone()
|
FixCustomFrameRate = source.FixCustomFrameRate;
|
||||||
{
|
DisableTileCulling = source.DisableTileCulling;
|
||||||
var clone = new AdvancedConfig();
|
HideFailedDecorations = source.HideFailedDecorations;
|
||||||
clone.CopyFrom(this);
|
FileExtension = source.FileExtension;
|
||||||
return clone;
|
firePlanetColor = source.firePlanetColor;
|
||||||
}
|
icePlanetColor = source.icePlanetColor;
|
||||||
}
|
windPlanetColor = source.windPlanetColor;
|
||||||
|
}
|
||||||
/// <summary>Video encoder settings (passed to FFmpegNativeEncoder.EncoderSettings).</summary>
|
|
||||||
public class VideoEncodingConfig
|
/// <summary>Returns a copy of this object.</summary>
|
||||||
{
|
public AdvancedConfig Clone()
|
||||||
/// <summary>Video codec (default H.264). Also selects the audio codec (CodecUtil.AudioCodecMatch).</summary>
|
{
|
||||||
public Codec Codec { get; set; } = Codec.H264;
|
var clone = new AdvancedConfig();
|
||||||
|
clone.CopyFrom(this);
|
||||||
/// <summary>Bitrate control mode (default CBR).</summary>
|
return clone;
|
||||||
public RateControlMode RateControlMode { get; set; } = RateControlMode.CBR;
|
}
|
||||||
|
}
|
||||||
/// <summary>Target video bitrate in kbps for CBR and VBR (default 30000 = 30 Mbps).</summary>
|
|
||||||
public int Bitrate { get; set; } = 30000;
|
/// <summary>Video encoder settings (passed to FFmpegNativeEncoder.EncoderSettings).</summary>
|
||||||
|
public class VideoEncodingConfig
|
||||||
/// <summary>Peak video bitrate in kbps for VBR, used when larger than Bitrate (otherwise 1.5 × Bitrate). Default 50000.</summary>
|
{
|
||||||
public int MaxBitrate { get; set; } = 50000;
|
/// <summary>Video codec (default H.264). Also selects the audio codec (CodecUtil.AudioCodecMatch).</summary>
|
||||||
|
public Codec Codec { get; set; } = Codec.H264;
|
||||||
/// <summary>
|
|
||||||
/// Quality value for CQP: lower is higher quality. Clamped to the range of the selected encoder; a negative value
|
/// <summary>Bitrate control mode (default CBR).</summary>
|
||||||
/// selects 18. Default 18.
|
public RateControlMode RateControlMode { get; set; } = RateControlMode.CBR;
|
||||||
/// </summary>
|
|
||||||
// ReSharper disable once InconsistentNaming
|
/// <summary>Target video bitrate in kbps for CBR and VBR (default 30000 = 30 Mbps).</summary>
|
||||||
public int QP { get; set; } = 18;
|
public int Bitrate { get; set; } = 30000;
|
||||||
|
|
||||||
/// <summary>Keyframe interval in seconds (GOP length = round(effective video FPS × interval) frames, at least 1). Default 2.</summary>
|
/// <summary>Peak video bitrate in kbps for VBR, used when larger than Bitrate (otherwise 1.5 × Bitrate). Default 50000.</summary>
|
||||||
public double KeyframeInterval { get; set; } = 2.0;
|
public int MaxBitrate { get; set; } = 50000;
|
||||||
|
|
||||||
/// <summary>When true, only software (CPU) encoders are tried; hardware encoders are skipped.</summary>
|
/// <summary>
|
||||||
public bool ForceSoftwareEncoder { get; set; }
|
/// Quality value for CQP: lower is higher quality. Clamped to the range of the selected encoder; a negative value
|
||||||
|
/// selects 18. Default 18.
|
||||||
/// <summary>Copies every value of <paramref name="source"/>; does nothing when it is null.</summary>
|
/// </summary>
|
||||||
public void CopyFrom(VideoEncodingConfig? source)
|
// ReSharper disable once InconsistentNaming
|
||||||
{
|
public int QP { get; set; } = 18;
|
||||||
if (source == null) return;
|
|
||||||
Codec = source.Codec;
|
/// <summary>Keyframe interval in seconds (GOP length = round(effective video FPS × interval) frames, at least 1). Default 2.</summary>
|
||||||
RateControlMode = source.RateControlMode;
|
public double KeyframeInterval { get; set; } = 2.0;
|
||||||
Bitrate = source.Bitrate;
|
|
||||||
MaxBitrate = source.MaxBitrate;
|
/// <summary>When true, only software (CPU) encoders are tried; hardware encoders are skipped.</summary>
|
||||||
QP = source.QP;
|
public bool ForceSoftwareEncoder { get; set; }
|
||||||
KeyframeInterval = source.KeyframeInterval;
|
|
||||||
ForceSoftwareEncoder = source.ForceSoftwareEncoder;
|
/// <summary>Copies every value of <paramref name="source"/>; does nothing when it is null.</summary>
|
||||||
}
|
public void CopyFrom(VideoEncodingConfig? source)
|
||||||
|
{
|
||||||
/// <summary>Returns a copy of this object.</summary>
|
if (source == null) return;
|
||||||
public VideoEncodingConfig Clone()
|
Codec = source.Codec;
|
||||||
{
|
RateControlMode = source.RateControlMode;
|
||||||
var clone = new VideoEncodingConfig();
|
Bitrate = source.Bitrate;
|
||||||
clone.CopyFrom(this);
|
MaxBitrate = source.MaxBitrate;
|
||||||
return clone;
|
QP = source.QP;
|
||||||
}
|
KeyframeInterval = source.KeyframeInterval;
|
||||||
}
|
ForceSoftwareEncoder = source.ForceSoftwareEncoder;
|
||||||
|
}
|
||||||
/// <summary>
|
|
||||||
/// Audio rendering and encoder settings. Non-positive values of the numeric settings read back as their defaults
|
/// <summary>Returns a copy of this object.</summary>
|
||||||
/// (48000 Hz, 2 channels, 256000 bps).
|
public VideoEncodingConfig Clone()
|
||||||
/// </summary>
|
{
|
||||||
public class AudioEncodingConfig
|
var clone = new VideoEncodingConfig();
|
||||||
{
|
clone.CopyFrom(this);
|
||||||
private int _audioSampleRate = 48000;
|
return clone;
|
||||||
private int _audioChannels = 2;
|
}
|
||||||
private int _audioBitrate = 256000;
|
}
|
||||||
|
|
||||||
/// <summary>When true, the song and sound effects are mixed offline and encoded as an audio stream. Default true.</summary>
|
/// <summary>
|
||||||
public bool RenderAudio { get; set; } = true;
|
/// Audio rendering and encoder settings. Non-positive values of the numeric settings read back as their defaults
|
||||||
|
/// (48000 Hz, 2 channels, 256000 bps).
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// Output sample rate in Hz (default 48000). A rate the audio codec does not support in the mod
|
public class AudioEncodingConfig
|
||||||
/// (CodecUtil.AvailableSampleRate) is replaced with 48000 when the render starts.
|
{
|
||||||
/// </summary>
|
private int _audioSampleRate = 48000;
|
||||||
public int AudioSampleRate
|
private int _audioChannels = 2;
|
||||||
{
|
private int _audioBitrate = 256000;
|
||||||
get => _audioSampleRate <= 0 ? 48000 : _audioSampleRate;
|
|
||||||
set => _audioSampleRate = value <= 0 ? 48000 : value;
|
/// <summary>When true, the song and sound effects are mixed offline and encoded as an audio stream. Default true.</summary>
|
||||||
}
|
public bool RenderAudio { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>Number of output audio channels (default 2).</summary>
|
/// <summary>
|
||||||
public int AudioChannels
|
/// Output sample rate in Hz (default 48000). A rate the audio codec does not support in the mod
|
||||||
{
|
/// (CodecUtil.AvailableSampleRate) is replaced with 48000 when the render starts.
|
||||||
get => _audioChannels <= 0 ? 2 : _audioChannels;
|
/// </summary>
|
||||||
set => _audioChannels = value <= 0 ? 2 : value;
|
public int AudioSampleRate
|
||||||
}
|
{
|
||||||
|
get => _audioSampleRate <= 0 ? 48000 : _audioSampleRate;
|
||||||
/// <summary>Audio bitrate in bits per second (default 256000 = 256 kbps). Unit differs from the video bitrate (kbps).</summary>
|
set => _audioSampleRate = value <= 0 ? 48000 : value;
|
||||||
public int AudioBitrate
|
}
|
||||||
{
|
|
||||||
get => _audioBitrate <= 0 ? 256000 : _audioBitrate;
|
/// <summary>Number of output audio channels (default 2).</summary>
|
||||||
set => _audioBitrate = value <= 0 ? 256000 : value;
|
public int AudioChannels
|
||||||
}
|
{
|
||||||
|
get => _audioChannels <= 0 ? 2 : _audioChannels;
|
||||||
/// <summary>Copies every value of <paramref name="source"/>; does nothing when it is null.</summary>
|
set => _audioChannels = value <= 0 ? 2 : value;
|
||||||
public void CopyFrom(AudioEncodingConfig? source)
|
}
|
||||||
{
|
|
||||||
if (source == null) return;
|
/// <summary>Audio bitrate in bits per second (default 256000 = 256 kbps). Unit differs from the video bitrate (kbps).</summary>
|
||||||
RenderAudio = source.RenderAudio;
|
public int AudioBitrate
|
||||||
AudioSampleRate = source.AudioSampleRate;
|
{
|
||||||
AudioChannels = source.AudioChannels;
|
get => _audioBitrate <= 0 ? 256000 : _audioBitrate;
|
||||||
AudioBitrate = source.AudioBitrate;
|
set => _audioBitrate = value <= 0 ? 256000 : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Returns a copy of this object.</summary>
|
/// <summary>Copies every value of <paramref name="source"/>; does nothing when it is null.</summary>
|
||||||
public AudioEncodingConfig Clone()
|
public void CopyFrom(AudioEncodingConfig? source)
|
||||||
{
|
{
|
||||||
var clone = new AudioEncodingConfig();
|
if (source == null) return;
|
||||||
clone.CopyFrom(this);
|
RenderAudio = source.RenderAudio;
|
||||||
return clone;
|
AudioSampleRate = source.AudioSampleRate;
|
||||||
}
|
AudioChannels = source.AudioChannels;
|
||||||
}␍
|
AudioBitrate = source.AudioBitrate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns a copy of this object.</summary>
|
||||||
|
public AudioEncodingConfig Clone()
|
||||||
|
{
|
||||||
|
var clone = new AudioEncodingConfig();
|
||||||
|
clone.CopyFrom(this);
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,11 +39,16 @@
|
|||||||
"ui.render_config.output_dir": "Output Directory",
|
"ui.render_config.output_dir": "Output Directory",
|
||||||
"ui.render_config.browse": "Browse...",
|
"ui.render_config.browse": "Browse...",
|
||||||
"ui.render_config.reset_default": "Reset to Default",
|
"ui.render_config.reset_default": "Reset to Default",
|
||||||
|
"ui.render_config.filename_format": "Filename Format",
|
||||||
|
"ui.render_config.filename_format_hint": "Tags: {song}, {artist}, {author}, {date}, {time}, {resolution}, {fps}, {preset}",
|
||||||
|
"ui.render_config.reset_filename_default": "Reset to Default",
|
||||||
"ui.render_config.file_extension": "File Extension",
|
"ui.render_config.file_extension": "File Extension",
|
||||||
"ui.render_config.process_audio": "Process Audio",
|
"ui.render_config.process_audio": "Process Audio",
|
||||||
"ui.render_config.non_tile": "Non-Tile Mode",
|
"ui.render_config.non_tile": "Non-Tile Mode",
|
||||||
"ui.render_config.high_res_bloom_correction": "High-Resolution Bloom Correction",
|
"ui.render_config.high_res_bloom_correction": "High-Resolution Bloom Correction",
|
||||||
"ui.render_config.fix_custom_frame_rate": "Fix Custom FPS Render Timing",
|
"ui.render_config.fix_custom_frame_rate": "Fix Custom FPS Render Timing",
|
||||||
|
"ui.render_config.disable_tile_culling": "Disable Tile Culling",
|
||||||
|
"ui.render_config.hide_failed_decorations": "Hide Failed Decorations",
|
||||||
"ui.render_config.random_seed": "Random Seed (Optional)",
|
"ui.render_config.random_seed": "Random Seed (Optional)",
|
||||||
"ui.render_config.validation_title": "Invalid Settings",
|
"ui.render_config.validation_title": "Invalid Settings",
|
||||||
"ui.render_config.validation_error": "Please check your input values:\n- Width and Height must be integers > 0\n- Render FPS and Video FPS must be > 0\n- Delay After End must be >= 0",
|
"ui.render_config.validation_error": "Please check your input values:\n- Width and Height must be integers > 0\n- Render FPS and Video FPS must be > 0\n- Delay After End must be >= 0",
|
||||||
|
|||||||
@@ -39,11 +39,16 @@
|
|||||||
"ui.render_config.output_dir": "출력 디렉토리",
|
"ui.render_config.output_dir": "출력 디렉토리",
|
||||||
"ui.render_config.browse": "찾아보기...",
|
"ui.render_config.browse": "찾아보기...",
|
||||||
"ui.render_config.reset_default": "기본값으로 초기화",
|
"ui.render_config.reset_default": "기본값으로 초기화",
|
||||||
|
"ui.render_config.filename_format": "파일 이름 형식",
|
||||||
|
"ui.render_config.filename_format_hint": "태그: {song}, {artist}, {author}, {date}, {time}, {resolution}, {fps}, {preset}",
|
||||||
|
"ui.render_config.reset_filename_default": "기본값으로 초기화",
|
||||||
"ui.render_config.file_extension": "파일 확장자",
|
"ui.render_config.file_extension": "파일 확장자",
|
||||||
"ui.render_config.process_audio": "오디오 처리",
|
"ui.render_config.process_audio": "오디오 처리",
|
||||||
"ui.render_config.non_tile": "논타일 모드",
|
"ui.render_config.non_tile": "논타일 모드",
|
||||||
"ui.render_config.high_res_bloom_correction": "고해상도 블룸 보정",
|
"ui.render_config.high_res_bloom_correction": "고해상도 블룸 보정",
|
||||||
"ui.render_config.fix_custom_frame_rate": "커스텀 FPS 렌더 타이밍 보정",
|
"ui.render_config.fix_custom_frame_rate": "커스텀 FPS 렌더 타이밍 보정",
|
||||||
|
"ui.render_config.disable_tile_culling": "타일 컬링 비활성화",
|
||||||
|
"ui.render_config.hide_failed_decorations": "로딩 실패한 장식 숨기기",
|
||||||
"ui.render_config.random_seed": "랜덤 시드 (선택 사항)",
|
"ui.render_config.random_seed": "랜덤 시드 (선택 사항)",
|
||||||
"ui.render_config.validation_title": "설정 오류",
|
"ui.render_config.validation_title": "설정 오류",
|
||||||
"ui.render_config.validation_error": "입력값을 확인해 주세요:\n- 너비와 높이는 0보다 큰 정수여야 합니다.\n- 렌더 FPS와 영상 FPS는 0보다 커야 합니다.\n- 종료 후 대기 시간은 0 이상이어야 합니다.",
|
"ui.render_config.validation_error": "입력값을 확인해 주세요:\n- 너비와 높이는 0보다 큰 정수여야 합니다.\n- 렌더 FPS와 영상 FPS는 0보다 커야 합니다.\n- 종료 후 대기 시간은 0 이상이어야 합니다.",
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ using UnityEngine;
|
|||||||
|
|
||||||
namespace ADOFAIRenderer.Patch.Game;
|
namespace ADOFAIRenderer.Patch.Game;
|
||||||
|
|
||||||
// === [MARKER: FIX_SET_FRAMERATE_BUG START] ===
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fixes the in-game and render bug where objects (floors, decorations, planets) disappear on the first frame of
|
/// Fixes the in-game and render bug where objects (floors, decorations, planets) disappear on the first frame of
|
||||||
/// a SetFramerate event (due to camera Z=0 near clip culling) and where parallax/tiles lag behind on subsequent frames.
|
/// a SetFramerate event (due to camera Z=0 near clip culling) and where parallax/tiles lag behind on subsequent frames.
|
||||||
@@ -112,4 +111,3 @@ internal class CustomFrameRateDeferredRunner : MonoBehaviour
|
|||||||
CustomFrameRateDeferPatch.ExecuteDeferredRender();
|
CustomFrameRateDeferPatch.ExecuteDeferredRender();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// === [MARKER: FIX_SET_FRAMERATE_BUG END] ===
|
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
using ADOFAIRenderer.Core;
|
||||||
|
using HarmonyLib;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace ADOFAIRenderer.Patch.Game;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Patches decoration rendering to hide decorations that failed to load (or are empty) and fell back to the default
|
||||||
|
/// placeholder sprite (tree inside a circle) during a render when <c>AdvancedConfig.HideFailedDecorations</c> is enabled.
|
||||||
|
/// </summary>
|
||||||
|
public static class DecorationPatch
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// True while a render is loading or active and <c>HideFailedDecorations</c> is enabled in the configuration.
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsEnabled =>
|
||||||
|
RenderManager.Instance &&
|
||||||
|
RenderManager.Instance.stage >= RenderStage.Load &&
|
||||||
|
RenderManager.Instance.Configuration?.AdvancedConfig?.HideFailedDecorations == true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether the given sprite is the default placeholder or not-found sprite of the decoration manager.
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsPlaceholderSprite(Sprite? sprite, scrDecorationManager? manager = null)
|
||||||
|
{
|
||||||
|
if (!sprite) return false;
|
||||||
|
manager ??= scrDecorationManager.instance;
|
||||||
|
if (!manager) return false;
|
||||||
|
|
||||||
|
return sprite == manager.defaultSprite || (manager.notFoundSprite && sprite == manager.notFoundSprite);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sweeps all visual decorations in <c>scrDecorationManager.instance.allDecorations</c> and clears any placeholder sprites.
|
||||||
|
/// Called from <see cref="scnGamePatch.ProcessFloors"/> when <c>scnGame.Play</c> runs.
|
||||||
|
/// </summary>
|
||||||
|
public static void ProcessDecorations()
|
||||||
|
{
|
||||||
|
if (!IsEnabled) return;
|
||||||
|
var manager = scrDecorationManager.instance;
|
||||||
|
if (!manager || manager.allDecorations == null) return;
|
||||||
|
|
||||||
|
foreach (var dec in manager.allDecorations)
|
||||||
|
{
|
||||||
|
if (dec is scrVisualDecoration visualDec && visualDec.spriteRenderer)
|
||||||
|
{
|
||||||
|
if (IsPlaceholderSprite(visualDec.spriteRenderer.sprite, manager))
|
||||||
|
{
|
||||||
|
visualDec.SetSprite((Sprite)null!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prefix of <c>scrVisualDecoration.SetSprite(Sprite)</c>: replaces placeholder default/not-found sprites with null
|
||||||
|
/// when <see cref="IsEnabled"/> is true.
|
||||||
|
/// </summary>
|
||||||
|
[HarmonyPatch(typeof(scrVisualDecoration), nameof(scrVisualDecoration.SetSprite), typeof(Sprite))]
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
public static class VisualDecorationSetSpritePatch
|
||||||
|
{
|
||||||
|
[HarmonyPrefix]
|
||||||
|
public static void Prefix(scrVisualDecoration __instance, ref Sprite? sprite)
|
||||||
|
{
|
||||||
|
if (!IsEnabled || !sprite) return;
|
||||||
|
|
||||||
|
if (IsPlaceholderSprite(sprite, __instance.manager))
|
||||||
|
{
|
||||||
|
sprite = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prefix of <c>scrDecorationManager.ShowEmptyDecorations(bool)</c>: suppresses showing placeholder default sprites
|
||||||
|
/// during render when <see cref="IsEnabled"/> is true.
|
||||||
|
/// </summary>
|
||||||
|
[HarmonyPatch(typeof(scrDecorationManager), nameof(scrDecorationManager.ShowEmptyDecorations))]
|
||||||
|
public static class ShowEmptyDecorationsPatch
|
||||||
|
{
|
||||||
|
[HarmonyPrefix]
|
||||||
|
public static bool Prefix(bool show)
|
||||||
|
{
|
||||||
|
if (show && IsEnabled)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,8 +30,21 @@ public static class scnGamePatch
|
|||||||
[HarmonyPatch("Play")]
|
[HarmonyPatch("Play")]
|
||||||
public static void ProcessFloors()
|
public static void ProcessFloors()
|
||||||
{
|
{
|
||||||
var nonTile = RenderManager.Instance.stage >= RenderStage.Load &&
|
if (RenderManager.Instance &&
|
||||||
RenderManager.Instance.Configuration.AdvancedConfig.NonTile;
|
RenderManager.Instance.stage >= RenderStage.Load &&
|
||||||
|
RenderManager.Instance.Configuration.AdvancedConfig.DisableTileCulling)
|
||||||
|
{
|
||||||
|
foreach (var floor in scrLevelMaker.instance.listFloors)
|
||||||
|
{
|
||||||
|
if (floor) floor.enabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DecorationPatch.ProcessDecorations();
|
||||||
|
|
||||||
|
var nonTile = RenderManager.Instance &&
|
||||||
|
RenderManager.Instance.stage >= RenderStage.Load &&
|
||||||
|
RenderManager.Instance.Configuration?.AdvancedConfig?.NonTile == true;
|
||||||
|
|
||||||
if (!nonTile) return;
|
if (!nonTile) return;
|
||||||
foreach (var floor in scrLevelMaker.instance.listFloors)
|
foreach (var floor in scrLevelMaker.instance.listFloors)
|
||||||
|
|||||||
@@ -33,4 +33,41 @@ public class scrFloorPatch
|
|||||||
RenderManager.Instance.Configuration.AdvancedConfig.NonTile;
|
RenderManager.Instance.Configuration.AdvancedConfig.NonTile;
|
||||||
return !nonTile;
|
return !nonTile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Postfix of <c>scrFloor.Start</c>.
|
||||||
|
/// The game sets <c>enabled = isVisible</c> in Start(), which disables all off-screen tiles right at spawn.
|
||||||
|
/// When <c>DisableTileCulling</c> is enabled, force every floor to remain enabled.
|
||||||
|
/// </summary>
|
||||||
|
[HarmonyPostfix]
|
||||||
|
[HarmonyPatch("Start")]
|
||||||
|
public static void ForceEnableOnStart(scrFloor __instance)
|
||||||
|
{
|
||||||
|
if (RenderManager.Instance &&
|
||||||
|
RenderManager.Instance.stage >= RenderStage.Load &&
|
||||||
|
RenderManager.Instance.Configuration.AdvancedConfig.DisableTileCulling)
|
||||||
|
{
|
||||||
|
__instance.enabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prefix of <c>scrFloor.OnBecameInvisible</c>.
|
||||||
|
/// Skips disabling the floor component (<c>enabled = false</c>) while rendering with
|
||||||
|
/// <c>AdvancedConfig.DisableTileCulling</c> enabled, preventing the 1-frame update lag/glitch
|
||||||
|
/// when tiles enter the camera view.
|
||||||
|
/// </summary>
|
||||||
|
[HarmonyPrefix]
|
||||||
|
[HarmonyPatch("OnBecameInvisible")]
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
public static bool PreventTileCulling(scrFloor __instance)
|
||||||
|
{
|
||||||
|
if (RenderManager.Instance &&
|
||||||
|
RenderManager.Instance.stage >= RenderStage.Load &&
|
||||||
|
RenderManager.Instance.Configuration.AdvancedConfig.DisableTileCulling)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -9,7 +9,7 @@ using MelonLoader.Utils;
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using Object = UnityEngine.Object;
|
using Object = UnityEngine.Object;
|
||||||
|
|
||||||
[assembly: MelonInfo(typeof(RenderMod), "ADOFAI Renderer", "0.3.2-INDEV", "ADOFAI Editor Forum")]
|
[assembly: MelonInfo(typeof(RenderMod), "ADOFAI Renderer", "0.3.3-INDEV", "ADOFAI Editor Forum")]
|
||||||
[assembly: MelonGame("7th Beat Games", "A Dance of Fire and Ice")]
|
[assembly: MelonGame("7th Beat Games", "A Dance of Fire and Ice")]
|
||||||
|
|
||||||
namespace ADOFAIRenderer;
|
namespace ADOFAIRenderer;
|
||||||
|
|||||||
@@ -135,14 +135,19 @@ internal static class RenderConfigWindow
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private static O5InputField? _outputDirInput;
|
private static O5InputField? _outputDirInput;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// File name format template input. Bound to <c>GlobalSettings.Current.FileNameFormat</c>.
|
||||||
|
/// </summary>
|
||||||
|
private static O5InputField? _fileNameFormatInput;
|
||||||
|
|
||||||
private static O5Dropdown<string>? _fileExtensionDropdown;
|
private static O5Dropdown<string>? _fileExtensionDropdown;
|
||||||
private static O5InputField? _randomSeedInput;
|
private static O5InputField? _randomSeedInput;
|
||||||
private static O5Toggle? _processAudioToggle;
|
private static O5Toggle? _processAudioToggle;
|
||||||
private static O5Toggle? _nonTileToggle;
|
private static O5Toggle? _nonTileToggle;
|
||||||
private static O5Toggle? _highResBloomCorrectionToggle;
|
private static O5Toggle? _highResBloomCorrectionToggle;
|
||||||
// === [MARKER: FIX_SET_FRAMERATE_BUG START] ===
|
|
||||||
private static O5Toggle? _fixCustomFrameRateToggle;
|
private static O5Toggle? _fixCustomFrameRateToggle;
|
||||||
// === [MARKER: FIX_SET_FRAMERATE_BUG END] ===
|
private static O5Toggle? _disableTileCullingToggle;
|
||||||
|
private static O5Toggle? _hideFailedDecorationsToggle;
|
||||||
|
|
||||||
// Planet color overrides: the toggle enables the override, the picker holds the color. A disabled toggle stores
|
// Planet color overrides: the toggle enables the override, the picker holds the color. A disabled toggle stores
|
||||||
// null in AdvancedConfig (no override is applied by PersistencePatch) and blocks the picker.
|
// null in AdvancedConfig (no override is applied by PersistencePatch) and blocks the picker.
|
||||||
@@ -698,6 +703,43 @@ internal static class RenderConfigWindow
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Filename format template: typing updates GlobalSettings.Current.FileNameFormat in memory only.
|
||||||
|
_fileNameFormatInput = CreateFullWidthInputField(
|
||||||
|
ctx, globalContent,
|
||||||
|
"ui.render_config.filename_format",
|
||||||
|
GlobalSettings.DefaultFileNameFormat,
|
||||||
|
GlobalSettings.EffectiveFileNameFormat,
|
||||||
|
"input_filename_format",
|
||||||
|
val =>
|
||||||
|
{
|
||||||
|
var trimmed = val?.Trim() ?? string.Empty;
|
||||||
|
if (string.IsNullOrWhiteSpace(trimmed) ||
|
||||||
|
string.Equals(trimmed, GlobalSettings.DefaultFileNameFormat, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
GlobalSettings.Current.FileNameFormat = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GlobalSettings.Current.FileNameFormat = trimmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
var fnHint = CreateLabel(ctx, globalContent, "ui.render_config.filename_format_hint", 11f);
|
||||||
|
fnHint.color = new Color(ctx.Theme.TextDim.r, ctx.Theme.TextDim.g, ctx.Theme.TextDim.b, ctx.Theme.TextDim.a * 0.7f);
|
||||||
|
|
||||||
|
CreateButton(
|
||||||
|
ctx, globalContent,
|
||||||
|
"ui.render_config.reset_filename_default",
|
||||||
|
"btn_reset_filename_format",
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
_fileNameFormatInput.Set(GlobalSettings.DefaultFileNameFormat);
|
||||||
|
GlobalSettings.Current.FileNameFormat = null;
|
||||||
|
GlobalSettings.Save();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------
|
||||||
// Section 6: Advanced Settings card (RenderConfig.AdvancedConfig)
|
// Section 6: Advanced Settings card (RenderConfig.AdvancedConfig)
|
||||||
// File Extension | Random Seed
|
// File Extension | Random Seed
|
||||||
@@ -760,6 +802,22 @@ internal static class RenderConfigWindow
|
|||||||
},
|
},
|
||||||
generateLabel: false
|
generateLabel: false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Disable Tile Culling (AdvancedConfig.DisableTileCulling): prevents tiles from being disabled outside the frustum
|
||||||
|
_disableTileCullingToggle = CreateToggleField(
|
||||||
|
ctx, CreateFieldSlot(advLeft),
|
||||||
|
labelKey: "",
|
||||||
|
defaultValue: false,
|
||||||
|
value: config.AdvancedConfig.DisableTileCulling,
|
||||||
|
toggleTextKey: "ui.render_config.disable_tile_culling",
|
||||||
|
id: "toggle_disable_tile_culling",
|
||||||
|
onChanged: val =>
|
||||||
|
{
|
||||||
|
config.AdvancedConfig.DisableTileCulling = val;
|
||||||
|
MarkConfigModified();
|
||||||
|
},
|
||||||
|
generateLabel: false
|
||||||
|
);
|
||||||
|
|
||||||
// Random Seed (AdvancedConfig.RandomSeed): integer passed to UnityEngine.Random.InitState before the game scene
|
// Random Seed (AdvancedConfig.RandomSeed): integer passed to UnityEngine.Random.InitState before the game scene
|
||||||
// loads; empty text stores null (no seeding). Text that is neither empty nor an integer keeps the previous value.
|
// loads; empty text stores null (no seeding). Text that is neither empty nor an integer keeps the previous value.
|
||||||
@@ -794,7 +852,6 @@ internal static class RenderConfigWindow
|
|||||||
generateLabel: false
|
generateLabel: false
|
||||||
);
|
);
|
||||||
|
|
||||||
// === [MARKER: FIX_SET_FRAMERATE_BUG START] ===
|
|
||||||
// Fix SetFramerate Render Timing (AdvancedConfig.FixCustomFrameRate): defers manual camera rendering to frame end
|
// Fix SetFramerate Render Timing (AdvancedConfig.FixCustomFrameRate): defers manual camera rendering to frame end
|
||||||
_fixCustomFrameRateToggle = CreateToggleField(
|
_fixCustomFrameRateToggle = CreateToggleField(
|
||||||
ctx, CreateFieldSlot(advRight),
|
ctx, CreateFieldSlot(advRight),
|
||||||
@@ -810,7 +867,22 @@ internal static class RenderConfigWindow
|
|||||||
},
|
},
|
||||||
generateLabel: false
|
generateLabel: false
|
||||||
);
|
);
|
||||||
// === [MARKER: FIX_SET_FRAMERATE_BUG END] ===
|
|
||||||
|
// Hide Failed Decorations (AdvancedConfig.HideFailedDecorations): hides placeholder tree decorations when decoration images fail to load
|
||||||
|
_hideFailedDecorationsToggle = CreateToggleField(
|
||||||
|
ctx, CreateFieldSlot(advRight),
|
||||||
|
labelKey: "",
|
||||||
|
defaultValue: false,
|
||||||
|
value: config.AdvancedConfig.HideFailedDecorations,
|
||||||
|
toggleTextKey: "ui.render_config.hide_failed_decorations",
|
||||||
|
id: "toggle_hide_failed_decorations",
|
||||||
|
onChanged: val =>
|
||||||
|
{
|
||||||
|
config.AdvancedConfig.HideFailedDecorations = val;
|
||||||
|
MarkConfigModified();
|
||||||
|
},
|
||||||
|
generateLabel: false
|
||||||
|
);
|
||||||
|
|
||||||
// 7-1. Planet Color Settings (Optional).
|
// 7-1. Planet Color Settings (Optional).
|
||||||
// For each planet: the toggle turns the override on/off (off stores null and blocks/collapses the picker);
|
// For each planet: the toggle turns the override on/off (off stores null and blocks/collapses the picker);
|
||||||
@@ -1015,15 +1087,16 @@ internal static class RenderConfigWindow
|
|||||||
_audioBitrateSlider?.Set(config.AudioConfig.AudioBitrate / 1000.0, false, noFilter: true);
|
_audioBitrateSlider?.Set(config.AudioConfig.AudioBitrate / 1000.0, false, noFilter: true);
|
||||||
|
|
||||||
_outputDirInput?.Set(GlobalSettings.EffectiveOutputDirectory, false);
|
_outputDirInput?.Set(GlobalSettings.EffectiveOutputDirectory, false);
|
||||||
|
_fileNameFormatInput?.Set(GlobalSettings.EffectiveFileNameFormat, false);
|
||||||
|
|
||||||
_fileExtensionDropdown?.Set(config.AdvancedConfig.FileExtension, false);
|
_fileExtensionDropdown?.Set(config.AdvancedConfig.FileExtension, false);
|
||||||
_randomSeedInput?.Set(config.AdvancedConfig.RandomSeed?.ToString() ?? "", false);
|
_randomSeedInput?.Set(config.AdvancedConfig.RandomSeed?.ToString() ?? "", false);
|
||||||
_processAudioToggle?.Set(config.AdvancedConfig.ProcessAudio, false);
|
_processAudioToggle?.Set(config.AdvancedConfig.ProcessAudio, false);
|
||||||
_nonTileToggle?.Set(config.AdvancedConfig.NonTile, false);
|
_nonTileToggle?.Set(config.AdvancedConfig.NonTile, false);
|
||||||
_highResBloomCorrectionToggle?.Set(config.AdvancedConfig.HighResBloomCorrection, false);
|
_highResBloomCorrectionToggle?.Set(config.AdvancedConfig.HighResBloomCorrection, false);
|
||||||
// === [MARKER: FIX_SET_FRAMERATE_BUG START] ===
|
|
||||||
_fixCustomFrameRateToggle?.Set(config.AdvancedConfig.FixCustomFrameRate, false);
|
_fixCustomFrameRateToggle?.Set(config.AdvancedConfig.FixCustomFrameRate, false);
|
||||||
// === [MARKER: FIX_SET_FRAMERATE_BUG END] ===
|
_disableTileCullingToggle?.Set(config.AdvancedConfig.DisableTileCulling, false);
|
||||||
|
_hideFailedDecorationsToggle?.Set(config.AdvancedConfig.HideFailedDecorations, false);
|
||||||
|
|
||||||
// Planet colors: toggle on when an override is stored; the picker shows the stored color or the default and is
|
// Planet colors: toggle on when an override is stored; the picker shows the stored color or the default and is
|
||||||
// blocked without an override
|
// blocked without an override
|
||||||
@@ -1141,6 +1214,20 @@ internal static class RenderConfigWindow
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_fileNameFormatInput != null)
|
||||||
|
{
|
||||||
|
var format = _fileNameFormatInput.Value.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(format) ||
|
||||||
|
string.Equals(format, GlobalSettings.DefaultFileNameFormat, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
GlobalSettings.Current.FileNameFormat = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GlobalSettings.Current.FileNameFormat = format;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
GlobalSettings.Save();
|
GlobalSettings.Save();
|
||||||
SetWindowActive(false);
|
SetWindowActive(false);
|
||||||
|
|
||||||
@@ -1206,9 +1293,9 @@ internal static class RenderConfigWindow
|
|||||||
if (_processAudioToggle != null) target.AdvancedConfig.ProcessAudio = _processAudioToggle.Value;
|
if (_processAudioToggle != null) target.AdvancedConfig.ProcessAudio = _processAudioToggle.Value;
|
||||||
if (_nonTileToggle != null) target.AdvancedConfig.NonTile = _nonTileToggle.Value;
|
if (_nonTileToggle != null) target.AdvancedConfig.NonTile = _nonTileToggle.Value;
|
||||||
if (_highResBloomCorrectionToggle != null) target.AdvancedConfig.HighResBloomCorrection = _highResBloomCorrectionToggle.Value;
|
if (_highResBloomCorrectionToggle != null) target.AdvancedConfig.HighResBloomCorrection = _highResBloomCorrectionToggle.Value;
|
||||||
// === [MARKER: FIX_SET_FRAMERATE_BUG START] ===
|
|
||||||
if (_fixCustomFrameRateToggle != null) target.AdvancedConfig.FixCustomFrameRate = _fixCustomFrameRateToggle.Value;
|
if (_fixCustomFrameRateToggle != null) target.AdvancedConfig.FixCustomFrameRate = _fixCustomFrameRateToggle.Value;
|
||||||
// === [MARKER: FIX_SET_FRAMERATE_BUG END] ===
|
if (_disableTileCullingToggle != null) target.AdvancedConfig.DisableTileCulling = _disableTileCullingToggle.Value;
|
||||||
|
if (_hideFailedDecorationsToggle != null) target.AdvancedConfig.HideFailedDecorations = _hideFailedDecorationsToggle.Value;
|
||||||
|
|
||||||
// Planet Colors
|
// Planet Colors
|
||||||
target.AdvancedConfig.firePlanetColor = (_firePlanetToggle?.Value == true && _firePlanetPicker != null)
|
target.AdvancedConfig.firePlanetColor = (_firePlanetToggle?.Value == true && _firePlanetPicker != null)
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using ADOFAIRenderer.Core.Data;
|
||||||
|
using ADOFAIRenderer.Data;
|
||||||
|
|
||||||
|
namespace ADOFAIRenderer.Util;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Formats and sanitizes render output file names according to customizable template patterns.
|
||||||
|
/// </summary>
|
||||||
|
internal static class RenderFileNameUtil
|
||||||
|
{
|
||||||
|
private static readonly Regex RichTextRegex = new(@"<[^>]+>", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex TokenRegex = new(@"\{([a-zA-Z0-9_]+)\}", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
private static readonly HashSet<string> ReservedWindowsNames = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
"CON", "PRN", "AUX", "NUL",
|
||||||
|
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
|
||||||
|
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes Unity rich text tags (such as <color=...>, <b>, etc.) from the given text.
|
||||||
|
/// </summary>
|
||||||
|
public static string StripRichText(string? input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input)) return string.Empty;
|
||||||
|
return RichTextRegex.Replace(input, string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Replaces invalid file name characters within a single token with an underscore.
|
||||||
|
/// </summary>
|
||||||
|
public static string SanitizeToken(string? token)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(token)) return string.Empty;
|
||||||
|
|
||||||
|
var invalidChars = Path.GetInvalidFileNameChars();
|
||||||
|
var sb = new StringBuilder(token.Length);
|
||||||
|
foreach (var c in token)
|
||||||
|
{
|
||||||
|
sb.Append(invalidChars.Contains(c) ? '_' : c);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString().Trim(' ', '.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sanitizes the final formatted file name, ensuring no illegal characters or Windows reserved device names.
|
||||||
|
/// </summary>
|
||||||
|
public static string SanitizeFileName(string? fileName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(fileName)) return string.Empty;
|
||||||
|
|
||||||
|
var invalidChars = Path.GetInvalidFileNameChars();
|
||||||
|
var sb = new StringBuilder(fileName.Length);
|
||||||
|
foreach (var c in fileName)
|
||||||
|
{
|
||||||
|
sb.Append(invalidChars.Contains(c) ? '_' : c);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = sb.ToString().Trim(' ', '.');
|
||||||
|
if (ReservedWindowsNames.Contains(result))
|
||||||
|
{
|
||||||
|
result = "_" + result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Formats a file name without extension by substituting template variables.
|
||||||
|
/// Available tokens: {song}, {title}, {artist}, {author}, {level}, {preset},
|
||||||
|
/// {date}, {time}, {year}, {month}, {day}, {hour}, {minute}, {second},
|
||||||
|
/// {resolution}, {width}, {height}, {fps}.
|
||||||
|
/// </summary>
|
||||||
|
public static string FormatFileName(
|
||||||
|
string? formatTemplate,
|
||||||
|
DateTime startTime,
|
||||||
|
RenderConfig config,
|
||||||
|
string? presetName = null,
|
||||||
|
string? levelPath = null)
|
||||||
|
{
|
||||||
|
var scn = scnGame.instance;
|
||||||
|
string rawSong = "";
|
||||||
|
string rawArtist = "";
|
||||||
|
string rawAuthor = "";
|
||||||
|
|
||||||
|
if (scn != null && scn.levelData != null)
|
||||||
|
{
|
||||||
|
rawSong = scn.levelData.song ?? "";
|
||||||
|
rawArtist = scn.levelData.artist ?? "";
|
||||||
|
rawAuthor = scn.levelData.author ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(rawSong) && !string.IsNullOrWhiteSpace(levelPath))
|
||||||
|
{
|
||||||
|
var dir = Path.GetDirectoryName(levelPath);
|
||||||
|
rawSong = !string.IsNullOrWhiteSpace(dir)
|
||||||
|
? Path.GetFileName(dir)
|
||||||
|
: Path.GetFileNameWithoutExtension(levelPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
var song = SanitizeToken(StripRichText(rawSong));
|
||||||
|
var artist = SanitizeToken(StripRichText(rawArtist));
|
||||||
|
var author = SanitizeToken(StripRichText(rawAuthor));
|
||||||
|
var level = !string.IsNullOrWhiteSpace(levelPath)
|
||||||
|
? SanitizeToken(Path.GetFileNameWithoutExtension(levelPath))
|
||||||
|
: song;
|
||||||
|
var preset = SanitizeToken(presetName ?? "");
|
||||||
|
|
||||||
|
var date = startTime.ToString("yyyy-MM-dd");
|
||||||
|
var time = startTime.ToString("HH-mm-ss");
|
||||||
|
var year = startTime.ToString("yyyy");
|
||||||
|
var month = startTime.ToString("MM");
|
||||||
|
var day = startTime.ToString("dd");
|
||||||
|
var hour = startTime.ToString("HH");
|
||||||
|
var minute = startTime.ToString("mm");
|
||||||
|
var second = startTime.ToString("ss");
|
||||||
|
|
||||||
|
var width = config.RenderWidth.ToString();
|
||||||
|
var height = config.RenderHeight.ToString();
|
||||||
|
var resolution = $"{config.RenderWidth}x{config.RenderHeight}";
|
||||||
|
|
||||||
|
var effectiveFps = Math.Min(config.VideoFPS, config.RenderFPS);
|
||||||
|
var fps = Math.Abs(effectiveFps % 1) < 0.001
|
||||||
|
? ((int)effectiveFps).ToString()
|
||||||
|
: effectiveFps.ToString("0.##");
|
||||||
|
|
||||||
|
var tokens = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
{ "song", song },
|
||||||
|
{ "title", song },
|
||||||
|
{ "artist", artist },
|
||||||
|
{ "author", author },
|
||||||
|
{ "level", level },
|
||||||
|
{ "preset", preset },
|
||||||
|
{ "date", date },
|
||||||
|
{ "time", time },
|
||||||
|
{ "year", year },
|
||||||
|
{ "month", month },
|
||||||
|
{ "day", day },
|
||||||
|
{ "hour", hour },
|
||||||
|
{ "minute", minute },
|
||||||
|
{ "second", second },
|
||||||
|
{ "width", width },
|
||||||
|
{ "height", height },
|
||||||
|
{ "resolution", resolution },
|
||||||
|
{ "fps", fps }
|
||||||
|
};
|
||||||
|
|
||||||
|
var template = string.IsNullOrWhiteSpace(formatTemplate)
|
||||||
|
? GlobalSettings.DefaultFileNameFormat
|
||||||
|
: formatTemplate;
|
||||||
|
|
||||||
|
var replaced = TokenRegex.Replace(template, match =>
|
||||||
|
{
|
||||||
|
var key = match.Groups[1].Value;
|
||||||
|
return tokens.TryGetValue(key, out var val) ? val : match.Value;
|
||||||
|
});
|
||||||
|
|
||||||
|
var sanitized = SanitizeFileName(replaced);
|
||||||
|
return string.IsNullOrWhiteSpace(sanitized)
|
||||||
|
? startTime.ToString("yyyy-MM-dd HH-mm-ss")
|
||||||
|
: sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a collision-free output file path by appending (1), (2), etc. if a file with the given name already exists.
|
||||||
|
/// </summary>
|
||||||
|
public static string GetUniqueFilePath(string directory, string fileNameWithoutExt, string ext)
|
||||||
|
{
|
||||||
|
var normalizedExt = "." + ext.TrimStart('.').ToLowerInvariant();
|
||||||
|
var fileName = $"{fileNameWithoutExt}{normalizedExt}";
|
||||||
|
var fullPath = Path.Combine(directory, fileName);
|
||||||
|
|
||||||
|
var counter = 1;
|
||||||
|
while (File.Exists(fullPath))
|
||||||
|
{
|
||||||
|
fileName = $"{fileNameWithoutExt} ({counter}){normalizedExt}";
|
||||||
|
fullPath = Path.Combine(directory, fileName);
|
||||||
|
counter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user