Translate Codebase Comments and Log Messages to English

This commit is contained in:
copecone committed 2026-08-21 23:35:02 +09:00
1 parent 111f5cf792
commit 25c38707c3
23 files changed
+915 -921

No files matched your search

+6 -6
View File
@@ -45,20 +45,20 @@ internal class FirstUpdateHook : MonoBehaviour
var currentVersion = driverVer.Value.ToString("F2"); var currentVersion = driverVer.Value.ToString("F2");
CommonWarningDialogUI.ShowDialog( CommonWarningDialogUI.ShowDialog(
"NVIDIA 그래픽 드라이버 업데이트 권장", "NVIDIA Graphics Driver Update Recommended",
$"현재 감지된 NVIDIA 그래픽 드라이버 버전: <b><color=#ff5555>{currentVersion}</color></b>\n\n하드웨어 가속(NVENC)을 사용하여 영상을 렌더링하려면 드라이버 버전 <b><color=#55ff55>610.00 이상</color></b>이 필요합니다.\n\n드라이버 버전이 낮으면 인코딩이 실패하거나 소프트웨어(CPU) 인코딩으로 대체되어 렌더링이 매우 느려집니다. 드라이버 업데이트를 진행해 주시기 바랍니다.", $"Currently detected NVIDIA graphics driver version: <b><color=#ff5555>{currentVersion}</color></b>\n\nDriver version <b><color=#55ff55>610.00 or higher</color></b> is required to render video using hardware acceleration (NVENC).\n\nIf the driver version is lower, encoding may fail or fallback to software (CPU) encoding, significantly slowing down rendering. Please update your graphics driver.",
new DialogButton("드라이버 업데이트", () => new DialogButton("Update Driver", () =>
{ {
Application.OpenURL("https://www.nvidia.com/Download/index.aspx"); Application.OpenURL("https://www.nvidia.com/Download/index.aspx");
Application.Quit(); Application.Quit();
}, new Color(0.0f, 0.8f, 1.0f, 1.0f)), }, new Color(0.0f, 0.8f, 1.0f, 1.0f)),
new DialogButton("기능 비활성화", () => new DialogButton("Disable Feature", () =>
{ {
RenderMod.IsFeatureDisabled = true; RenderMod.IsFeatureDisabled = true;
}), }),
new DialogButton("이번만 그냥 진행", () => new DialogButton("Continue Anyway", () =>
{ {
// 아무 동작 안 함 // ignored
}) })
); );
} }
+28 -28
View File
@@ -40,10 +40,10 @@ internal static class RenderContext
if (result.Success && Notification) if (result.Success && Notification)
{ {
var fileString = !string.IsNullOrEmpty(RenderManager.Instance.CurrentOutPath) ? var fileString = !string.IsNullOrEmpty(RenderManager.Instance.CurrentOutPath) ?
$"'{Path.GetFileName(RenderManager.Instance.CurrentOutPath)}'이(가)" : "동영상이"; $"'{Path.GetFileName(RenderManager.Instance.CurrentOutPath)}'" : "Video";
var renderFolder = Path.GetDirectoryName(RenderManager.Instance.CurrentOutPath); var renderFolder = Path.GetDirectoryName(RenderManager.Instance.CurrentOutPath);
OSNotification.Show("렌더링 완료!", $"{fileString} 성공적으로 저장되었습니다.", renderFolder); OSNotification.Show("Rendering Complete!", $"{fileString} saved successfully.", renderFolder);
} }
}; };
public static Action<RenderResult> OnComplete = OnCompleteDefault; public static Action<RenderResult> OnComplete = OnCompleteDefault;
@@ -178,7 +178,7 @@ internal class RenderManager : MonoBehaviour
if (_activeVideoFFmpeg != null) if (_activeVideoFFmpeg != null)
{ {
Melon<RenderMod>.Logger.Msg("게임 종료 또는 RenderManager 파괴 감지: FFmpeg 네이티브 인코더 리소스를 해제합니다."); Melon<RenderMod>.Logger.Msg("Game exit or RenderManager destruction detected: Disposing FFmpeg native encoder resources.");
_activeVideoFFmpeg.Dispose(); _activeVideoFFmpeg.Dispose();
_activeVideoFFmpeg = null; _activeVideoFFmpeg = null;
} }
@@ -187,7 +187,7 @@ internal class RenderManager : MonoBehaviour
{ {
if (_allVideoFFmpegInstances.Count > 0) if (_allVideoFFmpegInstances.Count > 0)
{ {
Melon<RenderMod>.Logger.Msg($"게임 종료 감지: 생성된 모든 FFmpeg 인코더 인스턴스({_allVideoFFmpegInstances.Count}개)를 수거 및 해제합니다."); Melon<RenderMod>.Logger.Msg($"Game exit detected: Cleaning up and disposing all created FFmpeg encoder instances ({_allVideoFFmpegInstances.Count}).");
foreach (var instance in _allVideoFFmpegInstances) foreach (var instance in _allVideoFFmpegInstances)
{ {
try try
@@ -196,7 +196,7 @@ internal class RenderManager : MonoBehaviour
} }
catch catch
{ {
// Ignore // ignored
} }
} }
_allVideoFFmpegInstances.Clear(); _allVideoFFmpegInstances.Clear();
@@ -278,18 +278,18 @@ internal class RenderManager : MonoBehaviour
{ {
if (!IsRendering) return; if (!IsRendering) return;
// [렉으로 인한 프레임 렌더링 스킵 시 재시도 로직 (원본 모드 이식)] // [Retry logic when frame rendering is skipped due to lag (ported from original mod)]
if (renderCheck && IsCapturing) if (renderCheck && IsCapturing)
{ {
if (skipCount < MaxRenderRetry) if (skipCount < MaxRenderRetry)
{ {
Melon<RenderMod>.Logger.Warning($"[Render Skipped] Frame {frameCount} rendering missed. Retrying... (Attempt {skipCount + 1}/{MaxRenderRetry})"); Melon<RenderMod>.Logger.Warning($"[Render Skipped] Frame {frameCount} rendering missed. Retrying... (Attempt {skipCount + 1}/{MaxRenderRetry})");
skipCount++; skipCount++;
Time.timeScale = 0f; // 진행 시간 멈추기 (재시도) Time.timeScale = 0f; // Pause progress time (retry)
return; // 업데이트 진행하지 않고 대기 return; // Wait without advancing update
} }
// 60회 초과 시 최후의 블랙 프레임 삽입하여 전체 파이프라인 마비 방지 // If retries exceed limit, supply a fallback black frame to prevent freezing the pipeline
Melon<RenderMod>.Logger.Error($"[Render Skipped] Frame {frameCount} rendering failed after 60 retries. Supplying black frame."); Melon<RenderMod>.Logger.Error($"[Render Skipped] Frame {frameCount} rendering failed after 60 retries. Supplying black frame.");
var currentFrame = frameCountForVideo++; var currentFrame = frameCountForVideo++;
@@ -318,11 +318,11 @@ internal class RenderManager : MonoBehaviour
FrameAvailableEvent.Set(); FrameAvailableEvent.Set();
} }
renderCheck = false; // 진행 허용 renderCheck = false; // Allow progress
} }
skipCount = 0; skipCount = 0;
renderCheck = true; // 다음 프레임 검사용으로 초기화 renderCheck = true; // Reset for next frame check
frameCount++; frameCount++;
} }
@@ -344,9 +344,9 @@ internal class RenderManager : MonoBehaviour
{ {
if (!IsCapturing || !texture) return; if (!IsCapturing || !texture) return;
renderCheck = false; // 이번 프레임 정상 렌더링 완료 renderCheck = false; // Normal rendering complete for this frame
// [Backpressure Throttling (2번 작업)] // [Backpressure Throttling]
isThrottling = false; isThrottling = false;
while (IsCapturing && IsRendering) while (IsCapturing && IsRendering)
@@ -458,11 +458,11 @@ internal class RenderManager : MonoBehaviour
} }
catch (Exception ex) catch (Exception ex)
{ {
Melon<RenderMod>.Logger.Error($"인코더 초기화 에러: {ex}"); Melon<RenderMod>.Logger.Error($"Encoder initialization error: {ex}");
CommonWarningDialogUI.ShowDialog( CommonWarningDialogUI.ShowDialog(
"인코더 오류", "Encoder Error",
$"비디오 인코더를 초기화할 수 없습니다.\n오류: {ex.Message}", $"Failed to initialize video encoder.\nError: {ex.Message}",
new DialogButton("확인", () => new DialogButton("OK", () =>
{ {
ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "scnEditor"); ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "scnEditor");
}) })
@@ -676,7 +676,7 @@ internal class RenderManager : MonoBehaviour
} }
catch (Exception ex) catch (Exception ex)
{ {
Melon<RenderMod>.Logger.Error($"FFmpeg 렌더링 스레드 오류: {ex}"); Melon<RenderMod>.Logger.Error($"FFmpeg rendering thread error: {ex}");
videoFFmpeg.Dispose(); videoFFmpeg.Dispose();
lock (_allVideoFFmpegInstances) lock (_allVideoFFmpegInstances)
{ {
@@ -720,22 +720,22 @@ internal class RenderManager : MonoBehaviour
if (success) StartCoroutine(StopRenderCoroutine()); if (success) StartCoroutine(StopRenderCoroutine());
else else
{ {
Melon<RenderMod>.Logger.Error("렌더링이 실패했거나 취소되었습니다. 상태 및 리소스를 안전하게 청소합니다."); Melon<RenderMod>.Logger.Error("Rendering failed or was cancelled. Safely cleaning up state and resources.");
// 1. 렌더링 백그라운드 스레드가 큐에 유입된 잔여 프레임 처리를 마무리하고 종료될 때까지 대기 // 1. Wait for render background thread to finish processing remaining queued frames and terminate
if (RenderThread is { IsAlive: true }) if (RenderThread is { IsAlive: true })
{ {
RenderThread.Join(2000); RenderThread.Join(2000);
} }
// 2. 스레드 종료 보장 후, 안전하게 FFmpeg 네이티브 인코더 리소스 해제 // 2. After ensuring thread termination, safely release FFmpeg native encoder resources
if (_activeVideoFFmpeg != null) if (_activeVideoFFmpeg != null)
{ {
_activeVideoFFmpeg.Dispose(); _activeVideoFFmpeg.Dispose();
_activeVideoFFmpeg = null; _activeVideoFFmpeg = null;
} }
// 3. 메모리 누수 방지: 큐에 렌트된 임시 프레임 버퍼 풀 반환 및 클리어 // 3. Memory leak prevention: Return rented temporary frame buffer pool in queue and clear
lock (ImageQueue) lock (ImageQueue)
{ {
foreach (var kvp in ImageQueue.Values) foreach (var kvp in ImageQueue.Values)
@@ -754,14 +754,14 @@ internal class RenderManager : MonoBehaviour
_frameReader?.Dispose(); _frameReader?.Dispose();
// 디버그 모니터 UI 정리 // Clean up Debug Monitor UI
if (_debugMonitorObj) if (_debugMonitorObj)
{ {
Destroy(_debugMonitorObj); Destroy(_debugMonitorObj);
_debugMonitorObj = null; _debugMonitorObj = null;
} }
// 4. 인게임 카메라 및 렌더링용 RTCam 상태 안전 원복 // 4. Safely restore in-game camera and RTCam state for rendering
if (scrCamera.instance) if (scrCamera.instance)
{ {
scrCamera.instance.forceRTCam = false; scrCamera.instance.forceRTCam = false;
@@ -771,24 +771,24 @@ internal class RenderManager : MonoBehaviour
if (hook) Destroy(hook); if (hook) Destroy(hook);
} }
// 5. 인게임 실패 방지 플래그 원상 복구 // 5. Restore in-game no-fail flags
if (scrController.instance) if (scrController.instance)
{ {
scrController.instance.noFail = false; scrController.instance.noFail = false;
scrController.instance.noFailInfiniteMargin = false; scrController.instance.noFailInfiniteMargin = false;
} }
// 6. 취소된 미완성 임시 동영상 파일 즉각 삭제 // 6. Immediately delete cancelled incomplete temporary video file
if (!string.IsNullOrEmpty(CurrentOutPath) && File.Exists(CurrentOutPath)) if (!string.IsNullOrEmpty(CurrentOutPath) && File.Exists(CurrentOutPath))
{ {
try try
{ {
File.Delete(CurrentOutPath); File.Delete(CurrentOutPath);
Melon<RenderMod>.Logger.Msg($"[File Cleanup] 미완성 동영상 삭제 완료: {CurrentOutPath}"); Melon<RenderMod>.Logger.Msg($"[File Cleanup] Deleted incomplete video: {CurrentOutPath}");
} }
catch (Exception ex) catch (Exception ex)
{ {
Melon<RenderMod>.Logger.Error($"[File Cleanup] 임시 동영상 삭제 실패: {ex.Message}"); Melon<RenderMod>.Logger.Error($"[File Cleanup] Failed to delete temporary video: {ex.Message}");
} }
CurrentOutPath = null; CurrentOutPath = null;
} }
+5 -5
View File
@@ -31,7 +31,7 @@ internal class PositiveOnlyDoubleLimit : DoubleLimit
return true; return true;
} }
errorMessage = "값은 0보다 큰 양수여야 합니다."; errorMessage = "Value must be a positive number greater than 0.";
return false; return false;
} }
} }
@@ -60,16 +60,16 @@ internal class RangeDoubleLimit : DoubleLimit
if (value < Min) if (value < Min)
{ {
errorMessage = Max.HasValue errorMessage = Max.HasValue
? $"값은 {Min.Value}에서 {Max.Value} 사이의 숫자여야 합니다." ? $"Value must be a number between {Min.Value} and {Max.Value}."
: $"값은 {Min.Value} 이상의 숫자여야 합니다."; : $"Value must be a number greater than or equal to {Min.Value}.";
return false; return false;
} }
if (value > Max) if (value > Max)
{ {
errorMessage = Min.HasValue errorMessage = Min.HasValue
? $"값은 {Min.Value}에서 {Max.Value} 사이의 숫자여야 합니다." ? $"Value must be a number between {Min.Value} and {Max.Value}."
: $"값은 {Max.Value} 이하의 숫자여야 합니다."; : $"Value must be a number less than or equal to {Max.Value}.";
return false; return false;
} }
+1 -1
View File
@@ -53,7 +53,7 @@ internal class DoubleInputValidator
if (field == null) if (field == null)
{ {
Melon<RenderMod>.Logger.Warning( Melon<RenderMod>.Logger.Warning(
$"DoubleField '{fieldName}'을(를) 찾을 수 없습니다. 유효성 검사를 건너뜁니다."); $"DoubleField '{fieldName}' not found. Skipping validation.");
continue; continue;
} }
+1 -1
View File
@@ -41,7 +41,7 @@ internal class EnumDropdownBinder<T> where T : struct, Enum
if (_dropdown == null) if (_dropdown == null)
{ {
Melon<RenderMod>.Logger.Error( Melon<RenderMod>.Logger.Error(
$"DropdownField '{fieldName}'을(를) 찾을 수 없습니다."); $"DropdownField '{fieldName}' not found.");
return; return;
} }
+1 -1
View File
@@ -53,7 +53,7 @@ internal class IntegerInputValidator
if (field == null) if (field == null)
{ {
Melon<RenderMod>.Logger.Warning( Melon<RenderMod>.Logger.Warning(
$"IntegerField '{fieldName}'을(를) 찾을 수 없습니다. 유효성 검사를 건너뜁니다."); $"IntegerField '{fieldName}' not found. Skipping validation.");
continue; continue;
} }
+5 -5
View File
@@ -31,7 +31,7 @@ internal class PositiveOnlyIntegerLimit : IntegerLimit
return true; return true;
} }
errorMessage = "값은 0보다 큰 양의 정수여야 합니다."; errorMessage = "Value must be a positive integer greater than 0.";
return false; return false;
} }
} }
@@ -60,16 +60,16 @@ internal class RangeIntegerLimit : IntegerLimit
if (value < Min) if (value < Min)
{ {
errorMessage = Max.HasValue errorMessage = Max.HasValue
? $"값은 {Min.Value}에서 {Max.Value} 사이의 정수여야 합니다." ? $"Value must be an integer between {Min.Value} and {Max.Value}."
: $"값은 {Min.Value} 이상의 정수여야 합니다."; : $"Value must be an integer greater than or equal to {Min.Value}.";
return false; return false;
} }
if (value > Max) if (value > Max)
{ {
errorMessage = Min.HasValue errorMessage = Min.HasValue
? $"값은 {Min.Value}에서 {Max.Value} 사이의 정수여야 합니다." ? $"Value must be an integer between {Min.Value} and {Max.Value}."
: $"값은 {Max.Value} 이하의 정수여야 합니다."; : $"Value must be an integer less than or equal to {Max.Value}.";
return false; return false;
} }
+3 -3
View File
@@ -31,7 +31,7 @@ internal static class PresetManager
{ {
if (string.IsNullOrWhiteSpace(name)) if (string.IsNullOrWhiteSpace(name))
{ {
throw new ArgumentException("프리셋 이름은 비어 있을 수 없습니다."); throw new ArgumentException("Preset name cannot be empty.");
} }
string dir = GetPresetsDirectory(); string dir = GetPresetsDirectory();
@@ -154,13 +154,13 @@ internal static class PresetManager
} }
catch (Exception ex) catch (Exception ex)
{ {
MelonLogger.Warning($"프리셋 파일 '{Path.GetFileName(file)}' 로드 중 오류 발생: {ex.Message}"); MelonLogger.Warning($"Error loading preset file '{Path.GetFileName(file)}': {ex.Message}");
} }
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
MelonLogger.Error($"프리셋 목록을 불러오는 중 오류 발생: {ex.Message}"); MelonLogger.Error($"Error retrieving preset list: {ex.Message}");
} }
return presets; return presets;
+1 -1
View File
@@ -18,7 +18,7 @@ public static class scnEditorPatch
if (RenderMod.IsFeatureDisabled) return; if (RenderMod.IsFeatureDisabled) return;
if (!__instance || !__instance.buttonExit) if (!__instance || !__instance.buttonExit)
{ {
Melon<RenderMod>.Logger.Msg("scnEditor에 렌더 버튼을 만들 수 없습니다."); Melon<RenderMod>.Logger.Msg("Cannot create render button in scnEditor.");
return; return;
} }
+4 -4
View File
@@ -48,11 +48,11 @@ namespace ADOFAIRenderer
{ {
ShaderBundle = BundleLoader.GetBundle("shaders"); ShaderBundle = BundleLoader.GetBundle("shaders");
ColorConversionShader = ShaderBundle.LoadAsset<ComputeShader>("ColorConversion"); ColorConversionShader = ShaderBundle.LoadAsset<ComputeShader>("ColorConversion");
Melon<RenderMod>.Logger.Msg("컬러 변환 Compute Shader 로드 완료."); Melon<RenderMod>.Logger.Msg("Color conversion Compute Shader loaded successfully.");
} }
catch (Exception ex) catch (Exception ex)
{ {
Melon<RenderMod>.Logger.Warning($"Shader 에셋번들을 로드할 수 없습니다. GPU 컬러 변환이 비활성화되며 CPU로 폴백합니다. 에러: {ex.Message}"); Melon<RenderMod>.Logger.Warning($"Failed to load Shader AssetBundle. GPU color conversion will be disabled and fallback to CPU. Error: {ex.Message}");
} }
} }
@@ -62,7 +62,7 @@ namespace ADOFAIRenderer
var currentModule = EventSystem.current.currentInputModule; var currentModule = EventSystem.current.currentInputModule;
if (currentModule) return; if (currentModule) return;
Melon<RenderMod>.Logger.Msg("Unity 6 인풋 모듈 이식."); Melon<RenderMod>.Logger.Msg("Ported Unity 6 Input Module.");
var newModule = EventSystem.current.gameObject.AddComponent<StandaloneInputModule>(); var newModule = EventSystem.current.gameObject.AddComponent<StandaloneInputModule>();
newModule.enabled = true; newModule.enabled = true;
@@ -71,7 +71,7 @@ namespace ADOFAIRenderer
public override void OnApplicationQuit() public override void OnApplicationQuit()
{ {
if (RenderManager.Instance == null) return; if (RenderManager.Instance == null) return;
Melon<RenderMod>.Logger.Msg("MelonLoader 종료 감지: RenderManager의 활성 프로세스를 강제 수거 및 종료합니다."); Melon<RenderMod>.Logger.Msg("MelonLoader shutdown detected: Forcefully terminating and cleaning up active RenderManager processes.");
RenderManager.Instance.CleanUpActiveRender(); RenderManager.Instance.CleanUpActiveRender();
} }
} }
+20 -20
View File
@@ -63,11 +63,11 @@ internal class CommonWarningDialogUI : MonoBehaviour
} }
catch catch
{ {
// 로딩 실패 // Loading failed
} }
} }
// 폴백 처리 // Fallback processing
if (!font) if (!font)
{ {
try try
@@ -121,15 +121,15 @@ internal class CommonWarningDialogUI : MonoBehaviour
} }
} }
// UI 레이어(5) 설정 // Set UI layer (5)
gameObject.layer = 5; gameObject.layer = 5;
// Canvas 설정 // Canvas setup
var canvas = gameObject.AddComponent<Canvas>(); var canvas = gameObject.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay; canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = 32766; // FFmpegDownloadUI(32767) 바로 아래 canvas.sortingOrder = 32766; // Just below FFmpegDownloadUI (32767)
// 해상도 대응 캔버스 스케일러 설정 // Responsive Canvas Scaler setup
var scaler = gameObject.AddComponent<CanvasScaler>(); var scaler = gameObject.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920, 1080); scaler.referenceResolution = new Vector2(1920, 1080);
@@ -138,13 +138,13 @@ internal class CommonWarningDialogUI : MonoBehaviour
gameObject.AddComponent<GraphicRaycaster>(); gameObject.AddComponent<GraphicRaycaster>();
// 1. 전체 화면 오버레이 배경 생성 // 1. Create full screen overlay background
var bgPanelObj = new GameObject("BackgroundPanel"); var bgPanelObj = new GameObject("BackgroundPanel");
bgPanelObj.transform.SetParent(transform, false); bgPanelObj.transform.SetParent(transform, false);
bgPanelObj.layer = 5; bgPanelObj.layer = 5;
var bgImage = bgPanelObj.AddComponent<Image>(); var bgImage = bgPanelObj.AddComponent<Image>();
bgImage.color = new Color(0f, 0f, 0f, 0.65f); // 어두운 투명 오버레이 bgImage.color = new Color(0f, 0f, 0f, 0.65f); // Dark transparent overlay
bgImage.raycastTarget = true; bgImage.raycastTarget = true;
var bgRect = bgPanelObj.GetComponent<RectTransform>(); var bgRect = bgPanelObj.GetComponent<RectTransform>();
@@ -155,17 +155,17 @@ internal class CommonWarningDialogUI : MonoBehaviour
var font = GetNanumSquareNeoBdFontAsset(); var font = GetNanumSquareNeoBdFontAsset();
// 2. 모달 컨테이너 생성 // 2. Create modal container
var modalContainerObj = new GameObject("ModalContainer"); var modalContainerObj = new GameObject("ModalContainer");
modalContainerObj.transform.SetParent(bgPanelObj.transform, false); modalContainerObj.transform.SetParent(bgPanelObj.transform, false);
modalContainerObj.layer = 5; modalContainerObj.layer = 5;
var modalImage = modalContainerObj.AddComponent<Image>(); var modalImage = modalContainerObj.AddComponent<Image>();
modalImage.color = new Color(0.08f, 0.08f, 0.1f, 0.88f); // 기존 UI의 배경 톤과 동일 modalImage.color = new Color(0.08f, 0.08f, 0.1f, 0.88f); // Matching background tone of existing UI
var modalOutline = modalContainerObj.AddComponent<Outline>(); var modalOutline = modalContainerObj.AddComponent<Outline>();
modalOutline.effectColor = Color.white; modalOutline.effectColor = Color.white;
modalOutline.effectDistance = new Vector2(3.0f, -3.0f); // 기존 UI 외곽선 스타일 일관성 modalOutline.effectDistance = new Vector2(3.0f, -3.0f); // Consistent with existing UI outline style
var modalRect = modalContainerObj.GetComponent<RectTransform>(); var modalRect = modalContainerObj.GetComponent<RectTransform>();
modalRect.anchorMin = new Vector2(0.5f, 0.5f); modalRect.anchorMin = new Vector2(0.5f, 0.5f);
@@ -173,7 +173,7 @@ internal class CommonWarningDialogUI : MonoBehaviour
modalRect.pivot = new Vector2(0.5f, 0.5f); modalRect.pivot = new Vector2(0.5f, 0.5f);
modalRect.sizeDelta = new Vector2(650, 360); modalRect.sizeDelta = new Vector2(650, 360);
// 3. 타이틀 텍스트 생성 // 3. Create title text
var titleObj = new GameObject("TitleText"); var titleObj = new GameObject("TitleText");
titleObj.transform.SetParent(modalContainerObj.transform, false); titleObj.transform.SetParent(modalContainerObj.transform, false);
titleObj.layer = 5; titleObj.layer = 5;
@@ -182,7 +182,7 @@ internal class CommonWarningDialogUI : MonoBehaviour
titleText.text = title; titleText.text = title;
titleText.fontSize = 24; titleText.fontSize = 24;
titleText.font = font; titleText.font = font;
titleText.color = new Color(1.0f, 0.8f, 0.2f, 1.0f); // 경고 테마에 맞는 주황/황색 계열 titleText.color = new Color(1.0f, 0.8f, 0.2f, 1.0f); // Orange/yellow tone matching warning theme
titleText.alignment = TextAlignmentOptions.Center; titleText.alignment = TextAlignmentOptions.Center;
var titleRect = titleObj.GetComponent<RectTransform>(); var titleRect = titleObj.GetComponent<RectTransform>();
@@ -192,7 +192,7 @@ internal class CommonWarningDialogUI : MonoBehaviour
titleRect.anchoredPosition = new Vector2(0, -35); titleRect.anchoredPosition = new Vector2(0, -35);
titleRect.sizeDelta = new Vector2(0, 40); titleRect.sizeDelta = new Vector2(0, 40);
// 4. 본문 설명 텍스트 생성 // 4. Create description text
var descObj = new GameObject("DescriptionText"); var descObj = new GameObject("DescriptionText");
descObj.transform.SetParent(modalContainerObj.transform, false); descObj.transform.SetParent(modalContainerObj.transform, false);
descObj.layer = 5; descObj.layer = 5;
@@ -212,7 +212,7 @@ internal class CommonWarningDialogUI : MonoBehaviour
descRect.anchoredPosition = new Vector2(0, -65); descRect.anchoredPosition = new Vector2(0, -65);
descRect.sizeDelta = new Vector2(-40, 120); descRect.sizeDelta = new Vector2(-40, 120);
// 5. 버튼 컨테이너 생성 // 5. Create button container
var buttonContainerObj = new GameObject("ButtonContainer", typeof(RectTransform)); var buttonContainerObj = new GameObject("ButtonContainer", typeof(RectTransform));
buttonContainerObj.transform.SetParent(modalContainerObj.transform, false); buttonContainerObj.transform.SetParent(modalContainerObj.transform, false);
buttonContainerObj.layer = 5; buttonContainerObj.layer = 5;
@@ -224,7 +224,7 @@ internal class CommonWarningDialogUI : MonoBehaviour
btnContainerRect.anchoredPosition = new Vector2(0, 35); btnContainerRect.anchoredPosition = new Vector2(0, 35);
btnContainerRect.sizeDelta = new Vector2(0, 50); btnContainerRect.sizeDelta = new Vector2(0, 50);
// 6. 가변 버튼 동적 배치 및 스타일링 // 6. Dynamic button layout and styling
float totalWidth = 560f; float totalWidth = 560f;
float spacing = 15f; float spacing = 15f;
float buttonHeight = 45f; float buttonHeight = 45f;
@@ -245,8 +245,8 @@ internal class CommonWarningDialogUI : MonoBehaviour
btnObj.transform.SetParent(buttonContainerObj.transform, false); btnObj.transform.SetParent(buttonContainerObj.transform, false);
btnObj.layer = 5; btnObj.layer = 5;
// 기본 색상 정의 // Define default colors
Color baseColor = btnData.BackgroundColor ?? new Color(0.2f, 0.2f, 0.23f, 1f); // 커스텀 색상 또는 회색톤 기본값 Color baseColor = btnData.BackgroundColor ?? new Color(0.2f, 0.2f, 0.23f, 1f); // Custom color or gray default
Color hoverColor = ApplyColorBrightness(baseColor, 1.2f); Color hoverColor = ApplyColorBrightness(baseColor, 1.2f);
Color pressedColor = ApplyColorBrightness(baseColor, 0.8f); Color pressedColor = ApplyColorBrightness(baseColor, 0.8f);
@@ -257,7 +257,7 @@ internal class CommonWarningDialogUI : MonoBehaviour
var btn = btnObj.AddComponent<Button>(); var btn = btnObj.AddComponent<Button>();
SetupButtonStyle(btn, baseColor, hoverColor, pressedColor); SetupButtonStyle(btn, baseColor, hoverColor, pressedColor);
// 클릭 콜백 등록 // Register click callback
btn.onClick.AddListener(() => btn.onClick.AddListener(() =>
{ {
try try
@@ -283,7 +283,7 @@ internal class CommonWarningDialogUI : MonoBehaviour
btnRect.anchoredPosition = new Vector2(posX, 0); btnRect.anchoredPosition = new Vector2(posX, 0);
btnRect.sizeDelta = new Vector2(buttonWidth, buttonHeight); btnRect.sizeDelta = new Vector2(buttonWidth, buttonHeight);
// 텍스트 라벨 추가 // Add text label
var txtObj = new GameObject("Text"); var txtObj = new GameObject("Text");
txtObj.transform.SetParent(btnObj.transform, false); txtObj.transform.SetParent(btnObj.transform, false);
txtObj.layer = 5; txtObj.layer = 5;
+18 -18
View File
@@ -15,14 +15,14 @@ internal class DebugMonitorUI : MonoBehaviour
private const float BarHeight = 28f; private const float BarHeight = 28f;
private const float BarPosX = 0f; private const float BarPosX = 0f;
private const float BarPosY = -420f; // Y position relative to screen center (Reference: 1920x1080) private const float BarPosY = -420f; // Y position relative to screen center (Reference: 1920x1080)
private const int BarTextFontSize = 16; // 눈에 띄게 증가된 텍스트 크기 private const int BarTextFontSize = 16; // Increased font size for visibility
private static readonly Color BarBackgroundColor = new(0.08f, 0.08f, 0.1f, 1.0f); private static readonly Color BarBackgroundColor = new(0.08f, 0.08f, 0.1f, 1.0f);
private static readonly Color BarFillColor = new(0.0f, 0.8f, 1.0f, 1.0f); // Sleek cyan/blue private static readonly Color BarFillColor = new(0.0f, 0.8f, 1.0f, 1.0f); // Sleek cyan/blue
private static readonly Color BarTextColor = new(1.0f, 1.0f, 1.0f, 1.0f); private static readonly Color BarTextColor = new(1.0f, 1.0f, 1.0f, 1.0f);
// --- Progress Bar Outline Configuration Constants --- // --- Progress Bar Outline Configuration Constants ---
private static readonly Color BarOutlineColor = Color.white; // 대비되는 흰색 윤곽선 private static readonly Color BarOutlineColor = Color.white; // Contrasting white outline
private static readonly Vector2 BarOutlineDistance = new(3.0f, -3.0f); // 3px의 굵고 직관적인 윤곽선 두께 private static readonly Vector2 BarOutlineDistance = new(3.0f, -3.0f); // Outline thickness
private TextMeshProUGUI _text = null!; private TextMeshProUGUI _text = null!;
private GameObject _progressBarObj = null!; private GameObject _progressBarObj = null!;
@@ -41,11 +41,11 @@ internal class DebugMonitorUI : MonoBehaviour
} }
catch catch
{ {
// 로딩 실패 // Loading failed
} }
} }
// 폴백 처리: 에셋 번들에서 실패 시 게임 자체 NanumSquareNeo 또는 내장 TMP SDF 폰트 사용 // Fallback: Use game's own font or built-in TMP SDF font if AssetBundle loading fails
if (font == null) if (font == null)
{ {
font = RDString.fontData.fontTMP; font = RDString.fontData.fontTMP;
@@ -65,15 +65,15 @@ internal class DebugMonitorUI : MonoBehaviour
private void Start() private void Start()
{ {
// UI 레이어(5) 설정 // Set UI layer (5)
gameObject.layer = 5; gameObject.layer = 5;
// Canvas 설정 // Canvas setup
var canvas = gameObject.AddComponent<Canvas>(); var canvas = gameObject.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay; canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = 9999; canvas.sortingOrder = 9999;
// 해상도 다이내믹 스케일링 설정 (Reference: 1920x1080) // Dynamic resolution scaling setup (Reference: 1920x1080)
var scaler = gameObject.AddComponent<CanvasScaler>(); var scaler = gameObject.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920, 1080); scaler.referenceResolution = new Vector2(1920, 1080);
@@ -82,13 +82,13 @@ internal class DebugMonitorUI : MonoBehaviour
gameObject.AddComponent<GraphicRaycaster>(); gameObject.AddComponent<GraphicRaycaster>();
// 배경 패널 생성 // Create background panel
var panelObj = new GameObject("BackgroundPanel"); var panelObj = new GameObject("BackgroundPanel");
panelObj.transform.SetParent(transform, false); panelObj.transform.SetParent(transform, false);
panelObj.layer = 5; panelObj.layer = 5;
var image = panelObj.AddComponent<Image>(); var image = panelObj.AddComponent<Image>();
image.color = new Color(0.08f, 0.08f, 0.1f, 0.88f); // 약간 더 고급스러운 다크톤 아크릴 감성 image.color = new Color(0.08f, 0.08f, 0.1f, 0.88f); // Dark tone acrylic style
var panelRect = panelObj.GetComponent<RectTransform>(); var panelRect = panelObj.GetComponent<RectTransform>();
panelRect.anchorMin = new Vector2(1, 1); panelRect.anchorMin = new Vector2(1, 1);
@@ -97,28 +97,28 @@ internal class DebugMonitorUI : MonoBehaviour
panelRect.anchoredPosition = new Vector2(-30, -30); panelRect.anchoredPosition = new Vector2(-30, -30);
panelRect.sizeDelta = new Vector2(360, 220); panelRect.sizeDelta = new Vector2(360, 220);
// 텍스트 생성 // Create text
var textObj = new GameObject("MonitorText"); var textObj = new GameObject("MonitorText");
textObj.transform.SetParent(panelObj.transform, false); textObj.transform.SetParent(panelObj.transform, false);
textObj.layer = 5; textObj.layer = 5;
_text = textObj.AddComponent<TextMeshProUGUI>(); _text = textObj.AddComponent<TextMeshProUGUI>();
_text.richText = true; _text.richText = true;
_text.fontSize = 16; // 13에서 16으로 증가 _text.fontSize = 16;
_text.alignment = TextAlignmentOptions.TopLeft; _text.alignment = TextAlignmentOptions.TopLeft;
// TMPro 텍스트 잘림 현상 예방 // Prevent TMPro text clipping
_text.textWrappingMode = TextWrappingModes.Normal; _text.textWrappingMode = TextWrappingModes.Normal;
_text.overflowMode = TextOverflowModes.Overflow; _text.overflowMode = TextOverflowModes.Overflow;
// NanumSquareNeo-Bd SDF 폰트 로드 및 적용 // Load and apply font
var font = GetNanumSquareNeoBdFontAsset(); var font = GetNanumSquareNeoBdFontAsset();
_text.font = font; _text.font = font;
var textRect = textObj.GetComponent<RectTransform>(); var textRect = textObj.GetComponent<RectTransform>();
textRect.anchorMin = Vector2.zero; textRect.anchorMin = Vector2.zero;
textRect.anchorMax = Vector2.one; textRect.anchorMax = Vector2.one;
textRect.offsetMin = new Vector2(20, 20); // 좀 더 넉넉하고 패딩 여백 textRect.offsetMin = new Vector2(20, 20); // Padding margin
textRect.offsetMax = new Vector2(-20, -20); textRect.offsetMax = new Vector2(-20, -20);
// --- Progress Bar UI Setup --- // --- Progress Bar UI Setup ---
@@ -130,7 +130,7 @@ internal class DebugMonitorUI : MonoBehaviour
var bgImage = _progressBarObj.AddComponent<Image>(); var bgImage = _progressBarObj.AddComponent<Image>();
bgImage.color = BarBackgroundColor; bgImage.color = BarBackgroundColor;
// 프로그레스 바 흰색 윤곽선 컴포넌트 추가 // Add progress bar outline component
var bgOutline = _progressBarObj.AddComponent<Outline>(); var bgOutline = _progressBarObj.AddComponent<Outline>();
bgOutline.effectColor = BarOutlineColor; bgOutline.effectColor = BarOutlineColor;
bgOutline.effectDistance = BarOutlineDistance; bgOutline.effectDistance = BarOutlineDistance;
@@ -164,12 +164,12 @@ internal class DebugMonitorUI : MonoBehaviour
_progressText = progressTextObj.AddComponent<TextMeshProUGUI>(); _progressText = progressTextObj.AddComponent<TextMeshProUGUI>();
_progressText.richText = true; _progressText.richText = true;
_progressText.fontSize = BarTextFontSize; // 14에서 18로 증가 _progressText.fontSize = BarTextFontSize;
_progressText.alignment = TextAlignmentOptions.Center; _progressText.alignment = TextAlignmentOptions.Center;
_progressText.font = font; _progressText.font = font;
_progressText.color = BarTextColor; _progressText.color = BarTextColor;
// 프로그레스 바 내부 텍스트 아웃라인 추가 (진행율 가독성 확보) // Add progress bar text outline for readability
var textOutline = progressTextObj.AddComponent<Outline>(); var textOutline = progressTextObj.AddComponent<Outline>();
textOutline.effectColor = new Color(0f, 0f, 0f, 0.9f); textOutline.effectColor = new Color(0f, 0f, 0f, 0.9f);
textOutline.effectDistance = new Vector2(1.5f, -1.5f); textOutline.effectDistance = new Vector2(1.5f, -1.5f);
+17 -18
View File
@@ -14,15 +14,15 @@ internal class EscapeHandlerUI : MonoBehaviour
private void Start() private void Start()
{ {
// UI 레이어(5) 설정 // Set UI layer (5)
gameObject.layer = 5; gameObject.layer = 5;
// Canvas 설정 // Canvas setup
var canvas = gameObject.AddComponent<Canvas>(); var canvas = gameObject.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay; canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = 10000; // 디버그 모니터보다 위에 오도록 설정 canvas.sortingOrder = 10000; // Placed above debug monitor
// 해상도 대응 캔버스 스케일러 설정 // Responsive Canvas Scaler setup
var scaler = gameObject.AddComponent<CanvasScaler>(); var scaler = gameObject.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920, 1080); scaler.referenceResolution = new Vector2(1920, 1080);
@@ -31,23 +31,22 @@ internal class EscapeHandlerUI : MonoBehaviour
gameObject.AddComponent<GraphicRaycaster>(); gameObject.AddComponent<GraphicRaycaster>();
// 1. 컨테이너 및 배경 패널 오브젝트 생성 // 1. Create container and background panel object
_containerObj = new GameObject("EscapeContainer"); _containerObj = new GameObject("EscapeContainer");
_containerObj.transform.SetParent(transform, false); _containerObj.transform.SetParent(transform, false);
_containerObj.layer = 5; _containerObj.layer = 5;
var bgImage = _containerObj.AddComponent<Image>(); var bgImage = _containerObj.AddComponent<Image>();
// 고급스러운 다크톤 아크릴 감성 bgImage.color = new Color(0.05f, 0.05f, 0.07f, 0.85f); // Dark tone acrylic style
bgImage.color = new Color(0.05f, 0.05f, 0.07f, 0.85f);
var containerRect = _containerObj.GetComponent<RectTransform>(); var containerRect = _containerObj.GetComponent<RectTransform>();
containerRect.anchorMin = new Vector2(0.5f, 0.5f); containerRect.anchorMin = new Vector2(0.5f, 0.5f);
containerRect.anchorMax = new Vector2(0.5f, 0.5f); containerRect.anchorMax = new Vector2(0.5f, 0.5f);
containerRect.pivot = new Vector2(0.5f, 0.5f); containerRect.pivot = new Vector2(0.5f, 0.5f);
containerRect.anchoredPosition = Vector2.zero; // 화면 중앙 containerRect.anchoredPosition = Vector2.zero; // Screen center
containerRect.sizeDelta = new Vector2(460, 160); containerRect.sizeDelta = new Vector2(460, 160);
// 2. 텍스트 오브젝트 생성 // 2. Create text object
var textObj = new GameObject("EscapeText"); var textObj = new GameObject("EscapeText");
textObj.transform.SetParent(_containerObj.transform, false); textObj.transform.SetParent(_containerObj.transform, false);
textObj.layer = 5; textObj.layer = 5;
@@ -58,7 +57,7 @@ internal class EscapeHandlerUI : MonoBehaviour
_textComponent.textWrappingMode = TextWrappingModes.NoWrap; _textComponent.textWrappingMode = TextWrappingModes.NoWrap;
_textComponent.richText = true; _textComponent.richText = true;
// 게임의 기본 폰트 적용 (RDString.fontData.fontTMP) // Apply game's default font (RDString.fontData.fontTMP)
if (RDString.fontData.fontTMP != null) if (RDString.fontData.fontTMP != null)
_textComponent.font = RDString.fontData.fontTMP; _textComponent.font = RDString.fontData.fontTMP;
@@ -68,13 +67,13 @@ internal class EscapeHandlerUI : MonoBehaviour
textRect.offsetMin = new Vector2(10, 10); textRect.offsetMin = new Vector2(10, 10);
textRect.offsetMax = new Vector2(-10, -10); textRect.offsetMax = new Vector2(-10, -10);
// 초기 상태는 비활성화 // Initial state disabled
_containerObj.SetActive(false); _containerObj.SetActive(false);
} }
private void Update() private void Update()
{ {
// 렌더링이 더 이상 활성화되어 있지 않으면 자가 파괴 // Destroy self if rendering is no longer active
if (!RenderManager.IsActive) if (!RenderManager.IsActive)
{ {
Destroy(gameObject); Destroy(gameObject);
@@ -93,10 +92,10 @@ internal class EscapeHandlerUI : MonoBehaviour
var elapsed = (float)_escapeStopwatch.Elapsed.TotalSeconds; var elapsed = (float)_escapeStopwatch.Elapsed.TotalSeconds;
var remaining = Mathf.Max(0f, 3f - elapsed); var remaining = Mathf.Max(0f, 3f - elapsed);
// 남은 시간 텍스트 업데이트 // Update remaining time text
_textComponent.text = $"<color=#FF5252>탈출까지 {remaining:F1}초..</color>\n<size=40><color=#DDDDDD>Esc 키를 계속 누르고 계세요</color></size>"; _textComponent.text = $"<color=#FF5252>Cancelling in {remaining:F1}s..</color>\n<size=40><color=#DDDDDD>Hold Esc to cancel render</color></size>";
// 마이크로 펄스 애니메이션 적용 (누르는 동안 컨테이너 전체 크기가 부드럽게 커짐) // Micro pulse animation (container smoothly scales up while holding)
var scale = 1.0f + elapsed / 3.0f * 0.15f; var scale = 1.0f + elapsed / 3.0f * 0.15f;
_containerObj.transform.localScale = new Vector3(scale, scale, 1f); _containerObj.transform.localScale = new Vector3(scale, scale, 1f);
@@ -104,10 +103,10 @@ internal class EscapeHandlerUI : MonoBehaviour
_escapeStopwatch.Stop(); _escapeStopwatch.Stop();
_escapeStopwatch = null; _escapeStopwatch = null;
_containerObj.transform.localScale = Vector3.one; // 스케일 초기화 _containerObj.transform.localScale = Vector3.one; // Reset scale
_containerObj.SetActive(false); _containerObj.SetActive(false);
// 렌더링 취소 및 씬 이탈 // Cancel rendering and exit scene
if (RenderManager.Instance) if (RenderManager.Instance)
RenderManager.Instance.CancelRender(); RenderManager.Instance.CancelRender();
} }
@@ -116,7 +115,7 @@ internal class EscapeHandlerUI : MonoBehaviour
if (_escapeStopwatch == null) return; if (_escapeStopwatch == null) return;
_escapeStopwatch.Stop(); _escapeStopwatch.Stop();
_escapeStopwatch = null; _escapeStopwatch = null;
_containerObj.transform.localScale = Vector3.one; // 스케일 초기화 _containerObj.transform.localScale = Vector3.one; // Reset scale
_containerObj.SetActive(false); _containerObj.SetActive(false);
} }
} }
+58 -58
View File
@@ -96,11 +96,11 @@ internal class FFmpegDownloadUI : MonoBehaviour
} }
catch catch
{ {
// 로딩 실패 // Loading failed
} }
} }
// 폴백 처리 // Fallback processing
if (font == null) if (font == null)
{ {
try try
@@ -127,15 +127,15 @@ internal class FFmpegDownloadUI : MonoBehaviour
private void Start() private void Start()
{ {
// UI 레이어(5) 설정 // Set UI layer (5)
gameObject.layer = 5; gameObject.layer = 5;
// Canvas 설정 // Canvas setup
var canvas = gameObject.AddComponent<Canvas>(); var canvas = gameObject.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay; canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = 32767; // 최상위 canvas.sortingOrder = 32767; // Topmost
// 해상도 대응 캔버스 스케일러 설정 // Responsive Canvas Scaler setup
var scaler = gameObject.AddComponent<CanvasScaler>(); var scaler = gameObject.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920, 1080); scaler.referenceResolution = new Vector2(1920, 1080);
@@ -144,13 +144,13 @@ internal class FFmpegDownloadUI : MonoBehaviour
gameObject.AddComponent<GraphicRaycaster>(); gameObject.AddComponent<GraphicRaycaster>();
// 1. 전체 화면 오버레이 배경 생성 // 1. Create full screen overlay background
var bgPanelObj = new GameObject("BackgroundPanel"); var bgPanelObj = new GameObject("BackgroundPanel");
bgPanelObj.transform.SetParent(transform, false); bgPanelObj.transform.SetParent(transform, false);
bgPanelObj.layer = 5; bgPanelObj.layer = 5;
var bgImage = bgPanelObj.AddComponent<Image>(); var bgImage = bgPanelObj.AddComponent<Image>();
bgImage.color = new Color(0f, 0f, 0f, 0.65f); // 어두운 투명 오버레이 bgImage.color = new Color(0f, 0f, 0f, 0.65f); // Dark transparent overlay
bgImage.raycastTarget = true; bgImage.raycastTarget = true;
var bgRect = bgPanelObj.GetComponent<RectTransform>(); var bgRect = bgPanelObj.GetComponent<RectTransform>();
@@ -161,16 +161,16 @@ internal class FFmpegDownloadUI : MonoBehaviour
var font = GetNanumSquareNeoBdFontAsset(); var font = GetNanumSquareNeoBdFontAsset();
// 2. 모달 컨테이너 생성 (다른 UI들과 디자인 일관성을 위해 동일한 아크릴 다크톤과 흰색 굵은 외곽선 적용) // 2. Create modal container
var modalContainerObj = new GameObject("ModalContainer"); var modalContainerObj = new GameObject("ModalContainer");
modalContainerObj.transform.SetParent(bgPanelObj.transform, false); modalContainerObj.transform.SetParent(bgPanelObj.transform, false);
modalContainerObj.layer = 5; modalContainerObj.layer = 5;
var modalImage = modalContainerObj.AddComponent<Image>(); var modalImage = modalContainerObj.AddComponent<Image>();
modalImage.color = new Color(0.08f, 0.08f, 0.1f, 0.88f); // 다른 UI의 배경 톤과 동일 modalImage.color = new Color(0.08f, 0.08f, 0.1f, 0.88f);
var modalOutline = modalContainerObj.AddComponent<Outline>(); var modalOutline = modalContainerObj.AddComponent<Outline>();
modalOutline.effectColor = Color.white; // DebugMonitorUI와 일치하는 흰색 외곽선 modalOutline.effectColor = Color.white;
modalOutline.effectDistance = new Vector2(3.0f, -3.0f); modalOutline.effectDistance = new Vector2(3.0f, -3.0f);
var modalRect = modalContainerObj.GetComponent<RectTransform>(); var modalRect = modalContainerObj.GetComponent<RectTransform>();
@@ -179,13 +179,13 @@ internal class FFmpegDownloadUI : MonoBehaviour
modalRect.pivot = new Vector2(0.5f, 0.5f); modalRect.pivot = new Vector2(0.5f, 0.5f);
modalRect.sizeDelta = new Vector2(600, 320); modalRect.sizeDelta = new Vector2(600, 320);
// 4. 타이틀 텍스트 생성 // 4. Create title text
var titleObj = new GameObject("TitleText"); var titleObj = new GameObject("TitleText");
titleObj.transform.SetParent(modalContainerObj.transform, false); titleObj.transform.SetParent(modalContainerObj.transform, false);
titleObj.layer = 5; titleObj.layer = 5;
var titleText = titleObj.AddComponent<TextMeshProUGUI>(); var titleText = titleObj.AddComponent<TextMeshProUGUI>();
titleText.text = "FFmpeg 라이브러리 설치 필요"; titleText.text = "FFmpeg Library Installation Required";
titleText.fontSize = 26; titleText.fontSize = 26;
titleText.font = font; titleText.font = font;
titleText.color = Color.white; titleText.color = Color.white;
@@ -198,23 +198,23 @@ internal class FFmpegDownloadUI : MonoBehaviour
titleRect.anchoredPosition = new Vector2(0, -35); titleRect.anchoredPosition = new Vector2(0, -35);
titleRect.sizeDelta = new Vector2(0, 40); titleRect.sizeDelta = new Vector2(0, 40);
// 5. 본문 설명 텍스트 생성 // 5. Create description text
var descObj = new GameObject("DescriptionText"); var descObj = new GameObject("DescriptionText");
descObj.transform.SetParent(modalContainerObj.transform, false); descObj.transform.SetParent(modalContainerObj.transform, false);
descObj.layer = 5; descObj.layer = 5;
var descText = descObj.AddComponent<TextMeshProUGUI>(); var descText = descObj.AddComponent<TextMeshProUGUI>();
var descStrBuilder = new StringBuilder(); var descStrBuilder = new StringBuilder();
descStrBuilder.AppendLine("동영상을 렌더링하려면 FFmpeg 공유 라이브러리가 필요합니다."); descStrBuilder.AppendLine("FFmpeg shared libraries are required to render video.");
if (Application.platform == RuntimePlatform.OSXPlayer) if (Application.platform == RuntimePlatform.OSXPlayer)
{ {
descStrBuilder.AppendLine("OSX 플랫폼의 경우 신뢰 가능한 공개 빌드가 없어 수동으로 FFmpeg 8.1을 빌드하여야 합니다."); descStrBuilder.AppendLine("For macOS, reliable public builds are unavailable, so FFmpeg 8.1 must be built manually.");
descStrBuilder.AppendLine("아래 버튼을 눌러 ffmpeg 공식 웹사이트를 열 수 있습니다."); descStrBuilder.AppendLine("Click the button below to open the official FFmpeg website.");
} }
else else
{ {
descStrBuilder.AppendLine("아래 버튼을 눌러 인터넷에서 자동으로 다운로드하여 설치합니다."); descStrBuilder.AppendLine("Click the button below to automatically download and install from the internet.");
descStrBuilder.AppendLine("<color=#aaaaaa><size=13>(다운로드 크기: 약 70MB, UserLibs 폴더에 압축 해제됨)</size></color>"); descStrBuilder.AppendLine("<color=#aaaaaa><size=13>(Download size: ~70MB, extracted to UserLibs folder)</size></color>");
} }
descText.text = descStrBuilder.ToString(); descText.text = descStrBuilder.ToString();
@@ -231,7 +231,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
descRect.anchoredPosition = new Vector2(0, -60); descRect.anchoredPosition = new Vector2(0, -60);
descRect.sizeDelta = new Vector2(-40, 100); descRect.sizeDelta = new Vector2(-40, 100);
// 6. 버튼 컨테이너 생성 // 6. Create button container
_buttonContainerObj = new GameObject("ButtonContainer", typeof(RectTransform)); _buttonContainerObj = new GameObject("ButtonContainer", typeof(RectTransform));
_buttonContainerObj.transform.SetParent(modalContainerObj.transform, false); _buttonContainerObj.transform.SetParent(modalContainerObj.transform, false);
_buttonContainerObj.layer = 5; _buttonContainerObj.layer = 5;
@@ -243,7 +243,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
btnContainerRect.anchoredPosition = new Vector2(0, 35); btnContainerRect.anchoredPosition = new Vector2(0, 35);
btnContainerRect.sizeDelta = new Vector2(0, 50); btnContainerRect.sizeDelta = new Vector2(0, 50);
// 7. 다운로드 버튼 생성 // 7. Create download button
var downloadBtnObj = new GameObject("DownloadButton"); var downloadBtnObj = new GameObject("DownloadButton");
downloadBtnObj.transform.SetParent(_buttonContainerObj.transform, false); downloadBtnObj.transform.SetParent(_buttonContainerObj.transform, false);
downloadBtnObj.layer = 5; downloadBtnObj.layer = 5;
@@ -273,7 +273,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
downloadTextObj.layer = 5; downloadTextObj.layer = 5;
var downloadText = downloadTextObj.AddComponent<TextMeshProUGUI>(); var downloadText = downloadTextObj.AddComponent<TextMeshProUGUI>();
downloadText.text = Application.platform != RuntimePlatform.OSXPlayer ? "다운로드 및 설치" : "사이트 열기"; downloadText.text = Application.platform != RuntimePlatform.OSXPlayer ? "Download & Install" : "Open Website";
downloadText.fontSize = 20; downloadText.fontSize = 20;
downloadText.font = font; downloadText.font = font;
downloadText.color = Color.white; downloadText.color = Color.white;
@@ -285,7 +285,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
downloadTextRect.offsetMin = Vector2.zero; downloadTextRect.offsetMin = Vector2.zero;
downloadTextRect.offsetMax = Vector2.zero; downloadTextRect.offsetMax = Vector2.zero;
// 8. 취소 버튼 생성 // 8. Create cancel button
var cancelBtnObj = new GameObject("CancelButton"); var cancelBtnObj = new GameObject("CancelButton");
cancelBtnObj.transform.SetParent(_buttonContainerObj.transform, false); cancelBtnObj.transform.SetParent(_buttonContainerObj.transform, false);
cancelBtnObj.layer = 5; cancelBtnObj.layer = 5;
@@ -310,7 +310,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
cancelTextObj.layer = 5; cancelTextObj.layer = 5;
var cancelText = cancelTextObj.AddComponent<TextMeshProUGUI>(); var cancelText = cancelTextObj.AddComponent<TextMeshProUGUI>();
cancelText.text = "취소"; cancelText.text = "Cancel";
cancelText.fontSize = 20; cancelText.fontSize = 20;
cancelText.font = font; cancelText.font = font;
cancelText.color = new Color(0.9f, 0.9f, 0.9f, 1f); cancelText.color = new Color(0.9f, 0.9f, 0.9f, 1f);
@@ -322,18 +322,18 @@ internal class FFmpegDownloadUI : MonoBehaviour
cancelTextRect.offsetMin = Vector2.zero; cancelTextRect.offsetMin = Vector2.zero;
cancelTextRect.offsetMax = Vector2.zero; cancelTextRect.offsetMax = Vector2.zero;
// 9. 프로그레스 바 컨테이너 생성 // 9. Create progress bar container
_progressBarObj = new GameObject("ProgressBarContainer"); _progressBarObj = new GameObject("ProgressBarContainer");
_progressBarObj.transform.SetParent(modalContainerObj.transform, false); _progressBarObj.transform.SetParent(modalContainerObj.transform, false);
_progressBarObj.layer = 5; _progressBarObj.layer = 5;
_progressBarObj.SetActive(false); _progressBarObj.SetActive(false);
var barBgImg = _progressBarObj.AddComponent<Image>(); var barBgImg = _progressBarObj.AddComponent<Image>();
barBgImg.color = new Color(0.08f, 0.08f, 0.1f, 1.0f); // DebugMonitorUI의 BarBackgroundColor와 동일 barBgImg.color = new Color(0.08f, 0.08f, 0.1f, 1.0f);
var barBgOutline = _progressBarObj.AddComponent<Outline>(); var barBgOutline = _progressBarObj.AddComponent<Outline>();
barBgOutline.effectColor = Color.white; // DebugMonitorUI의 BarOutlineColor와 동일 barBgOutline.effectColor = Color.white;
barBgOutline.effectDistance = new Vector2(3.0f, -3.0f); // DebugMonitorUI의 BarOutlineDistance와 동일 barBgOutline.effectDistance = new Vector2(3.0f, -3.0f);
var barRect = _progressBarObj.GetComponent<RectTransform>(); var barRect = _progressBarObj.GetComponent<RectTransform>();
barRect.anchorMin = new Vector2(0.5f, 0); barRect.anchorMin = new Vector2(0.5f, 0);
@@ -342,28 +342,28 @@ internal class FFmpegDownloadUI : MonoBehaviour
barRect.anchoredPosition = new Vector2(0, 75); barRect.anchoredPosition = new Vector2(0, 75);
barRect.sizeDelta = new Vector2(480, 24); barRect.sizeDelta = new Vector2(480, 24);
// 10. 프로그레스 바 채우기 이미지 생성 // 10. Create progress bar fill image
_progressBarFillObj = new GameObject("ProgressBarFill"); _progressBarFillObj = new GameObject("ProgressBarFill");
_progressBarFillObj.transform.SetParent(_progressBarObj.transform, false); _progressBarFillObj.transform.SetParent(_progressBarObj.transform, false);
_progressBarFillObj.layer = 5; _progressBarFillObj.layer = 5;
var fillImg = _progressBarFillObj.AddComponent<Image>(); var fillImg = _progressBarFillObj.AddComponent<Image>();
fillImg.color = new Color(0.0f, 0.8f, 1.0f, 1.0f); // DebugMonitorUI의 BarFillColor와 동일 fillImg.color = new Color(0.0f, 0.8f, 1.0f, 1.0f);
_fillRect = _progressBarFillObj.GetComponent<RectTransform>(); _fillRect = _progressBarFillObj.GetComponent<RectTransform>();
_fillRect.anchorMin = new Vector2(0, 0); _fillRect.anchorMin = new Vector2(0, 0);
_fillRect.anchorMax = new Vector2(0, 1); // anchorMax.x가 동적으로 변경됨 _fillRect.anchorMax = new Vector2(0, 1);
_fillRect.pivot = new Vector2(0, 0.5f); _fillRect.pivot = new Vector2(0, 0.5f);
_fillRect.anchoredPosition = Vector2.zero; _fillRect.anchoredPosition = Vector2.zero;
_fillRect.sizeDelta = Vector2.zero; _fillRect.sizeDelta = Vector2.zero;
// 11. 프로그레스 텍스트 생성 (진행률 표시) // 11. Create progress text
var progressTextObj = new GameObject("ProgressBarText"); var progressTextObj = new GameObject("ProgressBarText");
progressTextObj.transform.SetParent(_progressBarObj.transform, false); progressTextObj.transform.SetParent(_progressBarObj.transform, false);
progressTextObj.layer = 5; progressTextObj.layer = 5;
_progressText = progressTextObj.AddComponent<TextMeshProUGUI>(); _progressText = progressTextObj.AddComponent<TextMeshProUGUI>();
_progressText.fontSize = 16; // DebugMonitorUI의 BarTextFontSize와 일치 _progressText.fontSize = 16;
_progressText.font = font; _progressText.font = font;
_progressText.color = Color.white; _progressText.color = Color.white;
_progressText.alignment = TextAlignmentOptions.Center; _progressText.alignment = TextAlignmentOptions.Center;
@@ -378,14 +378,14 @@ internal class FFmpegDownloadUI : MonoBehaviour
progressTextRect.offsetMin = Vector2.zero; progressTextRect.offsetMin = Vector2.zero;
progressTextRect.offsetMax = Vector2.zero; progressTextRect.offsetMax = Vector2.zero;
// 12. 상태 텍스트 생성 // 12. Create status text
_statusTextObj = new GameObject("StatusText"); _statusTextObj = new GameObject("StatusText");
_statusTextObj.transform.SetParent(modalContainerObj.transform, false); _statusTextObj.transform.SetParent(modalContainerObj.transform, false);
_statusTextObj.layer = 5; _statusTextObj.layer = 5;
_statusTextObj.SetActive(false); _statusTextObj.SetActive(false);
_statusText = _statusTextObj.AddComponent<TextMeshProUGUI>(); _statusText = _statusTextObj.AddComponent<TextMeshProUGUI>();
_statusText.fontSize = 16; // DebugMonitorUI의 텍스트 크기와 일치 _statusText.fontSize = 16;
_statusText.font = font; _statusText.font = font;
_statusText.color = new Color(0.8f, 0.8f, 0.8f, 1f); _statusText.color = new Color(0.8f, 0.8f, 0.8f, 1f);
_statusText.alignment = TextAlignmentOptions.Center; _statusText.alignment = TextAlignmentOptions.Center;
@@ -411,7 +411,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
private void Update() private void Update()
{ {
// 백그라운드 스레드에서 메인 스레드로 넘겨진 작업 순차 실행 // Execute queued actions on main thread
while (_mainThreadQueue.TryDequeue(out var action)) while (_mainThreadQueue.TryDequeue(out var action))
{ {
action?.Invoke(); action?.Invoke();
@@ -428,20 +428,20 @@ internal class FFmpegDownloadUI : MonoBehaviour
if (_isDownloading) return; if (_isDownloading) return;
_isDownloading = true; _isDownloading = true;
// 버튼 숨기고 프로그레스 바 및 상태 텍스트 표시 // Hide buttons and show progress bar & status text
_buttonContainerObj.SetActive(false); _buttonContainerObj.SetActive(false);
_progressBarObj.SetActive(true); _progressBarObj.SetActive(true);
_statusTextObj.SetActive(true); _statusTextObj.SetActive(true);
_cts = new System.Threading.CancellationTokenSource(); _cts = new System.Threading.CancellationTokenSource();
// 백그라운드 스레드에서 다운로드 시작 // Start download in background thread
Task.Run(async () => Task.Run(async () =>
{ {
string tempZipPath = ""; string tempZipPath = "";
try try
{ {
UpdateStatus("다운로드 시작하는 중...", new Color(0.8f, 0.8f, 0.8f)); UpdateStatus("Starting download...", new Color(0.8f, 0.8f, 0.8f));
UpdateProgress(0f, "0%"); UpdateProgress(0f, "0%");
var userLibsDir = MelonEnvironment.UserLibsDirectory; var userLibsDir = MelonEnvironment.UserLibsDirectory;
@@ -461,24 +461,24 @@ internal class FFmpegDownloadUI : MonoBehaviour
var progressReporter = new Progress<float>(percent => var progressReporter = new Progress<float>(percent =>
{ {
UpdateProgress(percent, $"{percent * 100f:F1}%"); UpdateProgress(percent, $"{percent * 100f:F1}%");
UpdateStatus($"다운로드 중... ({(percent * 100f):F1}%)", new Color(0f, 0.8f, 1f)); UpdateStatus($"Downloading... ({(percent * 100f):F1}%)", new Color(0f, 0.8f, 1f));
}); });
await DownloadFileAsync(DownloadUrl, tempZipPath, progressReporter, _cts.Token); await DownloadFileAsync(DownloadUrl, tempZipPath, progressReporter, _cts.Token);
// 다운로드 완료 후 압축 해제 // Extract after download complete
UpdateStatus("압축 해제 및 DLL 파일 복사 중...", new Color(0.9f, 0.9f, 0.2f)); UpdateStatus("Extracting and copying library files...", new Color(0.9f, 0.9f, 0.2f));
UpdateProgress(0.99f, "압축 해제 중..."); UpdateProgress(0.99f, "Extracting...");
await ExtractDllsAsync(tempZipPath, userLibsDir); await ExtractDllsAsync(tempZipPath, userLibsDir);
// 완료 상태 업데이트 // Update completion status
UpdateStatus("FFmpeg 설치 완료!", new Color(0.2f, 0.9f, 0.2f)); UpdateStatus("FFmpeg installation complete!", new Color(0.2f, 0.9f, 0.2f));
UpdateProgress(1f, "100%"); UpdateProgress(1f, "100%");
await Task.Delay(1000); await Task.Delay(1000);
// 메인 스레드에서 UI를 삭제 // Destroy UI on main thread
ExecuteOnMainThread(() => ExecuteOnMainThread(() =>
{ {
Destroy(gameObject); Destroy(gameObject);
@@ -486,7 +486,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
// 취소 시 임시 파일 정리 // Clean up temporary file on cancellation
if (!string.IsNullOrEmpty(tempZipPath) && File.Exists(tempZipPath)) if (!string.IsNullOrEmpty(tempZipPath) && File.Exists(tempZipPath))
{ {
try { File.Delete(tempZipPath); } try { File.Delete(tempZipPath); }
@@ -495,14 +495,14 @@ internal class FFmpegDownloadUI : MonoBehaviour
// ignored // ignored
} }
} }
UpdateStatus("다운로드가 취소되었습니다.", new Color(0.9f, 0.5f, 0.2f)); UpdateStatus("Download was cancelled.", new Color(0.9f, 0.5f, 0.2f));
} }
catch (Exception ex) catch (Exception ex)
{ {
Melon<RenderMod>.Logger.Error($"FFmpeg 다운로드/설치 중 에러 발생: {ex}"); Melon<RenderMod>.Logger.Error($"Error during FFmpeg download/installation: {ex}");
UpdateStatus($"설치 실패: {ex.Message}", new Color(1f, 0.3f, 0.3f)); UpdateStatus($"Installation failed: {ex.Message}", new Color(1f, 0.3f, 0.3f));
// 실패 시 버튼 재설정 // Reset buttons on failure
ExecuteOnMainThread(() => ExecuteOnMainThread(() =>
{ {
_isDownloading = false; _isDownloading = false;
@@ -510,7 +510,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
var downloadBtnText = _buttonContainerObj.transform.Find("DownloadButton/DownloadText")?.GetComponent<TextMeshProUGUI>(); var downloadBtnText = _buttonContainerObj.transform.Find("DownloadButton/DownloadText")?.GetComponent<TextMeshProUGUI>();
if (downloadBtnText) if (downloadBtnText)
{ {
downloadBtnText.text = "재시도 (Retry)"; downloadBtnText.text = "Retry";
} }
}); });
} }
@@ -556,7 +556,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
private async Task DownloadFileAsync(string url, string destinationPath, IProgress<float> progress, System.Threading.CancellationToken cancellationToken) private async Task DownloadFileAsync(string url, string destinationPath, IProgress<float> progress, System.Threading.CancellationToken cancellationToken)
{ {
using var client = new HttpClient(); using var client = new HttpClient();
// User-Agent 헤더 추가하여 차단 방지 // Add User-Agent header to prevent blocking
client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ADOFAIRenderer/1.0"); client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ADOFAIRenderer/1.0");
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken); using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
@@ -611,7 +611,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
} }
else else
{ {
// 리눅스 (또는 기타 플랫폼) - tar.xz 해제 // Linux (or other platforms) - extract tar.xz
await Task.Run(() => await Task.Run(() =>
{ {
var tempExtractDir = Path.Combine(outputDirectory, "ffmpeg_temp_extracted"); var tempExtractDir = Path.Combine(outputDirectory, "ffmpeg_temp_extracted");
@@ -640,11 +640,11 @@ internal class FFmpegDownloadUI : MonoBehaviour
if (process.ExitCode != 0) if (process.ExitCode != 0)
{ {
var error = process.StandardError.ReadToEnd(); var error = process.StandardError.ReadToEnd();
throw new Exception($"tar 압축 해제 실패 (ExitCode: {process.ExitCode}): {error}"); throw new Exception($"tar extraction failed (ExitCode: {process.ExitCode}): {error}");
} }
} }
// 압축이 풀린 파일 중에서 .so 파일들을 찾아서 outputDirectory로 이동 // Find .so files and copy to outputDirectory
var soFiles = Directory.GetFiles(tempExtractDir, "*.so*", SearchOption.AllDirectories); var soFiles = Directory.GetFiles(tempExtractDir, "*.so*", SearchOption.AllDirectories);
foreach (var soFile in soFiles) foreach (var soFile in soFiles)
{ {
@@ -653,7 +653,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
File.Copy(soFile, targetPath, true); File.Copy(soFile, targetPath, true);
} }
// 임시 디렉토리 정리 // Clean up temporary directory
try try
{ {
Directory.Delete(tempExtractDir, true); Directory.Delete(tempExtractDir, true);
@@ -675,7 +675,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
} }
catch (Exception ex) catch (Exception ex)
{ {
Melon<RenderMod>.Logger.Warning($"임시 ZIP 파일 삭제 실패: {ex.Message}"); Melon<RenderMod>.Logger.Warning($"Failed to delete temporary zip file: {ex.Message}");
} }
} }
} }
+79 -82
View File
@@ -62,34 +62,34 @@ internal class RenderConfigUIManager : MonoBehaviour
_videoEncodingConfigButton = _root.Q<Button>("VideoEncodingSettingsButton"); _videoEncodingConfigButton = _root.Q<Button>("VideoEncodingSettingsButton");
if (_videoEncodingConfigButton != null) if (_videoEncodingConfigButton != null)
{ {
Melon<RenderMod>.Logger.Msg($"[UI] VideoEncodingSettingsButton 발견: pickingMode={_videoEncodingConfigButton.pickingMode}, enabledSelf={_videoEncodingConfigButton.enabledSelf}"); Melon<RenderMod>.Logger.Msg($"[UI] VideoEncodingSettingsButton found: pickingMode={_videoEncodingConfigButton.pickingMode}, enabledSelf={_videoEncodingConfigButton.enabledSelf}");
_videoEncodingConfigButton.RegisterCallback<ClickEvent>(_ => OpenVideoEncodingConfig()); _videoEncodingConfigButton.RegisterCallback<ClickEvent>(_ => OpenVideoEncodingConfig());
} }
else else
{ {
Melon<RenderMod>.Logger.Error("Button 'VideoEncodingSettingsButton'을(를) 찾을 수 없습니다."); Melon<RenderMod>.Logger.Error("Button 'VideoEncodingSettingsButton' not found.");
} }
_audioEncodingConfigButton = _root.Q<Button>("AudioEncodingSettingsButton"); _audioEncodingConfigButton = _root.Q<Button>("AudioEncodingSettingsButton");
if (_audioEncodingConfigButton != null) if (_audioEncodingConfigButton != null)
{ {
Melon<RenderMod>.Logger.Msg($"[UI] AudioEncodingSettingsButton 발견: pickingMode={_audioEncodingConfigButton.pickingMode}, enabledSelf={_audioEncodingConfigButton.enabledSelf}"); Melon<RenderMod>.Logger.Msg($"[UI] AudioEncodingSettingsButton found: pickingMode={_audioEncodingConfigButton.pickingMode}, enabledSelf={_audioEncodingConfigButton.enabledSelf}");
_audioEncodingConfigButton.RegisterCallback<ClickEvent>(_ => OpenAudioEncodingConfig()); _audioEncodingConfigButton.RegisterCallback<ClickEvent>(_ => OpenAudioEncodingConfig());
} }
else else
{ {
Melon<RenderMod>.Logger.Error("Button 'AudioEncodingSettingsButton'을(를) 찾을 수 없습니다."); Melon<RenderMod>.Logger.Error("Button 'AudioEncodingSettingsButton' not found.");
} }
var presetSettingsButton = _root.Q<Button>("PresetSettingsButton"); var presetSettingsButton = _root.Q<Button>("PresetSettingsButton");
if (presetSettingsButton != null) if (presetSettingsButton != null)
{ {
Melon<RenderMod>.Logger.Msg($"[UI] PresetSettingsButton 발견: pickingMode={presetSettingsButton.pickingMode}, enabledSelf={presetSettingsButton.enabledSelf}"); Melon<RenderMod>.Logger.Msg($"[UI] PresetSettingsButton found: pickingMode={presetSettingsButton.pickingMode}, enabledSelf={presetSettingsButton.enabledSelf}");
presetSettingsButton.RegisterCallback<ClickEvent>(_ => OpenPresetConfig()); presetSettingsButton.RegisterCallback<ClickEvent>(_ => OpenPresetConfig());
} }
else else
{ {
Melon<RenderMod>.Logger.Error("Button 'PresetSettingsButton'을(를) 찾을 수 없습니다."); Melon<RenderMod>.Logger.Error("Button 'PresetSettingsButton' not found.");
} }
var bottomRow = CreateRow(); var bottomRow = CreateRow();
@@ -99,11 +99,11 @@ internal class RenderConfigUIManager : MonoBehaviour
bottomRow.style.bottom = 0f; bottomRow.style.bottom = 0f;
bottomRow.style.justifyContent = Justify.FlexEnd; bottomRow.style.justifyContent = Justify.FlexEnd;
_advancedSettingsButton = new Button { name = "AdvancedSettingsButton", text = "고급 설정..." }; _advancedSettingsButton = new Button { name = "AdvancedSettingsButton", text = "Advanced Settings..." };
bottomRow.Add(_advancedSettingsButton); bottomRow.Add(_advancedSettingsButton);
(_root.childCount > 0 ? _root[0] : _root).Add(bottomRow); (_root.childCount > 0 ? _root[0] : _root).Add(bottomRow);
Melon<RenderMod>.Logger.Msg("[UI] AdvancedSettingsButton 동적 생성 완료."); Melon<RenderMod>.Logger.Msg("[UI] AdvancedSettingsButton dynamically created.");
_advancedSettingsButton.RegisterCallback<ClickEvent>(_ => OpenAdvancedConfig()); _advancedSettingsButton.RegisterCallback<ClickEvent>(_ => OpenAdvancedConfig());
@@ -126,7 +126,7 @@ internal class RenderConfigUIManager : MonoBehaviour
} }
}; };
// 기존 UI와 동일한 폰트 적용 // Apply same font as existing UI
if (RenderMod.FontBundle) if (RenderMod.FontBundle)
{ {
try try
@@ -140,7 +140,7 @@ internal class RenderConfigUIManager : MonoBehaviour
_versionLabel.RegisterCallback<PointerUpLinkTagEvent>(OnLinkTagClicked); _versionLabel.RegisterCallback<PointerUpLinkTagEvent>(OnLinkTagClicked);
(_root.childCount > 0 ? _root[0] : _root).Add(_versionLabel); (_root.childCount > 0 ? _root[0] : _root).Add(_versionLabel);
Melon<RenderMod>.Logger.Msg("[UI] VersionLabel 동적 생성 완료."); Melon<RenderMod>.Logger.Msg("[UI] VersionLabel dynamically created.");
} }
private void OpenAdvancedConfig() private void OpenAdvancedConfig()
@@ -176,9 +176,9 @@ internal class RenderConfigUIManager : MonoBehaviour
} }
}; };
// 1. 타이틀 로우 // 1. Title Row
var titleRow = CreateRow(); var titleRow = CreateRow();
var titleLabel = new Label("고급 설정") var titleLabel = new Label("Advanced Settings")
{ {
style = style =
{ {
@@ -189,9 +189,9 @@ internal class RenderConfigUIManager : MonoBehaviour
titleRow.Add(titleLabel); titleRow.Add(titleLabel);
_advancedConfigContainer.Add(titleRow); _advancedConfigContainer.Add(titleRow);
// 2. 확장자 로우 // 2. Extension Row
var fileExtensionRow = CreateRow(); var fileExtensionRow = CreateRow();
var fileExtensionLabel = CreateFormLabel("확장자"); var fileExtensionLabel = CreateFormLabel("File Extension");
var fileExtensionChoices = new List<string> { "mkv", "mp4", "mov", "webm", "avi" }; var fileExtensionChoices = new List<string> { "mkv", "mp4", "mov", "webm", "avi" };
var currentFileExt = string.IsNullOrWhiteSpace(tempConfig.FileExtension) ? "mkv" : tempConfig.FileExtension.TrimStart('.').ToLower(); var currentFileExt = string.IsNullOrWhiteSpace(tempConfig.FileExtension) ? "mkv" : tempConfig.FileExtension.TrimStart('.').ToLower();
if (!fileExtensionChoices.Contains(currentFileExt)) if (!fileExtensionChoices.Contains(currentFileExt))
@@ -203,25 +203,25 @@ internal class RenderConfigUIManager : MonoBehaviour
fileExtensionRow.Add(fileExtensionDropdown); fileExtensionRow.Add(fileExtensionDropdown);
_advancedConfigContainer.Add(fileExtensionRow); _advancedConfigContainer.Add(fileExtensionRow);
// 3. 오디오 처리 로우 // 3. Audio Processing Row
var processAudioRow = CreateRow(); var processAudioRow = CreateRow();
var processAudioLabel = CreateFormLabel("오디오 처리"); var processAudioLabel = CreateFormLabel("Process Audio");
var processAudioToggle = new Toggle { name = "AdvancedProcessAudioToggle", label = "", value = tempConfig.ProcessAudio }; var processAudioToggle = new Toggle { name = "AdvancedProcessAudioToggle", label = "", value = tempConfig.ProcessAudio };
processAudioRow.Add(processAudioLabel); processAudioRow.Add(processAudioLabel);
processAudioRow.Add(processAudioToggle); processAudioRow.Add(processAudioToggle);
_advancedConfigContainer.Add(processAudioRow); _advancedConfigContainer.Add(processAudioRow);
// 4. 논타일 모드 로우 // 4. Non-Tile Mode Row
var nonTileRow = CreateRow(); var nonTileRow = CreateRow();
var nonTileLabel = CreateFormLabel("논타일 모드"); var nonTileLabel = CreateFormLabel("Non-Tile Mode");
var nonTileToggle = new Toggle { name = "AdvancedNonTileToggle", label = "", value = tempConfig.NonTile }; var nonTileToggle = new Toggle { name = "AdvancedNonTileToggle", label = "", value = tempConfig.NonTile };
nonTileRow.Add(nonTileLabel); nonTileRow.Add(nonTileLabel);
nonTileRow.Add(nonTileToggle); nonTileRow.Add(nonTileToggle);
_advancedConfigContainer.Add(nonTileRow); _advancedConfigContainer.Add(nonTileRow);
// 5. 랜덤 시드 로우 // 5. Random Seed Row
var randomSeedRow = CreateRow(); var randomSeedRow = CreateRow();
var randomSeedLabel = CreateFormLabel("랜덤 시드 (입력 선택)"); var randomSeedLabel = CreateFormLabel("Random Seed (Optional)");
var randomSeedField = new TextField { name = "AdvancedRandomSeed", label = "", value = tempConfig.RandomSeed?.ToString() ?? string.Empty }; var randomSeedField = new TextField { name = "AdvancedRandomSeed", label = "", value = tempConfig.RandomSeed?.ToString() ?? string.Empty };
randomSeedField.RegisterValueChangedCallback(evt => randomSeedField.RegisterValueChangedCallback(evt =>
@@ -237,10 +237,10 @@ internal class RenderConfigUIManager : MonoBehaviour
randomSeedRow.Add(randomSeedField); randomSeedRow.Add(randomSeedField);
_advancedConfigContainer.Add(randomSeedRow); _advancedConfigContainer.Add(randomSeedRow);
// 6. 제어 버튼 로우 (취소 / 저장) // 6. Control Button Row (Cancel / Save)
var buttonRow = CreateRow(); var buttonRow = CreateRow();
var cancelBtn = new Button { text = "취소" }; var cancelBtn = new Button { text = "Cancel" };
var saveBtn = new Button { text = "저장" }; var saveBtn = new Button { text = "Save" };
buttonRow.Add(cancelBtn); buttonRow.Add(cancelBtn);
buttonRow.Add(saveBtn); buttonRow.Add(saveBtn);
@@ -315,7 +315,7 @@ internal class RenderConfigUIManager : MonoBehaviour
_root.Remove(_videoEncodingConfigContainer); _root.Remove(_videoEncodingConfigContainer);
} }
// 1. 전체 화면 컨테이너 생성 및 배경 적용 // 1. Create full screen container and apply background
_videoEncodingConfigContainer = new VisualElement _videoEncodingConfigContainer = new VisualElement
{ {
name = "VideoEncodingSettingsContainer", name = "VideoEncodingSettingsContainer",
@@ -332,9 +332,9 @@ internal class RenderConfigUIManager : MonoBehaviour
} }
}; };
// 2. 타이틀 로우 // 2. Title Row
var titleRow = CreateRow(); var titleRow = CreateRow();
var titleLabel = new Label("비디오 인코딩 설정") var titleLabel = new Label("Video Encoding Settings")
{ {
style = style =
{ {
@@ -345,71 +345,71 @@ internal class RenderConfigUIManager : MonoBehaviour
titleRow.Add(titleLabel); titleRow.Add(titleLabel);
_videoEncodingConfigContainer.Add(titleRow); _videoEncodingConfigContainer.Add(titleRow);
// 3. 코덱 로우 // 3. Codec Row
var codecRow = CreateRow(); var codecRow = CreateRow();
var codecLabel = CreateFormLabel("코덱"); var codecLabel = CreateFormLabel("Codec");
var codecChoices = new List<string>(Enum.GetNames(typeof(Codec))); var codecChoices = new List<string>(Enum.GetNames(typeof(Codec)));
var codecDropdown = new DropdownField(codecChoices, tempConfig.Codec.ToString()) { name = "VideoCodec" }; var codecDropdown = new DropdownField(codecChoices, tempConfig.Codec.ToString()) { name = "VideoCodec" };
codecRow.Add(codecLabel); codecRow.Add(codecLabel);
codecRow.Add(codecDropdown); codecRow.Add(codecDropdown);
_videoEncodingConfigContainer.Add(codecRow); _videoEncodingConfigContainer.Add(codecRow);
// 4. 모드 로우 // 4. Mode Row
var modeRow = CreateRow(); var modeRow = CreateRow();
var modeLabel = CreateFormLabel("모드"); var modeLabel = CreateFormLabel("Mode");
var modeChoices = new List<string>(Enum.GetNames(typeof(RateControlMode))); var modeChoices = new List<string>(Enum.GetNames(typeof(RateControlMode)));
var modeDropdown = new DropdownField(modeChoices, tempConfig.RateControlMode.ToString()) { name = "VideoRateControlMode" }; var modeDropdown = new DropdownField(modeChoices, tempConfig.RateControlMode.ToString()) { name = "VideoRateControlMode" };
modeRow.Add(modeLabel); modeRow.Add(modeLabel);
modeRow.Add(modeDropdown); modeRow.Add(modeDropdown);
_videoEncodingConfigContainer.Add(modeRow); _videoEncodingConfigContainer.Add(modeRow);
// 5. 비트레이트 로우 // 5. Bitrate Row
var bitrateRow = CreateRow(); var bitrateRow = CreateRow();
var bitrateLabel = CreateFormLabel("비트레이트(kbps)"); var bitrateLabel = CreateFormLabel("Bitrate (kbps)");
var bitrateField = new IntegerField { name = "VideoBitrate", label = "", value = tempConfig.Bitrate }; var bitrateField = new IntegerField { name = "VideoBitrate", label = "", value = tempConfig.Bitrate };
bitrateRow.Add(bitrateLabel); bitrateRow.Add(bitrateLabel);
bitrateRow.Add(bitrateField); bitrateRow.Add(bitrateField);
_videoEncodingConfigContainer.Add(bitrateRow); _videoEncodingConfigContainer.Add(bitrateRow);
// 5.5. 최대 비트레이트 로우 (VBR 전용) // 5.5. Max Bitrate Row (VBR only)
var maxBitrateRow = CreateRow(); var maxBitrateRow = CreateRow();
var maxBitrateLabel = CreateFormLabel("최대 비트레이트(kbps)"); var maxBitrateLabel = CreateFormLabel("Max Bitrate (kbps)");
var maxBitrateField = new IntegerField { name = "VideoMaxBitrate", label = "", value = tempConfig.MaxBitrate }; var maxBitrateField = new IntegerField { name = "VideoMaxBitrate", label = "", value = tempConfig.MaxBitrate };
maxBitrateRow.Add(maxBitrateLabel); maxBitrateRow.Add(maxBitrateLabel);
maxBitrateRow.Add(maxBitrateField); maxBitrateRow.Add(maxBitrateField);
_videoEncodingConfigContainer.Add(maxBitrateRow); _videoEncodingConfigContainer.Add(maxBitrateRow);
// 6. QP 로우 // 6. QP Row
var qpRow = CreateRow(); var qpRow = CreateRow();
var qpLabel = CreateFormLabel("품질 (QP)"); var qpLabel = CreateFormLabel("Quality (QP)");
var qpField = new IntegerField { name = "VideoQP", label = "", value = tempConfig.QP }; var qpField = new IntegerField { name = "VideoQP", label = "", value = tempConfig.QP };
qpRow.Add(qpLabel); qpRow.Add(qpLabel);
qpRow.Add(qpField); qpRow.Add(qpField);
_videoEncodingConfigContainer.Add(qpRow); _videoEncodingConfigContainer.Add(qpRow);
// 6.5. 키프레임 간격 로우 (초 단위) // 6.5. Keyframe Interval Row (seconds)
var keyframeRow = CreateRow(); var keyframeRow = CreateRow();
var keyframeLabel = CreateFormLabel("키프레임 간격(초)"); var keyframeLabel = CreateFormLabel("Keyframe Interval (s)");
var keyframeField = new DoubleField { name = "VideoKeyframeInterval", label = "", value = tempConfig.KeyframeInterval }; var keyframeField = new DoubleField { name = "VideoKeyframeInterval", label = "", value = tempConfig.KeyframeInterval };
keyframeRow.Add(keyframeLabel); keyframeRow.Add(keyframeLabel);
keyframeRow.Add(keyframeField); keyframeRow.Add(keyframeField);
_videoEncodingConfigContainer.Add(keyframeRow); _videoEncodingConfigContainer.Add(keyframeRow);
// 6.75. 강제 소프트웨어 인코더 로우 (라벨 + 토글) // 6.75. Force Software Encoder Row (Label + Toggle)
var forceSoftwareRow = CreateRow(); var forceSoftwareRow = CreateRow();
var forceSoftwareLabel = CreateFormLabel("소프트웨어 인코더"); var forceSoftwareLabel = CreateFormLabel("Software Encoder");
var forceSoftwareToggle = new Toggle { name = "VideoForceSoftwareEncoderToggle", label = "", value = tempConfig.ForceSoftwareEncoder }; var forceSoftwareToggle = new Toggle { name = "VideoForceSoftwareEncoderToggle", label = "", value = tempConfig.ForceSoftwareEncoder };
forceSoftwareRow.Add(forceSoftwareLabel); forceSoftwareRow.Add(forceSoftwareLabel);
forceSoftwareRow.Add(forceSoftwareToggle); forceSoftwareRow.Add(forceSoftwareToggle);
_videoEncodingConfigContainer.Add(forceSoftwareRow); _videoEncodingConfigContainer.Add(forceSoftwareRow);
// 7. QP = 0 (무손실) 경고 라벨 로우 생성 // 7. QP = 0 (Lossless) Warning Label Row
var warningRow = CreateRow(); var warningRow = CreateRow();
warningRow.style.height = StyleKeyword.Auto; warningRow.style.height = StyleKeyword.Auto;
warningRow.style.maxHeight = StyleKeyword.Null; warningRow.style.maxHeight = StyleKeyword.Null;
warningRow.style.minHeight = StyleKeyword.Null; warningRow.style.minHeight = StyleKeyword.Null;
var warningLabel = new Label("주의: QP를 0으로 설정하면 무손실 인코딩이 적용되어 용량이 매우 커질 수 있습니다.") var warningLabel = new Label("Warning: Setting QP to 0 applies lossless encoding and may result in very large file sizes.")
{ {
style = style =
{ {
@@ -441,10 +441,10 @@ internal class RenderConfigUIManager : MonoBehaviour
: DisplayStyle.None; : DisplayStyle.None;
}); });
// 8. 제어 버튼 로우 (취소 / 저장) // 8. Control Button Row (Cancel / Save)
var buttonRow = CreateRow(); var buttonRow = CreateRow();
var cancelBtn = new Button { text = "취소" }; var cancelBtn = new Button { text = "Cancel" };
var saveBtn = new Button { text = "저장" }; var saveBtn = new Button { text = "Save" };
buttonRow.Add(cancelBtn); buttonRow.Add(cancelBtn);
buttonRow.Add(saveBtn); buttonRow.Add(saveBtn);
@@ -489,11 +489,9 @@ internal class RenderConfigUIManager : MonoBehaviour
_root.Add(_videoEncodingConfigContainer); _root.Add(_videoEncodingConfigContainer);
// 화면 전환 활성화
ToggleScreens(true); ToggleScreens(true);
return; return;
// 화면 전환 제어
void ToggleScreens(bool showEncoding) void ToggleScreens(bool showEncoding)
{ {
if (_root.childCount > 0) if (_root.childCount > 0)
@@ -503,7 +501,6 @@ internal class RenderConfigUIManager : MonoBehaviour
_videoEncodingConfigContainer.style.display = showEncoding ? DisplayStyle.Flex : DisplayStyle.None; _videoEncodingConfigContainer.style.display = showEncoding ? DisplayStyle.Flex : DisplayStyle.None;
} }
// 가시성 필터링 업데이트 (행 단위로 보임/숨김 제어)
void UpdateVisibility(string selectedModeStr) void UpdateVisibility(string selectedModeStr)
{ {
if (!Enum.TryParse<RateControlMode>(selectedModeStr, out var mode)) return; if (!Enum.TryParse<RateControlMode>(selectedModeStr, out var mode)) return;
@@ -547,9 +544,9 @@ internal class RenderConfigUIManager : MonoBehaviour
} }
}; };
// 1. 타이틀 로우 // 1. Title Row
var titleRow = CreateRow(); var titleRow = CreateRow();
var titleLabel = new Label("오디오 인코딩 설정") var titleLabel = new Label("Audio Encoding Settings")
{ {
style = style =
{ {
@@ -560,17 +557,17 @@ internal class RenderConfigUIManager : MonoBehaviour
titleRow.Add(titleLabel); titleRow.Add(titleLabel);
_audioEncodingConfigContainer.Add(titleRow); _audioEncodingConfigContainer.Add(titleRow);
// 2. 오디오 렌더링 활성화 로우 (라벨 + 토글) // 2. Enable Audio Rendering Row (Label + Toggle)
var renderAudioRow = CreateRow(); var renderAudioRow = CreateRow();
var renderAudioLabel = CreateFormLabel("오디오 렌더링 활성화"); var renderAudioLabel = CreateFormLabel("Enable Audio Rendering");
var renderAudioToggle = new Toggle { name = "AudioRenderAudioToggle", label = "", value = tempAudioConfig.RenderAudio }; var renderAudioToggle = new Toggle { name = "AudioRenderAudioToggle", label = "", value = tempAudioConfig.RenderAudio };
renderAudioRow.Add(renderAudioLabel); renderAudioRow.Add(renderAudioLabel);
renderAudioRow.Add(renderAudioToggle); renderAudioRow.Add(renderAudioToggle);
_audioEncodingConfigContainer.Add(renderAudioRow); _audioEncodingConfigContainer.Add(renderAudioRow);
// 3. 샘플 레이트 로우 // 3. Sample Rate Row
var sampleRateRow = CreateRow(); var sampleRateRow = CreateRow();
var sampleRateLabel = CreateFormLabel("샘플 레이트(Hz)"); var sampleRateLabel = CreateFormLabel("Sample Rate (Hz)");
var isOpus = config.VideoConfig.Codec is Codec.AV1 or Codec.VP9 or Codec.VVC; var isOpus = config.VideoConfig.Codec is Codec.AV1 or Codec.VP9 or Codec.VVC;
List<string> sampleRateChoices; List<string> sampleRateChoices;
@@ -599,9 +596,9 @@ internal class RenderConfigUIManager : MonoBehaviour
sampleRateRow.Add(sampleRateDropdown); sampleRateRow.Add(sampleRateDropdown);
_audioEncodingConfigContainer.Add(sampleRateRow); _audioEncodingConfigContainer.Add(sampleRateRow);
// 4. 채널 로우 // 4. Channels Row
var channelsRow = CreateRow(); var channelsRow = CreateRow();
var channelsLabel = CreateFormLabel("오디오 채널"); var channelsLabel = CreateFormLabel("Audio Channels");
var channelsDropdown = new DropdownField( var channelsDropdown = new DropdownField(
new List<string> { "1 (Mono)", "2 (Stereo)" }, new List<string> { "1 (Mono)", "2 (Stereo)" },
tempAudioConfig.AudioChannels == 1 ? "1 (Mono)" : "2 (Stereo)" tempAudioConfig.AudioChannels == 1 ? "1 (Mono)" : "2 (Stereo)"
@@ -610,9 +607,9 @@ internal class RenderConfigUIManager : MonoBehaviour
channelsRow.Add(channelsDropdown); channelsRow.Add(channelsDropdown);
_audioEncodingConfigContainer.Add(channelsRow); _audioEncodingConfigContainer.Add(channelsRow);
// 5. 비트레이트 로우 (kbps 단위 입력) // 5. Bitrate Row (input in kbps)
var bitrateRow = CreateRow(); var bitrateRow = CreateRow();
var bitrateLabel = CreateFormLabel("오디오 비트레이트(kbps)"); var bitrateLabel = CreateFormLabel("Audio Bitrate (kbps)");
var bitrateField = new IntegerField { name = "AudioBitrateKbps", label = "", value = tempAudioConfig.AudioBitrate / 1000 }; var bitrateField = new IntegerField { name = "AudioBitrateKbps", label = "", value = tempAudioConfig.AudioBitrate / 1000 };
bitrateRow.Add(bitrateLabel); bitrateRow.Add(bitrateLabel);
bitrateRow.Add(bitrateField); bitrateRow.Add(bitrateField);
@@ -622,14 +619,14 @@ internal class RenderConfigUIManager : MonoBehaviour
audioValidator.AddLimits("AudioBitrateKbps", new PositiveOnlyIntegerLimit(), new RangeIntegerLimit(32, 1024)); audioValidator.AddLimits("AudioBitrateKbps", new PositiveOnlyIntegerLimit(), new RangeIntegerLimit(32, 1024));
audioValidator.SetupValidation(); audioValidator.SetupValidation();
// 가시성 업데이트 (오디오 렌더링 비활성화 시 하위 설정 숨김) // Update visibility (hide sub-settings when audio rendering is disabled)
renderAudioToggle.RegisterValueChangedCallback(evt => UpdateAudioVisibility(evt.newValue)); renderAudioToggle.RegisterValueChangedCallback(evt => UpdateAudioVisibility(evt.newValue));
UpdateAudioVisibility(tempAudioConfig.RenderAudio); UpdateAudioVisibility(tempAudioConfig.RenderAudio);
// 6. 제어 버튼 로우 (취소 / 저장) // 6. Control Button Row (Cancel / Save)
var buttonRow = CreateRow(); var buttonRow = CreateRow();
var cancelBtn = new Button { text = "취소" }; var cancelBtn = new Button { text = "Cancel" };
var saveBtn = new Button { text = "저장" }; var saveBtn = new Button { text = "Save" };
buttonRow.Add(cancelBtn); buttonRow.Add(cancelBtn);
buttonRow.Add(saveBtn); buttonRow.Add(saveBtn);
@@ -686,7 +683,7 @@ internal class RenderConfigUIManager : MonoBehaviour
{ {
if (!ValidateInputs()) if (!ValidateInputs())
{ {
Melon<RenderMod>.Logger.Error("필수 입력 값이 비어 있거나 올바르지 않습니다. 모든 설정을 입력해 주세요."); Melon<RenderMod>.Logger.Error("Required input fields are empty or invalid. Please fill in all settings.");
return; return;
} }
@@ -788,7 +785,7 @@ internal class RenderConfigUIManager : MonoBehaviour
titleRow.style.height = StyleKeyword.Auto; titleRow.style.height = StyleKeyword.Auto;
titleRow.style.minHeight = StyleKeyword.Null; titleRow.style.minHeight = StyleKeyword.Null;
titleRow.style.maxHeight = StyleKeyword.Null; titleRow.style.maxHeight = StyleKeyword.Null;
var title = new Label("프리셋 설정") var title = new Label("Preset Settings")
{ {
style = style =
{ {
@@ -877,7 +874,7 @@ internal class RenderConfigUIManager : MonoBehaviour
var saveRow = CreateRow(); var saveRow = CreateRow();
saveRow.style.marginBottom = 20; saveRow.style.marginBottom = 20;
var nameLabelLabel = CreateFormLabel("새 프리셋 이름"); var nameLabelLabel = CreateFormLabel("New Preset Name");
nameLabelLabel.style.marginRight = 10; nameLabelLabel.style.marginRight = 10;
var nameField = new TextField var nameField = new TextField
@@ -890,7 +887,7 @@ internal class RenderConfigUIManager : MonoBehaviour
} }
}; };
// 기존 폼 입력 필드 스타일과 맞춤 // Match existing form input field style
var textInput = nameField.Q("unity-text-input"); var textInput = nameField.Q("unity-text-input");
if (textInput != null) if (textInput != null)
{ {
@@ -919,7 +916,7 @@ internal class RenderConfigUIManager : MonoBehaviour
nameField.style.borderBottomRightRadius = 6f; nameField.style.borderBottomRightRadius = 6f;
} }
var saveBtn = new Button { text = "저장" }; var saveBtn = new Button { text = "Save" };
saveBtn.RegisterCallback<ClickEvent>(_ => saveBtn.RegisterCallback<ClickEvent>(_ =>
{ {
@@ -927,9 +924,9 @@ internal class RenderConfigUIManager : MonoBehaviour
if (string.IsNullOrWhiteSpace(presetName)) if (string.IsNullOrWhiteSpace(presetName))
{ {
CommonWarningDialogUI.ShowDialog( CommonWarningDialogUI.ShowDialog(
"저장 실패", "Save Failed",
"프리셋 이름을 입력해 주세요.", "Please enter a preset name.",
new DialogButton("확인", () => {}) new DialogButton("OK", () => {})
); );
return; return;
} }
@@ -937,9 +934,9 @@ internal class RenderConfigUIManager : MonoBehaviour
if (!ValidateInputs()) if (!ValidateInputs())
{ {
CommonWarningDialogUI.ShowDialog( CommonWarningDialogUI.ShowDialog(
"저장 실패", "Save Failed",
"필수 입력 값이 비어 있거나 올바르지 않아 프리셋을 저장할 수 없습니다.", "Cannot save preset because required input values are empty or invalid.",
new DialogButton("확인", () => {}) new DialogButton("OK", () => {})
); );
return; return;
} }
@@ -973,9 +970,9 @@ internal class RenderConfigUIManager : MonoBehaviour
catch (Exception ex) catch (Exception ex)
{ {
CommonWarningDialogUI.ShowDialog( CommonWarningDialogUI.ShowDialog(
"저장 실패", "Save Failed",
$"프리셋 저장 중 오류가 발생했습니다: {ex.Message}", $"An error occurred while saving the preset: {ex.Message}",
new DialogButton("확인", () => {}) new DialogButton("OK", () => {})
); );
} }
}); });
@@ -988,7 +985,7 @@ internal class RenderConfigUIManager : MonoBehaviour
// Bottom controls: Close Button // Bottom controls: Close Button
var closeRow = CreateRow(); var closeRow = CreateRow();
var closeBtn = new Button { text = "닫기" }; var closeBtn = new Button { text = "Close" };
closeBtn.RegisterCallback<ClickEvent>(_ => closeBtn.RegisterCallback<ClickEvent>(_ =>
{ {
@@ -1015,7 +1012,7 @@ internal class RenderConfigUIManager : MonoBehaviour
emptyRow.style.minHeight = StyleKeyword.Null; emptyRow.style.minHeight = StyleKeyword.Null;
emptyRow.style.maxHeight = StyleKeyword.Null; emptyRow.style.maxHeight = StyleKeyword.Null;
var emptyLabel = new Label("저장된 프리셋이 없습니다.") { style = { color = new StyleColor(Color.gray), unityTextAlign = TextAnchor.MiddleCenter, marginTop = 80, fontSize = 20 } }; var emptyLabel = new Label("No saved presets.") { style = { color = new StyleColor(Color.gray), unityTextAlign = TextAnchor.MiddleCenter, marginTop = 80, fontSize = 20 } };
emptyRow.Add(emptyLabel); emptyRow.Add(emptyLabel);
scrollView.Add(emptyRow); scrollView.Add(emptyRow);
} }
@@ -1045,7 +1042,7 @@ internal class RenderConfigUIManager : MonoBehaviour
var loadBtn = new Button var loadBtn = new Button
{ {
text = "불러오기", text = "Load",
style = style =
{ {
marginRight = 10, marginRight = 10,
@@ -1085,7 +1082,7 @@ internal class RenderConfigUIManager : MonoBehaviour
var deleteBtn = new Button var deleteBtn = new Button
{ {
text = "삭제", text = "Delete",
style = style =
{ {
width = 120f, width = 120f,
@@ -1116,11 +1113,11 @@ internal class RenderConfigUIManager : MonoBehaviour
deleteBtn.RegisterCallback<ClickEvent>(_ => deleteBtn.RegisterCallback<ClickEvent>(_ =>
{ {
CommonWarningDialogUI.ShowDialog("프리셋 삭제", $"'{preset.Name}' 프리셋을 정말 삭제하시겠습니까?", new DialogButton("예", () => CommonWarningDialogUI.ShowDialog("Delete Preset", $"Are you sure you want to delete the preset '{preset.Name}'?", new DialogButton("Yes", () =>
{ {
PresetManager.DeletePreset(preset.Name); PresetManager.DeletePreset(preset.Name);
RefreshPresets(); RefreshPresets();
}), new DialogButton("아니오", () => { })); }), new DialogButton("No", () => { }));
}); });
btnContainer.Add(loadBtn); btnContainer.Add(loadBtn);
+2 -2
View File
@@ -33,11 +33,11 @@ namespace ADOFAIRenderer.Util
return AssetBundle.LoadFromStream(resourceStream); return AssetBundle.LoadFromStream(resourceStream);
} }
Melon<RenderMod>.Logger.Error($"DLL에서 번들 리소스를 찾을 수 없습니다.\n{bundlePath}"); Melon<RenderMod>.Logger.Error($"Could not find bundle resource in DLL.\n{bundlePath}");
throw new MissingManifestResourceException("Could not find bundle resource in DLL."); throw new MissingManifestResourceException("Could not find bundle resource in DLL.");
} }
Melon<RenderMod>.Logger.Error("실행 어셈블리가 제대로 설정되지 않았습니다."); Melon<RenderMod>.Logger.Error("Executing assembly has not been set properly.");
throw new InvalidOperationException("ExecutingAssembly has not been set."); throw new InvalidOperationException("ExecutingAssembly has not been set.");
} }
} }
+69 -68
View File
@@ -6,8 +6,8 @@ using Unity.Collections.LowLevel.Unsafe;
namespace ADOFAIRenderer.Util.FFmpeg namespace ADOFAIRenderer.Util.FFmpeg
{ {
/// <summary> /// <summary>
/// FFmpeg의 C 네이티브 API를 사용하여 비디오 파일을 스트리밍 방식으로 디코딩하는 클래스입니다. /// Decodes video files in a streaming fashion using FFmpeg's native C API.
/// 플레이어 형태가 아니며, 필요할 때 프레임을 순차적으로 읽거나 앞으로 탐색(Seek/Skip)할 수 있습니다. /// Unlike a standalone player, it allows reading frames sequentially or seeking/skipping forward on demand.
/// </summary> /// </summary>
internal sealed unsafe class FFmpegNativeDecoder : IDisposable internal sealed unsafe class FFmpegNativeDecoder : IDisposable
{ {
@@ -26,41 +26,41 @@ namespace ADOFAIRenderer.Util.FFmpeg
private bool _isDraining; private bool _isDraining;
private readonly object _lock = new(); private readonly object _lock = new();
// 탐색(Seek) 후 매칭되는 프레임을 보관하기 위한 캐시 프레임 // Cached frame used to hold matching frames after seeking
private AVFrame* _pCachedFrame = null; private AVFrame* _pCachedFrame = null;
private bool _hasCachedFrame; private bool _hasCachedFrame;
// SwsContext 갱신 감지용 필드 // Field to detect SwsContext updates
private AVPixelFormat _srcPixFmt = AVPixelFormat.AV_PIX_FMT_NONE; private AVPixelFormat _srcPixFmt = AVPixelFormat.AV_PIX_FMT_NONE;
private AVPixelFormat _dstPixFmt = AVPixelFormat.AV_PIX_FMT_NONE; private AVPixelFormat _dstPixFmt = AVPixelFormat.AV_PIX_FMT_NONE;
private int _srcWidth; private int _srcWidth;
private int _srcHeight; private int _srcHeight;
/// <summary>비디오 가로 해상도 (픽셀).</summary> /// <summary>Video width in pixels.</summary>
public int Width { get; private set; } public int Width { get; private set; }
/// <summary>비디오 세로 해상도 (픽셀).</summary> /// <summary>Video height in pixels.</summary>
public int Height { get; private set; } public int Height { get; private set; }
/// <summary>초당 프레임 수.</summary> /// <summary>Frames per second.</summary>
public double FPS { get; private set; } public double FPS { get; private set; }
/// <summary>전체 재생 시간 (초).</summary> /// <summary>Total duration in seconds.</summary>
public double Duration { get; private set; } public double Duration { get; private set; }
/// <summary>전체 프레임 수 (추정치).</summary> /// <summary>Estimated total number of frames.</summary>
public long TotalFrames { get; private set; } public long TotalFrames { get; private set; }
/// <summary>현재 읽은 프레임의 인덱스.</summary> /// <summary>Index of the currently read frame.</summary>
public long CurrentFrameIndex => _currentFrameIndex; public long CurrentFrameIndex => _currentFrameIndex;
/// <summary>현재 재생 시간 (초).</summary> /// <summary>Current playback time in seconds.</summary>
public double CurrentTime => _currentFrameIndex / FPS; public double CurrentTime => _currentFrameIndex / FPS;
/// <summary>비디오 원본 코덱의 픽셀 포맷.</summary> /// <summary>Pixel format of the original video stream.</summary>
public AVPixelFormat PixelFormat => _pCodecContext != null ? _pCodecContext->pix_fmt : AVPixelFormat.AV_PIX_FMT_NONE; public AVPixelFormat PixelFormat => _pCodecContext != null ? _pCodecContext->pix_fmt : AVPixelFormat.AV_PIX_FMT_NONE;
/// <summary>비디오 원본 코덱의 색상 범위.</summary> /// <summary>Color range of the original video stream.</summary>
public AVColorRange ColorRange => _pCodecContext != null ? _pCodecContext->color_range : AVColorRange.AVCOL_RANGE_UNSPECIFIED; public AVColorRange ColorRange => _pCodecContext != null ? _pCodecContext->color_range : AVColorRange.AVCOL_RANGE_UNSPECIFIED;
public FFmpegNativeDecoder(string videoPath) public FFmpegNativeDecoder(string videoPath)
@@ -80,11 +80,11 @@ namespace ADOFAIRenderer.Util.FFmpeg
{ {
int ret = ffmpeg.avformat_open_input(ppFormatCtx, videoPath, null, null); int ret = ffmpeg.avformat_open_input(ppFormatCtx, videoPath, null, null);
if (ret < 0) if (ret < 0)
throw new Exception($"비디오 파일을 열 수 없습니다: {videoPath} ({GetFFmpegErrorString(ret)})"); throw new Exception($"Failed to open video file: {videoPath} ({GetFFmpegErrorString(ret)})");
} }
if (ffmpeg.avformat_find_stream_info(_pFormatContext, null) < 0) if (ffmpeg.avformat_find_stream_info(_pFormatContext, null) < 0)
throw new Exception("스트림 정보를 읽을 수 없습니다."); throw new Exception("Failed to retrieve stream info.");
_videoStreamIndex = -1; _videoStreamIndex = -1;
for (int i = 0; i < _pFormatContext->nb_streams; i++) for (int i = 0; i < _pFormatContext->nb_streams; i++)
@@ -97,12 +97,12 @@ namespace ADOFAIRenderer.Util.FFmpeg
} }
if (_videoStreamIndex == -1) if (_videoStreamIndex == -1)
throw new Exception("비디오 스트림을 찾을 수 없습니다."); throw new Exception("Failed to find video stream.");
var stream = _pFormatContext->streams[_videoStreamIndex]; var stream = _pFormatContext->streams[_videoStreamIndex];
_timeBase = stream->time_base; _timeBase = stream->time_base;
// FPS 계산 // Calculate FPS
if (stream->r_frame_rate.num > 0 && stream->r_frame_rate.den > 0) if (stream->r_frame_rate.num > 0 && stream->r_frame_rate.den > 0)
{ {
FPS = ffmpeg.av_q2d(stream->r_frame_rate); FPS = ffmpeg.av_q2d(stream->r_frame_rate);
@@ -113,32 +113,32 @@ namespace ADOFAIRenderer.Util.FFmpeg
} }
else else
{ {
FPS = 30.0; // 기본값 폴백 FPS = 30.0; // Fallback default
} }
var codecId = stream->codecpar->codec_id; var codecId = stream->codecpar->codec_id;
var decoder = ffmpeg.avcodec_find_decoder(codecId); var decoder = ffmpeg.avcodec_find_decoder(codecId);
if (decoder == null) if (decoder == null)
throw new Exception($"지원하지 않는 비디오 코덱입니다: {codecId}"); throw new Exception($"Unsupported video codec: {codecId}");
_pCodecContext = ffmpeg.avcodec_alloc_context3(decoder); _pCodecContext = ffmpeg.avcodec_alloc_context3(decoder);
if (_pCodecContext == null) if (_pCodecContext == null)
throw new Exception("코덱 컨텍스트를 할당할 수 없습니다."); throw new Exception("Failed to allocate codec context.");
if (ffmpeg.avcodec_parameters_to_context(_pCodecContext, stream->codecpar) < 0) if (ffmpeg.avcodec_parameters_to_context(_pCodecContext, stream->codecpar) < 0)
throw new Exception("코덱 파라미터를 복사할 수 없습니다."); throw new Exception("Failed to copy codec parameters.");
// 디코딩 성능 향상을 위한 멀티스레딩 설정 (자동) // Multi-threading settings for improved decoding performance (auto)
_pCodecContext->thread_count = 0; _pCodecContext->thread_count = 0;
_pCodecContext->thread_type = ffmpeg.FF_THREAD_FRAME; _pCodecContext->thread_type = ffmpeg.FF_THREAD_FRAME;
if (ffmpeg.avcodec_open2(_pCodecContext, decoder, null) < 0) if (ffmpeg.avcodec_open2(_pCodecContext, decoder, null) < 0)
throw new Exception("코덱을 열 수 없습니다."); throw new Exception("Failed to open codec.");
Width = _pCodecContext->width; Width = _pCodecContext->width;
Height = _pCodecContext->height; Height = _pCodecContext->height;
// 총 프레임 수 및 재생 시간 계산 // Calculate total frames and duration
if (stream->nb_frames > 0) if (stream->nb_frames > 0)
{ {
TotalFrames = stream->nb_frames; TotalFrames = stream->nb_frames;
@@ -183,7 +183,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
{ {
ffmpeg.av_packet_unref(_pPacket); ffmpeg.av_packet_unref(_pPacket);
int ret = ffmpeg.av_read_frame(_pFormatContext, _pPacket); int ret = ffmpeg.av_read_frame(_pFormatContext, _pPacket);
if (ret < 0) return false; // EOF 또는 에러 if (ret < 0) return false; // EOF or error
if (_pPacket->stream_index == _videoStreamIndex) if (_pPacket->stream_index == _videoStreamIndex)
{ {
@@ -191,7 +191,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
ffmpeg.av_packet_unref(_pPacket); ffmpeg.av_packet_unref(_pPacket);
if (ret < 0) if (ret < 0)
{ {
throw new Exception($"디코더에 패킷을 전송하는 중 오류 발생: {GetFFmpegErrorString(ret)}"); throw new Exception($"Error sending packet to decoder: {GetFFmpegErrorString(ret)}");
} }
return true; return true;
} }
@@ -224,7 +224,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
if (_pSwsContext == null) if (_pSwsContext == null)
{ {
throw new Exception("SwsContext를 초기화할 수 없습니다."); throw new Exception("Failed to initialize SwsContext.");
} }
} }
@@ -251,13 +251,13 @@ namespace ADOFAIRenderer.Util.FFmpeg
} }
/// <summary> /// <summary>
/// 다음 비디오 프레임을 읽고 RGBA 형태로 복사합니다. /// Reads the next video frame and copies it in RGBA format.
/// </summary> /// </summary>
/// <param name="outBuffer">RGBA 데이터를 저장할 버퍼의 포인터</param> /// <param name="outBuffer">Pointer to the output buffer storing RGBA data.</param>
/// <param name="outStride">출력 버퍼의 가로 한 행의 바이트 크기 (일반적으로 Width * 4)</param> /// <param name="outStride">Byte stride per row of the output buffer (typically Width * 4).</param>
/// <param name="vflip">Unity 텍스처 시스템에 맞게 상하 flip을 적용할지 여부</param> /// <param name="vflip">Whether to flip vertically to match Unity's texture coordinate system.</param>
/// <param name="timestamp">출력된 프레임의 타임스탬프 (초)</param> /// <param name="timestamp">Timestamp of the output frame in seconds.</param>
/// <returns>프레임을 성공적으로 디코딩하여 복사했으면 true, 재생이 끝났거나 실패했으면 false</returns> /// <returns>True if the frame was successfully decoded and copied; false on EOF or failure.</returns>
private bool TryReadFrame(byte* outBuffer, int outStride, bool vflip, out double timestamp) private bool TryReadFrame(byte* outBuffer, int outStride, bool vflip, out double timestamp)
{ {
timestamp = 0; timestamp = 0;
@@ -339,7 +339,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
} }
else else
{ {
throw new Exception($"디코딩 중 오류 발생: {GetFFmpegErrorString(ret)}"); throw new Exception($"Error during decoding: {GetFFmpegErrorString(ret)}");
} }
} }
} }
@@ -354,16 +354,17 @@ namespace ADOFAIRenderer.Util.FFmpeg
{ {
if (outBuffer.Length < Width * Height * 4) if (outBuffer.Length < Width * Height * 4)
{ {
throw new ArgumentException("출력 버퍼의 크기가 비디오 해상도보다 작습니다.", nameof(outBuffer)); throw new ArgumentException("Output buffer size is smaller than video resolution.", nameof(outBuffer));
} }
int stride = Width * 4; int stride = Width * 4;
return TryReadFrame((byte*)outBuffer.GetUnsafePtr(), stride, vflip, out timestamp); return TryReadFrame((byte*)outBuffer.GetUnsafePtr(), stride, vflip, out timestamp);
} }
/// <summary> /// <summary>
/// 다음 비디오 프레임을 읽어 Y, U, V 채널 데이터를 각각 복사합니다. /// Reads the next video frame and copies its Y, U, and V channel data separately.
/// 비디오 원본이 YUV420p 계열(YUV420P, YUVJ420P)이면 CPU 컬러 변환(sws_scale)을 우회하고 메모리를 다이렉트로 복사하여 속도가 매우 빠릅니다. /// If the source format is YUV420P family (YUV420P, YUVJ420P), CPU color conversion (sws_scale) is bypassed
/// 그 외 포맷일 경우 자동으로 YUV420p로 변환 후 복사합니다. /// and memory is copied directly for maximum performance.
/// Other formats are automatically converted to YUV420P before copying.
/// </summary> /// </summary>
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
private bool TryReadYUV420PFrame( private bool TryReadYUV420PFrame(
@@ -411,7 +412,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
} }
else else
{ {
throw new Exception($"디코딩 중 오류 발생: {GetFFmpegErrorString(ret)}"); throw new Exception($"Error during decoding: {GetFFmpegErrorString(ret)}");
} }
} }
} }
@@ -445,7 +446,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
int uvW = w / 2; int uvW = w / 2;
int uvH = h / 2; int uvH = h / 2;
// Y plane 복사 // Copy Y plane
for (int y = 0; y < h; y++) for (int y = 0; y < h; y++)
{ {
Buffer.MemoryCopy( Buffer.MemoryCopy(
@@ -455,7 +456,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
); );
} }
// U plane 복사 // Copy U plane
for (int y = 0; y < uvH; y++) for (int y = 0; y < uvH; y++)
{ {
Buffer.MemoryCopy( Buffer.MemoryCopy(
@@ -465,7 +466,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
); );
} }
// V plane 복사 // Copy V plane
for (int y = 0; y < uvH; y++) for (int y = 0; y < uvH; y++)
{ {
Buffer.MemoryCopy( Buffer.MemoryCopy(
@@ -495,7 +496,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
if (yBuffer.Length < w * h || uBuffer.Length < uvW * uvH || vBuffer.Length < uvW * uvH) if (yBuffer.Length < w * h || uBuffer.Length < uvW * uvH || vBuffer.Length < uvW * uvH)
{ {
throw new ArgumentException("버퍼 크기가 비디오 YUV 평면 해상도보다 작습니다."); throw new ArgumentException("Buffer size is smaller than video YUV plane resolution.");
} }
return TryReadYUV420PFrame( return TryReadYUV420PFrame(
@@ -507,9 +508,10 @@ namespace ADOFAIRenderer.Util.FFmpeg
} }
/// <summary> /// <summary>
/// 다음 비디오 프레임을 읽어 NV12 형식의 Y, UV 채널 데이터를 각각 복사합니다. /// Reads the next video frame and copies its NV12 format Y and UV channel data separately.
/// 비디오 원본이 NV12 계열이면 CPU 컬러 변환(sws_scale)을 우회하고 메모리를 다이렉트로 복사하여 속도가 매우 빠릅니다. /// If the source format is NV12 family, CPU color conversion (sws_scale) is bypassed
/// 그 외 포맷일 경우 자동으로 NV12로 변환 후 복사합니다. /// and memory is copied directly for maximum performance.
/// Other formats are automatically converted to NV12 before copying.
/// </summary> /// </summary>
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
private bool TryReadNV12Frame( private bool TryReadNV12Frame(
@@ -556,7 +558,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
} }
else else
{ {
throw new Exception($"디코딩 중 오류 발생: {GetFFmpegErrorString(ret)}"); throw new Exception($"Error during decoding: {GetFFmpegErrorString(ret)}");
} }
} }
} }
@@ -587,7 +589,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
int h = frame->height; int h = frame->height;
int uvH = h / 2; int uvH = h / 2;
// Y plane 복사 // Copy Y plane
for (int y = 0; y < h; y++) for (int y = 0; y < h; y++)
{ {
Buffer.MemoryCopy( Buffer.MemoryCopy(
@@ -597,7 +599,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
); );
} }
// UV plane 복사 // Copy UV plane
for (int y = 0; y < uvH; y++) for (int y = 0; y < uvH; y++)
{ {
Buffer.MemoryCopy( Buffer.MemoryCopy(
@@ -625,7 +627,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
if (yBuffer.Length < w * h || uvBuffer.Length < w * uvH) if (yBuffer.Length < w * h || uvBuffer.Length < w * uvH)
{ {
throw new ArgumentException("버퍼 크기가 비디오 NV12 평면 해상도보다 작습니다."); throw new ArgumentException("Buffer size is smaller than video NV12 plane resolution.");
} }
return TryReadNV12Frame( return TryReadNV12Frame(
@@ -636,10 +638,9 @@ namespace ADOFAIRenderer.Util.FFmpeg
} }
/// <summary> /// <summary>
/// 지정한 개수만큼 프레임을 디코딩 후 바로 버립니다. (빠른 전방 탐색용) /// Decodes and discards a specified number of frames immediately (for fast forward skipping).
/// </summary> /// </summary>
/// <param name="count">버릴 프레임 개수</param> /// <param name="count">Number of frames to discard.</param>
/// <returns>실제로 건너뛴 프레임 개수</returns>
private void SkipFrames(int count) private void SkipFrames(int count)
{ {
lock (_lock) lock (_lock)
@@ -682,16 +683,16 @@ namespace ADOFAIRenderer.Util.FFmpeg
} }
else else
{ {
throw new Exception($"프레임 건너뛰기 중 오류 발생: {GetFFmpegErrorString(ret)}"); throw new Exception($"Error skipping frames: {GetFFmpegErrorString(ret)}");
} }
} }
} }
} }
/// <summary> /// <summary>
/// 특정 재생 시간(초)으로 탐색(Seek)합니다. /// Seeks to a specific playback time in seconds.
/// </summary> /// </summary>
/// <param name="timeSeconds">탐색할 시간 (초)</param> /// <param name="timeSeconds">Target time in seconds to seek to.</param>
public void SeekToTime(double timeSeconds) public void SeekToTime(double timeSeconds)
{ {
lock (_lock) lock (_lock)
@@ -701,7 +702,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
if (timeSeconds < 0) timeSeconds = 0; if (timeSeconds < 0) timeSeconds = 0;
double targetFrameIndex = timeSeconds * FPS; double targetFrameIndex = timeSeconds * FPS;
// 이미 해당 인덱스이고 매우 인접한 미래라면 seek 대신 skip 처리 (I-프레임 찾기 오버헤드 방지) // If already at target index or very close future, skip frames instead of seek to avoid I-frame overhead
bool useSkip = targetFrameIndex >= _currentFrameIndex && targetFrameIndex - _currentFrameIndex < 60; bool useSkip = targetFrameIndex >= _currentFrameIndex && targetFrameIndex - _currentFrameIndex < 60;
if (useSkip) if (useSkip)
{ {
@@ -719,19 +720,19 @@ namespace ADOFAIRenderer.Util.FFmpeg
int ret = ffmpeg.av_seek_frame(_pFormatContext, _videoStreamIndex, targetPts, ffmpeg.AVSEEK_FLAG_BACKWARD); int ret = ffmpeg.av_seek_frame(_pFormatContext, _videoStreamIndex, targetPts, ffmpeg.AVSEEK_FLAG_BACKWARD);
if (ret < 0) if (ret < 0)
{ {
// 실패 시 파일 처음부터 탐색 시도 // Fallback to start of file on seek failure
ret = ffmpeg.av_seek_frame(_pFormatContext, _videoStreamIndex, 0, ffmpeg.AVSEEK_FLAG_BACKWARD); ret = ffmpeg.av_seek_frame(_pFormatContext, _videoStreamIndex, 0, ffmpeg.AVSEEK_FLAG_BACKWARD);
if (ret < 0) if (ret < 0)
{ {
throw new Exception($"Seek 실패: {GetFFmpegErrorString(ret)}"); throw new Exception($"Seek failed: {GetFFmpegErrorString(ret)}");
} }
} }
// 디코더 내부 캐시 초기화 // Flush decoder internal cache
ffmpeg.avcodec_flush_buffers(_pCodecContext); ffmpeg.avcodec_flush_buffers(_pCodecContext);
_isDraining = false; _isDraining = false;
// 지정된 시간 이후의 첫 번째 프레임을 찾을 때까지 디코딩을 진행하며 버립니다. // Decode and discard frames until the first frame past the specified time is found
while (true) while (true)
{ {
ret = ffmpeg.avcodec_receive_frame(_pCodecContext, _pDecodedFrame); ret = ffmpeg.avcodec_receive_frame(_pCodecContext, _pDecodedFrame);
@@ -739,10 +740,10 @@ namespace ADOFAIRenderer.Util.FFmpeg
{ {
double currentPts = GetTimestampAndUpdateIndex(_pDecodedFrame); double currentPts = GetTimestampAndUpdateIndex(_pDecodedFrame);
// 플로팅 포인트 오차 감안하여 하프 프레임 제외한 시간부터 체크 // Check from half frame earlier to account for floating point errors
if (currentPts >= timeSeconds - (0.5 / FPS)) if (currentPts >= timeSeconds - (0.5 / FPS))
{ {
// 매칭되는 첫 프레임을 캐시에 임시 저장하여 다음 TryReadFrame에서 소모하도록 유도 // Cache the first matching frame so it will be consumed by the next TryReadFrame call
ffmpeg.av_frame_move_ref(_pCachedFrame, _pDecodedFrame); ffmpeg.av_frame_move_ref(_pCachedFrame, _pDecodedFrame);
_hasCachedFrame = true; _hasCachedFrame = true;
break; break;
@@ -768,16 +769,16 @@ namespace ADOFAIRenderer.Util.FFmpeg
} }
else else
{ {
throw new Exception($"Seek 디코딩 중 오류 발생: {GetFFmpegErrorString(ret)}"); throw new Exception($"Error during seek decoding: {GetFFmpegErrorString(ret)}");
} }
} }
} }
} }
/// <summary> /// <summary>
/// 특정 프레임 인덱스로 탐색(Seek)합니다. /// Seeks to a specific frame index.
/// </summary> /// </summary>
/// <param name="frameIndex">탐색할 프레임 인덱스</param> /// <param name="frameIndex">Target frame index to seek to.</param>
public void SeekToFrame(long frameIndex) public void SeekToFrame(long frameIndex)
{ {
SeekToTime(frameIndex / FPS); SeekToTime(frameIndex / FPS);
@@ -789,9 +790,9 @@ namespace ADOFAIRenderer.Util.FFmpeg
byte* buffer = stackalloc byte[(int)bufferSize]; byte* buffer = stackalloc byte[(int)bufferSize];
if (ffmpeg.av_strerror(errnum, buffer, bufferSize) >= 0) if (ffmpeg.av_strerror(errnum, buffer, bufferSize) >= 0)
{ {
return System.Runtime.InteropServices.Marshal.PtrToStringAnsi((IntPtr)buffer) ?? $"알 수 없는 에러 (Code: {errnum})"; return System.Runtime.InteropServices.Marshal.PtrToStringAnsi((IntPtr)buffer) ?? $"Unknown error (Code: {errnum})";
} }
return $"알 수 없는 에러 (Code: {errnum})"; return $"Unknown error (Code: {errnum})";
} }
public void Dispose() public void Dispose()
File diff suppressed because it is too large. Load diff
+9 -9
View File
@@ -70,7 +70,7 @@ namespace ADOFAIRenderer.Util
if (!File.Exists(videoPath)) if (!File.Exists(videoPath))
{ {
Melon<RenderMod>.Logger.Warning($"[FFmpegVideoBGPlayer] 비디오 파일이 존재하지 않습니다: {videoPath}"); Melon<RenderMod>.Logger.Warning($"[FFmpegVideoBGPlayer] Video file does not exist: {videoPath}");
return; return;
} }
@@ -101,7 +101,7 @@ namespace ADOFAIRenderer.Util
_gpuMode = GPUMode.NV12; _gpuMode = GPUMode.NV12;
gpuInitSuccess = true; gpuInitSuccess = true;
Melon<RenderMod>.Logger.Msg($"[FFmpegVideoBGPlayer] Compute Shader NV12 모드 초기화 완료"); Melon<RenderMod>.Logger.Msg("[FFmpegVideoBGPlayer] Initialized Compute Shader NV12 mode successfully");
} }
catch catch
{ {
@@ -124,7 +124,7 @@ namespace ADOFAIRenderer.Util
_gpuMode = GPUMode.YUV420P; _gpuMode = GPUMode.YUV420P;
gpuInitSuccess = true; gpuInitSuccess = true;
Melon<RenderMod>.Logger.Msg($"[FFmpegVideoBGPlayer] Compute Shader YUV420P 모드 초기화 완료"); Melon<RenderMod>.Logger.Msg("[FFmpegVideoBGPlayer] Initialized Compute Shader YUV420P mode successfully");
} }
catch catch
{ {
@@ -133,7 +133,7 @@ namespace ADOFAIRenderer.Util
break; break;
default: default:
Melon<RenderMod>.Logger.Msg("[FFmpegVideoBGPlayer] 색공간 변환에 Compute Shader를 사용할 수 없습니다."); Melon<RenderMod>.Logger.Msg("[FFmpegVideoBGPlayer] Cannot use Compute Shader for colorspace conversion.");
break; break;
} }
} }
@@ -151,18 +151,18 @@ namespace ADOFAIRenderer.Util
_textureRGBA = new Texture2D(w, h, TextureFormat.RGBA32, false); _textureRGBA = new Texture2D(w, h, TextureFormat.RGBA32, false);
ConfigureTexture(_textureRGBA); ConfigureTexture(_textureRGBA);
_gpuMode = GPUMode.None; _gpuMode = GPUMode.None;
Melon<RenderMod>.Logger.Warning($"[FFmpegVideoBGPlayer] GPU 가속 미지원 또는 로드 실패로 CPU 폴백(RGBA)으로 진행합니다."); Melon<RenderMod>.Logger.Warning("[FFmpegVideoBGPlayer] GPU acceleration unsupported or failed to load, falling back to CPU (RGBA).");
} }
_currentFrameTimestamp = 0.0; _currentFrameTimestamp = 0.0;
_nextFrameTimestamp = 0.0; _nextFrameTimestamp = 0.0;
_isInitialized = true; _isInitialized = true;
Melon<RenderMod>.Logger.Msg($"[FFmpegVideoBGPlayer] 초기화 성공 - 해상도: {w}x{h}, FPS: {_decoder.FPS:F2}"); Melon<RenderMod>.Logger.Msg($"[FFmpegVideoBGPlayer] Successfully initialized - Resolution: {w}x{h}, FPS: {_decoder.FPS:F2}");
} }
catch (Exception ex) catch (Exception ex)
{ {
Melon<RenderMod>.Logger.Error($"[FFmpegVideoBGPlayer] 초기화 중 에러 발생: {ex}"); Melon<RenderMod>.Logger.Error($"[FFmpegVideoBGPlayer] Error during initialization: {ex}");
CleanUp(); CleanUp();
} }
} }
@@ -283,7 +283,7 @@ namespace ADOFAIRenderer.Util
} }
catch (Exception ex) catch (Exception ex)
{ {
Melon<RenderMod>.Logger.Error($"[FFmpegVideoBGPlayer] Update 재생 중 에러 발생: {ex}"); Melon<RenderMod>.Logger.Error($"[FFmpegVideoBGPlayer] Error during playback update: {ex}");
_isInitialized = false; _isInitialized = false;
} }
} }
@@ -395,7 +395,7 @@ namespace ADOFAIRenderer.Util
{ {
_decoder.Dispose(); _decoder.Dispose();
_decoder = null; _decoder = null;
Melon<RenderMod>.Logger.Msg("[FFmpegVideoBGPlayer] 디코더 해제 완료."); Melon<RenderMod>.Logger.Msg("[FFmpegVideoBGPlayer] Decoder successfully disposed.");
} }
if (_textureRGBA != null) if (_textureRGBA != null)
+4 -4
View File
@@ -70,14 +70,14 @@ namespace ADOFAIRenderer.Util
state.IsInFlight = true; state.IsInFlight = true;
} }
// GPU 컬러 변환 활성화 여부 검사 // Check if GPU color conversion is enabled
bool useGPUConversion = RenderMod.ColorConversionShader; bool useGPUConversion = RenderMod.ColorConversionShader;
AVPixelFormat expectedFmt = AVPixelFormat.AV_PIX_FMT_RGBA; AVPixelFormat expectedFmt = AVPixelFormat.AV_PIX_FMT_RGBA;
if (useGPUConversion) if (useGPUConversion)
{ {
expectedFmt = _targetFormat; expectedFmt = _targetFormat;
// Compute Shader는 NV12와 YUV420P만 지원하므로 그 외 포맷은 CPU 폴백 처리 // Compute Shader only supports NV12 and YUV420P; fallback to CPU for other formats
if (expectedFmt != AVPixelFormat.AV_PIX_FMT_NV12 && expectedFmt != AVPixelFormat.AV_PIX_FMT_YUV420P) if (expectedFmt != AVPixelFormat.AV_PIX_FMT_NV12 && expectedFmt != AVPixelFormat.AV_PIX_FMT_YUV420P)
{ {
useGPUConversion = false; useGPUConversion = false;
@@ -120,7 +120,7 @@ namespace ADOFAIRenderer.Util
} }
else else
{ {
// 커널을 못 찾은 경우 기존 RGBA 텍스처 백업 기법으로 폴백 // If kernel is not found, fallback to standard RGBA texture readback
AsyncGPUReadback.RequestIntoNativeArray( AsyncGPUReadback.RequestIntoNativeArray(
ref state.NativeBuffer, ref state.NativeBuffer,
texture, texture,
@@ -189,7 +189,7 @@ namespace ADOFAIRenderer.Util
if (state.IsGPUConversion) if (state.IsGPUConversion)
{ {
var data = request.GetData<byte>(); var data = request.GetData<byte>();
// 비동기 요청 버퍼 데이터를 지속성(NativeBuffer) 메모리로 복사 // Copy async request buffer data to persistent NativeBuffer memory
NativeArray<byte>.Copy(data, state.NativeBuffer, data.Length); NativeArray<byte>.Copy(data, state.NativeBuffer, data.Length);
} }
+11 -11
View File
@@ -48,7 +48,7 @@ namespace ADOFAIRenderer.Util
} }
catch (Exception ex) catch (Exception ex)
{ {
MelonLogger.Warning($"[OSNotification] 알림을 보내지 못했습니다: {ex.Message}"); MelonLogger.Warning($"[OSNotification] Failed to send notification: {ex.Message}");
} }
} }
@@ -74,7 +74,7 @@ namespace ADOFAIRenderer.Util
} }
catch (Exception ex) catch (Exception ex)
{ {
MelonLogger.Warning($"[OSNotification] Windows 알림 실패: {ex.Message}"); MelonLogger.Warning($"[OSNotification] Windows notification failed: {ex.Message}");
} }
}) })
{ {
@@ -110,7 +110,7 @@ namespace ADOFAIRenderer.Util
$"<text>{xmlTitle}</text><text>{xmlMessage}</text>" + $"<text>{xmlTitle}</text><text>{xmlMessage}</text>" +
"</binding></visual>" + "</binding></visual>" +
"<actions>" + "<actions>" +
"<action content=\"렌더 폴더 열기\" arguments=\"adofai-renderer://\" activationType=\"protocol\"/>" + "<action content=\"Open Render Folder\" arguments=\"adofai-renderer://\" activationType=\"protocol\"/>" +
"</actions></toast>"; "</actions></toast>";
psScript = psScript =
@@ -165,7 +165,7 @@ namespace ADOFAIRenderer.Util
{ {
string stderr = process.StandardError.ReadToEnd().Trim(); string stderr = process.StandardError.ReadToEnd().Trim();
if (!string.IsNullOrEmpty(stderr)) if (!string.IsNullOrEmpty(stderr))
MelonLogger.Warning($"[OSNotification] Toast 실패 (fallback 사용): {stderr}"); MelonLogger.Warning($"[OSNotification] Toast failed (using fallback): {stderr}");
return false; return false;
} }
@@ -173,7 +173,7 @@ namespace ADOFAIRenderer.Util
} }
catch (Exception ex) catch (Exception ex)
{ {
MelonLogger.Warning($"[OSNotification] Toast 실패 (fallback 사용): {ex.Message}"); MelonLogger.Warning($"[OSNotification] Toast failed (using fallback): {ex.Message}");
return false; return false;
} }
} }
@@ -283,7 +283,7 @@ namespace ADOFAIRenderer.Util
if (hWnd == IntPtr.Zero) if (hWnd == IntPtr.Zero)
{ {
MelonLogger.Warning("[OSNotification] 메시지 윈도우를 생성할 수 없습니다."); MelonLogger.Warning("[OSNotification] Cannot create message window.");
return; return;
} }
@@ -334,7 +334,7 @@ namespace ADOFAIRenderer.Util
catch (Exception ex) catch (Exception ex)
{ {
MelonLogger.Warning( MelonLogger.Warning(
$"[OSNotification] 폴더를 열 수 없습니다: {ex.Message}"); $"[OSNotification] Cannot open folder: {ex.Message}");
} }
break; break;
@@ -357,7 +357,7 @@ namespace ADOFAIRenderer.Util
} }
catch (Exception ex) catch (Exception ex)
{ {
MelonLogger.Warning($"[OSNotification] Balloon 알림 실패: {ex.Message}"); MelonLogger.Warning($"[OSNotification] Balloon notification failed: {ex.Message}");
} }
finally finally
{ {
@@ -465,7 +465,7 @@ namespace ADOFAIRenderer.Util
}; };
startInfo.ArgumentList.Add("-a"); startInfo.ArgumentList.Add("-a");
startInfo.ArgumentList.Add("ADOFAI Renderer"); startInfo.ArgumentList.Add("ADOFAI Renderer");
startInfo.ArgumentList.Add("--action=open_folder=렌더 폴더 열기"); startInfo.ArgumentList.Add("--action=open_folder=Open Render Folder");
startInfo.ArgumentList.Add(title); startInfo.ArgumentList.Add(title);
startInfo.ArgumentList.Add(message); startInfo.ArgumentList.Add(message);
@@ -491,7 +491,7 @@ namespace ADOFAIRenderer.Util
} }
catch (Exception ex) catch (Exception ex)
{ {
MelonLogger.Warning($"[OSNotification] 리눅스 알림 처리 실패: {ex.Message}"); MelonLogger.Warning($"[OSNotification] Linux notification processing failed: {ex.Message}");
// Fallback: show without action // Fallback: show without action
ShowSimple(title, message); ShowSimple(title, message);
@@ -554,7 +554,7 @@ namespace ADOFAIRenderer.Util
} }
catch (Exception ex) catch (Exception ex)
{ {
MelonLogger.Warning($"[OSNotification] gdbus 알림 실패: {ex.Message}"); MelonLogger.Warning($"[OSNotification] gdbus notification failed: {ex.Message}");
} }
} }
} }
+12 -12
View File
@@ -1,4 +1,4 @@
using System; using System;
namespace ADOFAIRenderer.Util namespace ADOFAIRenderer.Util
{ {
@@ -7,49 +7,49 @@ namespace ADOFAIRenderer.Util
public static (int Numerator, int Denominator) Double2MaxPrecisionInt(double value, public static (int Numerator, int Denominator) Double2MaxPrecisionInt(double value,
int maxDenominator = 100000000) int maxDenominator = 100000000)
{ {
// 1. 예외 처리 및 부호 판별 // 1. Edge case handling and sign detection
if (value == 0.0) return (0, 1); if (value == 0.0) return (0, 1);
var isNegative = value < 0; var isNegative = value < 0;
var target = Math.Abs(value); var target = Math.Abs(value);
// 2. 스턴-브로코 / 연속분수 경계값 초기화 // 2. Stern-Brocot / continued fraction boundary initialization
// 아래 두 행의 분수 (0/1, 1/0) 사이에서 시작하여 타겟에 가까워집니다. // Starts between (0/1, 1/0) and converges towards the target value.
long m00 = 0, m01 = 1; long m00 = 0, m01 = 1;
long m10 = 1, m11 = 0; long m10 = 1, m11 = 0;
var x = target; var x = target;
// int 범위 오버플로우를 방지하면서 반복 처리 // Iterate while preventing integer overflow
while (true) while (true)
{ {
var ai = (long)Math.Floor(x); var ai = (long)Math.Floor(x);
// 다음 단계의 분자, 분모 계산 (오버플로우 방지를 위해 long으로 임시 계산) // Calculate next numerator and denominator (temporarily computed as long to prevent overflow)
var nextNumerator = ai * m10 + m00; var nextNumerator = ai * m10 + m00;
var nextDenominator = ai * m11 + m01; var nextDenominator = ai * m11 + m01;
// int 범위를 벗어나거나, 설정한 최대 분모를 초과하면 이전 단계의 최적값을 반환 // If exceeding integer range or specified maximum denominator, return previous optimal value
if (nextNumerator > int.MaxValue || nextDenominator > maxDenominator) if (nextNumerator > int.MaxValue || nextDenominator > maxDenominator)
{ {
break; break;
} }
// 행렬 업데이트 // Update matrix
m00 = m10; m01 = m11; m00 = m10; m01 = m11;
m10 = nextNumerator; m11 = nextDenominator; m10 = nextNumerator; m11 = nextDenominator;
// 오차가 거의 없으면 (정밀도 도달) 종료 // Terminate if error is negligible (precision limit reached)
if (Math.Abs(x - ai) < 1e-15) break; if (Math.Abs(x - ai) < 1e-15) break;
// 다음 연속분수 항 준비 // Prepare next continued fraction term
x = 1.0 / (x - ai); x = 1.0 / (x - ai);
} }
// 3. 최종 결정된 int 기반의 분자 분모 // 3. Final determined integer numerator and denominator
var finalNumerator = (int)m10; var finalNumerator = (int)m10;
var finalDenominator = (int)m11; var finalDenominator = (int)m11;
// 부호 복원 // Restore sign
if (isNegative) finalNumerator = -finalNumerator; if (isNegative) finalNumerator = -finalNumerator;
return (finalNumerator, finalDenominator); return (finalNumerator, finalDenominator);
+2 -2
View File
@@ -1,4 +1,4 @@
using System; using System;
using System.IO; using System.IO;
using MelonLoader; using MelonLoader;
@@ -21,7 +21,7 @@ namespace ADOFAIRenderer.Util
} }
catch (IOException err) catch (IOException err)
{ {
Melon<RenderMod>.Logger.Error($"FFmpeg 파이프가 예상치 못하게 닫혔습니다: {err.Message}"); Melon<RenderMod>.Logger.Error($"FFmpeg pipe closed unexpectedly: {err.Message}");
} }
} }
} }