Streamingle_URP/Assets/Scripts/Editor/Utilities/AssetBundleLoaderWindow.cs
user 4a49ecd772 Refactor: 배경/프랍 브라우저 IMGUI→UI Toolkit 전환 + USS 리디자인
- BackgroundSceneLoaderWindow: OnGUI → CreateGUI (Toolbar + ToolbarSearchField)
- PropBrowserWindow: OnGUI → CreateGUI (Toolbar + ToolbarSearchField)
- StreamingleCommon.uss: 브라우저 공통 스타일 추가 (그리드/리스트/뷰토글/액션바/상태바)
- excludeFromWeb 상태 새로고침 시 보존 수정
- 삭제된 배경 리소스 정리

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 01:55:48 +09:00

1231 lines
43 KiB
C#

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<string, Object> loadedAssets = new Dictionary<string, Object>();
private Dictionary<string, bool> assetFoldouts = new Dictionary<string, bool>();
private string searchFilter = "";
private string fileHeaderInfo = "";
private int headerOffset = 0;
private bool showAdvanced = false;
private string tempExtractPath = "";
private List<string> zipEntries = new List<string>();
private bool isWarudoFormat = false;
// 멀티 번들 지원
private List<AssetBundle> additionalBundles = new List<AssetBundle>();
private string sceneBundlePath = "";
private AssetBundle sceneBundle;
[MenuItem("Tools/Utilities/AssetBundle Loader")]
public static void ShowWindow()
{
var window = GetWindow<AssetBundleLoaderWindow>("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<string> allScenePaths = new List<string>();
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<GameObject>(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<GameObject>(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();
}
}