SHA256
778 lines
30 KiB
C#
778 lines
30 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using ADOFAIRenderer.API;
|
|
using ADOFAIRenderer.Core;
|
|
using ADOFAIRenderer.Data;
|
|
using ADOFAIRenderer.Localization;
|
|
using ADOFAIRenderer.Util;
|
|
using ADOFAIRenderer.Util.FFmpeg;
|
|
using MelonLoader;
|
|
using O5Kit.Control;
|
|
using O5Kit.Core;
|
|
using O5Kit.Factory;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
|
|
namespace ADOFAIRenderer.UI.Window;
|
|
|
|
internal static class RenderConfigWindow
|
|
{
|
|
private static O5Window? _configWindow;
|
|
|
|
// Controls cache for validation & sync
|
|
private static O5Slider? _renderFpsSlider;
|
|
private static O5Slider? _videoFPSSlider;
|
|
private static O5Slider? _renderWidthSlider;
|
|
private static O5Slider? _renderHeightSlider;
|
|
private static O5Slider? _delayAfterEndSlider;
|
|
private static O5Toggle? _showJudgementToggle;
|
|
|
|
private static O5Dropdown<Codec>? _codecDropdown;
|
|
private static O5Dropdown<RateControlMode>? _rateControlDropdown;
|
|
private static O5InputField? _bitrateInput;
|
|
private static O5InputField? _maxBitrateInput;
|
|
private static O5InputField? _qpInput;
|
|
private static O5Slider? _keyframeIntervalSlider;
|
|
private static O5Toggle? _forceSoftwareToggle;
|
|
|
|
private static O5Toggle? _renderAudioToggle;
|
|
private static O5Dropdown<int>? _sampleRateDropdown;
|
|
private static O5Dropdown<int>? _channelsDropdown;
|
|
private static O5Slider? _audioBitrateSlider;
|
|
|
|
private static O5InputField? _outputDirInput;
|
|
|
|
private static O5Dropdown<string>? _fileExtensionDropdown;
|
|
private static O5InputField? _randomSeedInput;
|
|
private static O5Toggle? _processAudioToggle;
|
|
private static O5Toggle? _nonTileToggle;
|
|
|
|
public static void CreateWindow()
|
|
{
|
|
if (_configWindow == null)
|
|
{
|
|
_configWindow = RendererUIManager.Instance.WindowManager.Create(new O5WindowOptions
|
|
{
|
|
Id = "RendererMod_RenderConfig",
|
|
Title = LocalizationManager.Get("ui.render_config.window_title"),
|
|
Size = new Vector2(580f, 480f),
|
|
Draggable = true,
|
|
Resizable = true,
|
|
ShowCloseButton = true
|
|
});
|
|
|
|
_configWindow.CloseRequested += win =>
|
|
{
|
|
win.Rect.gameObject.SetActive(false);
|
|
};
|
|
|
|
BuildUI(_configWindow.Content);
|
|
}
|
|
|
|
// ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract
|
|
var config = RenderManager.Instance.Configuration ??= new RenderConfig();
|
|
ApplyConfigToUI(config);
|
|
|
|
SetWindowActive(true);
|
|
_configWindow.BringToFront();
|
|
}
|
|
|
|
private static void BuildUI(RectTransform content)
|
|
{
|
|
var ctx = RendererUIManager.Context;
|
|
var (viewport, scrollContent, scroller) =
|
|
O5Factory.ScrollView(ctx, content, spacing: 10f, padding: 8f);
|
|
|
|
// ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract
|
|
var config = RenderManager.Instance.Configuration ??= new RenderConfig();
|
|
EnsureDefaultConfig(config);
|
|
|
|
double[] renderFpsSnap = { 23.976, 24, 29.97, 30, 59.94, 60, 144,
|
|
120, 240, 360, 480, 600, 720, // +120
|
|
288, 576, 864, 1152, 1440, 1728, // +288
|
|
3000 // MAX
|
|
};
|
|
double[] videoFpsSnap = { 23.976, 24, 29.97, 30, 59.94, 60, 72,
|
|
120, 180, 240, 300, 360, 420, 480, 540, // +60
|
|
144, 216, 288, 432, 504, 576, // +72
|
|
600 // MAX
|
|
};
|
|
|
|
double[] audioBitrateSnap = { 64, 96, 128, 192, 256, 320, 384 };
|
|
|
|
// 1-3. Top section: 좌/우 컨테이너 1개씩으로 3행을 쌓는다 (중앙선 1개)
|
|
var (topLeft, topRight) = CreateTwoColumnPanel(scrollContent, spacing: 8f, columnSpacing: 10f);
|
|
|
|
_renderFpsSlider = O5Factory.Slider(
|
|
ctx: ctx, parent: CreateFieldSlot(topLeft),
|
|
defaultValue: 60, min: 0.01, max: 3000, value: 60,
|
|
format: "F3", clampMode: ClampMode.All,
|
|
|
|
sliderFilter: value => renderFpsSnap.OrderBy(n => Math.Abs(n - value)).First(),
|
|
inputFilter: null, onComplete: null, onChanged: val => { config.RenderFPS = val; },
|
|
text: LocalizationManager.Get("ui.render_config.render_fps"),
|
|
id: "slider_render_fps", showFill: true
|
|
);
|
|
|
|
_videoFPSSlider = O5Factory.Slider(
|
|
ctx: ctx, parent: CreateFieldSlot(topRight),
|
|
defaultValue: 60, min: 0.01, max: 600, value: 60,
|
|
format: "F3", clampMode: ClampMode.All,
|
|
|
|
sliderFilter: value => videoFpsSnap.OrderBy(n => Math.Abs(n - value)).First(),
|
|
inputFilter: null, onComplete: null, onChanged: val => { config.VideoFPS = val; },
|
|
text: LocalizationManager.Get("ui.render_config.video_fps"),
|
|
id: "slider_video_fps", showFill: true
|
|
);
|
|
|
|
_renderWidthSlider = O5Factory.Slider(
|
|
ctx: ctx, parent: CreateFieldSlot(topLeft),
|
|
defaultValue: 2560, min: 0, max: 15360, value: 2560,
|
|
format: "F0", clampMode: ClampMode.All,
|
|
|
|
sliderFilter: value => Math.Max(value, 2), inputFilter: value => Math.Max(value, 2),
|
|
onComplete: null, onChanged: val => { config.RenderWidth = (int)Math.Round(val); },
|
|
text: LocalizationManager.Get("ui.render_config.render_width"),
|
|
id: "slider_render_width", showFill: true
|
|
);
|
|
|
|
_renderHeightSlider = O5Factory.Slider(
|
|
ctx: ctx, parent: CreateFieldSlot(topRight),
|
|
defaultValue: 1440, min: 0, max: 8640, value: 1440,
|
|
format: "F0", clampMode: ClampMode.All,
|
|
|
|
sliderFilter: value => Math.Max(value, 2), inputFilter: value => Math.Max(value, 2),
|
|
onComplete: null, onChanged: val => { config.RenderWidth = (int)Math.Round(val); },
|
|
text: LocalizationManager.Get("ui.render_config.render_height"),
|
|
id: "slider_render_height", showFill: true
|
|
);
|
|
|
|
_delayAfterEndSlider = O5Factory.Slider(
|
|
ctx: ctx, parent: CreateFieldSlot(topLeft),
|
|
defaultValue: 15, min: 0, max: 9999.999, value: 15,
|
|
format: "F3", clampMode: ClampMode.All,
|
|
|
|
sliderFilter: null, inputFilter: null,
|
|
onComplete: null, onChanged: val => { config.DelayAfterEnd = val; },
|
|
text: LocalizationManager.Get("ui.render_config.delay_after_end"),
|
|
id: "slider_delay_after_end", showFill: true
|
|
);
|
|
|
|
_showJudgementToggle = CreateToggleField(
|
|
ctx, CreateFieldSlot(topRight), "",
|
|
false, config.ShowJudgementText,
|
|
LocalizationManager.Get("ui.render_config.show_judgement"),
|
|
"toggle_show_judgement",
|
|
val => config.ShowJudgementText = val,
|
|
generateLabel: false
|
|
);
|
|
|
|
// 4. Video Encoding Config (Collapsible Card)
|
|
var (_, videoContent) = O5Factory.Card(
|
|
ctx, scrollContent,
|
|
LocalizationManager.Get("ui.render_config.video_encoding"),
|
|
activeValue: true,
|
|
onActiveChanged: null,
|
|
onDeleteClick: null,
|
|
showDeleteButton: false,
|
|
showActiveToggle: false
|
|
);
|
|
|
|
var (videoLeft, videoRight) = CreateTwoColumnPanel(videoContent);
|
|
var codecs = (Codec[])Enum.GetValues(typeof(Codec));
|
|
_codecDropdown = CreateDropdownField(
|
|
ctx, CreateFieldSlot(videoLeft),
|
|
LocalizationManager.Get("ui.render_config.codec"),
|
|
Codec.H264, config.VideoConfig.Codec,
|
|
codecs, c => c.ToString(),
|
|
"dd_video_codec",
|
|
c => config.VideoConfig.Codec = c
|
|
);
|
|
_bitrateInput = CreateInputField(
|
|
ctx, CreateFieldSlot(videoLeft),
|
|
LocalizationManager.Get("ui.render_config.bitrate"),
|
|
"30000", config.VideoConfig.Bitrate.ToString(),
|
|
"input_bitrate",
|
|
TMPro.TMP_InputField.CharacterValidation.Integer,
|
|
val => { if (int.TryParse(val, out var b) && b > 0) config.VideoConfig.Bitrate = b; }
|
|
);
|
|
_qpInput = CreateInputField(
|
|
ctx, CreateFieldSlot(videoLeft),
|
|
LocalizationManager.Get("ui.render_config.qp"),
|
|
"18", config.VideoConfig.QP.ToString(),
|
|
"input_qp",
|
|
TMPro.TMP_InputField.CharacterValidation.Integer,
|
|
val => { if (int.TryParse(val, out var qp) && qp >= 0) config.VideoConfig.QP = qp; }
|
|
);
|
|
|
|
var rateModes = (RateControlMode[])Enum.GetValues(typeof(RateControlMode));
|
|
_rateControlDropdown = CreateDropdownField(
|
|
ctx, CreateFieldSlot(videoRight),
|
|
LocalizationManager.Get("ui.render_config.rate_control"),
|
|
RateControlMode.CBR, config.VideoConfig.RateControlMode,
|
|
rateModes, m => m.ToString(),
|
|
"dd_rate_control",
|
|
m => config.VideoConfig.RateControlMode = m
|
|
);
|
|
_maxBitrateInput = CreateInputField(
|
|
ctx, CreateFieldSlot(videoRight),
|
|
LocalizationManager.Get("ui.render_config.max_bitrate"),
|
|
"50000", config.VideoConfig.MaxBitrate.ToString(),
|
|
"input_max_bitrate",
|
|
TMPro.TMP_InputField.CharacterValidation.Integer,
|
|
val => { if (int.TryParse(val, out var mb) && mb > 0) config.VideoConfig.MaxBitrate = mb; }
|
|
);
|
|
|
|
_forceSoftwareToggle = CreateToggleField(
|
|
ctx, CreateFieldSlot(videoRight), "",
|
|
true, config.VideoConfig.ForceSoftwareEncoder,
|
|
LocalizationManager.Get("ui.render_config.force_software"),
|
|
"toggle_force_software",
|
|
val => config.AudioConfig.RenderAudio = val,
|
|
generateLabel: true
|
|
);
|
|
|
|
_keyframeIntervalSlider = O5Factory.Slider(
|
|
ctx: ctx, parent: videoContent,
|
|
defaultValue: 2, min: 0.1, max: 10, value: 2,
|
|
format: "F1", clampMode: ClampMode.All,
|
|
|
|
sliderFilter: null, inputFilter: null,
|
|
onComplete: null, onChanged: val => { config.VideoConfig.KeyframeInterval = val; },
|
|
text: LocalizationManager.Get("ui.render_config.keyframe_interval"),
|
|
id: "slider_keyframe_interval", showFill: true
|
|
);
|
|
|
|
// 5. Audio Encoding Config (Collapsible Card)
|
|
var (_, audioContent) = O5Factory.Card(
|
|
ctx, scrollContent,
|
|
LocalizationManager.Get("ui.render_config.audio_encoding"),
|
|
activeValue: true,
|
|
onActiveChanged: null,
|
|
onDeleteClick: null,
|
|
showDeleteButton: false,
|
|
showActiveToggle: false
|
|
);
|
|
|
|
var (audioLeft, audioRight) = CreateTwoColumnPanel(audioContent);
|
|
_renderAudioToggle = CreateToggleField(
|
|
ctx, CreateFieldSlot(audioLeft), "",
|
|
true, config.AudioConfig.RenderAudio,
|
|
LocalizationManager.Get("ui.render_config.render_audio"),
|
|
"toggle_render_audio",
|
|
val => config.AudioConfig.RenderAudio = val,
|
|
generateLabel: false
|
|
);
|
|
|
|
_audioBitrateSlider = O5Factory.Slider(
|
|
ctx: ctx, parent: CreateFieldSlot(audioRight),
|
|
defaultValue: 256, min: 64, max: 384, value: 256,
|
|
format: "F0", clampMode: ClampMode.All,
|
|
|
|
sliderFilter: value => audioBitrateSnap.OrderBy(n => Math.Abs(n - value)).First(),
|
|
inputFilter: null, onComplete: null, onChanged: val => { config.AudioConfig.AudioBitrate = (int)Math.Round(val) * 1000; },
|
|
text: LocalizationManager.Get("ui.render_config.audio_bitrate"),
|
|
id: "slider_audio_bitrate", showFill: true
|
|
);
|
|
|
|
var channels = new[] { 1, 2 };
|
|
_channelsDropdown = CreateDropdownField(
|
|
ctx, CreateFieldSlot(audioLeft),
|
|
LocalizationManager.Get("ui.render_config.channels"),
|
|
2, config.AudioConfig.AudioChannels,
|
|
channels, c => c == 1 ? "1 (Mono)" : "2 (Stereo)",
|
|
"dd_channels",
|
|
c => config.AudioConfig.AudioChannels = c
|
|
);
|
|
|
|
var sampleRates = new[] { 44100, 48000, 96000 };
|
|
_sampleRateDropdown = CreateDropdownField(
|
|
ctx, CreateFieldSlot(audioRight),
|
|
LocalizationManager.Get("ui.render_config.sample_rate"),
|
|
48000, config.AudioConfig.AudioSampleRate,
|
|
sampleRates, r => $"{r/1000.0} kHz",
|
|
"dd_sample_rate",
|
|
r => config.AudioConfig.AudioSampleRate = r
|
|
);
|
|
|
|
// 6. Global Settings (Collapsible Card)
|
|
var (_, globalContent) = O5Factory.Card(
|
|
ctx, scrollContent,
|
|
LocalizationManager.Get("ui.render_config.global_settings"),
|
|
activeValue: true,
|
|
onActiveChanged: null,
|
|
onDeleteClick: null,
|
|
showDeleteButton: false,
|
|
showActiveToggle: false
|
|
);
|
|
|
|
_outputDirInput = CreateFullWidthInputField(
|
|
ctx, globalContent,
|
|
LocalizationManager.Get("ui.render_config.output_dir"),
|
|
GlobalSettings.DefaultOutputDirectory,
|
|
GlobalSettings.EffectiveOutputDirectory,
|
|
"input_output_dir",
|
|
val =>
|
|
{
|
|
var trimmed = val?.Trim() ?? string.Empty;
|
|
if (string.IsNullOrWhiteSpace(trimmed) ||
|
|
string.Equals(trimmed, GlobalSettings.DefaultOutputDirectory, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
GlobalSettings.Current.OutputDirectory = null;
|
|
}
|
|
else
|
|
{
|
|
GlobalSettings.Current.OutputDirectory = trimmed;
|
|
}
|
|
}
|
|
);
|
|
|
|
var (globalLeft, globalRight) = CreateTwoColumnPanel(globalContent);
|
|
O5Factory.Button(
|
|
ctx, globalLeft,
|
|
() =>
|
|
{
|
|
var initial = string.IsNullOrWhiteSpace(_outputDirInput.Value)
|
|
? GlobalSettings.DefaultOutputDirectory
|
|
: _outputDirInput.Value;
|
|
FolderDialogUtil.OpenFolderBrowser(initial, selected =>
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(selected))
|
|
{
|
|
_outputDirInput.Set(selected);
|
|
GlobalSettings.Current.OutputDirectory = selected;
|
|
GlobalSettings.Save();
|
|
}
|
|
});
|
|
},
|
|
LocalizationManager.Get("ui.render_config.browse"),
|
|
"btn_browse_output"
|
|
);
|
|
|
|
O5Factory.Button(
|
|
ctx, globalRight,
|
|
() =>
|
|
{
|
|
_outputDirInput.Set(GlobalSettings.DefaultOutputDirectory);
|
|
GlobalSettings.Current.OutputDirectory = null;
|
|
GlobalSettings.Save();
|
|
},
|
|
LocalizationManager.Get("ui.render_config.reset_default"),
|
|
"btn_reset_output"
|
|
);
|
|
|
|
// 7. Advanced Settings (Collapsible Card)
|
|
var (_, advContent) = O5Factory.Card(
|
|
ctx, scrollContent,
|
|
LocalizationManager.Get("ui.render_config.advanced_settings"),
|
|
activeValue: true,
|
|
onActiveChanged: null,
|
|
onDeleteClick: null,
|
|
showDeleteButton: false,
|
|
showActiveToggle: false
|
|
);
|
|
|
|
var (advLeft, advRight) = CreateTwoColumnPanel(advContent);
|
|
var extensions = new[] { "mkv", "mp4", "mov", "webm", "avi" };
|
|
_fileExtensionDropdown = CreateDropdownField(
|
|
ctx, CreateFieldSlot(advLeft),
|
|
LocalizationManager.Get("ui.render_config.file_extension"),
|
|
"mkv", config.AdvancedConfig.FileExtension,
|
|
extensions, e => e,
|
|
"dd_adv_file_extension",
|
|
e => { if (!string.IsNullOrEmpty(e)) config.AdvancedConfig.FileExtension = e; }
|
|
);
|
|
_processAudioToggle = CreateToggleField(
|
|
ctx, CreateFieldSlot(advLeft), "",
|
|
true, config.AdvancedConfig.ProcessAudio,
|
|
LocalizationManager.Get("ui.render_config.process_audio"),
|
|
"toggle_process_audio",
|
|
val => config.AdvancedConfig.ProcessAudio = val,
|
|
generateLabel: false
|
|
);
|
|
|
|
_randomSeedInput = CreateInputField(
|
|
ctx, CreateFieldSlot(advRight),
|
|
LocalizationManager.Get("ui.render_config.random_seed"),
|
|
"", config.AdvancedConfig.RandomSeed?.ToString() ?? "",
|
|
"input_random_seed",
|
|
TMPro.TMP_InputField.CharacterValidation.Integer,
|
|
val =>
|
|
{
|
|
if (int.TryParse(val, out var s)) config.AdvancedConfig.RandomSeed = s;
|
|
else if (string.IsNullOrWhiteSpace(val)) config.AdvancedConfig.RandomSeed = null;
|
|
}
|
|
);
|
|
_nonTileToggle = CreateToggleField(
|
|
ctx, CreateFieldSlot(advRight), "",
|
|
false, config.AdvancedConfig.NonTile,
|
|
LocalizationManager.Get("ui.render_config.non_tile"),
|
|
"toggle_non_tile",
|
|
val => config.AdvancedConfig.NonTile = val,
|
|
generateLabel: false
|
|
);
|
|
|
|
// 8. Go Render Button
|
|
O5Factory.Button(
|
|
ctx, scrollContent,
|
|
TryGoRender,
|
|
LocalizationManager.Get("ui.render_config.go_render"),
|
|
"btn_go_render",
|
|
height: 48f
|
|
);
|
|
}
|
|
|
|
private static void EnsureDefaultConfig(RenderConfig config)
|
|
{
|
|
if (config.RenderWidth <= 0) config.RenderWidth = Screen.width > 0 ? Screen.width : 2560;
|
|
if (config.RenderHeight <= 0) config.RenderHeight = Screen.height > 0 ? Screen.height : 1440;
|
|
if (config.RenderFPS <= 0) config.RenderFPS = 60.0;
|
|
if (config.VideoFPS <= 0) config.VideoFPS = 60.0;
|
|
if (config.DelayAfterEnd < 0) config.DelayAfterEnd = 15.0;
|
|
}
|
|
|
|
private static void ApplyConfigToUI(RenderConfig config)
|
|
{
|
|
EnsureDefaultConfig(config);
|
|
|
|
_renderFpsSlider?.Set((float) config.RenderFPS, false);
|
|
_videoFPSSlider?.Set((float) config.VideoFPS, false);
|
|
_renderWidthSlider?.Set(config.RenderWidth, false);
|
|
_renderHeightSlider?.Set(config.RenderHeight, false);
|
|
_delayAfterEndSlider?.Set((float) config.DelayAfterEnd, false);
|
|
_showJudgementToggle?.Set(config.ShowJudgementText, false);
|
|
|
|
_codecDropdown?.Set(config.VideoConfig.Codec, false);
|
|
_rateControlDropdown?.Set(config.VideoConfig.RateControlMode, false);
|
|
_bitrateInput?.Set(config.VideoConfig.Bitrate.ToString(), false);
|
|
_maxBitrateInput?.Set(config.VideoConfig.MaxBitrate.ToString(), false);
|
|
_qpInput?.Set(config.VideoConfig.QP.ToString(), false);
|
|
_keyframeIntervalSlider?.Set(config.VideoConfig.KeyframeInterval, false);
|
|
_forceSoftwareToggle?.Set(config.VideoConfig.ForceSoftwareEncoder, false);
|
|
|
|
_renderAudioToggle?.Set(config.AudioConfig.RenderAudio, false);
|
|
_sampleRateDropdown?.Set(config.AudioConfig.AudioSampleRate, false);
|
|
_channelsDropdown?.Set(config.AudioConfig.AudioChannels, false);
|
|
_audioBitrateSlider?.Set(config.AudioConfig.AudioBitrate / 1000.0, false);
|
|
|
|
_outputDirInput?.Set(GlobalSettings.EffectiveOutputDirectory, false);
|
|
|
|
_fileExtensionDropdown?.Set(config.AdvancedConfig.FileExtension, false);
|
|
_randomSeedInput?.Set(config.AdvancedConfig.RandomSeed?.ToString() ?? "", false);
|
|
_processAudioToggle?.Set(config.AdvancedConfig.ProcessAudio, false);
|
|
_nonTileToggle?.Set(config.AdvancedConfig.NonTile, false);
|
|
}
|
|
|
|
private static void TryGoRender()
|
|
{
|
|
var config = RenderManager.Instance.Configuration;
|
|
|
|
// Sync Video Config
|
|
if (_bitrateInput != null && int.TryParse(_bitrateInput.Value, out var bitrate) && bitrate > 0)
|
|
{
|
|
config.VideoConfig.Bitrate = bitrate;
|
|
}
|
|
if (_maxBitrateInput != null && int.TryParse(_maxBitrateInput.Value, out var maxBitrate) && maxBitrate > 0)
|
|
{
|
|
config.VideoConfig.MaxBitrate = maxBitrate;
|
|
}
|
|
if (_qpInput != null && int.TryParse(_qpInput.Value, out var qp) && qp >= 0)
|
|
{
|
|
config.VideoConfig.QP = qp;
|
|
}
|
|
|
|
// Sync Advanced Config
|
|
if (_randomSeedInput != null)
|
|
{
|
|
if (int.TryParse(_randomSeedInput.Value, out var seed))
|
|
{
|
|
config.AdvancedConfig.RandomSeed = seed;
|
|
}
|
|
else if (string.IsNullOrWhiteSpace(_randomSeedInput.Value))
|
|
{
|
|
config.AdvancedConfig.RandomSeed = null;
|
|
}
|
|
}
|
|
|
|
// Sync Output Directory
|
|
if (_outputDirInput != null)
|
|
{
|
|
var path = _outputDirInput.Value.Trim();
|
|
if (string.IsNullOrWhiteSpace(path) ||
|
|
string.Equals(path, GlobalSettings.DefaultOutputDirectory, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
GlobalSettings.Current.OutputDirectory = null;
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
var fullPath = Path.GetFullPath(path);
|
|
if (!Directory.Exists(fullPath))
|
|
{
|
|
Directory.CreateDirectory(fullPath);
|
|
}
|
|
GlobalSettings.Current.OutputDirectory = fullPath;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ShowValidationError($"Output directory path is invalid:\n{ex.Message}");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
GlobalSettings.Save();
|
|
SetWindowActive(false);
|
|
|
|
new RenderSession().Start(config, RenderManager.Instance.levelPath);
|
|
}
|
|
|
|
private static void ShowValidationError(string message)
|
|
{
|
|
Melon<RenderMod>.Logger.Error(message);
|
|
CommonWarningDialogUI.ShowDialog(
|
|
LocalizationManager.Get("ui.render_config.validation_title"),
|
|
message,
|
|
new DialogButton("OK", () => { })
|
|
);
|
|
}
|
|
|
|
// 카드(또는 상단 섹션)당 하나의 좌/우 컨테이너를 만들어
|
|
// 가로 한 줄 두 필드 사이 중앙이 행마다 어긋나지 않도록 한다.
|
|
// - 패널 1개 = 중앙선 1개 (행마다 HLG를 만들던 기존 방식의 P1-P2 어긋남 방지)
|
|
// - 좌/우 컬럼은 preferredWidth=0 + flexibleWidth=1 로 항상 50:50
|
|
// - 각 필드는 Field 슬롯(label 18 + 간격 3 + 컨트롤 50)으로 높이를 고정해
|
|
// 좌/우 스택을 써도 행 단위 Y 정렬이 유지되도록 한다.
|
|
private static (RectTransform left, RectTransform right) CreateTwoColumnPanel(
|
|
Transform parent, float spacing = 8f, float columnSpacing = 8f)
|
|
{
|
|
var panelObj = new GameObject("TwoColPanel");
|
|
panelObj.transform.SetParent(parent, false);
|
|
|
|
panelObj.AddComponent<RectTransform>();
|
|
|
|
var hLayout = panelObj.AddComponent<HorizontalLayoutGroup>();
|
|
hLayout.spacing = spacing;
|
|
hLayout.padding = new RectOffset(0, 0, 0, 0);
|
|
hLayout.childAlignment = TextAnchor.UpperCenter;
|
|
hLayout.childControlWidth = true;
|
|
hLayout.childControlHeight = true;
|
|
hLayout.childForceExpandWidth = true;
|
|
hLayout.childForceExpandHeight = false;
|
|
|
|
// ReSharper disable once InconsistentNaming
|
|
var panelLE = panelObj.AddComponent<LayoutElement>();
|
|
panelLE.flexibleWidth = 1f;
|
|
|
|
var left = CreateColumn(panelObj.transform, "LeftCol", columnSpacing);
|
|
var right = CreateColumn(panelObj.transform, "RightCol", columnSpacing);
|
|
|
|
return (left, right);
|
|
}
|
|
|
|
private static RectTransform CreateColumn(Transform parent, string name, float spacing)
|
|
{
|
|
var colObj = new GameObject(name);
|
|
colObj.transform.SetParent(parent, false);
|
|
var colRect = colObj.AddComponent<RectTransform>();
|
|
|
|
var colLayout = colObj.AddComponent<VerticalLayoutGroup>();
|
|
colLayout.spacing = spacing;
|
|
colLayout.padding = new RectOffset(0, 0, 0, 0);
|
|
colLayout.childControlWidth = true;
|
|
colLayout.childControlHeight = true;
|
|
colLayout.childForceExpandWidth = true;
|
|
colLayout.childForceExpandHeight = false;
|
|
|
|
// ReSharper disable once InconsistentNaming
|
|
// 핵심: 좌/우 선호 너비를 0으로 강제해 항상 정확히 절반씩 나누도록 한다.
|
|
// (기존 행마다 HLG 방식은 라벨/값 텍스트 길이에 따라 preferred가 달라져
|
|
// 중앙이 (W + P1 - P2) / 2 로 행마다 틀어졌음)
|
|
var colLE = colObj.AddComponent<LayoutElement>();
|
|
colLE.minWidth = 0f;
|
|
colLE.preferredWidth = 0f;
|
|
colLE.flexibleWidth = 1f;
|
|
|
|
return colRect;
|
|
}
|
|
|
|
private static RectTransform CreateFieldSlot(Transform column)
|
|
{
|
|
var slotObj = new GameObject("Field");
|
|
slotObj.transform.SetParent(column, false);
|
|
var slotRect = slotObj.AddComponent<RectTransform>();
|
|
|
|
var slotLayout = slotObj.AddComponent<VerticalLayoutGroup>();
|
|
slotLayout.spacing = 3f;
|
|
slotLayout.padding = new RectOffset(0, 0, 0, 0);
|
|
slotLayout.childControlWidth = true;
|
|
slotLayout.childControlHeight = true;
|
|
slotLayout.childForceExpandWidth = true;
|
|
slotLayout.childForceExpandHeight = false;
|
|
|
|
// ReSharper disable once InconsistentNaming
|
|
var slotLE = slotObj.AddComponent<LayoutElement>();
|
|
slotLE.flexibleWidth = 1f;
|
|
|
|
return slotRect;
|
|
}
|
|
|
|
private static O5InputField CreateInputField(
|
|
O5Context ctx,
|
|
Transform colParent,
|
|
string labelText,
|
|
string? defaultValue,
|
|
string value,
|
|
string id,
|
|
TMPro.TMP_InputField.CharacterValidation characterValidation = TMPro.TMP_InputField.CharacterValidation.None,
|
|
Action<string>? onChanged = null)
|
|
{
|
|
var label = O5Factory.ControlText(ctx, colParent, 13f, noPad: true);
|
|
label.text = labelText;
|
|
label.color = ctx.Theme.TextDim;
|
|
|
|
// ReSharper disable once InconsistentNaming
|
|
var labelLE = label.gameObject.AddComponent<LayoutElement>();
|
|
labelLE.preferredHeight = 18f;
|
|
labelLE.minHeight = 18f;
|
|
|
|
var inputField = O5Factory.Input(
|
|
ctx: ctx,
|
|
parent: colParent,
|
|
defaultValue: defaultValue,
|
|
value: value,
|
|
onChanged: onChanged,
|
|
placeholder: labelText,
|
|
icon: null,
|
|
id: id
|
|
);
|
|
|
|
inputField.InputField.characterValidation = characterValidation;
|
|
return inputField;
|
|
}
|
|
|
|
private static O5InputField CreateFullWidthInputField(
|
|
O5Context ctx,
|
|
Transform parent,
|
|
string labelText,
|
|
string? defaultValue,
|
|
string value,
|
|
string id,
|
|
Action<string>? onChanged = null)
|
|
{
|
|
var container = new GameObject($"FullWidth_{id}");
|
|
container.transform.SetParent(parent, false);
|
|
|
|
var containerRect = container.AddComponent<RectTransform>();
|
|
var layout = container.AddComponent<VerticalLayoutGroup>();
|
|
layout.spacing = 3f;
|
|
layout.childControlWidth = true;
|
|
layout.childControlHeight = true;
|
|
layout.childForceExpandWidth = true;
|
|
layout.childForceExpandHeight = false;
|
|
|
|
var label = O5Factory.ControlText(ctx, containerRect, 13f, noPad: true);
|
|
label.text = labelText;
|
|
label.color = ctx.Theme.TextDim;
|
|
|
|
// ReSharper disable once InconsistentNaming
|
|
var labelLE = label.gameObject.AddComponent<LayoutElement>();
|
|
labelLE.preferredHeight = 18f;
|
|
labelLE.minHeight = 18f;
|
|
|
|
var input = O5Factory.Input(
|
|
ctx: ctx,
|
|
parent: containerRect,
|
|
defaultValue: defaultValue,
|
|
value: value,
|
|
onChanged: onChanged,
|
|
placeholder: labelText,
|
|
icon: null,
|
|
id: id
|
|
);
|
|
|
|
return input;
|
|
}
|
|
|
|
private static O5Toggle CreateToggleField(
|
|
O5Context ctx,
|
|
Transform colParent,
|
|
string labelText,
|
|
bool? defaultValue,
|
|
bool value,
|
|
string toggleText,
|
|
string id,
|
|
Action<bool>? onChanged = null,
|
|
bool generateLabel = true)
|
|
{
|
|
if (generateLabel)
|
|
{
|
|
var label = O5Factory.ControlText(ctx, colParent, 13f, noPad: true);
|
|
label.text = labelText;
|
|
label.color = ctx.Theme.TextDim;
|
|
|
|
// ReSharper disable once InconsistentNaming
|
|
var labelLE = label.gameObject.AddComponent<LayoutElement>();
|
|
labelLE.preferredHeight = 18f;
|
|
labelLE.minHeight = 18f;
|
|
}
|
|
|
|
return O5Factory.Toggle(
|
|
ctx: ctx,
|
|
parent: colParent,
|
|
defaultValue: defaultValue,
|
|
value: value,
|
|
onChanged: onChanged,
|
|
text: toggleText,
|
|
id: id
|
|
);
|
|
}
|
|
|
|
private static O5Dropdown<T> CreateDropdownField<T>(
|
|
O5Context ctx,
|
|
Transform colParent,
|
|
string labelText,
|
|
T defaultValue,
|
|
T value,
|
|
IReadOnlyList<T> values,
|
|
Func<T, string> display,
|
|
string id,
|
|
Action<T>? onChanged = null)
|
|
{
|
|
var label = O5Factory.ControlText(ctx, colParent, 13f, noPad: true);
|
|
label.text = labelText;
|
|
label.color = ctx.Theme.TextDim;
|
|
|
|
// ReSharper disable once InconsistentNaming
|
|
var labelLE = label.gameObject.AddComponent<LayoutElement>();
|
|
labelLE.preferredHeight = 18f;
|
|
labelLE.minHeight = 18f;
|
|
|
|
var ddContainer = new GameObject($"DD_{id}");
|
|
ddContainer.transform.SetParent(colParent, false);
|
|
|
|
// ReSharper disable once InconsistentNaming
|
|
var ddLE = ddContainer.AddComponent<LayoutElement>();
|
|
ddLE.preferredHeight = 50f;
|
|
ddLE.minHeight = 0f;
|
|
|
|
return O5Factory.DropDown(
|
|
ctx: ctx,
|
|
parent: ddContainer.transform,
|
|
defaultValue: defaultValue,
|
|
value: value,
|
|
values: values,
|
|
display: display,
|
|
onChanged: val => { if (val != null) onChanged?.Invoke(val); },
|
|
id: id
|
|
);
|
|
}
|
|
|
|
private static void SetWindowActive(bool enabled)
|
|
{
|
|
_configWindow?.Rect.gameObject.SetActive(enabled);
|
|
}
|
|
} |