diff --git a/Core/FirstUpdateHook.cs b/Core/FirstUpdateHook.cs index 42b9813..33cb51b 100644 --- a/Core/FirstUpdateHook.cs +++ b/Core/FirstUpdateHook.cs @@ -1,3 +1,4 @@ +using ADOFAIRenderer.UI; using UnityEngine; namespace ADOFAIRenderer.Core @@ -5,8 +6,16 @@ namespace ADOFAIRenderer.Core [DefaultExecutionOrder(int.MinValue)] public class FirstUpdateHook : MonoBehaviour { + private bool _checkedFFmpeg; + private void Update() { + if (!_checkedFFmpeg) + { + _checkedFFmpeg = true; + FFmpegDownloadUI.ShowDownloadUI(); + } + if (RenderManager.Instance) { RenderManager.Instance.OnPreRenderUpdate(); diff --git a/UI/FFmpegDownloadUI.cs b/UI/FFmpegDownloadUI.cs new file mode 100644 index 0000000..b0f06c0 --- /dev/null +++ b/UI/FFmpegDownloadUI.cs @@ -0,0 +1,577 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Net.Http; +using System.Threading.Tasks; +using TMPro; +using UnityEngine; +using UnityEngine.UI; +using MelonLoader; +using MelonLoader.Utils; + +namespace ADOFAIRenderer.UI +{ + public class FFmpegDownloadUI : MonoBehaviour + { + private GameObject _buttonContainerObj = null!; + private GameObject _progressBarObj = null!; + private GameObject _progressBarFillObj = null!; + private RectTransform _fillRect = null!; + private TextMeshProUGUI _progressText = null!; + private GameObject _statusTextObj = null!; + private TextMeshProUGUI _statusText = null!; + + private bool _isDownloading; + private readonly string _downloadUrl = "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl-shared.zip"; + + private System.Threading.CancellationTokenSource? _cts; + private readonly System.Collections.Concurrent.ConcurrentQueue _mainThreadQueue = new(); + + private static bool CheckFFmpegExists() + { + var userLibs = MelonEnvironment.UserLibsDirectory; + if (!Directory.Exists(userLibs)) return false; + + // Check for avcodec-*.dll + var files = Directory.GetFiles(userLibs, "avcodec-*.dll"); + return files.Length > 0; + } + + public static void ShowDownloadUI() + { + + if (CheckFFmpegExists()) return; + + var obj = new GameObject("FFmpegDownloadUI"); + DontDestroyOnLoad(obj); + obj.AddComponent(); + } + + private void Awake() + { + if (CheckFFmpegExists()) + { + Destroy(gameObject); + } + } + + private static TMP_FontAsset GetNanumSquareNeoBdFontAsset() + { + TMP_FontAsset? font = null; + if (RenderMod.FontBundle != null) + { + try + { + font = RenderMod.FontBundle.LoadAsset("NanumSquareNeoOTF-Bd SDF"); + } + catch + { + // 로딩 실패 + } + } + + // 폴백 처리 + if (font == null) + { + try + { + font = RDString.fontData.fontTMP; + } + catch + { + // ignored + } + } + if (font == null) + { + // ReSharper disable once Unity.UnknownResource + font = Resources.Load("Fonts & Materials/LiberationSans SDF"); + } + + if (font != null) return font; + var fonts = Resources.FindObjectsOfTypeAll(); + if (fonts.Length > 0) font = fonts[0]; + + return font!; + } + + private void Start() + { + // UI 레이어(5) 설정 + gameObject.layer = 5; + + // Canvas 설정 + var canvas = gameObject.AddComponent(); + canvas.renderMode = RenderMode.ScreenSpaceOverlay; + canvas.sortingOrder = 32767; // 최상위 + + // 해상도 대응 캔버스 스케일러 설정 + var scaler = gameObject.AddComponent(); + scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; + scaler.referenceResolution = new Vector2(1920, 1080); + scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight; + scaler.matchWidthOrHeight = 0.5f; + + gameObject.AddComponent(); + + // 1. 전체 화면 오버레이 배경 생성 + var bgPanelObj = new GameObject("BackgroundPanel"); + bgPanelObj.transform.SetParent(transform, false); + bgPanelObj.layer = 5; + + var bgImage = bgPanelObj.AddComponent(); + bgImage.color = new Color(0f, 0f, 0f, 0.65f); // 어두운 투명 오버레이 + bgImage.raycastTarget = true; + + var bgRect = bgPanelObj.GetComponent(); + bgRect.anchorMin = Vector2.zero; + bgRect.anchorMax = Vector2.one; + bgRect.offsetMin = Vector2.zero; + bgRect.offsetMax = Vector2.zero; + + var font = GetNanumSquareNeoBdFontAsset(); + + // 2. 모달 컨테이너 생성 (다른 UI들과 디자인 일관성을 위해 동일한 아크릴 다크톤과 흰색 굵은 외곽선 적용) + var modalContainerObj = new GameObject("ModalContainer"); + modalContainerObj.transform.SetParent(bgPanelObj.transform, false); + modalContainerObj.layer = 5; + + var modalImage = modalContainerObj.AddComponent(); + modalImage.color = new Color(0.08f, 0.08f, 0.1f, 0.88f); // 다른 UI의 배경 톤과 동일 + + var modalOutline = modalContainerObj.AddComponent(); + modalOutline.effectColor = Color.white; // DebugMonitorUI와 일치하는 흰색 외곽선 + modalOutline.effectDistance = new Vector2(3.0f, -3.0f); + + var modalRect = modalContainerObj.GetComponent(); + modalRect.anchorMin = new Vector2(0.5f, 0.5f); + modalRect.anchorMax = new Vector2(0.5f, 0.5f); + modalRect.pivot = new Vector2(0.5f, 0.5f); + modalRect.sizeDelta = new Vector2(600, 320); + + // 4. 타이틀 텍스트 생성 + var titleObj = new GameObject("TitleText"); + titleObj.transform.SetParent(modalContainerObj.transform, false); + titleObj.layer = 5; + + var titleText = titleObj.AddComponent(); + titleText.text = "FFmpeg 라이브러리 설치 필요"; + titleText.fontSize = 26; + titleText.font = font; + titleText.color = Color.white; + titleText.alignment = TextAlignmentOptions.Center; + + var titleRect = titleObj.GetComponent(); + titleRect.anchorMin = new Vector2(0, 1); + titleRect.anchorMax = new Vector2(1, 1); + titleRect.pivot = new Vector2(0.5f, 1); + titleRect.anchoredPosition = new Vector2(0, -35); + titleRect.sizeDelta = new Vector2(0, 40); + + // 5. 본문 설명 텍스트 생성 + var descObj = new GameObject("DescriptionText"); + descObj.transform.SetParent(modalContainerObj.transform, false); + descObj.layer = 5; + + var descText = descObj.AddComponent(); + descText.text = "동영상을 렌더링하려면 FFmpeg 공유 라이브러리가 필요합니다.\n아래 버튼을 눌러 인터넷에서 자동으로 다운로드하여 설치합니다.\n(다운로드 크기: 약 70MB, UserLibs 폴더에 압축 해제됨)"; + descText.fontSize = 16; + descText.font = font; + descText.color = new Color(0.85f, 0.85f, 0.85f, 1f); + descText.alignment = TextAlignmentOptions.Center; + descText.lineSpacing = 10; + + var descRect = descObj.GetComponent(); + descRect.anchorMin = new Vector2(0, 0.5f); + descRect.anchorMax = new Vector2(1, 1); + descRect.pivot = new Vector2(0.5f, 0.5f); + descRect.anchoredPosition = new Vector2(0, -60); + descRect.sizeDelta = new Vector2(-40, 100); + + // 6. 버튼 컨테이너 생성 + _buttonContainerObj = new GameObject("ButtonContainer", typeof(RectTransform)); + _buttonContainerObj.transform.SetParent(modalContainerObj.transform, false); + _buttonContainerObj.layer = 5; + + var btnContainerRect = _buttonContainerObj.GetComponent(); + btnContainerRect.anchorMin = new Vector2(0, 0); + btnContainerRect.anchorMax = new Vector2(1, 0); + btnContainerRect.pivot = new Vector2(0.5f, 0); + btnContainerRect.anchoredPosition = new Vector2(0, 35); + btnContainerRect.sizeDelta = new Vector2(0, 50); + + // 7. 다운로드 버튼 생성 + var downloadBtnObj = new GameObject("DownloadButton"); + downloadBtnObj.transform.SetParent(_buttonContainerObj.transform, false); + downloadBtnObj.layer = 5; + + var downloadImg = downloadBtnObj.AddComponent(); + downloadImg.color = new Color(0.0f, 0.8f, 1.0f, 1.0f); // Sleek cyan/blue + downloadImg.raycastTarget = true; + + var downloadBtn = downloadBtnObj.AddComponent