Add parallel export support for some asset types

This commit is contained in:
VaDiM
2024-03-25 05:00:58 +03:00
parent cadcf0b492
commit c9e9bc840c
21 changed files with 994 additions and 407 deletions

View File

@ -1966,7 +1966,7 @@ namespace AssetStudioGUI
private void toolStripMenuItem15_Click(object sender, EventArgs e)
{
logger.ShowDebugMessage = toolStripMenuItem15.Checked;
GUILogger.ShowDebugMessage = toolStripMenuItem15.Checked;
}
private void sceneTreeView_NodeMouseClick(object sender, TreeNodeMouseClickEventArgs e)

View File

@ -120,9 +120,6 @@
<metadata name="menuStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>312, 17</value>
</metadata>
<metadata name="statusStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>432, 17</value>
</metadata>
<data name="fontPreviewBox.Text" xml:space="preserve">
<value>abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWYZ
1234567890.:,;'\"(!?)+-*/=
@ -141,6 +138,9 @@ The quick brown fox jumps over the lazy dog. 1234567890
The quick brown fox jumps over the lazy dog. 1234567890</value>
</data>
<metadata name="statusStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>432, 17</value>
</metadata>
<metadata name="contextMenuStrip2.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>775, 21</value>
</metadata>

View File

@ -72,6 +72,9 @@
this.exportAllNodes = new System.Windows.Forms.CheckBox();
this.eulerFilter = new System.Windows.Forms.CheckBox();
this.optionTooltip = new System.Windows.Forms.ToolTip(this.components);
this.parallelExportUpDown = new System.Windows.Forms.NumericUpDown();
this.parallelExportCheckBox = new System.Windows.Forms.CheckBox();
this.parallelExportMaxLabel = new System.Windows.Forms.Label();
this.groupBox1.SuspendLayout();
this.panel1.SuspendLayout();
this.l2dGroupBox.SuspendLayout();
@ -80,6 +83,7 @@
((System.ComponentModel.ISupportInitialize)(this.scaleFactor)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.boneSize)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.filterPrecision)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.parallelExportUpDown)).BeginInit();
this.SuspendLayout();
//
// OKbutton
@ -106,6 +110,9 @@
// groupBox1
//
this.groupBox1.AutoSize = true;
this.groupBox1.Controls.Add(this.parallelExportMaxLabel);
this.groupBox1.Controls.Add(this.parallelExportCheckBox);
this.groupBox1.Controls.Add(this.parallelExportUpDown);
this.groupBox1.Controls.Add(this.filenameFormatLabel);
this.groupBox1.Controls.Add(this.filenameFormatComboBox);
this.groupBox1.Controls.Add(this.exportSpriteWithAlphaMask);
@ -593,6 +600,53 @@
this.eulerFilter.Text = "EulerFilter";
this.eulerFilter.UseVisualStyleBackColor = true;
//
// parallelExportUpDown
//
this.parallelExportUpDown.Location = new System.Drawing.Point(209, 218);
this.parallelExportUpDown.Maximum = new decimal(new int[] {
8,
0,
0,
0});
this.parallelExportUpDown.Minimum = new decimal(new int[] {
1,
0,
0,
0});
this.parallelExportUpDown.Name = "parallelExportUpDown";
this.parallelExportUpDown.Size = new System.Drawing.Size(42, 20);
this.parallelExportUpDown.TabIndex = 13;
this.parallelExportUpDown.Value = new decimal(new int[] {
1,
0,
0,
0});
//
// parallelExportCheckBox
//
this.parallelExportCheckBox.AutoSize = true;
this.parallelExportCheckBox.Checked = true;
this.parallelExportCheckBox.CheckState = System.Windows.Forms.CheckState.Checked;
this.parallelExportCheckBox.Location = new System.Drawing.Point(6, 219);
this.parallelExportCheckBox.Name = "parallelExportCheckBox";
this.parallelExportCheckBox.Size = new System.Drawing.Size(203, 17);
this.parallelExportCheckBox.TabIndex = 15;
this.parallelExportCheckBox.Text = "Export in parallel with number of tasks";
this.optionTooltip.SetToolTip(this.parallelExportCheckBox, "*Requires slightly more RAM than in single-task mode");
this.parallelExportCheckBox.UseVisualStyleBackColor = true;
this.parallelExportCheckBox.CheckedChanged += new System.EventHandler(this.parallelExportCheckBox_CheckedChanged);
//
// parallelExportMaxLabel
//
this.parallelExportMaxLabel.AutoSize = true;
this.parallelExportMaxLabel.ForeColor = System.Drawing.SystemColors.ControlDark;
this.parallelExportMaxLabel.Location = new System.Drawing.Point(256, 221);
this.parallelExportMaxLabel.Name = "parallelExportMaxLabel";
this.parallelExportMaxLabel.Size = new System.Drawing.Size(33, 13);
this.parallelExportMaxLabel.TabIndex = 16;
this.parallelExportMaxLabel.Text = "Max: ";
this.optionTooltip.SetToolTip(this.parallelExportMaxLabel, "*The maximum number matches the number of CPU cores");
//
// ExportOptions
//
this.AcceptButton = this.OKbutton;
@ -626,6 +680,7 @@
((System.ComponentModel.ISupportInitialize)(this.scaleFactor)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.boneSize)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.filterPrecision)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.parallelExportUpDown)).EndInit();
this.ResumeLayout(false);
this.PerformLayout();
@ -675,5 +730,8 @@
private System.Windows.Forms.Panel l2dMotionExportMethodPanel;
private System.Windows.Forms.ComboBox filenameFormatComboBox;
private System.Windows.Forms.Label filenameFormatLabel;
private System.Windows.Forms.NumericUpDown parallelExportUpDown;
private System.Windows.Forms.CheckBox parallelExportCheckBox;
private System.Windows.Forms.Label parallelExportMaxLabel;
}
}

