Fix some bugs with invalid names during export

This commit is contained in:
VaDiM
2026-06-19 21:04:55 +03:00
parent a5825bd693
commit f7352c18dd
6 changed files with 143 additions and 67 deletions
+54 -14
View File
@@ -11,10 +11,16 @@ namespace AssetStudioCLI
internal static class Exporter
{
private static readonly HashSet<string> ExportPathHashSet = new HashSet<string>(System.StringComparer.OrdinalIgnoreCase);
private static readonly HashSet<char> InvalidChars = new HashSet<char>(Path.GetInvalidFileNameChars());
static Exporter()
{
InvalidChars.UnionWith(new[] { '"', '<', '>', '|', ':', '*', '?', '\\', '/' });
}
private static bool TryExportFile(string dir, AssetItem item, string extension, out string fullPath, string mode = "Export")
{
var fileName = FixFileName(item.Text);
var fileName = item.Text;
var filenameFormat = CLIOptions.o_filenameFormat.Value;
var canOverwrite = CLIOptions.f_overwriteExisting.Value;
switch (filenameFormat)
@@ -26,7 +32,7 @@ namespace AssetStudioCLI
fileName = item.m_PathID.ToString();
break;
}
fullPath = Path.Combine(dir, fileName + extension);
fullPath = SafeFilePath(dir, $"{fileName}{extension}");
if (ExportPathHashSet.Add(fullPath))
{
if (CanWrite(fullPath, dir, canOverwrite))
@@ -36,7 +42,7 @@ namespace AssetStudioCLI
}
else if (filenameFormat == FilenameFormat.AssetName)
{
fullPath = Path.Combine(dir, fileName + item.UniqueID + extension);
fullPath = SafeFilePath(dir, $"{fileName}{item.UniqueID}{extension}");
if (CanWrite(fullPath, dir, canOverwrite))
{
return true;
@@ -268,10 +274,10 @@ namespace AssetStudioCLI
public static bool ExportAnimator(AssetItem item, string exportPath, List<AssetItem> animationList = null)
{
var exportFullPath = Path.Combine(exportPath, "FBX_Animator", item.Text, item.Text + ".fbx");
var exportFullPath = SafeFilePath(exportPath, "FBX_Animator", item.Text, item.Text + ".fbx");
if (File.Exists(exportFullPath))
{
exportFullPath = Path.Combine(exportPath, item.Text + item.UniqueID, item.Text + ".fbx");
exportFullPath = SafeFilePath(exportPath, item.Text + item.UniqueID, item.Text + ".fbx");
}
var m_Animator = (Animator)item.Asset;
var convert = animationList != null
@@ -302,10 +308,10 @@ namespace AssetStudioCLI
{
var jsonDoc = mat.ToJsonDoc();
var str = System.Text.Json.JsonSerializer.Serialize(jsonDoc, Object.JsonOptions);
var exportFullPath = Path.Combine(exportDir, $"{mat.m_Name}.json");
var exportFullPath = SafeFilePath(exportDir, $"{mat.m_Name}.json");
if (File.Exists(exportFullPath))
{
exportFullPath = Path.Combine(exportDir, $"{mat.m_Name} #{mat.GetHashCode():x}.json");
exportFullPath = SafeFilePath(exportDir, $"{mat.m_Name} #{mat.GetHashCode():x}.json");
}
File.WriteAllText(exportFullPath, str);
}
@@ -436,20 +442,54 @@ namespace AssetStudioCLI
var convert = animationList != null
? new ModelConverter(gameObject, CLIOptions.o_imageFormat.Value, animationList.Select(x => (AnimationClip)x.Asset).ToList())
: new ModelConverter(gameObject, CLIOptions.o_imageFormat.Value);
var modelName = FixFileName(gameObject.m_Name);
var exportFullPath = Path.Combine(exportPath, "FBX_GameObjects", modelName, modelName + ".fbx");
var modelName = gameObject.m_Name;
var exportFullPath = SafeFilePath(exportPath, "FBX_GameObjects", modelName, modelName + ".fbx");
if (File.Exists(exportFullPath))
{
exportFullPath = Path.Combine(exportPath, $"{modelName}_{gameObject.GetHashCode():X}", modelName + ".fbx");
exportFullPath = SafeFilePath(exportPath, $"{modelName}_{gameObject.GetHashCode():X}", modelName + ".fbx");
}
ExportFbx(convert, exportFullPath);
}
public static string FixFileName(string str)
public static string SafeFileName(string str)
{
return str.Length >= 260
? Path.GetRandomFileName()
: Path.GetInvalidFileNameChars().Aggregate(str, (current, c) => current.Replace(c, '_'));
if (str.Length >= 255)
{
var newName = Path.GetRandomFileName();
Logger.Warning("The asset file name is too long.\n" +
$"\"{str}\"\n" +
"Was replaced with:\n" +
$"\"{newName}\"");
return newName;
}
return InvalidChars.Aggregate(str, (current, c) => current.Replace(c, '_')).Trim();
}
public static string SafeFilePath(string path, params string[] names)
{
var result = path;
foreach (var name in names)
{
result = Path.Combine(result, SafeFileName(name));
}
if (result.Length >= 4096)
{
var newPath = Path.Combine(Directory.GetCurrentDirectory(), SafeFileName(names.Last()));
Logger.Warning("The export path is too long.\n" +
$"\"{result}\"\n" +
"Was reduced to:\n" +
$"\"{newPath}\"");
return newPath;
}
return Path.GetInvalidPathChars().Aggregate(result, (current, c) => current.Replace(c, '_'));
}
public static string SafePathCombine(string pathA, string pathB)
{
return SafeFilePath(pathA, pathB.Split('\\', '/'));
}
public static void ClearHash()
+3 -11
View File
@@ -3,7 +3,6 @@ using AssetStudioCLI.Options;
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Text;
namespace AssetStudioCLI
@@ -192,7 +191,7 @@ namespace AssetStudioCLI
private static bool TryExportFile(string dir, AssetItem item, string extension, out string fullPath)
{
var fileName = FixFileName(item.Text);
var fileName = item.Text;
var filenameFormat = CLIOptions.o_filenameFormat.Value;
var canOverwrite = CLIOptions.f_overwriteExisting.Value;
switch (filenameFormat)
@@ -208,7 +207,7 @@ namespace AssetStudioCLI
break;
}
extension = extension.ToLower();
fullPath = Path.Combine(dir, $"{fileName}{extension}");
fullPath = Exporter.SafeFilePath(dir, $"{fileName}{extension}");
if (ExportPathDict.TryAdd(fullPath, true))
{
if (CanWrite(fullPath, dir, canOverwrite))
@@ -218,7 +217,7 @@ namespace AssetStudioCLI
}
else if (filenameFormat == FilenameFormat.AssetName || (filenameFormat == FilenameFormat.AssetName_Hash && item.Hash == 0))
{
fullPath = Path.Combine(dir, $"{fileName}{item.UniqueID}{extension}");
fullPath = Exporter.SafeFilePath(dir, $"{fileName}{item.UniqueID}{extension}");
if (CanWrite(fullPath, dir, canOverwrite))
{
return true;
@@ -252,13 +251,6 @@ namespace AssetStudioCLI
}
}
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()
{
ExportPathDict.Clear();
+7 -7
View File
@@ -679,7 +679,7 @@ namespace AssetStudioCLI
exportPath = Path.Combine(savePath, containerPath);
if (groupOption == AssetGroupOption.ContainerPathFull)
{
exportPath = Path.Combine(exportPath, Path.GetFileNameWithoutExtension(asset.Container));
exportPath = Exporter.SafeFilePath(exportPath, Path.GetFileNameWithoutExtension(asset.Container));
}
}
else
@@ -700,7 +700,7 @@ namespace AssetStudioCLI
case AssetGroupOption.SceneHierarchy:
if (asset.Node != null)
{
exportPath = Path.Combine(savePath, asset.Node.FullPath);
exportPath = Exporter.SafePathCombine(savePath, asset.Node.FullPath);
}
else
{
@@ -866,7 +866,7 @@ namespace AssetStudioCLI
var exportedCount = 0;
var k = 0;
Logger.Info($"Searching for objects to export..");
Logger.Info("Searching for objects to export..");
Progress.Reset();
var count = gameObjectTree.Sum(x => x.nodes.Count);
foreach (var node in gameObjectTree)
@@ -913,14 +913,14 @@ namespace AssetStudioCLI
foreach (var gameObjectNode in exportableObjects)
{
var gameObject = gameObjectNode.gameObject;
var filename = Exporter.FixFileName(gameObject.m_Name);
var targetPath = $"{savePath}{filename}{Path.DirectorySeparatorChar}";
var filename = gameObject.m_Name;
var targetPath = $"{Exporter.SafeFilePath(savePath, filename)}{Path.DirectorySeparatorChar}";
//重名文件处理
for (int i = 1; ; i++)
{
if (Directory.Exists(targetPath))
{
targetPath = $"{savePath}{filename} ({i}){Path.DirectorySeparatorChar}";
targetPath = $"{Exporter.SafeFilePath(savePath, filename)} ({i}){Path.DirectorySeparatorChar}";
}
else
{
@@ -1229,7 +1229,7 @@ namespace AssetStudioCLI
break;
}
var destPath = Path.Combine(baseDestPath, modelPath);
var destPath = Exporter.SafePathCombine(baseDestPath, modelPath);
cubismExtractor.ExtractCubismModel(destPath, motionMode, forceBezier, parallelTaskCount);
modelCounter++;
}
+52 -12
View File
@@ -10,10 +10,16 @@ namespace AssetStudioGUI
internal static class Exporter
{
private static readonly HashSet<string> ExportPathHashSet = new HashSet<string>(System.StringComparer.OrdinalIgnoreCase);
private static readonly HashSet<char> InvalidChars = new HashSet<char>(Path.GetInvalidFileNameChars());
static Exporter()
{
InvalidChars.UnionWith(new[] { '"', '<', '>', '|', ':', '*', '?', '\\', '/' });
}
private static bool TryExportFile(string dir, AssetItem item, string extension, out string fullPath, string mode = "Export")
{
var fileName = FixFileName(item.Text);
var fileName = item.Text;
var filenameFormatIndex = Properties.Settings.Default.filenameFormat;
var canOverwrite = Properties.Settings.Default.overwriteExistingFiles;
switch (filenameFormatIndex)
@@ -25,7 +31,7 @@ namespace AssetStudioGUI
fileName = item.m_PathID.ToString();
break;
}
fullPath = Path.Combine(dir, fileName + extension);
fullPath = SafeFilePath(dir, $"{fileName}{extension}");
if (ExportPathHashSet.Add(fullPath))
{
if (CanWrite(fullPath, dir, canOverwrite))
@@ -35,7 +41,7 @@ namespace AssetStudioGUI
}
else if (filenameFormatIndex == 0) //assetName
{
fullPath = Path.Combine(dir, fileName + item.UniqueID + extension);
fullPath = SafeFilePath(dir, $"{fileName}{item.UniqueID}{extension}");
if (CanWrite(fullPath, dir, canOverwrite))
{
return true;
@@ -234,10 +240,10 @@ namespace AssetStudioGUI
public static bool ExportAnimator(AssetItem item, string exportPath, List<AssetItem> animationList = null)
{
var exportFullPath = Path.Combine(exportPath, item.Text, item.Text + ".fbx");
var exportFullPath = SafeFilePath(exportPath, item.Text, item.Text + ".fbx");
if (File.Exists(exportFullPath))
{
exportFullPath = Path.Combine(exportPath, item.Text + item.UniqueID, item.Text + ".fbx");
exportFullPath = SafeFilePath(exportPath, item.Text + item.UniqueID, item.Text + ".fbx");
}
if (!Studio.FbxSettings.ExportAnimations)
animationList = new List<AssetItem>();
@@ -261,10 +267,10 @@ namespace AssetStudioGUI
{
var jsonDoc = mat.ToJsonDoc();
var str = System.Text.Json.JsonSerializer.Serialize(jsonDoc, Object.JsonOptions);
var exportFullPath = Path.Combine(exportDir, $"{mat.m_Name}.json");
var exportFullPath = SafeFilePath(exportDir, $"{mat.m_Name}.json");
if (File.Exists(exportFullPath))
{
exportFullPath = Path.Combine(exportDir, $"{mat.m_Name} #{mat.GetHashCode():x}.json");
exportFullPath = SafeFilePath(exportDir, $"{mat.m_Name} #{mat.GetHashCode():x}.json");
}
File.WriteAllText(exportFullPath, str);
}
@@ -398,7 +404,7 @@ namespace AssetStudioGUI
var convert = animationList != null
? new ModelConverter(gameObject, Properties.Settings.Default.convertType, animationList.Select(x => (AnimationClip)x.Asset).ToList())
: new ModelConverter(gameObject, Properties.Settings.Default.convertType);
exportPath = exportPath + FixFileName(gameObject.m_Name) + ".fbx";
exportPath = SafeFilePath(exportPath, $"{gameObject.m_Name}.fbx");
ExportFbx(convert, exportPath);
}
@@ -413,11 +419,45 @@ namespace AssetStudioGUI
ExportFbx(convert, exportPath);
}
public static string FixFileName(string str)
public static string SafeFileName(string str)
{
return str.Length >= 260
? Path.GetRandomFileName()
: Path.GetInvalidFileNameChars().Aggregate(str, (current, c) => current.Replace(c, '_'));
if (str.Length >= 255)
{
var newName = Path.GetRandomFileName();
Logger.Warning("The asset file name is too long.\n" +
$"\"{str}\"\n" +
"Was replaced with:\n" +
$"\"{newName}\"");
return newName;
}
return InvalidChars.Aggregate(str, (current, c) => current.Replace(c, '_')).Trim();
}
public static string SafeFilePath(string path, params string[] names)
{
var result = path;
foreach (var name in names)
{
result = Path.Combine(result, SafeFileName(name));
}
if (result.Length >= 4096)
{
var newPath = Path.Combine(Directory.GetCurrentDirectory(), SafeFileName(names.Last()));
Logger.Warning("The export path is too long.\n" +
$"\"{result}\"\n" +
"Was reduced to:\n" +
$"\"{newPath}\"");
return newPath;
}
return Path.GetInvalidPathChars().Aggregate(result, (current, c) => current.Replace(c, '_'));
}
public static string SafePathCombine(string pathA, string pathB)
{
return SafeFilePath(pathA, pathB.Split('\\', '/'));
}
public static void ClearHash()
@@ -2,7 +2,6 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Text;
namespace AssetStudioGUI
@@ -191,7 +190,7 @@ namespace AssetStudioGUI
private static bool TryExportFile(string dir, AssetItem item, string extension, out string fullPath)
{
var fileName = FixFileName(item.Text);
var fileName = item.Text;
var filenameFormatIndex = Properties.Settings.Default.filenameFormat;
var canOverwrite = Properties.Settings.Default.overwriteExistingFiles;
switch (filenameFormatIndex)
@@ -207,7 +206,7 @@ namespace AssetStudioGUI
break;
}
extension = extension.ToLower();
fullPath = Path.Combine(dir, $"{fileName}{extension}");
fullPath = Exporter.SafeFilePath(dir, $"{fileName}{extension}");
if (ExportPathDict.TryAdd(fullPath, true))
{
if (CanWrite(fullPath, dir, canOverwrite))
@@ -217,7 +216,7 @@ namespace AssetStudioGUI
}
else if (filenameFormatIndex == 0 || (filenameFormatIndex == 3 && item.Hash == 0)) //assetName, asset name@hash
{
fullPath = Path.Combine(dir, $"{fileName}{item.UniqueID}{extension}");
fullPath = Exporter.SafeFilePath(dir, $"{fileName}{item.UniqueID}{extension}");
if (CanWrite(fullPath, dir, canOverwrite))
{
return true;
@@ -251,13 +250,6 @@ namespace AssetStudioGUI
}
}
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()
{
ExportPathDict.Clear();
+24 -12
View File
@@ -552,7 +552,7 @@ namespace AssetStudioGUI
exportPath = Path.Combine(savePath, Path.GetDirectoryName(asset.Container));
if (groupOption == AssetGroupOption.ContainerPathFull)
{
exportPath = Path.Combine(exportPath, Path.GetFileNameWithoutExtension(asset.Container));
exportPath = Exporter.SafeFilePath(exportPath, Path.GetFileNameWithoutExtension(asset.Container));
}
}
else
@@ -573,7 +573,7 @@ namespace AssetStudioGUI
case AssetGroupOption.SceneHierarchy:
if (asset.TreeNode != null)
{
exportPath = Path.Combine(savePath, asset.TreeNode.FullPath);
exportPath = Exporter.SafePathCombine(savePath, asset.TreeNode.FullPath);
}
else
{
@@ -793,15 +793,15 @@ namespace AssetStudioGUI
continue;
}
//处理非法文件名
var filename = Exporter.FixFileName(j.Text);
var fileName = j.Text;
//每个文件存放在单独的文件夹
var targetPath = $"{savePath}{filename}{Path.DirectorySeparatorChar}";
var targetPath = $"{Exporter.SafeFilePath(savePath, fileName)}{Path.DirectorySeparatorChar}";
//重名文件处理
for (int i = 1;; i++)
{
if (Directory.Exists(targetPath))
{
targetPath = $"{savePath}{filename} ({i}){Path.DirectorySeparatorChar}";
targetPath = $"{Exporter.SafeFilePath(savePath, fileName)} ({i}){Path.DirectorySeparatorChar}";
}
else
{
@@ -810,7 +810,7 @@ namespace AssetStudioGUI
}
Directory.CreateDirectory(targetPath);
//导出FBX
Logger.Info($"Exporting {filename}.fbx");
Logger.Info($"Exporting {fileName}.fbx");
try
{
Exporter.ExportGameObject(j.gameObject, targetPath);
@@ -821,7 +821,7 @@ namespace AssetStudioGUI
}
Progress.Report(++k, count);
Logger.Info($"Finished exporting {filename}.fbx");
Logger.Info($"Finished exporting {fileName}.fbx");
}
}
if (Properties.Settings.Default.openAfterExport)
@@ -878,16 +878,28 @@ namespace AssetStudioGUI
Progress.Reset();
foreach (var gameObject in gameObjects)
{
Logger.Info($"Exporting {gameObject.m_Name}");
var fileName = gameObject.m_Name;
Logger.Info($"Exporting {fileName}");
try
{
var modelExportPath = Path.Combine(exportPath, gameObject.m_Name) + Path.DirectorySeparatorChar;
var modelExportPath = $"{Exporter.SafeFilePath(exportPath, fileName)}{Path.DirectorySeparatorChar}";
for (var j = 1;; j++)
{
if (Directory.Exists(modelExportPath))
{
modelExportPath = $"{Exporter.SafeFilePath(exportPath, fileName)} ({i}){Path.DirectorySeparatorChar}";
}
else
{
break;
}
}
Exporter.ExportGameObject(gameObject, modelExportPath, animationList);
Logger.Info($"Finished exporting {gameObject.m_Name}");
Logger.Info($"Finished exporting {fileName}");
}
catch (Exception ex)
{
Logger.Error($"Export GameObject:{gameObject.m_Name} error", ex);
Logger.Error($"Export GameObject:{fileName} error", ex);
Logger.Info("Error in export");
}
@@ -1189,7 +1201,7 @@ namespace AssetStudioGUI
: container;
break;
}
var destPath = Path.Combine(baseDestPath, modelPath);
var destPath = Exporter.SafePathCombine(baseDestPath, modelPath);
cubismExtractor.ExtractCubismModel(destPath, motionMode, forceBezier, GetParallelExportCount());
modelCounter++;
}