SHA256
forked from ADOFAIEditorForum/ADOFAIRenderer
Add Output Path Config Prototype
This commit is contained in:
1 parent
4097e14e7a
commit
43763b0b35
5 files changed
+477
-11
No files matched your search
@@ -411,7 +411,7 @@ internal class RenderManager : MonoBehaviour
|
||||
|
||||
public void SetupCaptureDirectory()
|
||||
{
|
||||
var renderDir = Path.Combine(MelonEnvironment.GameRootDirectory, "Render");
|
||||
var renderDir = GlobalSettings.EffectiveOutputDirectory;
|
||||
FileUtil.CreateDirectory(renderDir);
|
||||
}
|
||||
|
||||
@@ -525,7 +525,7 @@ internal class RenderManager : MonoBehaviour
|
||||
else {
|
||||
var fileName = startTime.ToString("yyyy-MM-dd HH-mm-ss") +
|
||||
$".{Configuration.AdvancedConfig.FileExtension.TrimStart('.').ToLower()}";
|
||||
CurrentOutPath = Path.Combine(MelonEnvironment.GameRootDirectory, "Render", fileName);
|
||||
CurrentOutPath = Path.Combine(GlobalSettings.EffectiveOutputDirectory, fileName);
|
||||
}
|
||||
|
||||
FFmpegNativeEncoder? videoFFmpeg = null;
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using ADOFAIRenderer;
|
||||
using ADOFAIRenderer.Core;
|
||||
using ADOFAIRenderer.Data;
|
||||
using ADOFAIRenderer.Util;
|
||||
using FFmpeg.AutoGen;
|
||||
using MelonLoader;
|
||||
@@ -27,6 +28,7 @@ namespace ADOFAIRenderer
|
||||
|
||||
public override void OnInitializeMelon()
|
||||
{
|
||||
GlobalSettings.Load();
|
||||
ffmpeg.RootPath = MelonEnvironment.UserLibsDirectory;
|
||||
|
||||
var renderManagerObj = new GameObject("Render Manager");
|
||||
|
||||
+233
-9
@@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using ADOFAIRenderer.Core;
|
||||
using ADOFAIRenderer.Data;
|
||||
using ADOFAIRenderer.Data.Input;
|
||||
using ADOFAIRenderer.Util;
|
||||
using ADOFAIRenderer.Util.FFmpeg;
|
||||
using MelonLoader;
|
||||
using UnityEngine;
|
||||
@@ -27,6 +29,8 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
private Button _audioEncodingConfigButton = null!;
|
||||
private VisualElement _audioEncodingConfigContainer = null!;
|
||||
|
||||
private Button _globalSettingsButton = null!;
|
||||
private VisualElement? _globalConfigContainer;
|
||||
private Button _advancedSettingsButton = null!;
|
||||
private VisualElement? _advancedConfigContainer;
|
||||
private Label _versionLabel = null!;
|
||||
@@ -92,19 +96,38 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
Melon<RenderMod>.Logger.Error("Button 'PresetSettingsButton' not found.");
|
||||
}
|
||||
|
||||
var bottomRow = CreateRow();
|
||||
bottomRow.pickingMode = PickingMode.Ignore;
|
||||
bottomRow.style.position = Position.Absolute;
|
||||
bottomRow.style.right = 0f;
|
||||
bottomRow.style.bottom = 0f;
|
||||
bottomRow.style.justifyContent = Justify.FlexEnd;
|
||||
var rightBottomContainer = new VisualElement
|
||||
{
|
||||
name = "RightBottomButtonContainer",
|
||||
pickingMode = PickingMode.Ignore,
|
||||
style =
|
||||
{
|
||||
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..." };
|
||||
bottomRow.Add(_advancedSettingsButton);
|
||||
advancedRow.Add(_advancedSettingsButton);
|
||||
rightBottomContainer.Add(advancedRow);
|
||||
|
||||
(_root.childCount > 0 ? _root[0] : _root).Add(bottomRow);
|
||||
Melon<RenderMod>.Logger.Msg("[UI] AdvancedSettingsButton dynamically created.");
|
||||
(_root.childCount > 0 ? _root[0] : _root).Add(rightBottomContainer);
|
||||
Melon<RenderMod>.Logger.Msg("[UI] GlobalSettingsButton and AdvancedSettingsButton dynamically created.");
|
||||
|
||||
_globalSettingsButton.RegisterCallback<ClickEvent>(_ => OpenGlobalConfig());
|
||||
_advancedSettingsButton.RegisterCallback<ClickEvent>(_ => OpenAdvancedConfig());
|
||||
|
||||
var versionText =
|
||||
@@ -143,6 +166,207 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
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()
|
||||
{
|
||||
var config = RenderManager.Instance.Configuration;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user