View File

@ -34,6 +34,12 @@ namespace AssetStudioGUI
((RadioButton)l2dMotionExportMethodPanel.Controls.Cast<Control>().First(x => x.AccessibleName == defaultMotionMode)).Checked = true;
l2dForceBezierCheckBox.Checked = Properties.Settings.Default.l2dForceBezier;
filenameFormatComboBox.SelectedIndex = Properties.Settings.Default.filenameFormat;
var maxParallelTasks = Environment.ProcessorCount;
var taskCount = Properties.Settings.Default.parallelExportCount;
parallelExportUpDown.Maximum = maxParallelTasks;
parallelExportUpDown.Value = taskCount <= 0 ? maxParallelTasks : Math.Min(taskCount, maxParallelTasks);
parallelExportMaxLabel.Text += maxParallelTasks;
parallelExportCheckBox.Checked = Properties.Settings.Default.parallelExport;
}
private void OKbutton_Click(object sender, EventArgs e)
@ -62,6 +68,8 @@ namespace AssetStudioGUI
Properties.Settings.Default.l2dMotionMode = (CubismLive2DExtractor.Live2DMotionMode)Enum.Parse(typeof(CubismLive2DExtractor.Live2DMotionMode), checkedMotionMode.AccessibleName);
Properties.Settings.Default.l2dForceBezier = l2dForceBezierCheckBox.Checked;
Properties.Settings.Default.filenameFormat = filenameFormatComboBox.SelectedIndex;
Properties.Settings.Default.parallelExport = parallelExportCheckBox.Checked;
Properties.Settings.Default.parallelExportCount = (int)parallelExportUpDown.Value;
Properties.Settings.Default.Save();
DialogResult = DialogResult.OK;
Close();
@ -72,5 +80,10 @@ namespace AssetStudioGUI
DialogResult = DialogResult.Cancel;
Close();
}
private void parallelExportCheckBox_CheckedChanged(object sender, EventArgs e)
{
parallelExportUpDown.Enabled = parallelExportCheckBox.Checked;
}
}
}

