diff --git a/.gitignore b/.gitignore index 8b8c619..e63010a 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,5 @@ GEMINI.md # Developer Description Files /AsyncInputAutoplayFix.md -/AudioMixerExplanation.md \ No newline at end of file +/AudioMixerExplanation.md +/O5Theme.md \ No newline at end of file diff --git a/ADOFAIRenderer.csproj b/ADOFAIRenderer.csproj index 8e83765..ce4984d 100644 --- a/ADOFAIRenderer.csproj +++ b/ADOFAIRenderer.csproj @@ -16,6 +16,9 @@ $(ADOFAI_PATH)\MelonLoader\net35\0Harmony.dll + + $(ADOFAI_PATH)\UserLibs\O5Kit.dll + $(ADOFAI_PATH)\A Dance of Fire and Ice_Data\Managed\Assembly-CSharp.dll @@ -81,37 +84,21 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + diff --git a/Bundle/Linux/scenes b/Bundle/Linux/scenes deleted file mode 100644 index 79405c9..0000000 Binary files a/Bundle/Linux/scenes and /dev/null differ diff --git a/Bundle/OSX/scenes b/Bundle/OSX/scenes deleted file mode 100644 index 1ef2d9c..0000000 Binary files a/Bundle/OSX/scenes and /dev/null differ diff --git a/Bundle/Win64/fonts b/Bundle/Win64/fonts index 4466c93..61b8cc7 100644 Binary files a/Bundle/Win64/fonts and b/Bundle/Win64/fonts differ diff --git a/Bundle/Win64/scenes b/Bundle/Win64/scenes deleted file mode 100644 index 058ddc6..0000000 Binary files a/Bundle/Win64/scenes and /dev/null differ diff --git a/Core/RenderManager.cs b/Core/RenderManager.cs index 6a08601..0c08f0b 100644 --- a/Core/RenderManager.cs +++ b/Core/RenderManager.cs @@ -9,6 +9,7 @@ using System.Linq; using ADOFAIRenderer.API; using ADOFAIRenderer.Data; using ADOFAIRenderer.UI; +using ADOFAIRenderer.UI.Window; using ADOFAIRenderer.Util; using ADOFAIRenderer.Util.Audio; using ADOFAIRenderer.Util.FFmpeg; @@ -336,7 +337,8 @@ internal class RenderManager : MonoBehaviour (Func)Delegate.CreateDelegate(typeof(Func), scnEditor.instance, targetMethod); Application.wantsToQuit -= quitDelegate; - ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "Assets/Scenes/RenderSettingsScene.unity"); + RenderConfigWindow.CreateWindow(); + // ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "Assets/Scenes/RenderSettingsScene.unity"); }); } diff --git a/Localization/Locale/en.json b/Localization/Locale/en.json index f6a667e..fc2b0de 100644 --- a/Localization/Locale/en.json +++ b/Localization/Locale/en.json @@ -1,85 +1,35 @@ { - "ui.button.presets": "Presets", - "ui.placeholder.render_fps": "Render FPS", - "ui.placeholder.video_fps": "Video FPS", - "ui.placeholder.render_width": "Render Width (px)", - "ui.placeholder.render_height": "Render Height (px)", - "ui.button.video_config": "Video Encoding Config", - "ui.button.audio_config": "Audio Encoding Config", - "ui.placeholder.delay_after_end": "Delay After Level End", - "ui.toggle.show_judgement": "Show Judgement", - "ui.button.go_render": "Go Render", - "ui.button.advanced_settings": "Advanced Settings...", - "ui.version.expired": " (Expired)", - "ui.validation.required_fields": "Required input fields are empty or invalid. Please fill in all settings.", - - "ui.advanced.title": "Advanced Settings", - "ui.advanced.file_extension": "File Extension", - "ui.advanced.process_audio": "Process Audio", - "ui.advanced.non_tile_mode": "Non-Tile Mode", - "ui.advanced.random_seed": "Random Seed (Optional)", - "ui.button.cancel": "Cancel", - "ui.button.save": "Save", - - "ui.video.title": "Video Encoding Settings", - "ui.video.codec": "Codec", - "ui.video.rate_control": "Bitrate Control Mode", - "ui.video.bitrate": "Bitrate (kbps)", - "ui.video.max_bitrate": "Max Bitrate (kbps)", - "ui.video.qp": "Quality (QP)", - "ui.video.keyframe_interval": "Keyframe Interval (s)", - "ui.video.force_software": "Force Software Encoder", - "ui.video.qp_zero_warning": "Warning: Setting QP to 0 applies lossless encoding and may result in very large file sizes.", - - "ui.audio.title": "Audio Encoding Settings", - "ui.audio.rendering": "Audio Rendering", - "ui.audio.sample_rate": "Sample Rate (Hz)", - "ui.audio.channels": "Audio Channels", - "ui.audio.channels_mono": "1 (Mono)", - "ui.audio.channels_stereo": "2 (Stereo)", - "ui.audio.bitrate": "Audio Bitrate (kbps)", - - "ui.preset.title": "Preset Settings", - "ui.preset.new_name": "New Preset Name", - "ui.preset.no_presets": "No saved presets.", - "ui.button.load": "Load", - "ui.button.delete": "Delete", - "ui.button.close": "Close", - "ui.preset.save_failed": "Save Failed", - "ui.preset.enter_name": "Please enter a preset name.", - "ui.preset.invalid_inputs": "Cannot save preset because required input values are empty or invalid.", - "ui.preset.save_error": "An error occurred while saving the preset: {0}", - "ui.preset.delete_title": "Delete Preset", - "ui.preset.delete_confirm": "Are you sure you want to delete the preset '{0}'?", - "ui.button.yes": "Yes", - "ui.button.no": "No", - "ui.button.ok": "OK", - - "ui.ffmpeg.title": "FFmpeg Library Installation Required", - "ui.ffmpeg.desc_main": "FFmpeg shared libraries are required to render video.", - "ui.ffmpeg.desc_osx": "For macOS, reliable public builds are unavailable, so FFmpeg 8.1 must be built manually.\nClick the button below to open the official FFmpeg website.", - "ui.ffmpeg.desc_auto": "Click the button below to automatically download and install from the internet.\n(Download size: ~70MB, extracted to UserLibs folder)", - "ui.ffmpeg.button_download": "Download & Install", - "ui.ffmpeg.button_open_web": "Open Download Page", - "ui.ffmpeg.button_cancel": "Cancel", - "ui.ffmpeg.button_opening": "Opening...", - "ui.ffmpeg.status_downloading": "Downloading... ({0:F1}%)", - "ui.ffmpeg.status_extracting": "Extracting FFmpeg files...", - "ui.ffmpeg.status_done": "Done! Restarting audio/video engine...", - "ui.ffmpeg.status_failed": "Failed to download FFmpeg: {0}", - - "ui.escape.cancelling": "Cancelling in {0:F1}s..\nHold Esc to cancel render", - - "ui.monitor.title": "ADOFAI Renderer Realtime Monitor", - "ui.monitor.queue": "• Queue: {0} / {1} frames", - "ui.monitor.rendered_frames": "• Rendered Frames: {0}", - "ui.monitor.frames_sent": "• Frames Sent: {0}", - "ui.monitor.throttling": "• Throttling Controls: {0} times", - "ui.monitor.readback_failures": "• Readback Hard Failures: {0} times", - "ui.monitor.skip_retry": "• Render Skip-Retry: {0} / {1}", - "ui.monitor.memory": "• Mono Heap Memory: {0:F2} MB", - - "ui.notification.complete_title": "Rendering Complete!", - "ui.notification.complete_msg": "{0} saved successfully.", - "ui.notification.video_fallback": "Video" + "ui.render_config.window_title": "Render Config", + "ui.render_config.render_fps": "Render FPS", + "ui.render_config.video_fps": "Video FPS", + "ui.render_config.render_width": "Render Width", + "ui.render_config.render_height": "Render Height", + "ui.render_config.delay_after_end": "Delay After End (s)", + "ui.render_config.show_judgement": "Show Judgement Text", + "ui.render_config.video_encoding": "Video Encoding Config", + "ui.render_config.audio_encoding": "Audio Encoding Config", + "ui.render_config.global_settings": "Global Settings", + "ui.render_config.advanced_settings": "Advanced Settings", + "ui.render_config.go_render": "Go Render", + "ui.render_config.codec": "Codec", + "ui.render_config.rate_control": "Rate Control Mode", + "ui.render_config.bitrate": "Bitrate (kbps)", + "ui.render_config.max_bitrate": "Max Bitrate (kbps)", + "ui.render_config.qp": "Quality (QP)", + "ui.render_config.keyframe_interval": "Keyframe Interval (s)", + "ui.render_config.force_software": "Force Software Encoder", + "ui.render_config.render_audio": "Render Audio", + "ui.render_config.sample_rate": "Sample Rate (Hz)", + "ui.render_config.channels": "Audio Channels", + "ui.render_config.audio_bitrate": "Audio Bitrate (kbps)", + "ui.render_config.output_dir": "Output Directory", + "ui.render_config.browse": "Browse...", + "ui.render_config.reset_default": "Reset to Default", + "ui.render_config.file_extension": "File Extension", + "ui.render_config.process_audio": "Process Audio", + "ui.render_config.non_tile": "Non-Tile Mode", + "ui.render_config.random_seed": "Random Seed (Optional)", + "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.invalid_path": "Output directory path is invalid:\n{0}" } diff --git a/Localization/Locale/ko.json b/Localization/Locale/ko.json index ecc7747..204c9d7 100644 --- a/Localization/Locale/ko.json +++ b/Localization/Locale/ko.json @@ -1,85 +1,35 @@ { - "ui.button.presets": "프리셋", - "ui.placeholder.render_fps": "렌더링 FPS", - "ui.placeholder.video_fps": "영상 FPS", - "ui.placeholder.render_width": "렌더링 너비 (px)", - "ui.placeholder.render_height": "렌더링 높이 (px)", - "ui.button.video_config": "비디오 인코딩 설정", - "ui.button.audio_config": "오디오 인코딩 설정", - "ui.placeholder.delay_after_end": "레벨 종료 후 대기 시간 (초)", - "ui.toggle.show_judgement": "판정 텍스트 표시", - "ui.button.go_render": "렌더링 시작", - "ui.button.advanced_settings": "고급 설정...", - "ui.version.expired": " (만료됨)", - "ui.validation.required_fields": "필수 입력란이 비어 있거나 올바르지 않습니다. 모든 설정을 확인해 주세요.", - - "ui.advanced.title": "고급 설정", - "ui.advanced.file_extension": "파일 확장자", - "ui.advanced.process_audio": "오디오 처리", - "ui.advanced.non_tile_mode": "논타일 모드", - "ui.advanced.random_seed": "랜덤 시드 (선택 사항)", - "ui.button.cancel": "취소", - "ui.button.save": "저장", - - "ui.video.title": "비디오 인코딩 설정", - "ui.video.codec": "코덱", - "ui.video.rate_control": "비트레이트 제어 모드", - "ui.video.bitrate": "비트레이트 (kbps)", - "ui.video.max_bitrate": "최대 비트레이트 (kbps)", - "ui.video.qp": "품질 (QP)", - "ui.video.keyframe_interval": "키프레임 간격 (초)", - "ui.video.force_software": "강제 소프트웨어 인코더", - "ui.video.qp_zero_warning": "경고: QP를 0으로 설정하면 무손실 인코딩이 적용되어 파일 크기가 매우 커질 수 있습니다.", - - "ui.audio.title": "오디오 인코딩 설정", - "ui.audio.rendering": "오디오 렌더링", - "ui.audio.sample_rate": "샘플레이트 (Hz)", - "ui.audio.channels": "오디오 채널", - "ui.audio.channels_mono": "1 (모노)", - "ui.audio.channels_stereo": "2 (스테레오)", - "ui.audio.bitrate": "오디오 비트레이트 (kbps)", - - "ui.preset.title": "프리셋 설정", - "ui.preset.new_name": "새 프리셋 이름", - "ui.preset.no_presets": "저장된 프리셋이 없습니다.", - "ui.button.load": "불러오기", - "ui.button.delete": "삭제", - "ui.button.close": "닫기", - "ui.preset.save_failed": "저장 실패", - "ui.preset.enter_name": "프리셋 이름을 입력해 주세요.", - "ui.preset.invalid_inputs": "필수 입력값이 비어 있거나 올바르지 않아 프리셋을 저장할 수 없습니다.", - "ui.preset.save_error": "프리셋 저장 중 오류가 발생했습니다: {0}", - "ui.preset.delete_title": "프리셋 삭제", - "ui.preset.delete_confirm": "정말 '{0}' 프리셋을 삭제하시겠습니까?", - "ui.button.yes": "예", - "ui.button.no": "아니오", - "ui.button.ok": "확인", - - "ui.ffmpeg.title": "FFmpeg 라이브러리 설치 필요", - "ui.ffmpeg.desc_main": "영상을 렌더링하려면 FFmpeg 공유 라이브러리가 필요합니다.", - "ui.ffmpeg.desc_osx": "macOS의 경우 신뢰할 수 있는 빌드가 제공되지 않아 FFmpeg 8.1을 직접 빌드해야 합니다.\n아래 버튼을 눌러 공식 FFmpeg 웹사이트를 방문하세요.", - "ui.ffmpeg.desc_auto": "아래 버튼을 눌러 인터넷에서 자동으로 다운로드하여 설치합니다.\n(다운로드 크기: 약 70MB, UserLibs 폴더에 압축 해제됨)", - "ui.ffmpeg.button_download": "다운로드 및 설치", - "ui.ffmpeg.button_open_web": "다운로드 페이지 열기", - "ui.ffmpeg.button_cancel": "취소", - "ui.ffmpeg.button_opening": "열리는 중...", - "ui.ffmpeg.status_downloading": "다운로드 중... ({0:F1}%)", - "ui.ffmpeg.status_extracting": "FFmpeg 파일 압축 해제 중...", - "ui.ffmpeg.status_done": "완료! 오디오/비디오 엔진을 재시작합니다...", - "ui.ffmpeg.status_failed": "FFmpeg 다운로드 실패: {0}", - - "ui.escape.cancelling": "{0:F1}초 후 취소됩니다..\n렌더링을 취소하려면 Esc를 길게 누르세요", - - "ui.monitor.title": "ADOFAI Renderer 실시간 모니터", - "ui.monitor.queue": "• 대기열: {0} / {1} 프레임", - "ui.monitor.rendered_frames": "• 렌더링된 프레임: {0}", - "ui.monitor.frames_sent": "• 전송된 프레임: {0}", - "ui.monitor.throttling": "• 쓰로틀링 제어: {0}회", - "ui.monitor.readback_failures": "• 리드백 실패: {0}회", - "ui.monitor.skip_retry": "• 렌더 건너뜀-재시도: {0} / {1}", - "ui.monitor.memory": "• 모노 힙 메모리: {0:F2} MB", - - "ui.notification.complete_title": "렌더링 완료!", - "ui.notification.complete_msg": "{0}이(가) 성공적으로 저장되었습니다.", - "ui.notification.video_fallback": "동영상" + "ui.render_config.window_title": "렌더 설정", + "ui.render_config.render_fps": "렌더링 FPS", + "ui.render_config.video_fps": "영상 FPS", + "ui.render_config.render_width": "렌더링 너비", + "ui.render_config.render_height": "렌더링 높이", + "ui.render_config.delay_after_end": "종료 후 대기 시간 (초)", + "ui.render_config.show_judgement": "판정 텍스트 표시", + "ui.render_config.video_encoding": "Video Encoding Config", + "ui.render_config.audio_encoding": "Audio Encoding Config", + "ui.render_config.global_settings": "Global Settings", + "ui.render_config.advanced_settings": "Advanced Settings", + "ui.render_config.go_render": "렌더 시작", + "ui.render_config.codec": "코덱", + "ui.render_config.rate_control": "비트레이트 제어 모드", + "ui.render_config.bitrate": "비트레이트 (kbps)", + "ui.render_config.max_bitrate": "최대 비트레이트 (kbps)", + "ui.render_config.qp": "품질 (QP)", + "ui.render_config.keyframe_interval": "키프레임 간격 (초)", + "ui.render_config.force_software": "강제 소프트웨어 인코더", + "ui.render_config.render_audio": "오디오 렌더링", + "ui.render_config.sample_rate": "샘플레이트 (Hz)", + "ui.render_config.channels": "오디오 채널", + "ui.render_config.audio_bitrate": "오디오 비트레이트 (kbps)", + "ui.render_config.output_dir": "출력 디렉토리", + "ui.render_config.browse": "찾아보기...", + "ui.render_config.reset_default": "기본값으로 초기화", + "ui.render_config.file_extension": "파일 확장자", + "ui.render_config.process_audio": "오디오 처리", + "ui.render_config.non_tile": "논타일 모드", + "ui.render_config.random_seed": "랜덤 시드 (선택 사항)", + "ui.render_config.validation_title": "설정 오류", + "ui.render_config.validation_error": "입력값을 확인해 주세요:\n- 너비와 높이는 0보다 큰 정수여야 합니다.\n- 렌더 FPS와 영상 FPS는 0보다 커야 합니다.\n- 종료 후 대기 시간은 0 이상이어야 합니다.", + "ui.render_config.invalid_path": "출력 디렉토리 경로가 올바르지 않습니다:\n{0}" } diff --git a/Localization/LocalizationManager.cs b/Localization/LocalizationManager.cs index 181f77b..15bb674 100644 --- a/Localization/LocalizationManager.cs +++ b/Localization/LocalizationManager.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Reflection; using MelonLoader; using Newtonsoft.Json; using UnityEngine; @@ -33,6 +32,11 @@ internal static class LocalizationManager private static bool _initialized; + static LocalizationManager() + { + Initialize(); + } + /// /// Initializes the localization system and loads embedded language dictionaries. /// @@ -115,7 +119,7 @@ internal static class LocalizationManager localeCode = FallbackLocaleCode; } - var isChanged = (language != CurrentLanguage) || (localeCode != CurrentLocaleCode); + var isChanged = language != CurrentLanguage || localeCode != CurrentLocaleCode; CurrentLanguage = language; CurrentLocaleCode = localeCode; @@ -140,6 +144,7 @@ internal static class LocalizationManager public static string Get(string key) { if (string.IsNullOrEmpty(key)) return string.Empty; + if (!_initialized) Initialize(); // 1. Try currently active locale if (Locales.TryGetValue(CurrentLocaleCode, out var activeDict) && activeDict.TryGetValue(key, out var val)) @@ -165,7 +170,7 @@ internal static class LocalizationManager public static string Get(string key, params object[] args) { var template = Get(key); - if (args == null || args.Length == 0) return template; + if (args.Length == 0) return template; try { @@ -185,7 +190,6 @@ internal static class LocalizationManager return language switch { SystemLanguage.Korean => "ko", - SystemLanguage.English => "en", _ => "en" }; } @@ -218,6 +222,7 @@ internal static class LocalizationManager /// /// Convenient shorthand alias for . /// +// ReSharper disable once InconsistentNaming internal static class I18n { /// diff --git a/RenderMod.cs b/RenderMod.cs index 9be8d8f..593e541 100644 --- a/RenderMod.cs +++ b/RenderMod.cs @@ -2,12 +2,11 @@ using System; using ADOFAIRenderer; using ADOFAIRenderer.Core; using ADOFAIRenderer.Data; -using ADOFAIRenderer.Util; +using ADOFAIRenderer.UI; using FFmpeg.AutoGen; using MelonLoader; using MelonLoader.Utils; using UnityEngine; -using UnityEngine.EventSystems; using Object = UnityEngine.Object; [assembly: MelonInfo(typeof(RenderMod), "ADOFAI Renderer", "0.3.3-BETA", "ADOFAI Editor Forum")] @@ -17,57 +16,27 @@ namespace ADOFAIRenderer; internal class RenderMod : MelonMod { - public static AssetBundle FontBundle = null!; // ReSharper disable once MemberCanBePrivate.Global - public static AssetBundle SceneBundle = null!; - // ReSharper disable once MemberCanBePrivate.Global - public static AssetBundle ShaderBundle = null!; - public static ComputeShader ColorConversionShader = null!; public static bool IsFeatureDisabled = false; public static DateTime ExpirationTime = new(2026, 09, 21, 0, 0, 0, DateTimeKind.Utc); public override void OnInitializeMelon() { GlobalSettings.Load(); + Localization.LocalizationManager.Initialize(); ffmpeg.RootPath = MelonEnvironment.UserLibsDirectory; var renderManagerObj = new GameObject("Render Manager"); var updateHookerObj = new GameObject("Update Hooker"); + var uiManagerObj = new GameObject("Renderer UI Manager"); Object.DontDestroyOnLoad(renderManagerObj); Object.DontDestroyOnLoad(updateHookerObj); + Object.DontDestroyOnLoad(uiManagerObj); renderManagerObj.AddComponent(); updateHookerObj.AddComponent(); - LoadBundles(); - } - - private static void LoadBundles() - { - FontBundle = BundleLoader.GetBundle("fonts"); - SceneBundle = BundleLoader.GetBundle("scenes"); - SceneBundle.GetAllScenePaths(); - try - { - ShaderBundle = BundleLoader.GetBundle("shaders"); - ColorConversionShader = ShaderBundle.LoadAsset("ColorConversion"); - Melon.Logger.Msg("Color conversion Compute Shader loaded successfully."); - } - catch (Exception ex) - { - Melon.Logger.Warning($"Failed to load Shader AssetBundle. GPU color conversion will be disabled and fallback to CPU. Error: {ex.Message}"); - } - } - - public static void FixEventSystem() - { - if (EventSystem.current == null) return; - var currentModule = EventSystem.current.currentInputModule; - - if (currentModule) return; - Melon.Logger.Msg("Ported Unity 6 Input Module."); - - var newModule = EventSystem.current.gameObject.AddComponent(); - newModule.enabled = true; + uiManagerObj.AddComponent(); + ModAssets.Load(); } public override void OnApplicationQuit() diff --git a/UI/CommonWarningDialogUI.cs b/UI/CommonWarningDialogUI.cs index a26bd96..7403b96 100644 --- a/UI/CommonWarningDialogUI.cs +++ b/UI/CommonWarningDialogUI.cs @@ -52,46 +52,6 @@ internal class CommonWarningDialogUI : MonoBehaviour } } - private static TMP_FontAsset GetNanumSquareNeoBdFontAsset() - { - TMP_FontAsset? font = null; - if (RenderMod.FontBundle) - { - try - { - font = RenderMod.FontBundle.LoadAsset("NanumSquareNeoOTF-Bd SDF"); - } - catch - { - // Loading failed - } - } - - // Fallback processing - if (!font) - { - try - { - font = RDString.fontData.fontTMP; - } - catch - { - // ignored - } - } - if (!font) - { - // ReSharper disable once Unity.UnknownResource - font = Resources.Load("Fonts & Materials/LiberationSans SDF"); - } - - if (font) return font; - var fonts = Resources.FindObjectsOfTypeAll(); - if (fonts.Length > 0) font = fonts[0]; - - return font!; - } - public static void ShowDialog(string title, string description, params DialogButton[] buttons) { if (buttons.Length == 0) return; @@ -153,7 +113,7 @@ internal class CommonWarningDialogUI : MonoBehaviour bgRect.offsetMin = Vector2.zero; bgRect.offsetMax = Vector2.zero; - var font = GetNanumSquareNeoBdFontAsset(); + var font = ModAssets.NanumSquareNeoNormal; // 2. Create modal container var modalContainerObj = new GameObject("ModalContainer"); diff --git a/UI/DebugMonitorUI.cs b/UI/DebugMonitorUI.cs index 8710c8b..f2bc2c9 100644 --- a/UI/DebugMonitorUI.cs +++ b/UI/DebugMonitorUI.cs @@ -30,39 +30,6 @@ internal class DebugMonitorUI : MonoBehaviour private RectTransform _fillRect = null!; private TextMeshProUGUI _progressText = null!; - private static TMP_FontAsset GetNanumSquareNeoBdFontAsset() - { - TMP_FontAsset? font = null; - if (RenderMod.FontBundle != null) - { - try - { - font = RenderMod.FontBundle.LoadAsset("NanumSquareNeoOTF-Bd SDF"); - } - catch - { - // Loading failed - } - } - - // Fallback: Use game's own font or built-in TMP SDF font if AssetBundle loading fails - if (font == null) - { - font = RDString.fontData.fontTMP; - } - if (font == null) - { - // ReSharper disable once Unity.UnknownResource - font = Resources.Load("Fonts & Materials/LiberationSans SDF"); - } - - if (font != null) return font; - var fonts = Resources.FindObjectsOfTypeAll(); - if (fonts.Length > 0) font = fonts[0]; - - return font!; - } - private void Start() { // Set UI layer (5) @@ -112,8 +79,7 @@ internal class DebugMonitorUI : MonoBehaviour _text.overflowMode = TextOverflowModes.Overflow; // Load and apply font - var font = GetNanumSquareNeoBdFontAsset(); - _text.font = font; + _text.font = ModAssets.NanumSquareNeoNormal; var textRect = textObj.GetComponent(); textRect.anchorMin = Vector2.zero; @@ -166,7 +132,7 @@ internal class DebugMonitorUI : MonoBehaviour _progressText.richText = true; _progressText.fontSize = BarTextFontSize; _progressText.alignment = TextAlignmentOptions.Center; - _progressText.font = font; + _progressText.font = ModAssets.NanumSquareNeoNormal; _progressText.color = BarTextColor; // Add progress bar text outline for readability diff --git a/UI/FFmpegDownloadUI.cs b/UI/FFmpegDownloadUI.cs index 9ebaae3..737f7f0 100644 --- a/UI/FFmpegDownloadUI.cs +++ b/UI/FFmpegDownloadUI.cs @@ -88,11 +88,11 @@ internal class FFmpegDownloadUI : MonoBehaviour private static TMP_FontAsset GetNanumSquareNeoBdFontAsset() { TMP_FontAsset? font = null; - if (RenderMod.FontBundle) + if (ModAssets.FontBundle) { try { - font = RenderMod.FontBundle.LoadAsset("NanumSquareNeoOTF-Bd SDF"); + font = ModAssets.FontBundle.LoadAsset("NanumSquareNeoOTF-Bd SDF"); } catch { diff --git a/UI/RenderConfigUIManager.cs b/UI/RenderConfigUIManager.cs index 5ab3694..1fc5b8d 100644 --- a/UI/RenderConfigUIManager.cs +++ b/UI/RenderConfigUIManager.cs @@ -151,11 +151,11 @@ internal class RenderConfigUIManager : MonoBehaviour }; // Apply same font as existing UI - if (RenderMod.FontBundle) + if (ModAssets.FontBundle) { try { - var font = RenderMod.FontBundle.LoadAsset("NanumSquareNeoOTF-Bd"); + var font = ModAssets.FontBundle.LoadAsset("NanumSquareNeoOTF-Bd"); if (font) { _versionLabel.style.unityFontDefinition = FontDefinition.FromFont(font); } } catch { /* ignored */ } diff --git a/UI/RendererUIManager.cs b/UI/RendererUIManager.cs new file mode 100644 index 0000000..7644820 --- /dev/null +++ b/UI/RendererUIManager.cs @@ -0,0 +1,102 @@ +using System.Diagnostics.CodeAnalysis; +using ADOFAIRenderer.Util; +using MelonLoader; +using O5Kit.Core; +using TMPro; + +namespace ADOFAIRenderer.UI; + +using UnityEngine; + +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +internal static class ModAssets +{ + public static void Load() + { + FontBundle = BundleLoader.GetBundle("fonts"); + NanumSquareNeoNormal = FontBundle.LoadAsset("NanumSquareNeoOTF-Bd SDF"); + NanumSquareNeoBold = FontBundle.LoadAsset("NanumSquareNeoOTF-Eb SDF"); + Melon.Logger.Msg("Fonts loaded successfully."); + + ShaderBundle = BundleLoader.GetBundle("shaders"); + ColorConversionShader = ShaderBundle.LoadAsset("ColorConversion"); + Melon.Logger.Msg("Shader loaded successfully."); + } + + public static AssetBundle FontBundle = null!; + public static AssetBundle ShaderBundle = null!; + + public static TMP_FontAsset NanumSquareNeoNormal = null!; + public static TMP_FontAsset NanumSquareNeoBold = null!; + + public static ComputeShader ColorConversionShader = null!; +} + +internal sealed class RendererFontProvider : IFontProvider +{ + private static readonly DefaultFontProvider DefaultFontProvider = new(); + private bool _disposed; + + public TMP_FontAsset Regular => ModAssets.NanumSquareNeoNormal; + public TMP_FontAsset Medium => ModAssets.NanumSquareNeoBold; + public TMP_FontAsset Monospace => DefaultFontProvider.Monospace; + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + } +} + +internal class RendererUIManager : MonoBehaviour +{ + private static RendererUIManager? _instance; + public static RendererUIManager Instance + { + get + { + if (_instance != null) return _instance; + + var go = new GameObject("RendererUIManager"); + DontDestroyOnLoad(go); + + _instance = go.AddComponent(); + return _instance; + } + } + + public static O5Context Context => Instance._context; + public O5WindowManager WindowManager => _windowManager; + + private O5Context _context = null!; + private O5WindowManager _windowManager = null!; + + private void Awake() { + _instance = this; + + _context = new O5Context( + config: new O5Config { + UIScale = 1.0f, + MiddleClickToDefault = true, + SliderSensitivity = 1.0f + }, + theme: O5Theme.Dark, + fonts: new RendererFontProvider() + ); + + _windowManager = O5WindowManager.Create(_context, transform, "RendererMod_Canvas"); + } + + private void Update() { + O5Object.TickAll(); + Context.Tooltip.Tick(); + O5Kit.Input.O5ShortcutManager.HandleUpdate(); + } + + private void OnDestroy() { + _windowManager.Dispose(); + _context.Dispose(); + + if (_instance == this) _instance = null; + } +} \ No newline at end of file diff --git a/UI/Window/RenderConfigWindow.cs b/UI/Window/RenderConfigWindow.cs new file mode 100644 index 0000000..cda2d9e --- /dev/null +++ b/UI/Window/RenderConfigWindow.cs @@ -0,0 +1,782 @@ +using System; +using System.Collections.Generic; +using System.IO; +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 O5InputField? _renderFpsInput; + private static O5InputField? _videoFpsInput; + private static O5InputField? _renderWidthInput; + private static O5InputField? _renderHeightInput; + private static O5InputField? _delayAfterEndInput; + private static O5Toggle? _showJudgementToggle; + + private static O5Dropdown? _codecDropdown; + private static O5Dropdown? _rateControlDropdown; + private static O5InputField? _bitrateInput; + private static O5InputField? _maxBitrateInput; + private static O5InputField? _qpInput; + private static O5InputField? _keyframeIntervalInput; + private static O5Toggle? _forceSoftwareToggle; + + private static O5Toggle? _renderAudioToggle; + private static O5Dropdown? _sampleRateDropdown; + private static O5Dropdown? _channelsDropdown; + private static O5InputField? _audioBitrateInput; + + private static O5InputField? _outputDirInput; + + private static O5Dropdown? _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); + } + + 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); + + var config = RenderManager.Instance.Configuration ??= new RenderConfig(); + EnsureDefaultConfig(config); + + // 1-3. Top section: 좌/우 컨테이너 1개씩으로 3행을 쌓는다 (중앙선 1개) + var (topLeft, topRight) = CreateTwoColumnPanel(scrollContent, spacing: 8f, columnSpacing: 10f); + _renderFpsInput = CreateInputField( + ctx, CreateFieldSlot(topLeft), + LocalizationManager.Get("ui.render_config.render_fps"), + "60", config.RenderFPS.ToString("0.##"), + "input_render_fps", + TMPro.TMP_InputField.CharacterValidation.Decimal, + val => { if (double.TryParse(val, out var d) && d > 0) config.RenderFPS = d; } + ); + _renderWidthInput = CreateInputField( + ctx, CreateFieldSlot(topLeft), + LocalizationManager.Get("ui.render_config.render_width"), + "1920", config.RenderWidth.ToString(), + "input_render_width", + TMPro.TMP_InputField.CharacterValidation.Integer, + val => { if (int.TryParse(val, out var i) && i > 0) config.RenderWidth = i; } + ); + _delayAfterEndInput = CreateInputField( + ctx, CreateFieldSlot(topLeft), + LocalizationManager.Get("ui.render_config.delay_after_end"), + "1.0", config.DelayAfterEnd.ToString("0.##"), + "input_delay_after_end", + TMPro.TMP_InputField.CharacterValidation.Decimal, + val => { if (double.TryParse(val, out var d) && d >= 0) config.DelayAfterEnd = d; } + ); + _videoFpsInput = CreateInputField( + ctx, CreateFieldSlot(topRight), + LocalizationManager.Get("ui.render_config.video_fps"), + "60", config.VideoFPS.ToString("0.##"), + "input_video_fps", + TMPro.TMP_InputField.CharacterValidation.Decimal, + val => { if (double.TryParse(val, out var d) && d > 0) config.VideoFPS = d; } + ); + _renderHeightInput = CreateInputField( + ctx, CreateFieldSlot(topRight), + LocalizationManager.Get("ui.render_config.render_height"), + "1080", config.RenderHeight.ToString(), + "input_render_height", + TMPro.TMP_InputField.CharacterValidation.Integer, + val => { if (int.TryParse(val, out var i) && i > 0) config.RenderHeight = i; } + ); + _showJudgementToggle = CreateToggleField( + ctx, CreateFieldSlot(topRight), + LocalizationManager.Get("ui.render_config.show_judgement"), + false, config.ShowJudgementText, + LocalizationManager.Get("ui.render_config.show_judgement"), + "toggle_show_judgement", + val => config.ShowJudgementText = val + ); + + // 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; } + ); + _keyframeIntervalInput = CreateInputField( + ctx, CreateFieldSlot(videoRight), + LocalizationManager.Get("ui.render_config.keyframe_interval"), + "2.0", config.VideoConfig.KeyframeInterval.ToString("0.##"), + "input_keyframe_interval", + TMPro.TMP_InputField.CharacterValidation.Decimal, + val => { if (double.TryParse(val, out var kf) && kf > 0) config.VideoConfig.KeyframeInterval = kf; } + ); + + _forceSoftwareToggle = O5Factory.Toggle( + ctx, videoContent, + false, config.VideoConfig.ForceSoftwareEncoder, + val => config.VideoConfig.ForceSoftwareEncoder = val, + LocalizationManager.Get("ui.render_config.force_software"), + "toggle_force_software" + ); + + // 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), + LocalizationManager.Get("ui.render_config.render_audio"), + true, config.AudioConfig.RenderAudio, + LocalizationManager.Get("ui.render_config.render_audio"), + "toggle_render_audio", + val => config.AudioConfig.RenderAudio = val + ); + 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 }; + _sampleRateDropdown = CreateDropdownField( + ctx, CreateFieldSlot(audioRight), + LocalizationManager.Get("ui.render_config.sample_rate"), + 44100, config.AudioConfig.AudioSampleRate, + sampleRates, r => $"{r} Hz", + "dd_sample_rate", + r => config.AudioConfig.AudioSampleRate = r + ); + _audioBitrateInput = CreateInputField( + ctx, CreateFieldSlot(audioRight), + LocalizationManager.Get("ui.render_config.audio_bitrate"), + "256", (config.AudioConfig.AudioBitrate / 1000).ToString(), + "input_audio_bitrate", + TMPro.TMP_InputField.CharacterValidation.Integer, + val => { if (int.TryParse(val, out var ab) && ab > 0) config.AudioConfig.AudioBitrate = ab * 1000; } + ); + + // 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), + LocalizationManager.Get("ui.render_config.process_audio"), + true, config.AdvancedConfig.ProcessAudio, + LocalizationManager.Get("ui.render_config.process_audio"), + "toggle_process_audio", + val => config.AdvancedConfig.ProcessAudio = val + ); + + _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), + LocalizationManager.Get("ui.render_config.non_tile"), + false, config.AdvancedConfig.NonTile, + LocalizationManager.Get("ui.render_config.non_tile"), + "toggle_non_tile", + val => config.AdvancedConfig.NonTile = val + ); + + // 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 : 1920; + if (config.RenderHeight <= 0) config.RenderHeight = Screen.height > 0 ? Screen.height : 1080; + if (config.RenderFPS <= 0) config.RenderFPS = 60.0; + if (config.VideoFPS <= 0) config.VideoFPS = 60.0; + if (config.DelayAfterEnd < 0) config.DelayAfterEnd = 1.0; + } + + private static void ApplyConfigToUI(RenderConfig config) + { + EnsureDefaultConfig(config); + + _renderFpsInput?.Set(config.RenderFPS.ToString("0.##"), false); + _videoFpsInput?.Set(config.VideoFPS.ToString("0.##"), false); + _renderWidthInput?.Set(config.RenderWidth.ToString(), false); + _renderHeightInput?.Set(config.RenderHeight.ToString(), false); + _delayAfterEndInput?.Set(config.DelayAfterEnd.ToString("0.##"), 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); + _keyframeIntervalInput?.Set(config.VideoConfig.KeyframeInterval.ToString("0.##"), 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); + _audioBitrateInput?.Set((config.AudioConfig.AudioBitrate / 1000).ToString(), 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; + + // Validate RenderFPS + if (!double.TryParse(_renderFpsInput?.Value, out var renderFps) || renderFps <= 0) + { + ShowValidationError("Render FPS must be a positive number."); + return; + } + config.RenderFPS = renderFps; + + // Validate VideoFPS + if (!double.TryParse(_videoFpsInput?.Value, out var videoFps) || videoFps <= 0) + { + ShowValidationError("Video FPS must be a positive number."); + return; + } + config.VideoFPS = videoFps; + + // Validate RenderWidth + if (!int.TryParse(_renderWidthInput?.Value, out var renderWidth) || renderWidth <= 0) + { + ShowValidationError("Render Width must be an integer greater than 0."); + return; + } + config.RenderWidth = renderWidth; + + // Validate RenderHeight + if (!int.TryParse(_renderHeightInput?.Value, out var renderHeight) || renderHeight <= 0) + { + ShowValidationError("Render Height must be an integer greater than 0."); + return; + } + config.RenderHeight = renderHeight; + + // Validate DelayAfterEnd + if (!double.TryParse(_delayAfterEndInput?.Value, out var delay) || delay < 0) + { + ShowValidationError("Delay After End must be a non-negative number."); + return; + } + config.DelayAfterEnd = delay; + + // 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; + } + if (_keyframeIntervalInput != null && double.TryParse(_keyframeIntervalInput.Value, out var kf) && kf > 0) + { + config.VideoConfig.KeyframeInterval = kf; + } + + // Sync Audio Config + if (_audioBitrateInput != null && int.TryParse(_audioBitrateInput.Value, out var ab) && ab > 0) + { + config.AudioConfig.AudioBitrate = ab * 1000; + } + + // 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.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(); + + var hLayout = panelObj.AddComponent(); + 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; + + var panelLE = panelObj.AddComponent(); + 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(); + + var colLayout = colObj.AddComponent(); + colLayout.spacing = spacing; + colLayout.padding = new RectOffset(0, 0, 0, 0); + colLayout.childControlWidth = true; + colLayout.childControlHeight = true; + colLayout.childForceExpandWidth = true; + colLayout.childForceExpandHeight = false; + + // 핵심: 좌/우 선호 너비를 0으로 강제해 항상 정확히 절반씩 나누도록 한다. + // (기존 행마다 HLG 방식은 라벨/값 텍스트 길이에 따라 preferred가 달라져 + // 중앙이 (W + P1 - P2) / 2 로 행마다 틀어졌음) + var colLE = colObj.AddComponent(); + 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(); + + var slotLayout = slotObj.AddComponent(); + slotLayout.spacing = 3f; + slotLayout.padding = new RectOffset(0, 0, 0, 0); + slotLayout.childControlWidth = true; + slotLayout.childControlHeight = true; + slotLayout.childForceExpandWidth = true; + slotLayout.childForceExpandHeight = false; + + var slotLE = slotObj.AddComponent(); + 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? 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(); + 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? onChanged = null) + { + var container = new GameObject($"FullWidth_{id}"); + container.transform.SetParent(parent, false); + + var containerRect = container.AddComponent(); + var layout = container.AddComponent(); + 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(); + 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? 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(); + 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 CreateDropdownField( + O5Context ctx, + Transform colParent, + string labelText, + T defaultValue, + T value, + IReadOnlyList values, + Func display, + string id, + Action? 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(); + labelLE.preferredHeight = 18f; + labelLE.minHeight = 18f; + + var ddContainer = new GameObject($"DD_{id}"); + ddContainer.transform.SetParent(colParent, false); + var ddRect = ddContainer.AddComponent(); + + // ReSharper disable once InconsistentNaming + var ddLE = ddContainer.AddComponent(); + ddLE.preferredHeight = 50f; + ddLE.minHeight = 50f; + + 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); + } +} \ No newline at end of file diff --git a/Util/BundleLoader.cs b/Util/BundleLoader.cs index 585a9d1..668e65c 100644 --- a/Util/BundleLoader.cs +++ b/Util/BundleLoader.cs @@ -4,41 +4,40 @@ using System.Resources; using MelonLoader; using UnityEngine; -namespace ADOFAIRenderer.Util +namespace ADOFAIRenderer.Util; + +internal static class BundleLoader { - internal static class BundleLoader + private static readonly string BundlePlatform; + private static readonly Assembly? ExecutingAssembly; + + static BundleLoader() { - private static readonly string BundlePlatform; - private static readonly Assembly? ExecutingAssembly; - - static BundleLoader() + BundlePlatform = Application.platform switch { - BundlePlatform = Application.platform switch - { - RuntimePlatform.WindowsPlayer => "Win64", - RuntimePlatform.OSXPlayer => "OSX", - _ => "Linux" - }; - ExecutingAssembly = Assembly.GetExecutingAssembly(); - } + RuntimePlatform.WindowsPlayer => "Win64", + RuntimePlatform.OSXPlayer => "OSX", + _ => "Linux" + }; + ExecutingAssembly = Assembly.GetExecutingAssembly(); + } - public static AssetBundle GetBundle(string resourcePath) + public static AssetBundle GetBundle(string resourcePath) + { + if (ExecutingAssembly != null) { - if (ExecutingAssembly != null) + var bundlePath = $"ADOFAIRenderer.Bundle.{BundlePlatform}.{resourcePath}"; + using var resourceStream = ExecutingAssembly.GetManifestResourceStream(bundlePath); + if (resourceStream != null) { - var bundlePath = $"ADOFAIRenderer.Bundle.{BundlePlatform}.{resourcePath}"; - using var resourceStream = ExecutingAssembly.GetManifestResourceStream(bundlePath); - if (resourceStream != null) - { - return AssetBundle.LoadFromStream(resourceStream); - } - - Melon.Logger.Error($"Could not find bundle resource in DLL.\n{bundlePath}"); - throw new MissingManifestResourceException("Could not find bundle resource in DLL."); + return AssetBundle.LoadFromStream(resourceStream); } - - Melon.Logger.Error("Executing assembly has not been set properly."); - throw new InvalidOperationException("ExecutingAssembly has not been set."); + + Melon.Logger.Error($"Could not find bundle resource in DLL.\n{bundlePath}"); + throw new MissingManifestResourceException("Could not find bundle resource in DLL."); } + + Melon.Logger.Error("Executing assembly has not been set properly."); + throw new InvalidOperationException("ExecutingAssembly has not been set."); } } \ No newline at end of file diff --git a/Util/FFmpegVideoBGPlayer.cs b/Util/FFmpegVideoBGPlayer.cs index a09eeda..c804819 100644 --- a/Util/FFmpegVideoBGPlayer.cs +++ b/Util/FFmpegVideoBGPlayer.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.IO; +using ADOFAIRenderer.UI; using ADOFAIRenderer.Util.FFmpeg; using MelonLoader; using UnityEngine; @@ -77,7 +78,7 @@ namespace ADOFAIRenderer.Util try { _decoder = new FFmpegNativeDecoder(videoPath); - _computeShader = RenderMod.ColorConversionShader; + _computeShader = ModAssets.ColorConversionShader; var w = _decoder.Width; var h = _decoder.Height; diff --git a/Util/FrameReader.cs b/Util/FrameReader.cs index 3db2637..0f4e7a5 100644 --- a/Util/FrameReader.cs +++ b/Util/FrameReader.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Collections.Generic; using System.Linq; using ADOFAIRenderer.Core; +using ADOFAIRenderer.UI; using FFmpeg.AutoGen; using MelonLoader; using Unity.Collections; @@ -71,7 +72,7 @@ namespace ADOFAIRenderer.Util } // Check if GPU color conversion is enabled - bool useGPUConversion = RenderMod.ColorConversionShader; + bool useGPUConversion = ModAssets.ColorConversionShader; AVPixelFormat expectedFmt = AVPixelFormat.AV_PIX_FMT_RGBA; if (useGPUConversion) @@ -87,31 +88,31 @@ namespace ADOFAIRenderer.Util var bufferSize = texture.width * texture.height * 4; // RGBA = 4 channel state.Setup(currentFrame, bufferSize, useGPUConversion, expectedFmt, texture.width, texture.height); - if (useGPUConversion && state.GPUBuffer != null && RenderMod.ColorConversionShader) + if (useGPUConversion && state.GPUBuffer != null && ModAssets.ColorConversionShader) { int kernelIndex = -1; if (expectedFmt == AVPixelFormat.AV_PIX_FMT_NV12) { - kernelIndex = RenderMod.ColorConversionShader.FindKernel("CSMain_NV12"); + kernelIndex = ModAssets.ColorConversionShader.FindKernel("CSMain_NV12"); } else if (expectedFmt == AVPixelFormat.AV_PIX_FMT_YUV420P) { - kernelIndex = RenderMod.ColorConversionShader.FindKernel("CSMain_YUV420P"); + kernelIndex = ModAssets.ColorConversionShader.FindKernel("CSMain_YUV420P"); } if (kernelIndex >= 0) { - RenderMod.ColorConversionShader.SetTexture(kernelIndex, SourceTexture, texture); - RenderMod.ColorConversionShader.SetBuffer(kernelIndex, DestinationBuffer, state.GPUBuffer); - RenderMod.ColorConversionShader.SetInt(Width, texture.width); - RenderMod.ColorConversionShader.SetInt(Height, texture.height); + ModAssets.ColorConversionShader.SetTexture(kernelIndex, SourceTexture, texture); + ModAssets.ColorConversionShader.SetBuffer(kernelIndex, DestinationBuffer, state.GPUBuffer); + ModAssets.ColorConversionShader.SetInt(Width, texture.width); + ModAssets.ColorConversionShader.SetInt(Height, texture.height); var threadGroupsX = expectedFmt == AVPixelFormat.AV_PIX_FMT_NV12 ? (texture.width + 31) / 32 : (texture.width + 63) / 64; var threadGroupsY = (texture.height + 15) / 16; - RenderMod.ColorConversionShader.Dispatch(kernelIndex, threadGroupsX, threadGroupsY, 1); + ModAssets.ColorConversionShader.Dispatch(kernelIndex, threadGroupsX, threadGroupsY, 1); AsyncGPUReadback.Request( state.GPUBuffer,