using UnityEngine; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine.SceneManagement; using UnityEngine.Rendering; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Text; public class AssetBundleLoaderWindow : EditorWindow { private string bundlePath = ""; private AssetBundle loadedBundle; private string[] assetNames; private Vector2 scrollPosition; private Dictionary loadedAssets = new Dictionary(); private Dictionary assetFoldouts = new Dictionary(); private string searchFilter = ""; private string fileHeaderInfo = ""; private int headerOffset = 0; private bool showAdvanced = false; private string tempExtractPath = ""; private List zipEntries = new List(); private bool isWarudoFormat = false; // 멀티 번들 지원 private List additionalBundles = new List(); private string sceneBundlePath = ""; private AssetBundle sceneBundle; [MenuItem("Tools/Utilities/AssetBundle Loader")] public static void ShowWindow() { var window = GetWindow("AssetBundle Loader"); window.minSize = new Vector2(400, 300); } private void OnGUI() { GUILayout.Label("AssetBundle 테스트 로더", EditorStyles.boldLabel); GUILayout.Space(5); DrawBundlePathSection(); GUILayout.Space(10); DrawLoadUnloadButtons(); GUILayout.Space(10); if (loadedBundle != null) { DrawBundleInfo(); GUILayout.Space(5); DrawSearchFilter(); GUILayout.Space(5); DrawAssetList(); } } private void DrawBundlePathSection() { // Warudo 파일 원클릭 로드 EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Warudo 원클릭", GUILayout.Width(85)); if (GUILayout.Button(".warudo 파일 열기", GUILayout.Height(24))) { string selectedPath = EditorUtility.OpenFilePanel("Warudo 파일 선택", "", "warudo"); if (!string.IsNullOrEmpty(selectedPath)) { LoadWarudoFileOneClick(selectedPath); } } EditorGUILayout.EndHorizontal(); GUILayout.Space(5); EditorGUILayout.LabelField("", GUI.skin.horizontalSlider); GUILayout.Space(5); // 리소스 번들 (sharedassets) EditorGUILayout.BeginHorizontal(); bundlePath = EditorGUILayout.TextField("리소스 번들", bundlePath); if (GUILayout.Button("찾기", GUILayout.Width(50))) { string selectedPath = EditorUtility.OpenFilePanel("리소스 번들 선택 (sharedassets)", "", "bin"); if (!string.IsNullOrEmpty(selectedPath)) { bundlePath = selectedPath; AnalyzeFileHeader(); AutoDetectSceneBundle(); } } EditorGUILayout.EndHorizontal(); // 씬 번들 (sceneassets) EditorGUILayout.BeginHorizontal(); sceneBundlePath = EditorGUILayout.TextField("씬 번들", sceneBundlePath); if (GUILayout.Button("찾기", GUILayout.Width(50))) { string selectedPath = EditorUtility.OpenFilePanel("씬 번들 선택 (sceneassets)", "", "bin"); if (!string.IsNullOrEmpty(selectedPath)) { sceneBundlePath = selectedPath; } } EditorGUILayout.EndHorizontal(); // 드래그 앤 드롭 지원 var dropArea = GUILayoutUtility.GetLastRect(); var evt = Event.current; if (evt.type == EventType.DragUpdated || evt.type == EventType.DragPerform) { if (dropArea.Contains(evt.mousePosition)) { DragAndDrop.visualMode = DragAndDropVisualMode.Copy; if (evt.type == EventType.DragPerform) { DragAndDrop.AcceptDrag(); if (DragAndDrop.paths.Length > 0) { bundlePath = DragAndDrop.paths[0]; AnalyzeFileHeader(); } } evt.Use(); } } // 고급 옵션 showAdvanced = EditorGUILayout.Foldout(showAdvanced, "고급 옵션"); if (showAdvanced) { EditorGUI.indentLevel++; headerOffset = EditorGUILayout.IntField("헤더 오프셋 (bytes)", headerOffset); if (!string.IsNullOrEmpty(bundlePath) && File.Exists(bundlePath)) { if (GUILayout.Button("파일 헤더 분석")) { AnalyzeFileHeader(); } } if (!string.IsNullOrEmpty(fileHeaderInfo)) { EditorGUILayout.HelpBox(fileHeaderInfo, MessageType.None); } EditorGUI.indentLevel--; } } private void DrawLoadUnloadButtons() { EditorGUILayout.BeginHorizontal(); GUI.enabled = loadedBundle == null && !string.IsNullOrEmpty(bundlePath); if (GUILayout.Button("번들 로드", GUILayout.Height(30))) { LoadBundle(); } GUI.enabled = loadedBundle != null; if (GUILayout.Button("번들 언로드", GUILayout.Height(30))) { UnloadBundle(); } GUI.enabled = true; EditorGUILayout.EndHorizontal(); } private void DrawBundleInfo() { // 씬 경로 확인 (리소스 번들 + 씬 번들) List allScenePaths = new List(); string[] resourceScenes = loadedBundle.GetAllScenePaths(); if (resourceScenes != null) allScenePaths.AddRange(resourceScenes); if (sceneBundle != null) { string[] sceneScenes = sceneBundle.GetAllScenePaths(); if (sceneScenes != null) allScenePaths.AddRange(sceneScenes); } var sb = new StringBuilder(); sb.AppendLine($"리소스 번들: {Path.GetFileName(bundlePath)}"); sb.AppendLine($"에셋 개수: {(assetNames != null ? assetNames.Length : 0)}개"); if (sceneBundle != null) { sb.AppendLine($"씬 번들: {Path.GetFileName(sceneBundlePath)} (로드됨)"); } if (allScenePaths.Count > 0) { sb.AppendLine($"씬 개수: {allScenePaths.Count}개"); } EditorGUILayout.HelpBox(sb.ToString().TrimEnd(), MessageType.Info); // 씬이 있으면 씬 로드 버튼 표시 if (allScenePaths.Count > 0) { GUILayout.Label("씬 목록", EditorStyles.boldLabel); foreach (var scenePath in allScenePaths) { EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField(scenePath); if (GUILayout.Button("씬 로드", GUILayout.Width(70))) { LoadSceneFromBundle(scenePath); } EditorGUILayout.EndHorizontal(); } GUILayout.Space(5); } // 빠른 액션 버튼들 EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("모든 GameObject 씬에 추가", GUILayout.Height(25))) { LoadAllGameObjectsToScene(); } if (GUILayout.Button("GLB/Prefab만 추가", GUILayout.Height(25))) { LoadMainAssetsToScene(); } EditorGUILayout.EndHorizontal(); } private void AutoDetectSceneBundle() { if (string.IsNullOrEmpty(bundlePath)) return; string dir = Path.GetDirectoryName(bundlePath); string baseName = Path.GetFileNameWithoutExtension(bundlePath); // sharedassets -> sceneassets 자동 매핑 if (baseName.ToLower().Contains("sharedassets")) { string sceneFile = Path.Combine(dir, "sceneassets.bin"); if (File.Exists(sceneFile)) { sceneBundlePath = sceneFile; Debug.Log($"[AssetBundle Loader] 씬 번들 자동 감지: {sceneBundlePath}"); } } else if (baseName.ToLower().Contains("sceneassets")) { // 반대로 sceneassets를 선택한 경우 string sharedFile = Path.Combine(dir, "sharedassets.bin"); if (File.Exists(sharedFile)) { sceneBundlePath = bundlePath; bundlePath = sharedFile; Debug.Log($"[AssetBundle Loader] 리소스/씬 번들 자동 스왑"); } } } private void LoadWarudoFileOneClick(string warudoPath) { Debug.Log($"[AssetBundle Loader] Warudo 파일 원클릭 로드: {warudoPath}"); // 기존 번들 언로드 UnloadBundle(); try { // 파일 읽기 byte[] fileData = File.ReadAllBytes(warudoPath); // UMOD 헤더 확인 if (fileData.Length < 16 || Encoding.ASCII.GetString(fileData, 0, 4) != "UMOD") { EditorUtility.DisplayDialog("오류", "올바른 Warudo 파일이 아닙니다.", "확인"); return; } // ZIP 시그니처 찾기 byte[] pkSig = { 0x50, 0x4B, 0x03, 0x04 }; int zipOffset = -1; for (int i = 0; i < System.Math.Min(64, fileData.Length - 4); i++) { if (fileData[i] == pkSig[0] && fileData[i + 1] == pkSig[1] && fileData[i + 2] == pkSig[2] && fileData[i + 3] == pkSig[3]) { zipOffset = i; break; } } if (zipOffset < 0) { EditorUtility.DisplayDialog("오류", "ZIP 데이터를 찾을 수 없습니다.", "확인"); return; } // 임시 폴더 생성 tempExtractPath = Path.Combine(Path.GetTempPath(), "WarudoExtract_" + Path.GetFileNameWithoutExtension(warudoPath) + "_" + System.Guid.NewGuid().ToString("N").Substring(0, 6)); Directory.CreateDirectory(tempExtractPath); // ZIP 추출 using (var zipStream = new MemoryStream(fileData, zipOffset, fileData.Length - zipOffset)) using (var archive = new ZipArchive(zipStream, ZipArchiveMode.Read)) { Debug.Log($"[AssetBundle Loader] ZIP 엔트리 {archive.Entries.Count}개 발견"); foreach (var entry in archive.Entries) { if (string.IsNullOrEmpty(entry.Name)) continue; string destPath = Path.Combine(tempExtractPath, entry.FullName); Directory.CreateDirectory(Path.GetDirectoryName(destPath)); entry.ExtractToFile(destPath, true); Debug.Log($" - 추출: {entry.FullName} ({entry.Length:N0} bytes)"); } } // sharedassets.bin, sceneassets.bin 찾기 string sharedAssetsPath = Path.Combine(tempExtractPath, "sharedassets.bin"); string sceneAssetsPath = Path.Combine(tempExtractPath, "sceneassets.bin"); if (!File.Exists(sharedAssetsPath)) { // 다른 이름의 번들 파일 찾기 foreach (var file in Directory.GetFiles(tempExtractPath, "*.bin")) { byte[] header = new byte[16]; using (var fs = new FileStream(file, FileMode.Open, FileAccess.Read)) { fs.Read(header, 0, header.Length); } string sig = Encoding.ASCII.GetString(header, 0, 7); if (sig == "UnityFS") { if (Path.GetFileName(file).ToLower().Contains("scene")) sceneAssetsPath = file; else sharedAssetsPath = file; } } } // 번들 경로 설정 if (File.Exists(sharedAssetsPath)) { bundlePath = sharedAssetsPath; } if (File.Exists(sceneAssetsPath)) { sceneBundlePath = sceneAssetsPath; } Debug.Log($"[AssetBundle Loader] 리소스 번들: {bundlePath}"); Debug.Log($"[AssetBundle Loader] 씬 번들: {sceneBundlePath}"); // 번들 로드 if (!string.IsNullOrEmpty(bundlePath) && File.Exists(bundlePath)) { LoadBundle(); } else { EditorUtility.DisplayDialog("오류", "AssetBundle을 찾을 수 없습니다.", "확인"); } } catch (System.Exception e) { EditorUtility.DisplayDialog("오류", $"Warudo 파일 로드 실패:\n{e.Message}", "확인"); Debug.LogError($"[AssetBundle Loader] 오류: {e}"); } Repaint(); } private void LoadSceneFromBundle(string scenePath) { try { // Additive로 씬 로드 SceneManager.LoadScene(scenePath, LoadSceneMode.Additive); Debug.Log($"[AssetBundle Loader] 씬 로드 완료: {scenePath}"); // 로드된 씬 처리 EditorApplication.delayCall += () => { // 로드된 씬의 모든 루트 오브젝트 활성화 ActivateLoadedSceneObjects(scenePath); // 라이트 세팅 복사할지 물어보기 if (EditorUtility.DisplayDialog("라이트 세팅", "로드된 씬의 라이트 세팅을 현재 씬에 적용하시겠습니까?\n\n" + "(Skybox, Ambient Light, Fog, Reflection 등)", "적용", "건너뛰기")) { CopyLightingFromLoadedScene(scenePath); } }; } catch (System.Exception e) { Debug.LogError($"[AssetBundle Loader] 씬 로드 실패: {e.Message}"); EditorUtility.DisplayDialog("씬 로드 실패", $"씬을 로드할 수 없습니다.\n\n{e.Message}", "확인"); } } private void ActivateLoadedSceneObjects(string scenePath) { string sceneName = Path.GetFileNameWithoutExtension(scenePath); for (int i = 0; i < SceneManager.sceneCount; i++) { var scene = SceneManager.GetSceneAt(i); if (scene.name == sceneName || scene.path == scenePath) { GameObject[] rootObjects = scene.GetRootGameObjects(); foreach (var obj in rootObjects) { // Environment 오브젝트만 활성화 if (obj.name == "Environment" && !obj.activeSelf) { obj.SetActive(true); Debug.Log($"[AssetBundle Loader] Environment 활성화됨"); } } break; } } } private void CopyLightingFromLoadedScene(string scenePath) { // 로드된 씬 찾기 string sceneName = Path.GetFileNameWithoutExtension(scenePath); Scene loadedScene = default; for (int i = 0; i < SceneManager.sceneCount; i++) { var scene = SceneManager.GetSceneAt(i); if (scene.name == sceneName || scene.path == scenePath) { loadedScene = scene; break; } } if (!loadedScene.IsValid()) { Debug.LogWarning("[AssetBundle Loader] 로드된 씬을 찾을 수 없습니다."); return; } // 활성 씬 임시 변경해서 RenderSettings 복사 Scene originalActiveScene = SceneManager.GetActiveScene(); SceneManager.SetActiveScene(loadedScene); // 현재 RenderSettings 저장 var settings = new LightingSettingsData(); settings.CaptureFromRenderSettings(); // 원래 씬으로 복원 SceneManager.SetActiveScene(originalActiveScene); // 저장한 설정 적용 settings.ApplyToRenderSettings(); Debug.Log("[AssetBundle Loader] 라이트 세팅 복사 완료!"); Debug.Log($" - Skybox: {RenderSettings.skybox?.name ?? "None"}"); Debug.Log($" - Ambient Mode: {RenderSettings.ambientMode}"); Debug.Log($" - Ambient Color: {RenderSettings.ambientLight}"); Debug.Log($" - Fog: {RenderSettings.fog}"); } // 라이트 세팅 데이터 저장용 클래스 private class LightingSettingsData { // Skybox public Material skybox; // Ambient public AmbientMode ambientMode; public Color ambientLight; public Color ambientSkyColor; public Color ambientEquatorColor; public Color ambientGroundColor; public float ambientIntensity; // Fog public bool fog; public FogMode fogMode; public Color fogColor; public float fogDensity; public float fogStartDistance; public float fogEndDistance; // Reflection public DefaultReflectionMode defaultReflectionMode; public int defaultReflectionResolution; public Cubemap customReflection; public float reflectionIntensity; public int reflectionBounces; // Halo/Flare public float haloStrength; public float flareFadeSpeed; public float flareStrength; // Sun public Light sun; public void CaptureFromRenderSettings() { // Skybox skybox = RenderSettings.skybox; // Ambient ambientMode = RenderSettings.ambientMode; ambientLight = RenderSettings.ambientLight; ambientSkyColor = RenderSettings.ambientSkyColor; ambientEquatorColor = RenderSettings.ambientEquatorColor; ambientGroundColor = RenderSettings.ambientGroundColor; ambientIntensity = RenderSettings.ambientIntensity; // Fog fog = RenderSettings.fog; fogMode = RenderSettings.fogMode; fogColor = RenderSettings.fogColor; fogDensity = RenderSettings.fogDensity; fogStartDistance = RenderSettings.fogStartDistance; fogEndDistance = RenderSettings.fogEndDistance; // Reflection defaultReflectionMode = RenderSettings.defaultReflectionMode; defaultReflectionResolution = RenderSettings.defaultReflectionResolution; customReflection = RenderSettings.customReflection; reflectionIntensity = RenderSettings.reflectionIntensity; reflectionBounces = RenderSettings.reflectionBounces; // Halo/Flare haloStrength = RenderSettings.haloStrength; flareFadeSpeed = RenderSettings.flareFadeSpeed; flareStrength = RenderSettings.flareStrength; // Sun sun = RenderSettings.sun; } public void ApplyToRenderSettings() { // Skybox RenderSettings.skybox = skybox; // Ambient RenderSettings.ambientMode = ambientMode; RenderSettings.ambientLight = ambientLight; RenderSettings.ambientSkyColor = ambientSkyColor; RenderSettings.ambientEquatorColor = ambientEquatorColor; RenderSettings.ambientGroundColor = ambientGroundColor; RenderSettings.ambientIntensity = ambientIntensity; // Fog RenderSettings.fog = fog; RenderSettings.fogMode = fogMode; RenderSettings.fogColor = fogColor; RenderSettings.fogDensity = fogDensity; RenderSettings.fogStartDistance = fogStartDistance; RenderSettings.fogEndDistance = fogEndDistance; // Reflection RenderSettings.defaultReflectionMode = defaultReflectionMode; RenderSettings.defaultReflectionResolution = defaultReflectionResolution; RenderSettings.customReflection = customReflection; RenderSettings.reflectionIntensity = reflectionIntensity; RenderSettings.reflectionBounces = reflectionBounces; // Halo/Flare RenderSettings.haloStrength = haloStrength; RenderSettings.flareFadeSpeed = flareFadeSpeed; RenderSettings.flareStrength = flareStrength; // Sun RenderSettings.sun = sun; } } private void LoadAllGameObjectsToScene() { if (loadedBundle == null || assetNames == null) return; int count = 0; GameObject rootParent = new GameObject($"[Bundle] {Path.GetFileNameWithoutExtension(bundlePath)}"); Undo.RegisterCreatedObjectUndo(rootParent, "Load All Assets from Bundle"); foreach (var assetName in assetNames) { try { var asset = loadedBundle.LoadAsset(assetName); if (asset != null) { var instance = Instantiate(asset); instance.name = asset.name; instance.transform.SetParent(rootParent.transform); Undo.RegisterCreatedObjectUndo(instance, "Load Asset from Bundle"); count++; } } catch { } } Selection.activeGameObject = rootParent; Debug.Log($"[AssetBundle Loader] {count}개 GameObject 씬에 추가됨"); } private void LoadMainAssetsToScene() { if (loadedBundle == null || assetNames == null) return; int count = 0; GameObject rootParent = new GameObject($"[Bundle] {Path.GetFileNameWithoutExtension(bundlePath)}"); Undo.RegisterCreatedObjectUndo(rootParent, "Load Main Assets from Bundle"); foreach (var assetName in assetNames) { // GLB, prefab 파일만 필터링 string ext = Path.GetExtension(assetName).ToLower(); if (ext != ".glb" && ext != ".gltf" && ext != ".prefab" && ext != ".fbx") continue; // 하위 머티리얼/텍스처 제외 if (assetName.Contains(".materials/") || assetName.Contains(".textures/")) continue; try { var asset = loadedBundle.LoadAsset(assetName); if (asset != null) { var instance = Instantiate(asset); instance.name = asset.name; instance.transform.SetParent(rootParent.transform); Undo.RegisterCreatedObjectUndo(instance, "Load Asset from Bundle"); count++; Debug.Log($"[AssetBundle Loader] 로드: {assetName}"); } } catch { } } Selection.activeGameObject = rootParent; Debug.Log($"[AssetBundle Loader] {count}개 메인 에셋 씬에 추가됨"); } private void DrawSearchFilter() { EditorGUILayout.BeginHorizontal(); GUILayout.Label("검색:", GUILayout.Width(40)); searchFilter = EditorGUILayout.TextField(searchFilter); if (GUILayout.Button("X", GUILayout.Width(20))) { searchFilter = ""; GUI.FocusControl(null); } EditorGUILayout.EndHorizontal(); } private void DrawAssetList() { GUILayout.Label("에셋 목록", EditorStyles.boldLabel); scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); if (assetNames != null) { foreach (var assetName in assetNames) { // 검색 필터 적용 if (!string.IsNullOrEmpty(searchFilter) && !assetName.ToLower().Contains(searchFilter.ToLower())) { continue; } DrawAssetItem(assetName); } } EditorGUILayout.EndScrollView(); } private void DrawAssetItem(string assetName) { EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.BeginHorizontal(); // 폴드아웃 토글 if (!assetFoldouts.ContainsKey(assetName)) assetFoldouts[assetName] = false; assetFoldouts[assetName] = EditorGUILayout.Foldout(assetFoldouts[assetName], assetName, true); // 로드 버튼 bool isLoaded = loadedAssets.ContainsKey(assetName) && loadedAssets[assetName] != null; if (GUILayout.Button(isLoaded ? "로드됨" : "로드", GUILayout.Width(60))) { if (!isLoaded) { LoadAsset(assetName); } } // 씬에 추가 버튼 (GameObject인 경우) if (isLoaded && loadedAssets[assetName] is GameObject) { if (GUILayout.Button("씬에 추가", GUILayout.Width(70))) { InstantiateAsset(assetName); } } EditorGUILayout.EndHorizontal(); // 폴드아웃 내용 if (assetFoldouts[assetName] && isLoaded) { EditorGUI.indentLevel++; var asset = loadedAssets[assetName]; EditorGUILayout.ObjectField("에셋", asset, asset.GetType(), false); EditorGUILayout.LabelField("타입", asset.GetType().Name); EditorGUI.indentLevel--; } EditorGUILayout.EndVertical(); } private void AnalyzeFileHeader() { if (!File.Exists(bundlePath)) { fileHeaderInfo = "파일을 찾을 수 없습니다."; return; } try { byte[] headerBytes = new byte[256]; using (var fs = new FileStream(bundlePath, FileMode.Open, FileAccess.Read)) { fs.Read(headerBytes, 0, headerBytes.Length); } var sb = new StringBuilder(); sb.AppendLine($"파일 크기: {new FileInfo(bundlePath).Length:N0} bytes"); // 첫 32바이트를 16진수로 표시 sb.Append("Header (hex): "); for (int i = 0; i < 32 && i < headerBytes.Length; i++) { sb.Append(headerBytes[i].ToString("X2") + " "); } sb.AppendLine(); // ASCII로 표시 (출력 가능한 문자만) sb.Append("Header (ascii): "); for (int i = 0; i < 32 && i < headerBytes.Length; i++) { char c = (char)headerBytes[i]; sb.Append(char.IsControl(c) ? '.' : c); } sb.AppendLine(); // UMod/Warudo 포맷 체크 (UMOD 헤더 + PK ZIP) isWarudoFormat = CheckWarudoFormat(headerBytes); if (isWarudoFormat) { sb.AppendLine("포맷: UMod/Warudo (.warudo)"); sb.AppendLine("ZIP 압축된 AssetBundle 컨테이너입니다."); // ZIP 내용물 미리보기 try { zipEntries.Clear(); int zipOffset = FindZipSignature(headerBytes); if (zipOffset >= 0) { sb.AppendLine($"ZIP 시작 위치: offset {zipOffset}"); using (var fs = new FileStream(bundlePath, FileMode.Open, FileAccess.Read)) { fs.Seek(zipOffset, SeekOrigin.Begin); using (var zipStream = new MemoryStream()) { fs.CopyTo(zipStream); zipStream.Seek(0, SeekOrigin.Begin); using (var archive = new ZipArchive(zipStream, ZipArchiveMode.Read)) { sb.AppendLine($"ZIP 엔트리 ({archive.Entries.Count}개):"); foreach (var entry in archive.Entries) { zipEntries.Add(entry.FullName); sb.AppendLine($" - {entry.FullName} ({entry.Length:N0} bytes)"); } } } } } } catch (System.Exception ex) { sb.AppendLine($"ZIP 분석 오류: {ex.Message}"); } } else { // Unity AssetBundle 시그니처 검색 int signatureOffset = FindUnitySignature(headerBytes); if (signatureOffset >= 0) { sb.AppendLine($"Unity 시그니처 발견 위치: offset {signatureOffset}"); headerOffset = signatureOffset; } else { // 파일 전체에서 검색 (처음 64KB만) byte[] searchBytes = new byte[65536]; using (var fs = new FileStream(bundlePath, FileMode.Open, FileAccess.Read)) { int bytesRead = fs.Read(searchBytes, 0, searchBytes.Length); signatureOffset = FindUnitySignature(searchBytes, bytesRead); if (signatureOffset >= 0) { sb.AppendLine($"Unity 시그니처 발견 위치: offset {signatureOffset}"); headerOffset = signatureOffset; } else { sb.AppendLine("Unity AssetBundle 시그니처를 찾을 수 없습니다."); sb.AppendLine("이 파일은 암호화되었거나 다른 포맷일 수 있습니다."); } } } } fileHeaderInfo = sb.ToString(); } catch (System.Exception e) { fileHeaderInfo = $"헤더 분석 오류: {e.Message}"; } Repaint(); } private bool CheckWarudoFormat(byte[] header) { // UMOD 시그니처 체크 if (header.Length >= 4) { string sig = Encoding.ASCII.GetString(header, 0, 4); if (sig == "UMOD") return true; } return false; } private int FindZipSignature(byte[] data) { // PK 시그니처 (0x50, 0x4B, 0x03, 0x04) byte[] pkSig = { 0x50, 0x4B, 0x03, 0x04 }; for (int i = 0; i <= data.Length - pkSig.Length; i++) { bool match = true; for (int j = 0; j < pkSig.Length; j++) { if (data[i + j] != pkSig[j]) { match = false; break; } } if (match) return i; } return -1; } private int FindUnitySignature(byte[] data, int length = -1) { if (length < 0) length = data.Length; // Unity AssetBundle 시그니처: "UnityFS", "UnityRaw", "UnityWeb" string[] signatures = { "UnityFS", "UnityRaw", "UnityWeb" }; foreach (var sig in signatures) { byte[] sigBytes = Encoding.ASCII.GetBytes(sig); for (int i = 0; i <= length - sigBytes.Length; i++) { bool match = true; for (int j = 0; j < sigBytes.Length; j++) { if (data[i + j] != sigBytes[j]) { match = false; break; } } if (match) return i; } } return -1; } private void LoadBundle() { if (!File.Exists(bundlePath)) { EditorUtility.DisplayDialog("오류", "파일을 찾을 수 없습니다: " + bundlePath, "확인"); return; } // 먼저 헤더 분석 if (string.IsNullOrEmpty(fileHeaderInfo)) { AnalyzeFileHeader(); } try { // UMod/Warudo 포맷인 경우 ZIP에서 추출 if (isWarudoFormat) { LoadWarudoBundle(); return; } // 방법 1: 직접 로드 if (headerOffset == 0) { loadedBundle = AssetBundle.LoadFromFile(bundlePath); } // 방법 2: 오프셋이 있으면 메모리에서 로드 if (loadedBundle == null && headerOffset > 0) { Debug.Log($"[AssetBundle Loader] 오프셋 {headerOffset}에서 로드 시도..."); byte[] allBytes = File.ReadAllBytes(bundlePath); int dataLength = allBytes.Length - headerOffset; byte[] bundleBytes = new byte[dataLength]; System.Array.Copy(allBytes, headerOffset, bundleBytes, 0, dataLength); loadedBundle = AssetBundle.LoadFromMemory(bundleBytes); } // 방법 3: 직접 로드 실패 시 시그니처 자동 검색 if (loadedBundle == null && headerOffset == 0) { Debug.Log("[AssetBundle Loader] 직접 로드 실패, 시그니처 검색 중..."); AnalyzeFileHeader(); if (headerOffset > 0) { byte[] allBytes = File.ReadAllBytes(bundlePath); int dataLength = allBytes.Length - headerOffset; byte[] bundleBytes = new byte[dataLength]; System.Array.Copy(allBytes, headerOffset, bundleBytes, 0, dataLength); loadedBundle = AssetBundle.LoadFromMemory(bundleBytes); } } if (loadedBundle == null) { EditorUtility.DisplayDialog("로드 실패", "AssetBundle 로드 실패.\n\n" + "가능한 원인:\n" + "- 다른 Unity 버전으로 빌드됨\n" + "- 암호화된 번들\n" + "- 지원하지 않는 포맷\n\n" + "'고급 옵션'에서 파일 헤더를 확인하세요.", "확인"); return; } assetNames = loadedBundle.GetAllAssetNames(); loadedAssets.Clear(); assetFoldouts.Clear(); Debug.Log($"[AssetBundle Loader] 리소스 번들 로드 완료: {bundlePath}"); Debug.Log($"[AssetBundle Loader] 에셋 {assetNames.Length}개 발견"); // 씬 번들도 로드 LoadSceneBundleIfExists(); } catch (System.Exception e) { EditorUtility.DisplayDialog("오류", "번들 로드 중 오류 발생: " + e.Message, "확인"); Debug.LogError($"[AssetBundle Loader] 오류: {e}"); } } private void LoadSceneBundleIfExists() { if (string.IsNullOrEmpty(sceneBundlePath) || !File.Exists(sceneBundlePath)) return; try { sceneBundle = AssetBundle.LoadFromFile(sceneBundlePath); if (sceneBundle != null) { string[] scenePaths = sceneBundle.GetAllScenePaths(); Debug.Log($"[AssetBundle Loader] 씬 번들 로드 완료: {sceneBundlePath}"); Debug.Log($"[AssetBundle Loader] 씬 {scenePaths.Length}개 발견"); foreach (var path in scenePaths) { Debug.Log($" - {path}"); } } } catch (System.Exception e) { Debug.LogWarning($"[AssetBundle Loader] 씬 번들 로드 실패: {e.Message}"); } } private void LoadWarudoBundle() { Debug.Log("[AssetBundle Loader] UMod/Warudo 포맷 감지, ZIP에서 추출 중..."); try { // UMOD 헤더 뒤의 ZIP 오프셋 찾기 byte[] headerBytes = new byte[64]; using (var fs = new FileStream(bundlePath, FileMode.Open, FileAccess.Read)) { fs.Read(headerBytes, 0, headerBytes.Length); } int zipOffset = FindZipSignature(headerBytes); if (zipOffset < 0) { EditorUtility.DisplayDialog("오류", "ZIP 시그니처를 찾을 수 없습니다.", "확인"); return; } // 임시 폴더에 추출 tempExtractPath = Path.Combine(Path.GetTempPath(), "WarudoExtract_" + System.Guid.NewGuid().ToString("N").Substring(0, 8)); Directory.CreateDirectory(tempExtractPath); using (var fs = new FileStream(bundlePath, FileMode.Open, FileAccess.Read)) { fs.Seek(zipOffset, SeekOrigin.Begin); using (var zipStream = new MemoryStream()) { fs.CopyTo(zipStream); zipStream.Seek(0, SeekOrigin.Begin); using (var archive = new ZipArchive(zipStream, ZipArchiveMode.Read)) { string assetBundleEntry = null; foreach (var entry in archive.Entries) { string destPath = Path.Combine(tempExtractPath, entry.FullName); // 디렉토리면 생성 if (string.IsNullOrEmpty(entry.Name)) { Directory.CreateDirectory(destPath); continue; } // 파일 추출 Directory.CreateDirectory(Path.GetDirectoryName(destPath)); entry.ExtractToFile(destPath, true); Debug.Log($"[AssetBundle Loader] 추출: {entry.FullName}"); // AssetBundle 찾기 (modinfo.dat, .dll 제외) string ext = Path.GetExtension(entry.Name).ToLower(); if (ext != ".dat" && ext != ".dll" && ext != ".json" && ext != ".xml" && ext != ".txt") { // Unity 시그니처 확인 if (File.Exists(destPath)) { byte[] firstBytes = new byte[16]; using (var checkFs = new FileStream(destPath, FileMode.Open, FileAccess.Read)) { checkFs.Read(firstBytes, 0, firstBytes.Length); } if (FindUnitySignature(firstBytes) >= 0) { assetBundleEntry = destPath; } } } } // AssetBundle 로드 if (!string.IsNullOrEmpty(assetBundleEntry)) { Debug.Log($"[AssetBundle Loader] AssetBundle 발견: {assetBundleEntry}"); loadedBundle = AssetBundle.LoadFromFile(assetBundleEntry); if (loadedBundle != null) { assetNames = loadedBundle.GetAllAssetNames(); loadedAssets.Clear(); assetFoldouts.Clear(); Debug.Log($"[AssetBundle Loader] Warudo 번들 로드 완료!"); Debug.Log($"[AssetBundle Loader] 에셋 {assetNames.Length}개 발견"); } else { EditorUtility.DisplayDialog("로드 실패", "AssetBundle 로드 실패.\n\n" + "이 번들은 다른 Unity 버전(2021.3.18f1)으로 빌드되었습니다.\n" + "현재 프로젝트의 Unity 버전과 호환되지 않을 수 있습니다.", "확인"); } } else { EditorUtility.DisplayDialog("오류", "ZIP 내에서 AssetBundle을 찾을 수 없습니다.\n\n" + "추출된 파일들:\n" + string.Join("\n", zipEntries), "확인"); } } } } } catch (System.Exception e) { EditorUtility.DisplayDialog("오류", $"Warudo 번들 로드 오류:\n{e.Message}", "확인"); Debug.LogError($"[AssetBundle Loader] Warudo 로드 오류: {e}"); } } private void UnloadBundle() { // 씬 번들 먼저 언로드 if (sceneBundle != null) { sceneBundle.Unload(true); sceneBundle = null; Debug.Log("[AssetBundle Loader] 씬 번들 언로드 완료"); } // 리소스 번들 언로드 if (loadedBundle != null) { loadedBundle.Unload(true); loadedBundle = null; assetNames = null; loadedAssets.Clear(); assetFoldouts.Clear(); Debug.Log("[AssetBundle Loader] 리소스 번들 언로드 완료"); } // 추가 번들들 언로드 foreach (var bundle in additionalBundles) { if (bundle != null) bundle.Unload(true); } additionalBundles.Clear(); // 임시 추출 폴더 정리 if (!string.IsNullOrEmpty(tempExtractPath) && Directory.Exists(tempExtractPath)) { try { Directory.Delete(tempExtractPath, true); Debug.Log("[AssetBundle Loader] 임시 폴더 정리 완료"); } catch { } tempExtractPath = ""; } isWarudoFormat = false; zipEntries.Clear(); fileHeaderInfo = ""; } private void LoadAsset(string assetName) { if (loadedBundle == null) return; try { var asset = loadedBundle.LoadAsset(assetName); if (asset != null) { loadedAssets[assetName] = asset; Debug.Log($"[AssetBundle Loader] 에셋 로드: {assetName} ({asset.GetType().Name})"); } } catch (System.Exception e) { Debug.LogError($"[AssetBundle Loader] 에셋 로드 실패: {assetName}\n{e}"); } } private void InstantiateAsset(string assetName) { if (!loadedAssets.ContainsKey(assetName)) return; var asset = loadedAssets[assetName] as GameObject; if (asset == null) return; var instance = Instantiate(asset); instance.name = asset.name; Undo.RegisterCreatedObjectUndo(instance, "Instantiate Asset from Bundle"); Selection.activeGameObject = instance; Debug.Log($"[AssetBundle Loader] 씬에 추가: {asset.name}"); } private void OnDestroy() { UnloadBundle(); } }