View File

@ -120,4 +120,10 @@
<metadata name="optionTooltip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
<metadata name="optionTooltip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
<metadata name="optionTooltip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
</root>

View File

@ -9,79 +9,6 @@ namespace AssetStudioGUI
{
internal static class Exporter
{
public static bool ExportTexture2D(AssetItem item, string exportPath)
{
var m_Texture2D = (Texture2D)item.Asset;
if (Properties.Settings.Default.convertTexture)
{
var type = Properties.Settings.Default.convertType;
if (!TryExportFile(exportPath, item, "." + type.ToString().ToLower(), out var exportFullPath))
return false;
var image = m_Texture2D.ConvertToImage(true);
if (image == null)
return false;
using (image)
{
using (var file = File.OpenWrite(exportFullPath))
{
image.WriteToStream(file, type);
}
return true;
}
}
else
{
if (!TryExportFile(exportPath, item, ".tex", out var exportFullPath))
return false;
File.WriteAllBytes(exportFullPath, m_Texture2D.image_data.GetData());
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++;
}
}
return count > 0;
}
public static bool ExportAudioClip(AssetItem item, string exportPath)
{
var m_AudioClip = (AudioClip)item.Asset;
var m_AudioData = m_AudioClip.m_AudioData.GetData();
if (m_AudioData == null || m_AudioData.Length == 0)
return false;
var converter = new AudioClipConverter(m_AudioClip);
if (Properties.Settings.Default.convertAudio && converter.IsSupport)
{
if (!TryExportFile(exportPath, item, ".wav", out var exportFullPath))
return false;
var buffer = converter.ConvertToWav(m_AudioData);
if (buffer == null)
return false;
File.WriteAllBytes(exportFullPath, buffer);
}
else
{
if (!TryExportFile(exportPath, item, converter.GetExtensionName(), out var exportFullPath))
return false;
File.WriteAllBytes(exportFullPath, m_AudioData);
}
return true;
}
public static bool ExportShader(AssetItem item, string exportPath)
{
if (!TryExportFile(exportPath, item, ".shader", out var exportFullPath))
@ -256,36 +183,15 @@ namespace AssetStudioGUI
return true;
}
public static bool ExportSprite(AssetItem item, string exportPath)
{
var type = Properties.Settings.Default.convertType;
var spriteMaskMode = Properties.Settings.Default.exportSpriteWithMask ? SpriteMaskMode.Export : SpriteMaskMode.Off;
if (!TryExportFile(exportPath, item, "." + type.ToString().ToLower(), out var exportFullPath))
return false;
var image = ((Sprite)item.Asset).GetImage(spriteMaskMode: spriteMaskMode);
if (image != null)
{
using (image)
{
using (var file = File.OpenWrite(exportFullPath))
{
image.WriteToStream(file, type);
}
return true;
}
}
return false;
}
public static bool ExportRawFile(AssetItem item, string exportPath)
{
if (!TryExportFile(exportPath, item, ".dat", out var exportFullPath))
if (!TryExportFile(exportPath, item, ".dat", out var exportFullPath, mode: "ExportRaw"))
return false;
File.WriteAllBytes(exportFullPath, item.Asset.GetRawData());
return true;
}
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 filenameFormatIndex = Properties.Settings.Default.filenameFormat;
@ -313,7 +219,7 @@ namespace AssetStudioGUI
return true;
}
}
Logger.Warning($"Export error. File \"{fullPath.Color(ColorConsole.BrightYellow)}\" already exist");
Logger.Warning($"{mode} failed. File \"{fullPath.Color(ColorConsole.BrightYellow)}\" already exist");
return false;
}
@ -370,7 +276,7 @@ namespace AssetStudioGUI
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)
@ -396,11 +302,10 @@ namespace AssetStudioGUI
{
case ClassIDType.Texture2D:
case ClassIDType.Texture2DArrayImage:
return ExportTexture2D(item, exportPath);
case ClassIDType.Texture2DArray:
return ExportTexture2DArray(item, exportPath);
case ClassIDType.AudioClip:
return ExportAudioClip(item, exportPath);
case ClassIDType.Sprite:
throw new System.NotImplementedException();
case ClassIDType.Shader:
return ExportShader(item, exportPath);
case ClassIDType.TextAsset:
@ -415,8 +320,6 @@ namespace AssetStudioGUI
return ExportVideoClip(item, exportPath);
case ClassIDType.MovieTexture:
return ExportMovieTexture(item, exportPath);
case ClassIDType.Sprite:
return ExportSprite(item, exportPath);
case ClassIDType.Animator:
return ExportAnimator(item, exportPath);
case ClassIDType.AnimationClip:
@ -428,8 +331,9 @@ namespace AssetStudioGUI
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, '_'));
}
}
}

