mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-13 12:14:17 +01:00
Use existing game path functionality for sklb resolving, some cleanup.
This commit is contained in:
parent
c33545acdf
commit
51bb9cf7cd
4 changed files with 69 additions and 113 deletions
|
|
@ -1 +1 @@
|
||||||
Subproject commit ac3fc0981ac8f503ac91d2419bd28c54f271763e
|
Subproject commit 83c012752cd9d13d39248eda85ab18cc59070a76
|
||||||
|
|
@ -1,27 +1,20 @@
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using OtterGui.Tasks;
|
using OtterGui.Tasks;
|
||||||
using Penumbra.Collections.Manager;
|
using Penumbra.GameData.Data;
|
||||||
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.GameData.Files;
|
using Penumbra.GameData.Files;
|
||||||
using Penumbra.Import.Models.Export;
|
using Penumbra.Import.Models.Export;
|
||||||
using SharpGLTF.Scenes;
|
using SharpGLTF.Scenes;
|
||||||
|
|
||||||
namespace Penumbra.Import.Models;
|
namespace Penumbra.Import.Models;
|
||||||
|
|
||||||
public sealed class ModelManager : SingleTaskQueue, IDisposable
|
public sealed class ModelManager(IFramework framework, GamePathParser _parser) : SingleTaskQueue, IDisposable
|
||||||
{
|
{
|
||||||
private readonly IFramework _framework;
|
private readonly IFramework _framework = framework;
|
||||||
private readonly IDataManager _gameData;
|
|
||||||
private readonly ActiveCollectionData _activeCollectionData;
|
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<IAction, (Task, CancellationTokenSource)> _tasks = new();
|
private readonly ConcurrentDictionary<IAction, (Task, CancellationTokenSource)> _tasks = new();
|
||||||
private bool _disposed = false;
|
|
||||||
|
|
||||||
public ModelManager(IFramework framework, IDataManager gameData, ActiveCollectionData activeCollectionData)
|
private bool _disposed;
|
||||||
{
|
|
||||||
_framework = framework;
|
|
||||||
_gameData = gameData;
|
|
||||||
_activeCollectionData = activeCollectionData;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
|
@ -31,6 +24,31 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable
|
||||||
_tasks.Clear();
|
_tasks.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task ExportToGltf(MdlFile mdl, SklbFile? sklb, string outputPath)
|
||||||
|
=> Enqueue(new ExportToGltfAction(this, mdl, sklb, outputPath));
|
||||||
|
|
||||||
|
/// <summary> Try to find the .sklb path for a .mdl file. </summary>
|
||||||
|
/// <param name="mdlPath"> .mdl file to look up the skeleton for. </param>
|
||||||
|
public string? ResolveSklbForMdl(string mdlPath)
|
||||||
|
{
|
||||||
|
var info = _parser.GetFileInfo(mdlPath);
|
||||||
|
if (info.FileType is not FileType.Model)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return info.ObjectType switch
|
||||||
|
{
|
||||||
|
ObjectType.Equipment => GamePaths.Skeleton.Sklb.Path(info.GenderRace, "base", 1),
|
||||||
|
ObjectType.Accessory => GamePaths.Skeleton.Sklb.Path(info.GenderRace, "base", 1),
|
||||||
|
ObjectType.Character when info.BodySlot is BodySlot.Body or BodySlot.Tail => GamePaths.Skeleton.Sklb.Path(info.GenderRace, "base",
|
||||||
|
1),
|
||||||
|
ObjectType.Character => throw new Exception($"Currently unsupported human model type \"{info.BodySlot}\"."),
|
||||||
|
ObjectType.DemiHuman => GamePaths.DemiHuman.Sklb.Path(info.PrimaryId),
|
||||||
|
ObjectType.Monster => GamePaths.Monster.Sklb.Path(info.PrimaryId),
|
||||||
|
ObjectType.Weapon => GamePaths.Weapon.Sklb.Path(info.PrimaryId),
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private Task Enqueue(IAction action)
|
private Task Enqueue(IAction action)
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
|
|
@ -39,44 +57,34 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable
|
||||||
Task task;
|
Task task;
|
||||||
lock (_tasks)
|
lock (_tasks)
|
||||||
{
|
{
|
||||||
task = _tasks.GetOrAdd(action, action =>
|
task = _tasks.GetOrAdd(action, a =>
|
||||||
{
|
{
|
||||||
var token = new CancellationTokenSource();
|
var token = new CancellationTokenSource();
|
||||||
var task = Enqueue(action, token.Token);
|
var t = Enqueue(a, token.Token);
|
||||||
task.ContinueWith(_ => _tasks.TryRemove(action, out var unused), CancellationToken.None);
|
t.ContinueWith(_ =>
|
||||||
return (task, token);
|
{
|
||||||
|
lock (_tasks)
|
||||||
|
{
|
||||||
|
return _tasks.TryRemove(a, out var unused);
|
||||||
|
}
|
||||||
|
}, CancellationToken.None);
|
||||||
|
return (t, token);
|
||||||
}).Item1;
|
}).Item1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task ExportToGltf(MdlFile mdl, SklbFile? sklb, string outputPath)
|
private class ExportToGltfAction(ModelManager manager, MdlFile mdl, SklbFile? sklb, string outputPath)
|
||||||
=> Enqueue(new ExportToGltfAction(this, mdl, sklb, outputPath));
|
: IAction
|
||||||
|
|
||||||
private class ExportToGltfAction : IAction
|
|
||||||
{
|
{
|
||||||
private readonly ModelManager _manager;
|
|
||||||
|
|
||||||
private readonly MdlFile _mdl;
|
|
||||||
private readonly SklbFile? _sklb;
|
|
||||||
private readonly string _outputPath;
|
|
||||||
|
|
||||||
public ExportToGltfAction(ModelManager manager, MdlFile mdl, SklbFile? sklb, string outputPath)
|
|
||||||
{
|
|
||||||
_manager = manager;
|
|
||||||
_mdl = mdl;
|
|
||||||
_sklb = sklb;
|
|
||||||
_outputPath = outputPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Execute(CancellationToken cancel)
|
public void Execute(CancellationToken cancel)
|
||||||
{
|
{
|
||||||
Penumbra.Log.Debug("Reading skeleton.");
|
Penumbra.Log.Debug("Reading skeleton.");
|
||||||
var xivSkeleton = BuildSkeleton(cancel);
|
var xivSkeleton = BuildSkeleton(cancel);
|
||||||
|
|
||||||
Penumbra.Log.Debug("Converting model.");
|
Penumbra.Log.Debug("Converting model.");
|
||||||
var model = ModelExporter.Export(_mdl, xivSkeleton);
|
var model = ModelExporter.Export(mdl, xivSkeleton);
|
||||||
|
|
||||||
Penumbra.Log.Debug("Building scene.");
|
Penumbra.Log.Debug("Building scene.");
|
||||||
var scene = new SceneBuilder();
|
var scene = new SceneBuilder();
|
||||||
|
|
@ -84,16 +92,16 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable
|
||||||
|
|
||||||
Penumbra.Log.Debug("Saving.");
|
Penumbra.Log.Debug("Saving.");
|
||||||
var gltfModel = scene.ToGltf2();
|
var gltfModel = scene.ToGltf2();
|
||||||
gltfModel.SaveGLTF(_outputPath);
|
gltfModel.SaveGLTF(outputPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Attempt to read out the pertinent information from a .sklb. </summary>
|
/// <summary> Attempt to read out the pertinent information from a .sklb. </summary>
|
||||||
private XivSkeleton? BuildSkeleton(CancellationToken cancel)
|
private XivSkeleton? BuildSkeleton(CancellationToken cancel)
|
||||||
{
|
{
|
||||||
if (_sklb == null)
|
if (sklb == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var xmlTask = _manager._framework.RunOnFrameworkThread(() => HavokConverter.HkxToXml(_sklb.Skeleton));
|
var xmlTask = manager._framework.RunOnFrameworkThread(() => HavokConverter.HkxToXml(sklb.Skeleton));
|
||||||
xmlTask.Wait(cancel);
|
xmlTask.Wait(cancel);
|
||||||
var xml = xmlTask.Result;
|
var xml = xmlTask.Result;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ namespace Penumbra.UI.AdvancedWindow;
|
||||||
|
|
||||||
public partial class ModEditWindow
|
public partial class ModEditWindow
|
||||||
{
|
{
|
||||||
private partial class MdlTab : IWritable
|
private class MdlTab : IWritable
|
||||||
{
|
{
|
||||||
private ModEditWindow _edit;
|
private readonly ModEditWindow _edit;
|
||||||
|
|
||||||
public readonly MdlFile Mdl;
|
public readonly MdlFile Mdl;
|
||||||
private readonly List<string>[] _attributes;
|
private readonly List<string>[] _attributes;
|
||||||
|
|
@ -18,21 +18,10 @@ public partial class ModEditWindow
|
||||||
public List<Utf8GamePath>? GamePaths { get; private set; }
|
public List<Utf8GamePath>? GamePaths { get; private set; }
|
||||||
public int GamePathIndex;
|
public int GamePathIndex;
|
||||||
|
|
||||||
public bool PendingIo { get; private set; } = false;
|
public bool PendingIo { get; private set; }
|
||||||
public string? IoException { get; private set; } = null;
|
public string? IoException { get; private set; }
|
||||||
|
|
||||||
[GeneratedRegex(@"chara/(?:equipment|accessory)/(?'Set'[a-z]\d{4})/model/(?'Race'c\d{4})\k'Set'_[^/]+\.mdl", RegexOptions.Compiled)]
|
public MdlTab(ModEditWindow edit, byte[] bytes, string path, IMod? mod)
|
||||||
private static partial Regex CharaEquipmentRegex();
|
|
||||||
|
|
||||||
[GeneratedRegex(@"chara/human/(?'Race'c\d{4})/obj/(?'Type'[^/]+)/(?'Set'[^/]\d{4})/model/(?'Race'c\d{4})\k'Set'_[^/]+\.mdl",
|
|
||||||
RegexOptions.Compiled)]
|
|
||||||
private static partial Regex CharaHumanRegex();
|
|
||||||
|
|
||||||
[GeneratedRegex(@"chara/(?'SubCategory'demihuman|monster|weapon)/(?'Set'w\d{4})/obj/body/(?'Body'b\d{4})/model/\k'Set'\k'Body'.mdl",
|
|
||||||
RegexOptions.Compiled)]
|
|
||||||
private static partial Regex CharaBodyRegex();
|
|
||||||
|
|
||||||
public MdlTab(ModEditWindow edit, byte[] bytes, string path, Mod? mod)
|
|
||||||
{
|
{
|
||||||
_edit = edit;
|
_edit = edit;
|
||||||
|
|
||||||
|
|
@ -54,7 +43,7 @@ public partial class ModEditWindow
|
||||||
/// <summary> Find the list of game paths that may correspond to this model. </summary>
|
/// <summary> Find the list of game paths that may correspond to this model. </summary>
|
||||||
/// <param name="path"> Resolved path to a .mdl. </param>
|
/// <param name="path"> Resolved path to a .mdl. </param>
|
||||||
/// <param name="mod"> Mod within which the .mdl is resolved. </param>
|
/// <param name="mod"> Mod within which the .mdl is resolved. </param>
|
||||||
private void FindGamePaths(string path, Mod mod)
|
private void FindGamePaths(string path, IMod mod)
|
||||||
{
|
{
|
||||||
if (!Path.IsPathRooted(path) && Utf8GamePath.FromString(path, out var p))
|
if (!Path.IsPathRooted(path) && Utf8GamePath.FromString(path, out var p))
|
||||||
{
|
{
|
||||||
|
|
@ -77,8 +66,8 @@ public partial class ModEditWindow
|
||||||
task.ContinueWith(t =>
|
task.ContinueWith(t =>
|
||||||
{
|
{
|
||||||
IoException = t.Exception?.ToString();
|
IoException = t.Exception?.ToString();
|
||||||
PendingIo = false;
|
|
||||||
GamePaths = t.Result;
|
GamePaths = t.Result;
|
||||||
|
PendingIo = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,7 +78,7 @@ public partial class ModEditWindow
|
||||||
SklbFile? sklb = null;
|
SklbFile? sklb = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var sklbPath = GetSklbPath(mdlPath.ToString());
|
var sklbPath = _edit._models.ResolveSklbForMdl(mdlPath.ToString());
|
||||||
sklb = sklbPath != null ? ReadSklb(sklbPath) : null;
|
sklb = sklbPath != null ? ReadSklb(sklbPath) : null;
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
|
|
@ -107,43 +96,6 @@ public partial class ModEditWindow
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Try to find the .sklb path for a .mdl file. </summary>
|
|
||||||
/// <param name="mdlPath"> .mdl file to look up the skeleton for. </param>
|
|
||||||
private string? GetSklbPath(string mdlPath)
|
|
||||||
{
|
|
||||||
// Equipment is skinned to the base body skeleton of the race they target.
|
|
||||||
var match = CharaEquipmentRegex().Match(mdlPath);
|
|
||||||
if (match.Success)
|
|
||||||
{
|
|
||||||
var race = match.Groups["Race"].Value;
|
|
||||||
return $"chara/human/{race}/skeleton/base/b0001/skl_{race}b0001.sklb";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some parts of human have their own skeletons.
|
|
||||||
match = CharaHumanRegex().Match(mdlPath);
|
|
||||||
if (match.Success)
|
|
||||||
{
|
|
||||||
var type = match.Groups["Type"].Value;
|
|
||||||
var race = match.Groups["Race"].Value;
|
|
||||||
return type switch
|
|
||||||
{
|
|
||||||
"body" or "tail" => $"chara/human/{race}/skeleton/base/b0001/skl_{race}b0001.sklb",
|
|
||||||
_ => throw new Exception($"Currently unsupported human model type \"{type}\"."),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// A few subcategories - such as weapons, demihumans, and monsters - have dedicated per-"body" skeletons.
|
|
||||||
match = CharaBodyRegex().Match(mdlPath);
|
|
||||||
if (match.Success)
|
|
||||||
{
|
|
||||||
var subCategory = match.Groups["SubCategory"].Value;
|
|
||||||
var set = match.Groups["Set"].Value;
|
|
||||||
return $"chara/{subCategory}/{set}/skeleton/base/b0001/skl_{set}b0001.sklb";
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary> Read a .sklb from the active collection or game. </summary>
|
/// <summary> Read a .sklb from the active collection or game. </summary>
|
||||||
/// <param name="sklbPath"> Game path to the .sklb to load. </param>
|
/// <param name="sklbPath"> Game path to the .sklb to load. </param>
|
||||||
private SklbFile ReadSklb(string sklbPath)
|
private SklbFile ReadSklb(string sklbPath)
|
||||||
|
|
@ -153,17 +105,12 @@ public partial class ModEditWindow
|
||||||
throw new Exception($"Resolved skeleton path {sklbPath} could not be converted to a game path.");
|
throw new Exception($"Resolved skeleton path {sklbPath} could not be converted to a game path.");
|
||||||
|
|
||||||
var resolvedPath = _edit._activeCollections.Current.ResolvePath(utf8SklbPath);
|
var resolvedPath = _edit._activeCollections.Current.ResolvePath(utf8SklbPath);
|
||||||
// TODO: is it worth trying to use streams for these instead? i'll need to do this for mtrl/tex too, so might be a good idea. that said, the mtrl reader doesn't accept streams, so...
|
// TODO: is it worth trying to use streams for these instead? I'll need to do this for mtrl/tex too, so might be a good idea. that said, the mtrl reader doesn't accept streams, so...
|
||||||
var bytes = resolvedPath switch
|
var bytes = resolvedPath == null ? _edit._gameData.GetFile(sklbPath)?.Data : File.ReadAllBytes(resolvedPath.Value.ToPath());
|
||||||
{
|
return bytes != null
|
||||||
null => _edit._gameData.GetFile(sklbPath)?.Data,
|
? new SklbFile(bytes)
|
||||||
FullPath path => File.ReadAllBytes(path.ToPath()),
|
: throw new Exception(
|
||||||
};
|
|
||||||
if (bytes == null)
|
|
||||||
throw new Exception(
|
|
||||||
$"Resolved skeleton path {sklbPath} could not be found. If modded, is it enabled in the current collection?");
|
$"Resolved skeleton path {sklbPath} could not be found. If modded, is it enabled in the current collection?");
|
||||||
|
|
||||||
return new SklbFile(bytes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Remove the material given by the index. </summary>
|
/// <summary> Remove the material given by the index. </summary>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ public partial class ModEditWindow
|
||||||
private const int MdlMaterialMaximum = 4;
|
private const int MdlMaterialMaximum = 4;
|
||||||
|
|
||||||
private readonly FileEditor<MdlTab> _modelTab;
|
private readonly FileEditor<MdlTab> _modelTab;
|
||||||
|
|
||||||
private readonly ModelManager _models;
|
private readonly ModelManager _models;
|
||||||
|
|
||||||
private string _modelNewMaterial = string.Empty;
|
private string _modelNewMaterial = string.Empty;
|
||||||
|
|
@ -91,19 +90,21 @@ public partial class ModEditWindow
|
||||||
|
|
||||||
private void DrawGamePathCombo(MdlTab tab)
|
private void DrawGamePathCombo(MdlTab tab)
|
||||||
{
|
{
|
||||||
if (tab.GamePaths!.Count == 0)
|
if (tab.GamePaths!.Count != 0)
|
||||||
{
|
{
|
||||||
ImGui.TextUnformatted("No associated game path detected. Valid game paths are currently necessary for exporting.");
|
DrawComboButton(tab);
|
||||||
if (ImGui.InputTextWithHint("##customInput", "Enter custom game path...", ref _customPath, 256))
|
|
||||||
if (!Utf8GamePath.FromString(_customPath, out _customGamePath, false))
|
|
||||||
_customGamePath = Utf8GamePath.Empty;
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
DrawComboButton(tab);
|
ImGui.TextUnformatted("No associated game path detected. Valid game paths are currently necessary for exporting.");
|
||||||
|
if (!ImGui.InputTextWithHint("##customInput", "Enter custom game path...", ref _customPath, 256))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!Utf8GamePath.FromString(_customPath, out _customGamePath, false))
|
||||||
|
_customGamePath = Utf8GamePath.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary> I disliked the combo with only one selection so turn it into a button in that case. </summary>
|
||||||
private static void DrawComboButton(MdlTab tab)
|
private static void DrawComboButton(MdlTab tab)
|
||||||
{
|
{
|
||||||
const string label = "Game Path";
|
const string label = "Game Path";
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue