using AssetStudio; using AssetStudioCLI.Options; using System; using System.Collections.Concurrent; using System.IO; using System.Text; namespace AssetStudioCLI { internal static class ParallelExporter { private static readonly ConcurrentDictionary ExportPathDict = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); public static bool ExportTexture2D(AssetItem item, string exportPath, out string debugLog) { debugLog = ""; var m_Texture2D = (Texture2D)item.Asset; if (CLIOptions.convertTexture) { var type = CLIOptions.o_imageFormat.Value; var converter = new Texture2DConverter(m_Texture2D); if (CLIOptions.o_filenameFormat.Value == FilenameFormat.AssetName_Hash) item.Hash = converter.GetTextureHash(); if (!TryExportFile(exportPath, item, $".{type}", out var exportFullPath)) return false; if (CLIOptions.o_logLevel.Value <= LoggerEvent.Debug) { var sb = new StringBuilder(); sb.AppendLine($"Converting {item.TypeString} \"{m_Texture2D.m_Name}\" to {type}.."); sb.AppendLine($"Width: {m_Texture2D.m_Width}"); sb.AppendLine($"Height: {m_Texture2D.m_Height}"); sb.AppendLine($"Format: {m_Texture2D.m_TextureFormat}"); switch (m_Texture2D.m_TextureSettings.m_FilterMode) { case 0: sb.AppendLine("Filter Mode: Point "); break; case 1: sb.AppendLine("Filter Mode: Bilinear "); break; case 2: sb.AppendLine("Filter Mode: Trilinear "); break; } sb.AppendLine($"Anisotropic level: {m_Texture2D.m_TextureSettings.m_Aniso}"); sb.AppendLine($"Mip map bias: {m_Texture2D.m_TextureSettings.m_MipBias}"); switch (m_Texture2D.m_TextureSettings.m_WrapMode) { case 0: sb.AppendLine($"Wrap mode: Repeat"); break; case 1: sb.AppendLine($"Wrap mode: Clamp"); break; } debugLog += sb.ToString(); } if ((type == ImageFormat.Png && m_Texture2D.m_TextureFormat.IsPng()) || (type == ImageFormat.Webp && m_Texture2D.m_TextureFormat.IsWebP())) { File.WriteAllBytes(exportFullPath, m_Texture2D.image_data.GetData()); debugLog += $"{item.TypeString} \"{item.Text}\" exported to \"{exportFullPath}\""; return true; } if (!converter.TryConvertToBgra32Image(flip: true, out var image)) { Logger.Error($"{debugLog}Export error. Failed to convert texture \"{m_Texture2D.m_Name}\" into image"); return false; } using (image) { using (var file = File.OpenWrite(exportFullPath)) { image.WriteToStream(file, type); } debugLog += $"{item.TypeString} \"{item.Text}\" exported to \"{exportFullPath}\""; return true; } } else { if (!TryExportFile(exportPath, item, ".tex", out var exportFullPath)) return false; File.WriteAllBytes(exportFullPath, m_Texture2D.image_data.GetData()); debugLog += $"{item.TypeString} \"{item.Text}\" exported to \"{exportFullPath}\""; return true; } } public static bool ExportSprite(AssetItem item, string exportPath, out string debugLog) { debugLog = ""; var type = CLIOptions.o_imageFormat.Value; var keepSpriteSize = CLIOptions.o_spriteCroppingMode.Value == SpriteCroppingMode.SpriteRect; var converter = new SpriteConverter((Sprite)item.Asset, keepSpriteSize); if (CLIOptions.o_filenameFormat.Value == FilenameFormat.AssetName_Hash) item.Hash = converter.GetSpriteHash(maskMode: SpriteMaskMode.On); if (!TryExportFile(exportPath, item, $".{type}", out var exportFullPath)) return false; if (converter.TryConvertToImage(out var image, maskMode: SpriteMaskMode.On)) { using (image) { using (var file = File.OpenWrite(exportFullPath)) { image.WriteToStream(file, type); } debugLog += $"{item.TypeString} \"{item.Text}\" exported to \"{exportFullPath}\""; return true; } } Logger.Error($"Failed to convert sprite \"{item.Text}\" into image"); return false; } public static bool ExportAudioClip(AssetItem item, string exportPath, out string debugLog) { debugLog = string.Empty; var m_AudioClip = (AudioClip)item.Asset; var audioBuff = BigArrayPool.Shared.Rent(m_AudioClip.m_AudioData.Size); try { string exportFullPath; var dataLen = m_AudioClip.m_AudioData.GetData(audioBuff); if (dataLen <= 0) { Logger.Error($"Export error. \"{item.Text}\": AudioData was not found"); return false; } var isWav = m_AudioClip.m_Type == FMODSoundType.WAV || m_AudioClip.IsLegacyRawWav(); var converter = new AudioClipConverter(m_AudioClip, audioBuff); if (CLIOptions.o_audioFormat.Value != AudioFormat.None && !isWav && converter.IsSupport) { if (!TryExportFile(exportPath, item, ".wav", out exportFullPath)) return false; if (CLIOptions.o_logLevel.Value <= LoggerEvent.Debug) { debugLog += $"Converting {item.TypeString} \"{m_AudioClip.m_Name}\" to wav..\n"; debugLog += GenerateAudioClipInfo(m_AudioClip); } if (!converter.TryConvertToWav(out var buffer, ref debugLog)) { Logger.Error($"{debugLog}Export error. \"{item.Text}\": Failed to convert audio to wav"); return false; } File.WriteAllBytes(exportFullPath, buffer); } else { if (!TryExportFile(exportPath, item, converter.GetExtensionName(), out exportFullPath)) return false; if (CLIOptions.o_logLevel.Value <= LoggerEvent.Debug) { debugLog += $"Exporting {item.TypeString} \"{m_AudioClip.m_Name}\"..\n"; debugLog += GenerateAudioClipInfo(m_AudioClip); } using (var file = File.OpenWrite(exportFullPath)) { if (m_AudioClip.IsLegacyRawWav()) { if (m_AudioClip.m_AudioData.Endian == EndianType.BigEndian) { AudioClipConverter.LegacyRawWavSwapEndian(audioBuff, m_AudioClip.m_AudioData.Size, (LegacyAudioType)m_AudioClip.m_Format); } var wavHeaderBuff = AudioClipConverter.LegacyRawWavGenerateHeader(m_AudioClip, ref debugLog); file.Write(wavHeaderBuff, 0, wavHeaderBuff.Length); } file.Write(audioBuff, 0, m_AudioClip.m_AudioData.Size); } } debugLog += $"{item.TypeString} \"{item.Text}\" exported to \"{exportFullPath}\""; return true; } finally { BigArrayPool.Shared.Return(audioBuff, clearArray: true); } } private static string GenerateAudioClipInfo(AudioClip m_AudioClip) { var sb = new StringBuilder(); sb.AppendLine($"AudioClip compression format: {m_AudioClip.GetCompressionFormatString()}"); if (m_AudioClip.m_Length > 0) sb.AppendLine($"AudioClip length: {m_AudioClip.m_Length:0.0##}"); if (m_AudioClip.m_Frequency > 0) sb.AppendLine($"AudioClip sample rate: {m_AudioClip.m_Frequency}"); if (m_AudioClip.m_Channels > 0) sb.AppendLine($"AudioClip channel count: {m_AudioClip.m_Channels}"); if (m_AudioClip.version >= 5) sb.AppendLine($"AudioClip bit depth: {m_AudioClip.m_BitsPerSample}"); return sb.ToString(); } private static bool TryExportFile(string dir, AssetItem item, string extension, out string fullPath) { var fileName = item.Text; var filenameFormat = CLIOptions.o_filenameFormat.Value; var canOverwrite = CLIOptions.f_overwriteExisting.Value; switch (filenameFormat) { case FilenameFormat.AssetName_PathID: fileName = $"{fileName} @{item.m_PathID}"; break; case FilenameFormat.PathID: fileName = item.m_PathID.ToString(); break; case FilenameFormat.AssetName_Hash when item.Hash > 0: fileName = $"{fileName} @{item.Hash:x}"; break; } extension = extension.ToLower(); fullPath = Exporter.SafeFilePath(dir, $"{fileName}{extension}"); if (ExportPathDict.TryAdd(fullPath, true)) { if (CanWrite(fullPath, dir, canOverwrite)) { return true; } } else if (filenameFormat == FilenameFormat.AssetName || (filenameFormat == FilenameFormat.AssetName_Hash && item.Hash == 0)) { fullPath = Exporter.SafeFilePath(dir, $"{fileName}{item.UniqueID}{extension}"); if (CanWrite(fullPath, dir, canOverwrite)) { return true; } } Logger.Error($"Export error. File \"{fullPath.Color(ColorConsole.BrightRed)}\" already exist"); return false; } private static bool CanWrite(string fullPath, string dir, bool canOverwrite) { if (!canOverwrite && File.Exists(fullPath)) return false; Directory.CreateDirectory(dir); return true; } public static bool ParallelExportConvertFile(AssetItem item, string exportPath, out string debugLog) { switch (item.Type) { case ClassIDType.Texture2D: case ClassIDType.Texture2DArrayImage: return ExportTexture2D(item, exportPath, out debugLog); case ClassIDType.Sprite: return ExportSprite(item, exportPath, out debugLog); case ClassIDType.AudioClip: return ExportAudioClip(item, exportPath, out debugLog); default: throw new NotImplementedException(); } } public static void ClearHash() { ExportPathDict.Clear(); } } }