View File

@ -1,20 +1,26 @@
using AssetStudio;
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace AssetStudioGUI
{
class GUILogger : ILogger
{
public bool ShowDebugMessage = false;
private bool IsFileLoggerRunning = false;
private string LoggerInitString;
private string FileLogName;
private string FileLogPath;
public static bool ShowDebugMessage = false;
private bool isFileLoggerRunning = false;
private string loggerInitString;
private string fileLogName;
private string fileLogPath;
private Action<string> action;
private CancellationTokenSource tokenSource;
private BlockingCollection<string> consoleLogMessageCollection = new BlockingCollection<string>();
private BlockingCollection<string> fileLogMessageCollection = new BlockingCollection<string>();
private bool _useFileLogger = false;
public bool UseFileLogger
@ -23,19 +29,23 @@ namespace AssetStudioGUI
set
{
_useFileLogger = value;
if (_useFileLogger && !IsFileLoggerRunning)
if (_useFileLogger && !isFileLoggerRunning)
{
var appAssembly = typeof(Program).Assembly.GetName();
FileLogName = $"{appAssembly.Name}_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.log";
FileLogPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, FileLogName);
fileLogName = $"{appAssembly.Name}_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.log";
fileLogPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileLogName);
tokenSource = new CancellationTokenSource();
isFileLoggerRunning = true;
LogToFile(LoggerEvent.Verbose, $"# {LoggerInitString} - Logger launched #");
IsFileLoggerRunning = true;
ConcurrentFileWriter(tokenSource.Token);
LogToFile(LoggerEvent.Verbose, $"# {loggerInitString} - Logger launched #");
}
else if (!_useFileLogger && IsFileLoggerRunning)
else if (!_useFileLogger && isFileLoggerRunning)
{
LogToFile(LoggerEvent.Verbose, "# Logger closed #");
IsFileLoggerRunning = false;
isFileLoggerRunning = false;
tokenSource.Cancel();
tokenSource.Dispose();
}
}
}
@ -47,7 +57,7 @@ namespace AssetStudioGUI
var appAssembly = typeof(Program).Assembly.GetName();
var arch = Environment.Is64BitProcess ? "x64" : "x32";
var frameworkName = AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName;
LoggerInitString = $"{appAssembly.Name} v{appAssembly.Version} [{arch}] [{frameworkName}]";
loggerInitString = $"{appAssembly.Name} v{appAssembly.Version} [{arch}] [{frameworkName}]";
try
{
Console.Title = $"Console Logger - {appAssembly.Name} v{appAssembly.Version}";
@ -57,7 +67,9 @@ namespace AssetStudioGUI
{
// ignored
}
Console.WriteLine($"# {LoggerInitString}");
ConcurrentConsoleWriter();
Console.WriteLine($"# {loggerInitString}");
}
private static string ColorLogLevel(LoggerEvent logLevel)
@ -79,7 +91,7 @@ namespace AssetStudioGUI
private static string FormatMessage(LoggerEvent logMsgLevel, string message, bool toConsole)
{
message = message.TrimEnd();
var multiLine = message.Contains('\n');
var multiLine = message.Contains("\n");
string formattedMessage;
if (toConsole)
@ -88,7 +100,7 @@ namespace AssetStudioGUI
formattedMessage = $"{colorLogLevel} {message}";
if (multiLine)
{
formattedMessage = formattedMessage.Replace("\n", $"\n{colorLogLevel} ");
formattedMessage = formattedMessage.Replace("\n", $"\n{colorLogLevel} ") + $"\n{colorLogLevel}";
}
}
else
@ -99,19 +111,48 @@ namespace AssetStudioGUI
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;
}
private async void LogToFile(LoggerEvent logMsgLevel, string message)
private void ConcurrentFileWriter(CancellationToken token)
{
using (var sw = new StreamWriter(FileLogPath, append: true, System.Text.Encoding.UTF8))
Task.Run(() =>
{
await sw.WriteLineAsync(FormatMessage(logMsgLevel, message, toConsole: false));
}
using (var sw = new StreamWriter(fileLogPath, append: true, System.Text.Encoding.UTF8))
{
sw.AutoFlush = true;
foreach (var msg in fileLogMessageCollection.GetConsumingEnumerable())
{
sw.WriteLine(msg);
if (token.IsCancellationRequested)
break;
}
}
}, token);
}
private void ConcurrentConsoleWriter()
{
Task.Run(() =>
{
foreach (var msg in consoleLogMessageCollection.GetConsumingEnumerable())
{
Console.WriteLine(msg);
}
});
}
private void LogToFile(LoggerEvent logMsgLevel, string message)
{
fileLogMessageCollection.Add(FormatMessage(logMsgLevel, message, toConsole: false));
}
private void LogToConsole(LoggerEvent logMsgLevel, string message)
{
consoleLogMessageCollection.Add(FormatMessage(logMsgLevel, message, toConsole: true));
}
public void Log(LoggerEvent loggerEvent, string message, bool ignoreLevel)
@ -125,7 +166,7 @@ namespace AssetStudioGUI
//Console logger
if (!ShowDebugMessage && loggerEvent == LoggerEvent.Debug)
return;
Console.WriteLine(FormatMessage(loggerEvent, message, toConsole: true));
LogToConsole(loggerEvent, message);
//GUI logger
switch (loggerEvent)

