using System; using System.IO; using System.IO.Compression; using System.Net.Http; using System.Text; 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 string DownloadUrl => Application.platform switch { RuntimePlatform.WindowsPlayer => "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl-shared.zip", _ => "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl-shared.tar.xz" }; private System.Threading.CancellationTokenSource? _cts; private readonly System.Collections.Concurrent.ConcurrentQueue _mainThreadQueue = new(); public static bool CheckFFmpegExists() { var userLibs = MelonEnvironment.UserLibsDirectory; if (!Directory.Exists(userLibs)) return false; var searchPattern = Application.platform switch { RuntimePlatform.WindowsPlayer => "avcodec-*.dll", RuntimePlatform.OSXPlayer => "libavcodec.*.dylib", _ => "libavcodec.so*" }; var files = Directory.GetFiles(userLibs, searchPattern); return files.Length > 0; } public static void ShowDownloadUI() { if (CheckFFmpegExists()) return; var obj = new GameObject("FFmpegDownloadUI"); DontDestroyOnLoad(obj); obj.AddComponent(); } public static FFmpegDownloadUI? Instance { get; private set; } private void Awake() { Instance = this; 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(); var descStrBuilder = new StringBuilder(); descStrBuilder.AppendLine("동영상을 렌더링하려면 FFmpeg 공유 라이브러리가 필요합니다."); if (Application.platform == RuntimePlatform.OSXPlayer) { descStrBuilder.AppendLine("OSX 플랫폼의 경우 신뢰 가능한 공개 빌드가 없어 수동으로 FFmpeg를 빌드하여야 합니다."); descStrBuilder.AppendLine("아래 버튼을 눌러 ffmpeg 공식 웹사이트를 열 수 있습니다."); } else { descStrBuilder.AppendLine("아래 버튼을 눌러 인터넷에서 자동으로 다운로드하여 설치합니다."); descStrBuilder.AppendLine("(다운로드 크기: 약 70MB, UserLibs 폴더에 압축 해제됨)"); } descText.text = descStrBuilder.ToString(); 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