Add Output Path Config Prototype

This commit is contained in:
copecone committed 2026-09-01 08:09:55 +09:00
1 parent 4097e14e7a
commit 43763b0b35
5 files changed
+478 -12

No files matched your search

+2 -2
View File
@@ -411,7 +411,7 @@ internal class RenderManager : MonoBehaviour
public void SetupCaptureDirectory() public void SetupCaptureDirectory()
{ {
var renderDir = Path.Combine(MelonEnvironment.GameRootDirectory, "Render"); var renderDir = GlobalSettings.EffectiveOutputDirectory;
FileUtil.CreateDirectory(renderDir); FileUtil.CreateDirectory(renderDir);
} }
@@ -525,7 +525,7 @@ internal class RenderManager : MonoBehaviour
else { else {
var fileName = startTime.ToString("yyyy-MM-dd HH-mm-ss") + var fileName = startTime.ToString("yyyy-MM-dd HH-mm-ss") +
$".{Configuration.AdvancedConfig.FileExtension.TrimStart('.').ToLower()}"; $".{Configuration.AdvancedConfig.FileExtension.TrimStart('.').ToLower()}";
CurrentOutPath = Path.Combine(MelonEnvironment.GameRootDirectory, "Render", fileName); CurrentOutPath = Path.Combine(GlobalSettings.EffectiveOutputDirectory, fileName);
} }
FFmpegNativeEncoder? videoFFmpeg = null; FFmpegNativeEncoder? videoFFmpeg = null;
+79
View File
@@ -0,0 +1,79 @@
using System;
using System.IO;
using MelonLoader;
using MelonLoader.Utils;
using Newtonsoft.Json;
namespace ADOFAIRenderer.Data;
public class GlobalConfig
{
public string? OutputDirectory { get; set; }
}
public static class GlobalSettings
{
private static string SettingsFilePath =>
Path.Combine(MelonEnvironment.GameRootDirectory, "UserData", "ADOFAIRenderer", "global_settings.json");
public static GlobalConfig Current { get; set; } = new();
public static string DefaultOutputDirectory =>
Path.Combine(MelonEnvironment.GameRootDirectory, "Render");
public static string EffectiveOutputDirectory
{
get
{
if (!string.IsNullOrWhiteSpace(Current.OutputDirectory) && Directory.Exists(Current.OutputDirectory))
{
return Current.OutputDirectory;
}
return DefaultOutputDirectory;
}
}
static GlobalSettings()
{
Load();
}
public static void Load()
{
try
{
if (File.Exists(SettingsFilePath))
{
var json = File.ReadAllText(SettingsFilePath);
Current = JsonConvert.DeserializeObject<GlobalConfig>(json) ?? new GlobalConfig();
return;
}
}
catch (Exception ex)
{
MelonLogger.Warning($"[GlobalSettings] Failed to load global settings: {ex.Message}");
}
Current = new GlobalConfig();
}
public static void Save()
{
try
{
var dir = Path.GetDirectoryName(SettingsFilePath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
var json = JsonConvert.SerializeObject(Current, Formatting.Indented);
File.WriteAllText(SettingsFilePath, json);
}
catch (Exception ex)
{
MelonLogger.Warning($"[GlobalSettings] Failed to save global settings: {ex.Message}");
}
}
}
+2
View File
@@ -1,6 +1,7 @@
using System; using System;
using ADOFAIRenderer; using ADOFAIRenderer;
using ADOFAIRenderer.Core; using ADOFAIRenderer.Core;
using ADOFAIRenderer.Data;
using ADOFAIRenderer.Util; using ADOFAIRenderer.Util;
using FFmpeg.AutoGen; using FFmpeg.AutoGen;
using MelonLoader; using MelonLoader;
@@ -27,6 +28,7 @@ namespace ADOFAIRenderer
public override void OnInitializeMelon() public override void OnInitializeMelon()
{ {
GlobalSettings.Load();
ffmpeg.RootPath = MelonEnvironment.UserLibsDirectory; ffmpeg.RootPath = MelonEnvironment.UserLibsDirectory;
var renderManagerObj = new GameObject("Render Manager"); var renderManagerObj = new GameObject("Render Manager");
+234 -10
View File
@@ -1,8 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using ADOFAIRenderer.Core; using ADOFAIRenderer.Core;
using ADOFAIRenderer.Data; using ADOFAIRenderer.Data;
using ADOFAIRenderer.Data.Input; using ADOFAIRenderer.Data.Input;
using ADOFAIRenderer.Util;
using ADOFAIRenderer.Util.FFmpeg; using ADOFAIRenderer.Util.FFmpeg;
using MelonLoader; using MelonLoader;
using UnityEngine; using UnityEngine;
@@ -26,7 +28,9 @@ internal class RenderConfigUIManager : MonoBehaviour
private Button _audioEncodingConfigButton = null!; private Button _audioEncodingConfigButton = null!;
private VisualElement _audioEncodingConfigContainer = null!; private VisualElement _audioEncodingConfigContainer = null!;
private Button _globalSettingsButton = null!;
private VisualElement? _globalConfigContainer;
private Button _advancedSettingsButton = null!; private Button _advancedSettingsButton = null!;
private VisualElement? _advancedConfigContainer; private VisualElement? _advancedConfigContainer;
private Label _versionLabel = null!; private Label _versionLabel = null!;
@@ -92,19 +96,38 @@ internal class RenderConfigUIManager : MonoBehaviour
Melon<RenderMod>.Logger.Error("Button 'PresetSettingsButton' not found."); Melon<RenderMod>.Logger.Error("Button 'PresetSettingsButton' not found.");
} }
var bottomRow = CreateRow(); var rightBottomContainer = new VisualElement
bottomRow.pickingMode = PickingMode.Ignore; {
bottomRow.style.position = Position.Absolute; name = "RightBottomButtonContainer",
bottomRow.style.right = 0f; pickingMode = PickingMode.Ignore,
bottomRow.style.bottom = 0f; style =
bottomRow.style.justifyContent = Justify.FlexEnd; {
position = Position.Absolute,
right = 0f,
bottom = 0f,
flexDirection = FlexDirection.Column,
alignItems = Align.FlexEnd
}
};
var globalRow = CreateRow();
globalRow.pickingMode = PickingMode.Ignore;
globalRow.style.justifyContent = Justify.FlexEnd;
_globalSettingsButton = new Button { name = "GlobalSettingsButton", text = "Global Settings..." };
globalRow.Add(_globalSettingsButton);
rightBottomContainer.Add(globalRow);
var advancedRow = CreateRow();
advancedRow.pickingMode = PickingMode.Ignore;
advancedRow.style.justifyContent = Justify.FlexEnd;
_advancedSettingsButton = new Button { name = "AdvancedSettingsButton", text = "Advanced Settings..." }; _advancedSettingsButton = new Button { name = "AdvancedSettingsButton", text = "Advanced Settings..." };
bottomRow.Add(_advancedSettingsButton); advancedRow.Add(_advancedSettingsButton);
rightBottomContainer.Add(advancedRow);
(_root.childCount > 0 ? _root[0] : _root).Add(bottomRow); (_root.childCount > 0 ? _root[0] : _root).Add(rightBottomContainer);
Melon<RenderMod>.Logger.Msg("[UI] AdvancedSettingsButton dynamically created."); Melon<RenderMod>.Logger.Msg("[UI] GlobalSettingsButton and AdvancedSettingsButton dynamically created.");
_globalSettingsButton.RegisterCallback<ClickEvent>(_ => OpenGlobalConfig());
_advancedSettingsButton.RegisterCallback<ClickEvent>(_ => OpenAdvancedConfig()); _advancedSettingsButton.RegisterCallback<ClickEvent>(_ => OpenAdvancedConfig());
var versionText = var versionText =
@@ -143,6 +166,207 @@ internal class RenderConfigUIManager : MonoBehaviour
Melon<RenderMod>.Logger.Msg("[UI] VersionLabel dynamically created."); Melon<RenderMod>.Logger.Msg("[UI] VersionLabel dynamically created.");
} }
private void OpenGlobalConfig()
{
var tempGlobal = new GlobalConfig
{
OutputDirectory = GlobalSettings.Current.OutputDirectory
};
if (_globalConfigContainer != null && _root.Contains(_globalConfigContainer))
{
_root.Remove(_globalConfigContainer);
}
_globalConfigContainer = new VisualElement
{
name = "GlobalSettingsContainer",
style =
{
position = Position.Absolute,
width = Length.Percent(100),
height = Length.Percent(100),
left = 0,
top = 0,
backgroundColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f, 1.0f)),
justifyContent = Justify.Center,
alignItems = Align.Center
}
};
// 1. Title Row
var titleRow = CreateRow();
var titleLabel = new Label("Global Settings")
{
style =
{
fontSize = 24,
color = new StyleColor(Color.white)
}
};
titleRow.Add(titleLabel);
_globalConfigContainer.Add(titleRow);
// 2. Output Path Row
var pathRow = CreateRow();
var pathLabel = CreateFormLabel("Output Directory");
var defaultDir = GlobalSettings.DefaultOutputDirectory;
var currentDisplayDir = !string.IsNullOrWhiteSpace(tempGlobal.OutputDirectory)
? tempGlobal.OutputDirectory
: defaultDir;
var pathField = new TextField
{
name = "GlobalOutputDirectory",
label = "",
value = currentDisplayDir,
style =
{
width = 520f,
fontSize = 12f
}
};
var textInput = pathField.Q("unity-text-input");
if (textInput != null)
{
textInput.style.backgroundColor = new StyleColor(new Color(0.1f, 0.1f, 0.1f, 1.0f));
textInput.style.borderTopLeftRadius = 6f;
textInput.style.borderTopRightRadius = 6f;
textInput.style.borderBottomLeftRadius = 6f;
textInput.style.borderBottomRightRadius = 6f;
textInput.style.borderTopWidth = 1f;
textInput.style.borderBottomWidth = 1f;
textInput.style.borderLeftWidth = 1f;
textInput.style.borderRightWidth = 1f;
textInput.style.borderTopColor = new StyleColor(new Color(0.3f, 0.3f, 0.35f, 0.8f));
textInput.style.borderBottomColor = new StyleColor(new Color(0.3f, 0.3f, 0.35f, 0.8f));
textInput.style.borderLeftColor = new StyleColor(new Color(0.3f, 0.3f, 0.35f, 0.8f));
textInput.style.borderRightColor = new StyleColor(new Color(0.3f, 0.3f, 0.35f, 0.8f));
textInput.style.paddingLeft = 8f;
textInput.style.paddingRight = 8f;
textInput.style.fontSize = 12f;
textInput.style.unityTextAlign = TextAnchor.MiddleLeft;
textInput.style.color = new StyleColor(Color.white);
}
pathRow.Add(pathLabel);
pathRow.Add(pathField);
_globalConfigContainer.Add(pathRow);
// 3. Action Buttons Row (Browse & Reset)
var actionRow = CreateRow();
actionRow.style.height = StyleKeyword.Auto;
actionRow.style.maxHeight = StyleKeyword.Null;
actionRow.style.minHeight = StyleKeyword.Null;
actionRow.style.marginBottom = 15f;
var browseBtn = new Button { text = "Browse..." };
browseBtn.RegisterCallback<ClickEvent>(_ =>
{
var initial = string.IsNullOrWhiteSpace(pathField.value) ? defaultDir : pathField.value;
FolderDialogUtil.OpenFolderBrowser(initial, selected =>
{
if (!string.IsNullOrWhiteSpace(selected))
{
pathField.value = selected;
}
});
});
var resetBtn = new Button { text = "Reset to Default" };
resetBtn.RegisterCallback<ClickEvent>(_ =>
{
pathField.value = defaultDir;
});
actionRow.Add(browseBtn);
actionRow.Add(resetBtn);
_globalConfigContainer.Add(actionRow);
// 4. Info Row
var infoRow = CreateRow();
infoRow.style.height = StyleKeyword.Auto;
infoRow.style.maxHeight = StyleKeyword.Null;
infoRow.style.minHeight = StyleKeyword.Null;
infoRow.style.marginBottom = 10f;
var infoLabel = new Label("Global output directory applies to all renders unless overridden by API.")
{
style =
{
color = new StyleColor(new Color(0.7f, 0.7f, 0.7f)),
fontSize = 18,
unityTextAlign = TextAnchor.MiddleCenter
}
};
infoRow.Add(infoLabel);
_globalConfigContainer.Add(infoRow);
// 5. Control Button Row (Cancel / Save)
var buttonRow = CreateRow();
var cancelBtn = new Button { text = "Cancel" };
var saveBtn = new Button { text = "Save" };
buttonRow.Add(cancelBtn);
buttonRow.Add(saveBtn);
_globalConfigContainer.Add(buttonRow);
cancelBtn.RegisterCallback<ClickEvent>(_ =>
{
ToggleGlobalScreens(false);
_root.Remove(_globalConfigContainer);
_globalConfigContainer = null!;
});
saveBtn.RegisterCallback<ClickEvent>(_ =>
{
var input = pathField.value?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(input) ||
string.Equals(Path.GetFullPath(input).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar),
Path.GetFullPath(defaultDir).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar),
StringComparison.OrdinalIgnoreCase))
{
GlobalSettings.Current.OutputDirectory = null;
}
else
{
try
{
var fullPath = Path.GetFullPath(input);
if (!Directory.Exists(fullPath))
{
Directory.CreateDirectory(fullPath);
}
GlobalSettings.Current.OutputDirectory = fullPath;
}
catch (Exception ex)
{
CommonWarningDialogUI.ShowDialog("Invalid Path", $"Directory path is invalid:\n{ex.Message}", new DialogButton("OK", () => { }));
return;
}
}
GlobalSettings.Save();
ToggleGlobalScreens(false);
_root.Remove(_globalConfigContainer);
_globalConfigContainer = null!;
});
_root.Add(_globalConfigContainer);
ToggleGlobalScreens(true);
void ToggleGlobalScreens(bool showGlobal)
{
if (_root.childCount > 0)
{
_root[0].style.display = showGlobal ? DisplayStyle.None : DisplayStyle.Flex;
}
_globalConfigContainer.style.display = showGlobal ? DisplayStyle.Flex : DisplayStyle.None;
}
}
private void OpenAdvancedConfig() private void OpenAdvancedConfig()
{ {
var config = RenderManager.Instance.Configuration; var config = RenderManager.Instance.Configuration;
+161
View File
@@ -0,0 +1,161 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using MelonLoader;
namespace ADOFAIRenderer.Util;
internal static class FolderDialogUtil
{
public static void OpenFolderBrowser(string initialDirectory, Action<string?> onSelected)
{
var syncContext = SynchronizationContext.Current;
Task.Run(() =>
{
string? selected = null;
try
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
selected = OpenWindowsFolderBrowser(initialDirectory);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
selected = OpenMacFolderBrowser(initialDirectory);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
selected = OpenLinuxFolderBrowser(initialDirectory);
}
}
catch (Exception ex)
{
MelonLogger.Warning($"[FolderDialogUtil] Failed to open folder browser: {ex.Message}");
}
if (syncContext != null)
{
syncContext.Post(_ => onSelected(selected), null);
}
else
{
onSelected(selected);
}
});
}
private static string? OpenWindowsFolderBrowser(string initialDirectory)
{
try
{
var escapedDir = initialDirectory.Replace("'", "''");
var script = $@"
[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms') | Out-Null
$f = New-Object System.Windows.Forms.FolderBrowserDialog
$f.Description = 'Select Output Directory'
if ([System.IO.Directory]::Exists('{escapedDir}')) {{
$f.SelectedPath = '{escapedDir}'
}}
$f.ShowNewFolderButton = $true
if ($f.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {{
[Console]::Write($f.SelectedPath)
}}";
var bytes = System.Text.Encoding.Unicode.GetBytes(script);
var base64 = Convert.ToBase64String(bytes);
var startInfo = new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = $"-NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand {base64}",
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardOutput = true,
WindowStyle = ProcessWindowStyle.Hidden
};
using var process = Process.Start(startInfo);
if (process == null) return null;
var output = process.StandardOutput.ReadToEnd().Trim();
process.WaitForExit();
return !string.IsNullOrEmpty(output) && Directory.Exists(output) ? output : null;
}
catch (Exception ex)
{
MelonLogger.Warning($"[FolderDialogUtil] Windows folder dialog error: {ex.Message}");
return null;
}
}
private static string? OpenMacFolderBrowser(string initialDirectory)
{
try
{
var escaped = initialDirectory.Replace("\"", "\\\"");
var script = $"POSIX path of (choose folder with prompt \"Select Output Directory\" default location \"{escaped}\")";
var startInfo = new ProcessStartInfo
{
FileName = "osascript",
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardOutput = true,
WindowStyle = ProcessWindowStyle.Hidden
};
startInfo.ArgumentList.Add("-e");
startInfo.ArgumentList.Add(script);
using var process = Process.Start(startInfo);
if (process == null) return null;
var output = process.StandardOutput.ReadToEnd().Trim();
process.WaitForExit();
return !string.IsNullOrEmpty(output) && Directory.Exists(output) ? output : null;
}
catch
{
return null;
}
}
private static string? OpenLinuxFolderBrowser(string initialDirectory)
{
try
{
var startInfo = new ProcessStartInfo
{
FileName = "zenity",
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardOutput = true,
WindowStyle = ProcessWindowStyle.Hidden
};
startInfo.ArgumentList.Add("--file-selection");
startInfo.ArgumentList.Add("--directory");
startInfo.ArgumentList.Add("--title=Select Output Directory");
if (!string.IsNullOrEmpty(initialDirectory) && Directory.Exists(initialDirectory))
{
startInfo.ArgumentList.Add($"--filename={initialDirectory}");
}
using var process = Process.Start(startInfo);
if (process == null) return null;
var output = process.StandardOutput.ReadToEnd().Trim();
process.WaitForExit();
return !string.IsNullOrEmpty(output) && Directory.Exists(output) ? output : null;
}
catch
{
return null;
}
}
}