View File

@ -0,0 +1,226 @@
using AssetStudio;
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Text;
namespace AssetStudioGUI
{
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 (Properties.Settings.Default.convertTexture)
{
var type = Properties.Settings.Default.convertType;
if (!TryExportFile(exportPath, item, "." + type.ToString().ToLower(), out var exportFullPath))
return false;
if (GUILogger.ShowDebugMessage)
{
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.Warning($"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 = Properties.Settings.Default.convertType;
var spriteMaskMode = Properties.Settings.Default.exportSpriteWithMask ? SpriteMaskMode.Export : SpriteMaskMode.Off;
if (!TryExportFile(exportPath, item, "." + type.ToString().ToLower(), out var exportFullPath))
return false;
var image = ((Sprite)item.Asset).GetImage(spriteMaskMode: spriteMaskMode);
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.Warning($"Failed to export \"{item.Text}\": AudioData was not found");
return false;
}
var converter = new AudioClipConverter(m_AudioClip);
if (Properties.Settings.Default.convertAudio && converter.IsSupport)
{
if (!TryExportFile(exportPath, item, ".wav", out exportFullPath))
return false;
if (GUILogger.ShowDebugMessage)
{
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.Warning($"{debugLog}Failed to export \"{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 (GUILogger.ShowDebugMessage)
{
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 filenameFormatIndex = Properties.Settings.Default.filenameFormat;
switch (filenameFormatIndex)
{
case 1: //assetName@pathID
fileName = $"{fileName} @{item.m_PathID}";
break;
case 2: //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 (filenameFormatIndex == 0) //assetName
{
fullPath = Path.Combine(dir, fileName + item.UniqueID + extension);
if (!File.Exists(fullPath))
{
Directory.CreateDirectory(dir);
return true;
}
}
Logger.Warning($"Export failed. File \"{fullPath.Color(ColorConsole.BrightYellow)}\" 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();
}
}
}

View File

@ -12,7 +12,7 @@ namespace AssetStudioGUI.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.8.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.9.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
@ -358,5 +358,29 @@ namespace AssetStudioGUI.Properties {
this["filenameFormat"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("True")]
public bool parallelExport {
get {
return ((bool)(this["parallelExport"]));
}
set {
this["parallelExport"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("-1")]
public int parallelExportCount {
get {
return ((int)(this["parallelExportCount"]));
}
set {
this["parallelExportCount"] = value;
}
}
}
}

View File

@ -86,5 +86,11 @@
<Setting Name="filenameFormat" Type="System.Int32" Scope="User">
<Value Profile="(Default)">0</Value>
</Setting>
<Setting Name="parallelExport" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">True</Value>
</Setting>
<Setting Name="parallelExportCount" Type="System.Int32" Scope="User">
<Value Profile="(Default)">-1</Value>
</Setting>
</Settings>
</SettingsFile>

View File

@ -1,12 +1,14 @@
using AssetStudio;
using CubismLive2DExtractor;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Xml.Linq;
using static AssetStudioGUI.Exporter;
@ -468,13 +470,21 @@ namespace AssetStudioGUI
{
Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
int toExportCount = toExportAssets.Count;
int exportedCount = 0;
int i = 0;
Progress.Reset();
var groupOption = (AssetGroupOption)Properties.Settings.Default.assetGroupOption;
var parallelExportCount = Properties.Settings.Default.parallelExportCount <= 0
? Environment.ProcessorCount - 1
: Math.Min(Properties.Settings.Default.parallelExportCount, Environment.ProcessorCount - 1);
parallelExportCount = Properties.Settings.Default.parallelExport ? parallelExportCount : 1;
var toExportAssetDict = new ConcurrentDictionary<AssetItem, string>();
var toParallelExportAssetDict = new ConcurrentDictionary<AssetItem, string>();
var exceptionMsgs = new ConcurrentDictionary<Exception, string>();
var mode = exportType == ExportType.Dump ? "Dump" : "Export";
foreach (var asset in toExportAssets)
var toExportCount = toExportAssets.Count;
var exportedCount = 0;
var i = 0;
Progress.Reset();
Parallel.ForEach(toExportAssets, asset =>
{
string exportPath;
switch (groupOption)
@ -522,10 +532,49 @@ namespace AssetStudioGUI
break;
}
exportPath += Path.DirectorySeparatorChar;
Logger.Info($"[{exportedCount + 1}/{toExportCount}] {mode}ing {asset.TypeString}: {asset.Text}");
if (exportType == ExportType.Convert)
{
switch (asset.Type)
{
case ClassIDType.Texture2D:
case ClassIDType.Texture2DArrayImage:
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
{
Logger.Info($"[{exportedCount + 1}/{toExportCount}] {mode}ing {asset.TypeString}: {asset.Text}");
switch (exportType)
{
case ExportType.Raw:
@ -556,14 +605,58 @@ namespace AssetStudioGUI
Progress.Report(++i, toExportCount);
}
var statusText = exportedCount == 0 ? "Nothing exported." : $"Finished {mode.ToLower()}ing {exportedCount} assets.";
if (toExportCount > exportedCount)
Parallel.ForEach(toParallelExportAssetDict, new ParallelOptions { MaxDegreeOfParallelism = parallelExportCount }, (toExportAsset, loopState) =>
{
statusText += $" {toExportCount - exportedCount} assets skipped (not extractable or files already exist)";
var asset = toExportAsset.Key;
var exportPath = toExportAsset.Value;
try
{
if (ParallelExporter.ParallelExportConvertFile(asset, exportPath, out var debugLog))
{
Interlocked.Increment(ref exportedCount);
if (GUILogger.ShowDebugMessage)
{
Logger.Debug(debugLog);
StatusStripUpdate($"[{exportedCount}/{toExportCount}] Exporting {asset.TypeString}: {asset.Text}");
}
else
{
Logger.Info($"[{exportedCount}/{toExportCount}] Exporting {asset.TypeString}: {asset.Text}");
}
}
Interlocked.Increment(ref i);
Progress.Report(i, toExportCount);
}
catch (Exception ex)
{
if (parallelExportCount == 1)
{
Logger.Error($"{mode} {asset.TypeString}: {asset.Text} error", ex);
}
else
{
loopState.Break();
exceptionMsgs.TryAdd(ex, $"Exception occurred when exporting {asset.TypeString}: {asset.Text}\n{ex}\n");
}
}
});
ParallelExporter.ClearHash();
foreach (var ex in exceptionMsgs)
{
Logger.Error(ex.Value);
}
var statusText = exportedCount == 0 ? "Nothing exported." : $"Finished {mode.ToLower()}ing [{exportedCount}/{toExportCount}] assets.";
if (toExportCount > exportedCount)
{
statusText += exceptionMsgs.IsEmpty
? $" {toExportCount - exportedCount} assets skipped (not extractable or files already exist)."
: " Export process was stopped because one or more exceptions occurred.";
Progress.Report(toExportCount, toExportCount);
}
Logger.Info(statusText);
exceptionMsgs.Clear();
if (Properties.Settings.Default.openAfterExport && exportedCount > 0)
{
@ -607,11 +700,11 @@ namespace AssetStudioGUI
break;
}
var statusText = $"Finished exporting asset list with {toExportAssets.Count()} items.";
var statusText = $"Finished exporting asset list with {toExportAssets.Count} items.";
Logger.Info(statusText);
if (Properties.Settings.Default.openAfterExport && toExportAssets.Count() > 0)
if (Properties.Settings.Default.openAfterExport && toExportAssets.Count > 0)
{
OpenFolderInExplorer(savePath);
}
@ -884,7 +977,7 @@ namespace AssetStudioGUI
}
mocPathDict.Clear();
var lookup = l2dResourceContainers.ToLookup(
var lookup = l2dResourceContainers.AsParallel().ToLookup(
x => mocPathList.Find(b => x.Value.Contains(b) && x.Value.Split('/').Any(y => y == b.Substring(b.LastIndexOf("/") + 1))),
x => x.Key
);
@ -897,6 +990,10 @@ namespace AssetStudioGUI
var totalModelCount = lookup.LongCount(x => x.Key != null);
var modelCounter = 0;
var parallelExportCount = Properties.Settings.Default.parallelExportCount <= 0
? Environment.ProcessorCount - 1
: Math.Min(Properties.Settings.Default.parallelExportCount, Environment.ProcessorCount - 1);
parallelExportCount = Properties.Settings.Default.parallelExport ? parallelExportCount : 1;
foreach (var assets in lookup)
{
var srcContainer = assets.Key;
@ -916,7 +1013,7 @@ namespace AssetStudioGUI
var destPath = Path.Combine(baseDestPath, container) + Path.DirectorySeparatorChar;
var modelExtractor = new Live2DExtractor(assets, selClipMotions, selFadeMotions, selFadeLst);
modelExtractor.ExtractCubismModel(destPath, modelName, motionMode, assemblyLoader, forceBezier);
modelExtractor.ExtractCubismModel(destPath, modelName, motionMode, assemblyLoader, forceBezier, parallelExportCount);
modelCounter++;
}
catch (Exception ex)