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