mirror of
https://github.com/aelurum/AssetStudio.git
synced 2025-07-18 03:24:15 -04:00
Add parallel export support for some asset types
This commit is contained in:
@ -1,9 +1,10 @@
|
||||
using AssetStudio;
|
||||
using AssetStudioCLI.Options;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AssetStudioCLI
|
||||
{
|
||||
@ -16,21 +17,29 @@ namespace AssetStudioCLI
|
||||
|
||||
internal class CLILogger : ILogger
|
||||
{
|
||||
private readonly LogOutputMode logOutput;
|
||||
private readonly LoggerEvent logMinLevel;
|
||||
public string LogName;
|
||||
public string LogPath;
|
||||
|
||||
private static BlockingCollection<string> logMessageCollection = new BlockingCollection<string>();
|
||||
private readonly LogOutputMode logOutput;
|
||||
private readonly LoggerEvent logMinLevel;
|
||||
|
||||
public CLILogger()
|
||||
{
|
||||
logOutput = CLIOptions.o_logOutput.Value;
|
||||
logMinLevel = CLIOptions.o_logLevel.Value;
|
||||
|
||||
var appAssembly = typeof(Program).Assembly.GetName();
|
||||
var arch = Environment.Is64BitProcess ? "x64" : "x32";
|
||||
LogName = $"{appAssembly.Name}_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.log";
|
||||
LogPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, LogName);
|
||||
var arch = Environment.Is64BitProcess ? "x64" : "x32";
|
||||
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
||||
|
||||
if (logOutput != LogOutputMode.Console)
|
||||
{
|
||||
ConcurrentFileWriter();
|
||||
}
|
||||
|
||||
LogToFile(LoggerEvent.Verbose, $"---{appAssembly.Name} v{appAssembly.Version} [{arch}] | Logger launched---\n" +
|
||||
$"CMD Args: {string.Join(" ", CLIOptions.cliArgs)}");
|
||||
}
|
||||
@ -55,7 +64,7 @@ namespace AssetStudioCLI
|
||||
{
|
||||
var curTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
message = message.TrimEnd();
|
||||
var multiLine = message.Contains('\n');
|
||||
var multiLine = message.Contains("\n");
|
||||
|
||||
string formattedMessage;
|
||||
if (consoleMode)
|
||||
@ -64,7 +73,7 @@ namespace AssetStudioCLI
|
||||
formattedMessage = $"{colorLogLevel} {message}";
|
||||
if (multiLine)
|
||||
{
|
||||
formattedMessage = formattedMessage.Replace("\n", $"\n{colorLogLevel} ");
|
||||
formattedMessage = formattedMessage.Replace("\n", $"\n{colorLogLevel} ") + $"\n{colorLogLevel}";
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -74,7 +83,7 @@ namespace AssetStudioCLI
|
||||
formattedMessage = $"{curTime} | {logLevel} | {message}";
|
||||
if (multiLine)
|
||||
{
|
||||
formattedMessage = formattedMessage.Replace("\n", $"\n{curTime} | {logLevel} | ");
|
||||
formattedMessage = formattedMessage.Replace("\n", $"\n{curTime} | {logLevel} | ") + $"\n{curTime} | {logLevel} |";
|
||||
}
|
||||
}
|
||||
return formattedMessage;
|
||||
@ -88,15 +97,27 @@ namespace AssetStudioCLI
|
||||
}
|
||||
}
|
||||
|
||||
public async void LogToFile(LoggerEvent logMsgLevel, string message)
|
||||
public void LogToFile(LoggerEvent logMsgLevel, string message)
|
||||
{
|
||||
if (logOutput != LogOutputMode.Console)
|
||||
{
|
||||
logMessageCollection.Add(FormatMessage(logMsgLevel, message));
|
||||
}
|
||||
}
|
||||
|
||||
private void ConcurrentFileWriter()
|
||||
{
|
||||
Task.Run(() =>
|
||||
{
|
||||
using (var sw = new StreamWriter(LogPath, append: true, System.Text.Encoding.UTF8))
|
||||
{
|
||||
await sw.WriteLineAsync(FormatMessage(logMsgLevel, message));
|
||||
sw.AutoFlush = true;
|
||||
foreach (var msg in logMessageCollection.GetConsumingEnumerable())
|
||||
{
|
||||
sw.WriteLine(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void Log(LoggerEvent logMsgLevel, string message, bool ignoreLevel)
|
||||
|
@ -10,141 +10,6 @@ namespace AssetStudioCLI
|
||||
{
|
||||
internal static class Exporter
|
||||
{
|
||||
public static bool ExportTexture2D(AssetItem item, string exportPath)
|
||||
{
|
||||
var m_Texture2D = (Texture2D)item.Asset;
|
||||
if (CLIOptions.convertTexture)
|
||||
{
|
||||
var type = CLIOptions.o_imageFormat.Value;
|
||||
if (!TryExportFile(exportPath, item, "." + type.ToString().ToLower(), out var exportFullPath))
|
||||
return false;
|
||||
|
||||
if (CLIOptions.o_logLevel.Value <= LoggerEvent.Debug)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"Converting \"{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;
|
||||
}
|
||||
Logger.Debug(sb.ToString());
|
||||
}
|
||||
|
||||
var image = m_Texture2D.ConvertToImage(flip: true);
|
||||
if (image == null)
|
||||
{
|
||||
Logger.Error($"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);
|
||||
}
|
||||
Logger.Debug($"{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());
|
||||
Logger.Debug($"{item.TypeString} \"{item.Text}\" exported to \"{exportFullPath}\"");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool ExportTexture2DArray(AssetItem item, string exportPath)
|
||||
{
|
||||
var m_Texture2DArray = (Texture2DArray)item.Asset;
|
||||
var count = 0;
|
||||
foreach (var texture in m_Texture2DArray.TextureList)
|
||||
{
|
||||
var fakeItem = new AssetItem(texture)
|
||||
{
|
||||
Text = texture.m_Name,
|
||||
Container = item.Container,
|
||||
};
|
||||
if (ExportTexture2D(fakeItem, exportPath))
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
Logger.Debug($"{item.TypeString} \"{item.Text}\" exported to \"{exportPath}\"");
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
public static bool ExportAudioClip(AssetItem item, string exportPath)
|
||||
{
|
||||
string exportFullPath;
|
||||
var m_AudioClip = (AudioClip)item.Asset;
|
||||
var m_AudioData = m_AudioClip.m_AudioData.GetData();
|
||||
if (m_AudioData == null || m_AudioData.Length == 0)
|
||||
{
|
||||
Logger.Error($"Export error. \"{item.Text}\": AudioData was not found");
|
||||
return false;
|
||||
}
|
||||
var converter = new AudioClipConverter(m_AudioClip);
|
||||
if (CLIOptions.o_audioFormat.Value != AudioFormat.None && converter.IsSupport)
|
||||
{
|
||||
if (!TryExportFile(exportPath, item, ".wav", out exportFullPath))
|
||||
return false;
|
||||
|
||||
if (CLIOptions.o_logLevel.Value <= LoggerEvent.Debug)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"Converting \"{m_AudioClip.m_Name}\" to wav..");
|
||||
sb.AppendLine(m_AudioClip.version[0] < 5 ? $"AudioClip type: {m_AudioClip.m_Type}" : $"AudioClip compression format: {m_AudioClip.m_CompressionFormat}");
|
||||
sb.AppendLine($"AudioClip channel count: {m_AudioClip.m_Channels}");
|
||||
sb.AppendLine($"AudioClip sample rate: {m_AudioClip.m_Frequency}");
|
||||
sb.AppendLine($"AudioClip bit depth: {m_AudioClip.m_BitsPerSample}");
|
||||
Logger.Debug(sb.ToString());
|
||||
}
|
||||
|
||||
var buffer = converter.ConvertToWav(m_AudioData);
|
||||
if (buffer == null)
|
||||
{
|
||||
Logger.Error($"Export error. \"{item.Text}\": Failed to convert fmod 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)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"Exporting non-fmod {item.TypeString} \"{m_AudioClip.m_Name}\"..");
|
||||
sb.AppendLine(m_AudioClip.version[0] < 5 ? $"AudioClip type: {m_AudioClip.m_Type}" : $"AudioClip compression format: {m_AudioClip.m_CompressionFormat}");
|
||||
sb.AppendLine($"AudioClip channel count: {m_AudioClip.m_Channels}");
|
||||
sb.AppendLine($"AudioClip sample rate: {m_AudioClip.m_Frequency}");
|
||||
sb.AppendLine($"AudioClip bit depth: {m_AudioClip.m_BitsPerSample}");
|
||||
Logger.Debug(sb.ToString());
|
||||
}
|
||||
File.WriteAllBytes(exportFullPath, m_AudioData);
|
||||
}
|
||||
|
||||
Logger.Debug($"{item.TypeString} \"{item.Text}\" exported to \"{exportFullPath}\"");
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool ExportVideoClip(AssetItem item, string exportPath)
|
||||
{
|
||||
var m_VideoClip = (VideoClip)item.Asset;
|
||||
@ -264,38 +129,6 @@ namespace AssetStudioCLI
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool ExportSprite(AssetItem item, string exportPath)
|
||||
{
|
||||
var type = CLIOptions.o_imageFormat.Value;
|
||||
var alphaMask = SpriteMaskMode.On;
|
||||
if (!TryExportFile(exportPath, item, "." + type.ToString().ToLower(), out var exportFullPath))
|
||||
return false;
|
||||
var image = ((Sprite)item.Asset).GetImage(alphaMask);
|
||||
if (image != null)
|
||||
{
|
||||
using (image)
|
||||
{
|
||||
using (var file = File.OpenWrite(exportFullPath))
|
||||
{
|
||||
image.WriteToStream(file, type);
|
||||
}
|
||||
Logger.Debug($"{item.TypeString} \"{item.Text}\" exported to \"{exportFullPath}\"");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool ExportRawFile(AssetItem item, string exportPath)
|
||||
{
|
||||
if (!TryExportFile(exportPath, item, ".dat", out var exportFullPath))
|
||||
return false;
|
||||
File.WriteAllBytes(exportFullPath, item.Asset.GetRawData());
|
||||
|
||||
Logger.Debug($"{item.TypeString} \"{item.Text}\" exported to \"{exportFullPath}\"");
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void ExportGameObject(GameObject gameObject, string exportPath, List<AssetItem> animationList = null)
|
||||
{
|
||||
var convert = animationList != null
|
||||
@ -323,10 +156,19 @@ namespace AssetStudioCLI
|
||||
exportAllNodes, exportSkins, exportAnimations, exportBlendShape, castToBone, boneSize, exportAllUvsAsDiffuseMaps, scaleFactor, fbxVersion, fbxFormat == 1);
|
||||
}
|
||||
|
||||
public static bool ExportRawFile(AssetItem item, string exportPath)
|
||||
{
|
||||
if (!TryExportFile(exportPath, item, ".dat", out var exportFullPath, mode: "ExportRaw"))
|
||||
return false;
|
||||
File.WriteAllBytes(exportFullPath, item.Asset.GetRawData());
|
||||
|
||||
Logger.Debug($"{item.TypeString} \"{item.Text}\" exported to \"{exportFullPath}\"");
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool ExportDumpFile(AssetItem item, string exportPath)
|
||||
{
|
||||
if (!TryExportFile(exportPath, item, ".txt", out var exportFullPath))
|
||||
if (!TryExportFile(exportPath, item, ".txt", out var exportFullPath, mode: "Dump"))
|
||||
return false;
|
||||
var str = item.Asset.Dump();
|
||||
if (str == null && item.Asset is MonoBehaviour m_MonoBehaviour)
|
||||
@ -347,7 +189,7 @@ namespace AssetStudioCLI
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryExportFile(string dir, AssetItem item, string extension, out string fullPath)
|
||||
private static bool TryExportFile(string dir, AssetItem item, string extension, out string fullPath, string mode = "Export")
|
||||
{
|
||||
var fileName = FixFileName(item.Text);
|
||||
var filenameFormat = CLIOptions.o_filenameFormat.Value;
|
||||
@ -375,7 +217,7 @@ namespace AssetStudioCLI
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Logger.Error($"Export error. File \"{fullPath.Color(ColorConsole.BrightRed)}\" already exist");
|
||||
Logger.Error($"{mode} error. File \"{fullPath.Color(ColorConsole.BrightRed)}\" already exist");
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -481,11 +323,10 @@ namespace AssetStudioCLI
|
||||
switch (item.Type)
|
||||
{
|
||||
case ClassIDType.Texture2D:
|
||||
return ExportTexture2D(item, exportPath);
|
||||
case ClassIDType.Texture2DArray:
|
||||
return ExportTexture2DArray(item, exportPath);
|
||||
case ClassIDType.Sprite:
|
||||
case ClassIDType.AudioClip:
|
||||
return ExportAudioClip(item, exportPath);
|
||||
throw new System.NotImplementedException();
|
||||
case ClassIDType.VideoClip:
|
||||
return ExportVideoClip(item, exportPath);
|
||||
case ClassIDType.MovieTexture:
|
||||
@ -498,8 +339,6 @@ namespace AssetStudioCLI
|
||||
return ExportMonoBehaviour(item, exportPath);
|
||||
case ClassIDType.Font:
|
||||
return ExportFont(item, exportPath);
|
||||
case ClassIDType.Sprite:
|
||||
return ExportSprite(item, exportPath);
|
||||
case ClassIDType.Mesh:
|
||||
return ExportMesh(item, exportPath);
|
||||
default:
|
||||
@ -509,8 +348,9 @@ namespace AssetStudioCLI
|
||||
|
||||
public static string FixFileName(string str)
|
||||
{
|
||||
if (str.Length >= 260) return Path.GetRandomFileName();
|
||||
return Path.GetInvalidFileNameChars().Aggregate(str, (current, c) => current.Replace(c, '_'));
|
||||
return str.Length >= 260
|
||||
? Path.GetRandomFileName()
|
||||
: Path.GetInvalidFileNameChars().Aggregate(str, (current, c) => current.Replace(c, '_'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ namespace AssetStudioCLI.Options
|
||||
ExportRaw,
|
||||
Dump,
|
||||
Info,
|
||||
ExportLive2D,
|
||||
Live2D,
|
||||
SplitObjects,
|
||||
}
|
||||
|
||||
@ -112,6 +112,7 @@ namespace AssetStudioCLI.Options
|
||||
public static Option<List<string>> o_filterByText;
|
||||
//advanced
|
||||
public static Option<CustomCompressionType> o_customCompressionType;
|
||||
public static Option<int> o_maxParallelExportTasks;
|
||||
public static Option<ExportListType> o_exportAssetList;
|
||||
public static Option<string> o_assemblyPath;
|
||||
public static Option<string> o_unityVersion;
|
||||
@ -190,7 +191,7 @@ namespace AssetStudioCLI.Options
|
||||
"ExportRaw - Exports raw data\n" +
|
||||
"Dump - Makes asset dumps\n" +
|
||||
"Info - Loads file(s), shows the number of available for export assets and exits\n" +
|
||||
"Live2D - Exports Live2D Cubism 3 models\n" +
|
||||
"Live2D - Exports Live2D Cubism models\n" +
|
||||
"SplitObjects - Exports split objects (fbx)\n",
|
||||
optionExample: "Example: \"-m info\"\n",
|
||||
optionHelpGroup: HelpGroups.General
|
||||
@ -329,7 +330,7 @@ namespace AssetStudioCLI.Options
|
||||
optionDefaultValue: 1f,
|
||||
optionName: "--fbx-scale-factor <value>",
|
||||
optionDescription: "Specify the FBX Scale Factor\n" +
|
||||
"<Value: float number from 0 to 100 (default=1)\n",
|
||||
"<Value: float number from 0 to 100 (default=1)>\n",
|
||||
optionExample: "Example: \"--fbx-scale-factor 50\"\n",
|
||||
optionHelpGroup: HelpGroups.FBX
|
||||
);
|
||||
@ -338,7 +339,7 @@ namespace AssetStudioCLI.Options
|
||||
optionDefaultValue: 10,
|
||||
optionName: "--fbx-bone-size <value>",
|
||||
optionDescription: "Specify the FBX Bone Size\n" +
|
||||
"<Value: integer number from 0 to 100 (default=10)\n",
|
||||
"<Value: integer number from 0 to 100 (default=10)>\n",
|
||||
optionExample: "Example: \"--fbx-bone-size 10\"",
|
||||
optionHelpGroup: HelpGroups.FBX
|
||||
);
|
||||
@ -397,6 +398,17 @@ namespace AssetStudioCLI.Options
|
||||
optionHelpGroup: HelpGroups.Advanced
|
||||
);
|
||||
|
||||
o_maxParallelExportTasks = new GroupedOption<int>
|
||||
(
|
||||
optionDefaultValue: Environment.ProcessorCount - 1,
|
||||
optionName: "--max-export-tasks <value>",
|
||||
optionDescription: "Specify the number of parallel tasks for asset export\n" +
|
||||
"<Value: integer number from 1 to max number of cores (default=max)>\n" +
|
||||
"Max - Number of cores in your CPU\n",
|
||||
optionExample: "Example: \"--max-export-tasks 8\"\n",
|
||||
optionHelpGroup: HelpGroups.Advanced
|
||||
);
|
||||
|
||||
o_exportAssetList = new GroupedOption<ExportListType>
|
||||
(
|
||||
optionDefaultValue: ExportListType.None,
|
||||
@ -528,7 +540,7 @@ namespace AssetStudioCLI.Options
|
||||
break;
|
||||
case "l2d":
|
||||
case "live2d":
|
||||
o_workMode.Value = WorkMode.ExportLive2D;
|
||||
o_workMode.Value = WorkMode.Live2D;
|
||||
o_exportAssetTypes.Value = new List<ClassIDType>()
|
||||
{
|
||||
ClassIDType.AnimationClip,
|
||||
@ -564,7 +576,7 @@ namespace AssetStudioCLI.Options
|
||||
switch(flag)
|
||||
{
|
||||
case "--l2d-force-bezier":
|
||||
if (o_workMode.Value != WorkMode.ExportLive2D)
|
||||
if (o_workMode.Value != WorkMode.Live2D)
|
||||
{
|
||||
Console.WriteLine($"{"Error".Color(brightRed)} during parsing [{flag.Color(brightYellow)}] flag. This flag is not suitable for the current working mode [{o_workMode.Value}].\n");
|
||||
ShowOptionDescription(o_workMode);
|
||||
@ -611,7 +623,7 @@ namespace AssetStudioCLI.Options
|
||||
{
|
||||
case "-t":
|
||||
case "--asset-type":
|
||||
if (o_workMode.Value == WorkMode.ExportLive2D || o_workMode.Value == WorkMode.SplitObjects)
|
||||
if (o_workMode.Value == WorkMode.Live2D || o_workMode.Value == WorkMode.SplitObjects)
|
||||
{
|
||||
i++;
|
||||
continue;
|
||||
@ -820,7 +832,7 @@ namespace AssetStudioCLI.Options
|
||||
}
|
||||
break;
|
||||
case "--l2d-motion-mode":
|
||||
if (o_workMode.Value != WorkMode.ExportLive2D)
|
||||
if (o_workMode.Value != WorkMode.Live2D)
|
||||
{
|
||||
Console.WriteLine($"{"Error".Color(brightRed)} during parsing [{option.Color(brightYellow)}] option. This option is not suitable for the current working mode [{o_workMode.Value}].\n");
|
||||
ShowOptionDescription(o_workMode);
|
||||
@ -846,7 +858,8 @@ namespace AssetStudioCLI.Options
|
||||
}
|
||||
break;
|
||||
case "--fbx-scale-factor":
|
||||
var isFloat = float.TryParse(value, out float floatValue);
|
||||
{
|
||||
var isFloat = float.TryParse(value, out var floatValue);
|
||||
if (isFloat && floatValue >= 0 && floatValue <= 100)
|
||||
{
|
||||
o_fbxScaleFactor.Value = floatValue;
|
||||
@ -858,8 +871,10 @@ namespace AssetStudioCLI.Options
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "--fbx-bone-size":
|
||||
var isInt = int.TryParse(value, out int intValue);
|
||||
{
|
||||
var isInt = int.TryParse(value, out var intValue);
|
||||
if (isInt && intValue >= 0 && intValue <= 100)
|
||||
{
|
||||
o_fbxBoneSize.Value = intValue;
|
||||
@ -871,6 +886,7 @@ namespace AssetStudioCLI.Options
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "--custom-compression":
|
||||
switch (value.ToLower())
|
||||
{
|
||||
@ -887,6 +903,29 @@ namespace AssetStudioCLI.Options
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "--max-export-tasks":
|
||||
{
|
||||
var processorCount = Environment.ProcessorCount;
|
||||
if (value.ToLower() == "max")
|
||||
{
|
||||
o_maxParallelExportTasks.Value = processorCount - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
var isInt = int.TryParse(value, out var intValue);
|
||||
if (isInt && intValue >= 0 && intValue <= processorCount)
|
||||
{
|
||||
o_maxParallelExportTasks.Value = Math.Min(intValue, processorCount - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"{"Error".Color(brightRed)} during parsing [{option.Color(brightYellow)}] option. Unsupported number of parallel tasks: [{value.Color(brightRed)}].\n");
|
||||
ShowOptionDescription(o_maxParallelExportTasks);
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "--export-asset-list":
|
||||
switch (value.ToLower())
|
||||
{
|
||||
@ -1125,6 +1164,7 @@ namespace AssetStudioCLI.Options
|
||||
sb.AppendLine($"# Unity Version: \"{o_unityVersion}\"");
|
||||
if (o_workMode.Value == WorkMode.Export)
|
||||
{
|
||||
sb.AppendLine($"# Max Parallel Export Tasks: {o_maxParallelExportTasks}");
|
||||
sb.AppendLine($"# Restore TextAsset Extension: {!f_notRestoreExtensionName.Value}");
|
||||
}
|
||||
break;
|
||||
@ -1137,7 +1177,7 @@ namespace AssetStudioCLI.Options
|
||||
sb.AppendLine(ShowCurrentFilter());
|
||||
sb.AppendLine($"# Unity Version: \"{o_unityVersion}\"");
|
||||
break;
|
||||
case WorkMode.ExportLive2D:
|
||||
case WorkMode.Live2D:
|
||||
case WorkMode.SplitObjects:
|
||||
sb.AppendLine($"# Output Path: \"{o_outputFolder}\"");
|
||||
sb.AppendLine($"# Log Level: {o_logLevel}");
|
||||
|
223
AssetStudioCLI/ParallelExporter.cs
Normal file
223
AssetStudioCLI/ParallelExporter.cs
Normal file
@ -0,0 +1,223 @@
|
||||
using AssetStudio;
|
||||
using AssetStudioCLI.Options;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace AssetStudioCLI
|
||||
{
|
||||
internal static class ParallelExporter
|
||||
{
|
||||
private static ConcurrentDictionary<string, bool> savePathHash = new ConcurrentDictionary<string, bool>();
|
||||
|
||||
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;
|
||||
if (!TryExportFile(exportPath, item, "." + type.ToString().ToLower(), 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();
|
||||
}
|
||||
|
||||
var image = m_Texture2D.ConvertToImage(flip: true);
|
||||
if (image == null)
|
||||
{
|
||||
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 alphaMask = SpriteMaskMode.On;
|
||||
if (!TryExportFile(exportPath, item, "." + type.ToString().ToLower(), out var exportFullPath))
|
||||
return false;
|
||||
var image = ((Sprite)item.Asset).GetImage(alphaMask);
|
||||
if (image != null)
|
||||
{
|
||||
using (image)
|
||||
{
|
||||
using (var file = File.OpenWrite(exportFullPath))
|
||||
{
|
||||
image.WriteToStream(file, type);
|
||||
}
|
||||
debugLog += $"{item.TypeString} \"{item.Text}\" exported to \"{exportFullPath}\"";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool ExportAudioClip(AssetItem item, string exportPath, out string debugLog)
|
||||
{
|
||||
debugLog = "";
|
||||
string exportFullPath;
|
||||
var m_AudioClip = (AudioClip)item.Asset;
|
||||
var m_AudioData = BigArrayPool<byte>.Shared.Rent(m_AudioClip.m_AudioData.Size);
|
||||
try
|
||||
{
|
||||
m_AudioClip.m_AudioData.GetData(m_AudioData);
|
||||
if (m_AudioData == null || m_AudioData.Length == 0)
|
||||
{
|
||||
Logger.Error($"Export error. \"{item.Text}\": AudioData was not found");
|
||||
return false;
|
||||
}
|
||||
var converter = new AudioClipConverter(m_AudioClip);
|
||||
if (CLIOptions.o_audioFormat.Value != AudioFormat.None && converter.IsSupport)
|
||||
{
|
||||
if (!TryExportFile(exportPath, item, ".wav", out exportFullPath))
|
||||
return false;
|
||||
|
||||
if (CLIOptions.o_logLevel.Value <= LoggerEvent.Debug)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"Converting {item.TypeString} \"{m_AudioClip.m_Name}\" to wav..");
|
||||
sb.AppendLine(m_AudioClip.version[0] < 5 ? $"AudioClip type: {m_AudioClip.m_Type}" : $"AudioClip compression format: {m_AudioClip.m_CompressionFormat}");
|
||||
sb.AppendLine($"AudioClip channel count: {m_AudioClip.m_Channels}");
|
||||
sb.AppendLine($"AudioClip sample rate: {m_AudioClip.m_Frequency}");
|
||||
sb.AppendLine($"AudioClip bit depth: {m_AudioClip.m_BitsPerSample}");
|
||||
debugLog += sb.ToString();
|
||||
}
|
||||
|
||||
var buffer = converter.ConvertToWav(m_AudioData, out var debugLogConverter);
|
||||
debugLog += debugLogConverter;
|
||||
if (buffer == null)
|
||||
{
|
||||
Logger.Error($"{debugLog}Export error. \"{item.Text}\": Failed to convert fmod 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)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"Exporting non-fmod {item.TypeString} \"{m_AudioClip.m_Name}\"..");
|
||||
sb.AppendLine(m_AudioClip.version[0] < 5 ? $"AudioClip type: {m_AudioClip.m_Type}" : $"AudioClip compression format: {m_AudioClip.m_CompressionFormat}");
|
||||
sb.AppendLine($"AudioClip channel count: {m_AudioClip.m_Channels}");
|
||||
sb.AppendLine($"AudioClip sample rate: {m_AudioClip.m_Frequency}");
|
||||
sb.AppendLine($"AudioClip bit depth: {m_AudioClip.m_BitsPerSample}");
|
||||
debugLog += sb.ToString();
|
||||
}
|
||||
File.WriteAllBytes(exportFullPath, m_AudioData);
|
||||
}
|
||||
debugLog += $"{item.TypeString} \"{item.Text}\" exported to \"{exportFullPath}\"";
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
BigArrayPool<byte>.Shared.Return(m_AudioData, clearArray: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryExportFile(string dir, AssetItem item, string extension, out string fullPath)
|
||||
{
|
||||
var fileName = FixFileName(item.Text);
|
||||
var filenameFormat = CLIOptions.o_filenameFormat.Value;
|
||||
switch (filenameFormat)
|
||||
{
|
||||
case FilenameFormat.AssetName_PathID:
|
||||
fileName = $"{fileName} @{item.m_PathID}";
|
||||
break;
|
||||
case FilenameFormat.PathID:
|
||||
fileName = item.m_PathID.ToString();
|
||||
break;
|
||||
}
|
||||
fullPath = Path.Combine(dir, fileName + extension);
|
||||
if (savePathHash.TryAdd(fullPath.ToLower(), true) && !File.Exists(fullPath))
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
return true;
|
||||
}
|
||||
if (filenameFormat == FilenameFormat.AssetName)
|
||||
{
|
||||
fullPath = Path.Combine(dir, fileName + item.UniqueID + extension);
|
||||
if (!File.Exists(fullPath))
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Logger.Error($"Export error. File \"{fullPath.Color(ColorConsole.BrightRed)}\" already exist");
|
||||
return false;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
private static string FixFileName(string str)
|
||||
{
|
||||
return str.Length >= 260
|
||||
? Path.GetRandomFileName()
|
||||
: Path.GetInvalidFileNameChars().Aggregate(str, (current, c) => current.Replace(c, '_'));
|
||||
}
|
||||
|
||||
public static void ClearHash()
|
||||
{
|
||||
savePathHash.Clear();
|
||||
}
|
||||
}
|
||||
}
|
@ -48,7 +48,7 @@ namespace AssetStudioCLI
|
||||
case WorkMode.Info:
|
||||
Studio.ShowExportableAssetsInfo();
|
||||
break;
|
||||
case WorkMode.ExportLive2D:
|
||||
case WorkMode.Live2D:
|
||||
Studio.ExportLive2D();
|
||||
break;
|
||||
case WorkMode.SplitObjects:
|
||||
|
@ -2,9 +2,12 @@
|
||||
using AssetStudioCLI.Options;
|
||||
using CubismLive2DExtractor;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
using static AssetStudioCLI.Exporter;
|
||||
using Ansi = AssetStudio.ColorConsole;
|
||||
@ -191,7 +194,7 @@ namespace AssetStudioCLI
|
||||
parsedAssetsList.AddRange(fileAssetsList);
|
||||
fileAssetsList.Clear();
|
||||
tex2dArrayAssetList.Clear();
|
||||
if (CLIOptions.o_workMode.Value != WorkMode.ExportLive2D)
|
||||
if (CLIOptions.o_workMode.Value != WorkMode.Live2D)
|
||||
{
|
||||
containers.Clear();
|
||||
}
|
||||
@ -364,7 +367,7 @@ namespace AssetStudioCLI
|
||||
{
|
||||
switch (CLIOptions.o_workMode.Value)
|
||||
{
|
||||
case WorkMode.ExportLive2D:
|
||||
case WorkMode.Live2D:
|
||||
case WorkMode.SplitObjects:
|
||||
break;
|
||||
default:
|
||||
@ -434,7 +437,10 @@ namespace AssetStudioCLI
|
||||
var exportedCount = 0;
|
||||
|
||||
var groupOption = CLIOptions.o_groupAssetsBy.Value;
|
||||
foreach (var asset in parsedAssetsList)
|
||||
var parallelExportCount = CLIOptions.o_maxParallelExportTasks.Value;
|
||||
var toExportAssetDict = new ConcurrentDictionary<AssetItem, string>();
|
||||
var toParallelExportAssetDict = new ConcurrentDictionary<AssetItem, string>();
|
||||
Parallel.ForEach(parsedAssetsList, asset =>
|
||||
{
|
||||
string exportPath;
|
||||
switch (groupOption)
|
||||
@ -481,32 +487,61 @@ namespace AssetStudioCLI
|
||||
exportPath = savePath;
|
||||
break;
|
||||
}
|
||||
|
||||
exportPath += Path.DirectorySeparatorChar;
|
||||
|
||||
if (CLIOptions.o_workMode.Value == WorkMode.Export)
|
||||
{
|
||||
switch (asset.Type)
|
||||
{
|
||||
case ClassIDType.Texture2D:
|
||||
case ClassIDType.Sprite:
|
||||
case ClassIDType.AudioClip:
|
||||
toParallelExportAssetDict.TryAdd(asset, exportPath);
|
||||
break;
|
||||
case ClassIDType.Texture2DArray:
|
||||
var m_Texture2DArray = (Texture2DArray)asset.Asset;
|
||||
toExportCount += m_Texture2DArray.TextureList.Count - 1;
|
||||
foreach (var texture in m_Texture2DArray.TextureList)
|
||||
{
|
||||
var fakeItem = new AssetItem(texture)
|
||||
{
|
||||
Text = texture.m_Name,
|
||||
Container = asset.Container,
|
||||
};
|
||||
toParallelExportAssetDict.TryAdd(fakeItem, exportPath);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
toExportAssetDict.TryAdd(asset, exportPath);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
toExportAssetDict.TryAdd(asset, exportPath);
|
||||
}
|
||||
});
|
||||
|
||||
foreach (var toExportAsset in toExportAssetDict)
|
||||
{
|
||||
var asset = toExportAsset.Key;
|
||||
var exportPath = toExportAsset.Value;
|
||||
var isExported = false;
|
||||
try
|
||||
{
|
||||
switch (CLIOptions.o_workMode.Value)
|
||||
{
|
||||
case WorkMode.ExportRaw:
|
||||
Logger.Debug($"{CLIOptions.o_workMode}: {asset.Type} : {asset.Container} : {asset.Text}");
|
||||
if (ExportRawFile(asset, exportPath))
|
||||
{
|
||||
exportedCount++;
|
||||
}
|
||||
isExported = ExportRawFile(asset, exportPath);
|
||||
break;
|
||||
case WorkMode.Dump:
|
||||
Logger.Debug($"{CLIOptions.o_workMode}: {asset.Type} : {asset.Container} : {asset.Text}");
|
||||
if (ExportDumpFile(asset, exportPath))
|
||||
{
|
||||
exportedCount++;
|
||||
}
|
||||
isExported = ExportDumpFile(asset, exportPath);
|
||||
break;
|
||||
case WorkMode.Export:
|
||||
Logger.Debug($"{CLIOptions.o_workMode}: {asset.Type} : {asset.Container} : {asset.Text}");
|
||||
if (ExportConvertFile(asset, exportPath))
|
||||
{
|
||||
exportedCount++;
|
||||
}
|
||||
isExported = ExportConvertFile(asset, exportPath);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -514,8 +549,33 @@ namespace AssetStudioCLI
|
||||
{
|
||||
Logger.Error($"{asset.SourceFile.originalPath}: [{$"{asset.Type}: {asset.Text}".Color(Ansi.BrightRed)}] : Export error\n{ex}");
|
||||
}
|
||||
|
||||
if (isExported)
|
||||
{
|
||||
exportedCount++;
|
||||
}
|
||||
Console.Write($"Exported [{exportedCount}/{toExportCount}]\r");
|
||||
}
|
||||
|
||||
Parallel.ForEach(toParallelExportAssetDict, new ParallelOptions { MaxDegreeOfParallelism = parallelExportCount }, toExportAsset =>
|
||||
{
|
||||
var asset = toExportAsset.Key;
|
||||
var exportPath = toExportAsset.Value;
|
||||
try
|
||||
{
|
||||
if (ParallelExporter.ParallelExportConvertFile(asset, exportPath, out var debugLog))
|
||||
{
|
||||
Interlocked.Increment(ref exportedCount);
|
||||
Logger.Debug(debugLog);
|
||||
Console.Write($"Exported [{exportedCount}/{toExportCount}]\r");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error($"{asset.SourceFile.originalPath}: [{$"{asset.Type}: {asset.Text}".Color(Ansi.BrightRed)}] : Export error\n{ex}");
|
||||
}
|
||||
});
|
||||
ParallelExporter.ClearHash();
|
||||
Console.WriteLine("");
|
||||
|
||||
if (exportedCount == 0)
|
||||
@ -708,7 +768,7 @@ namespace AssetStudioCLI
|
||||
}
|
||||
basePathSet.Clear();
|
||||
|
||||
var lookup = containers.ToLookup(
|
||||
var lookup = containers.AsParallel().ToLookup(
|
||||
x => mocPathList.Find(b => x.Value.Contains(b) && x.Value.Split('/').Any(y => y == b.Substring(b.LastIndexOf("/") + 1))),
|
||||
x => x.Key
|
||||
);
|
||||
@ -720,6 +780,7 @@ namespace AssetStudioCLI
|
||||
|
||||
var totalModelCount = lookup.LongCount(x => x.Key != null);
|
||||
Logger.Info($"Found {totalModelCount} model(s).");
|
||||
var parallelTaskCount = CLIOptions.o_maxParallelExportTasks.Value;
|
||||
var modelCounter = 0;
|
||||
foreach (var assets in lookup)
|
||||
{
|
||||
@ -740,7 +801,7 @@ namespace AssetStudioCLI
|
||||
var destPath = Path.Combine(baseDestPath, container) + Path.DirectorySeparatorChar;
|
||||
|
||||
var modelExtractor = new Live2DExtractor(assets);
|
||||
modelExtractor.ExtractCubismModel(destPath, modelName, motionMode, assemblyLoader, forceBezier);
|
||||
modelExtractor.ExtractCubismModel(destPath, modelName, motionMode, assemblyLoader, forceBezier, parallelTaskCount);
|
||||
modelCounter++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
Reference in New Issue
Block a user