mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-21 07:59:22 +01:00
Merge branch 'master' into mdl-import
# Conflicts: # Penumbra/Import/Models/ModelManager.cs # Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs
This commit is contained in:
commit
4c18b747b1
7 changed files with 143 additions and 73 deletions
|
|
@ -1 +1 @@
|
||||||
Subproject commit a2db1b309c3121e84c75e639e70575af7d936c3e
|
Subproject commit 1dad8d07047be0851f518cdac2b1c8bc76a7be98
|
||||||
|
|
@ -17,10 +17,10 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
|
||||||
private readonly ModStorage _modStorage;
|
private readonly ModStorage _modStorage;
|
||||||
|
|
||||||
/// <remarks> The empty collection is always available at Index 0. </remarks>
|
/// <remarks> The empty collection is always available at Index 0. </remarks>
|
||||||
private readonly List<ModCollection> _collections = new()
|
private readonly List<ModCollection> _collections =
|
||||||
{
|
[
|
||||||
ModCollection.Empty,
|
ModCollection.Empty,
|
||||||
};
|
];
|
||||||
|
|
||||||
public readonly ModCollection DefaultNamed;
|
public readonly ModCollection DefaultNamed;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
using OtterGui.Classes;
|
using OtterGui.Classes;
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
using Penumbra.GameData.Enums;
|
|
||||||
|
|
||||||
namespace Penumbra.Communication;
|
namespace Penumbra.Communication;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ public class ModelExporter
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Export a model in preparation for usage in a glTF file. If provided, skeleton will be used to skin the resulting meshes where appropriate. </summary>
|
/// <summary> Export a model in preparation for usage in a glTF file. If provided, skeleton will be used to skin the resulting meshes where appropriate. </summary>
|
||||||
public static Model Export(MdlFile mdl, XivSkeleton? xivSkeleton)
|
public static Model Export(MdlFile mdl, IEnumerable<XivSkeleton>? xivSkeleton)
|
||||||
{
|
{
|
||||||
var gltfSkeleton = xivSkeleton != null ? ConvertSkeleton(xivSkeleton) : null;
|
var gltfSkeleton = xivSkeleton != null ? ConvertSkeleton(xivSkeleton) : null;
|
||||||
var meshes = ConvertMeshes(mdl, gltfSkeleton);
|
var meshes = ConvertMeshes(mdl, gltfSkeleton);
|
||||||
|
|
@ -50,14 +50,18 @@ public class ModelExporter
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Convert XIV skeleton data into a glTF-compatible node tree, with mappings. </summary>
|
/// <summary> Convert XIV skeleton data into a glTF-compatible node tree, with mappings. </summary>
|
||||||
private static GltfSkeleton? ConvertSkeleton(XivSkeleton skeleton)
|
private static GltfSkeleton? ConvertSkeleton(IEnumerable<XivSkeleton> skeletons)
|
||||||
{
|
{
|
||||||
NodeBuilder? root = null;
|
NodeBuilder? root = null;
|
||||||
var names = new Dictionary<string, int>();
|
var names = new Dictionary<string, int>();
|
||||||
var joints = new List<NodeBuilder>();
|
var joints = new List<NodeBuilder>();
|
||||||
foreach (var bone in skeleton.Bones)
|
|
||||||
|
// Flatten out the bones across all the received skeletons, but retain a reference to the parent skeleton for lookups.
|
||||||
|
var iterator = skeletons.SelectMany(skeleton => skeleton.Bones.Select(bone => (skeleton, bone)));
|
||||||
|
foreach (var (skeleton, bone) in iterator)
|
||||||
{
|
{
|
||||||
if (names.ContainsKey(bone.Name)) continue;
|
if (names.ContainsKey(bone.Name))
|
||||||
|
continue;
|
||||||
|
|
||||||
var node = new NodeBuilder(bone.Name);
|
var node = new NodeBuilder(bone.Name);
|
||||||
names[bone.Name] = joints.Count;
|
names[bone.Name] = joints.Count;
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,21 @@
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
|
using OtterGui;
|
||||||
using OtterGui.Tasks;
|
using OtterGui.Tasks;
|
||||||
|
using Penumbra.Collections.Manager;
|
||||||
|
using Penumbra.GameData;
|
||||||
using Penumbra.GameData.Data;
|
using Penumbra.GameData.Data;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.GameData.Files;
|
using Penumbra.GameData.Files;
|
||||||
|
using Penumbra.GameData.Structs;
|
||||||
using Penumbra.Import.Models.Export;
|
using Penumbra.Import.Models.Export;
|
||||||
using Penumbra.Import.Models.Import;
|
using Penumbra.Import.Models.Import;
|
||||||
|
using Penumbra.Meta.Manipulations;
|
||||||
using SharpGLTF.Scenes;
|
using SharpGLTF.Scenes;
|
||||||
using SharpGLTF.Schema2;
|
using SharpGLTF.Schema2;
|
||||||
|
|
||||||
namespace Penumbra.Import.Models;
|
namespace Penumbra.Import.Models;
|
||||||
|
|
||||||
public sealed class ModelManager(IFramework framework, GamePathParser parser) : SingleTaskQueue, IDisposable
|
public sealed class ModelManager(IFramework framework, ActiveCollections collections, GamePathParser parser) : SingleTaskQueue, IDisposable
|
||||||
{
|
{
|
||||||
private readonly IFramework _framework = framework;
|
private readonly IFramework _framework = framework;
|
||||||
|
|
||||||
|
|
@ -26,37 +31,70 @@ public sealed class ModelManager(IFramework framework, GamePathParser parser) :
|
||||||
_tasks.Clear();
|
_tasks.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task ExportToGltf(MdlFile mdl, SklbFile? sklb, string outputPath)
|
public Task ExportToGltf(MdlFile mdl, IEnumerable<SklbFile> sklbs, string outputPath)
|
||||||
=> Enqueue(new ExportToGltfAction(this, mdl, sklb, outputPath));
|
=> Enqueue(new ExportToGltfAction(this, mdl, sklbs, outputPath));
|
||||||
|
|
||||||
public Task<MdlFile?> ImportGltf(string inputPath)
|
public Task<MdlFile?> ImportGltf(string inputPath)
|
||||||
{
|
{
|
||||||
var action = new ImportGltfAction(inputPath);
|
var action = new ImportGltfAction(inputPath);
|
||||||
return Enqueue(action).ContinueWith(_ => action.Out);
|
return Enqueue(action).ContinueWith(_ => action.Out);
|
||||||
}
|
}
|
||||||
|
/// <summary> Try to find the .sklb paths for a .mdl file. </summary>
|
||||||
/// <summary> Try to find the .sklb path for a .mdl file. </summary>
|
/// <param name="mdlPath"> .mdl file to look up the skeletons for. </param>
|
||||||
/// <param name="mdlPath"> .mdl file to look up the skeleton for. </param>
|
/// <param name="estManipulations"> Modified extra skeleton template parameters. </param>
|
||||||
public string? ResolveSklbForMdl(string mdlPath)
|
public string[] ResolveSklbsForMdl(string mdlPath, EstManipulation[] estManipulations)
|
||||||
{
|
{
|
||||||
var info = parser.GetFileInfo(mdlPath);
|
var info = parser.GetFileInfo(mdlPath);
|
||||||
if (info.FileType is not FileType.Model)
|
if (info.FileType is not FileType.Model)
|
||||||
return null;
|
return [];
|
||||||
|
|
||||||
|
var baseSkeleton = GamePaths.Skeleton.Sklb.Path(info.GenderRace, "base", 1);
|
||||||
|
|
||||||
return info.ObjectType switch
|
return info.ObjectType switch
|
||||||
{
|
{
|
||||||
ObjectType.Equipment => GamePaths.Skeleton.Sklb.Path(info.GenderRace, "base", 1),
|
ObjectType.Equipment when info.EquipSlot.ToSlot() is EquipSlot.Body
|
||||||
ObjectType.Accessory => GamePaths.Skeleton.Sklb.Path(info.GenderRace, "base", 1),
|
=> [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Body, info, estManipulations)],
|
||||||
ObjectType.Character when info.BodySlot is BodySlot.Body or BodySlot.Tail => GamePaths.Skeleton.Sklb.Path(info.GenderRace, "base",
|
ObjectType.Equipment when info.EquipSlot.ToSlot() is EquipSlot.Head
|
||||||
1),
|
=> [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Head, info, estManipulations)],
|
||||||
|
ObjectType.Equipment => [baseSkeleton],
|
||||||
|
ObjectType.Accessory => [baseSkeleton],
|
||||||
|
ObjectType.Character when info.BodySlot is BodySlot.Body or BodySlot.Tail => [baseSkeleton],
|
||||||
|
ObjectType.Character when info.BodySlot is BodySlot.Hair
|
||||||
|
=> [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Hair, info, estManipulations)],
|
||||||
|
ObjectType.Character when info.BodySlot is BodySlot.Face or BodySlot.Ear
|
||||||
|
=> [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Face, info, estManipulations)],
|
||||||
ObjectType.Character => throw new Exception($"Currently unsupported human model type \"{info.BodySlot}\"."),
|
ObjectType.Character => throw new Exception($"Currently unsupported human model type \"{info.BodySlot}\"."),
|
||||||
ObjectType.DemiHuman => GamePaths.DemiHuman.Sklb.Path(info.PrimaryId),
|
ObjectType.DemiHuman => [GamePaths.DemiHuman.Sklb.Path(info.PrimaryId)],
|
||||||
ObjectType.Monster => GamePaths.Monster.Sklb.Path(info.PrimaryId),
|
ObjectType.Monster => [GamePaths.Monster.Sklb.Path(info.PrimaryId)],
|
||||||
ObjectType.Weapon => GamePaths.Weapon.Sklb.Path(info.PrimaryId),
|
ObjectType.Weapon => [GamePaths.Weapon.Sklb.Path(info.PrimaryId)],
|
||||||
_ => null,
|
_ => [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string[] ResolveEstSkeleton(EstManipulation.EstType type, GameObjectInfo info, EstManipulation[] estManipulations)
|
||||||
|
{
|
||||||
|
// Try to find an EST entry from the manipulations provided.
|
||||||
|
var (gender, race) = info.GenderRace.Split();
|
||||||
|
var modEst = estManipulations
|
||||||
|
.FirstOrNull(est =>
|
||||||
|
est.Gender == gender
|
||||||
|
&& est.Race == race
|
||||||
|
&& est.Slot == type
|
||||||
|
&& est.SetId == info.PrimaryId
|
||||||
|
);
|
||||||
|
|
||||||
|
// Try to use an entry from provided manipulations, falling back to the current collection.
|
||||||
|
var targetId = modEst?.Entry
|
||||||
|
?? collections.Current.MetaCache?.GetEstEntry(type, info.GenderRace, info.PrimaryId)
|
||||||
|
?? 0;
|
||||||
|
|
||||||
|
// If there's no entries, we can assume that there's no additional skeleton.
|
||||||
|
if (targetId == 0)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
return [GamePaths.Skeleton.Sklb.Path(info.GenderRace, EstManipulation.ToName(type), targetId)];
|
||||||
|
}
|
||||||
|
|
||||||
private Task Enqueue(IAction action)
|
private Task Enqueue(IAction action)
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
|
|
@ -83,37 +121,47 @@ public sealed class ModelManager(IFramework framework, GamePathParser parser) :
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ExportToGltfAction(ModelManager manager, MdlFile mdl, SklbFile? sklb, string outputPath)
|
private class ExportToGltfAction(ModelManager manager, MdlFile mdl, IEnumerable<SklbFile> sklbs, string outputPath)
|
||||||
: IAction
|
: IAction
|
||||||
{
|
{
|
||||||
public void Execute(CancellationToken cancel)
|
public void Execute(CancellationToken cancel)
|
||||||
{
|
{
|
||||||
Penumbra.Log.Debug("Reading skeleton.");
|
Penumbra.Log.Debug($"[GLTF Export] Exporting model to {outputPath}...");
|
||||||
var xivSkeleton = BuildSkeleton(cancel);
|
Penumbra.Log.Debug("[GLTF Export] Reading skeletons...");
|
||||||
|
var xivSkeletons = BuildSkeletons(cancel);
|
||||||
|
|
||||||
Penumbra.Log.Debug("Converting model.");
|
Penumbra.Log.Debug("[GLTF Export] Converting model...");
|
||||||
var model = ModelExporter.Export(mdl, xivSkeleton);
|
var model = ModelExporter.Export(mdl, xivSkeletons);
|
||||||
|
|
||||||
Penumbra.Log.Debug("Building scene.");
|
Penumbra.Log.Debug("[GLTF Export] Building scene...");
|
||||||
var scene = new SceneBuilder();
|
var scene = new SceneBuilder();
|
||||||
model.AddToScene(scene);
|
model.AddToScene(scene);
|
||||||
|
|
||||||
Penumbra.Log.Debug("Saving.");
|
Penumbra.Log.Debug("[GLTF Export] Saving...");
|
||||||
var gltfModel = scene.ToGltf2();
|
var gltfModel = scene.ToGltf2();
|
||||||
gltfModel.SaveGLTF(outputPath);
|
gltfModel.SaveGLTF(outputPath);
|
||||||
|
Penumbra.Log.Debug("[GLTF Export] Done.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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 IEnumerable<XivSkeleton> BuildSkeletons(CancellationToken cancel)
|
||||||
{
|
{
|
||||||
if (sklb == null)
|
var havokTasks = sklbs
|
||||||
return null;
|
.WithIndex()
|
||||||
|
.Select(CreateHavokTask)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
var xmlTask = manager._framework.RunOnFrameworkThread(() => HavokConverter.HkxToXml(sklb.Skeleton));
|
// Result waits automatically.
|
||||||
xmlTask.Wait(cancel);
|
return havokTasks.Select(task => SkeletonConverter.FromXml(task.Result));
|
||||||
var xml = xmlTask.Result;
|
|
||||||
|
|
||||||
return SkeletonConverter.FromXml(xml);
|
// The havok methods we're relying on for this conversion are a bit
|
||||||
|
// finicky at the best of times, and can outright cause a CTD if they
|
||||||
|
// get upset. Running each conversion on its own tick seems to make
|
||||||
|
// this consistently non-crashy across my testing.
|
||||||
|
Task<string> CreateHavokTask((SklbFile Sklb, int Index) pair) =>
|
||||||
|
manager._framework.RunOnTick(
|
||||||
|
() => HavokConverter.HkxToXml(pair.Sklb.Skeleton),
|
||||||
|
delayTicks: pair.Index, cancellationToken: cancel);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Equals(IAction? other)
|
public bool Equals(IAction? other)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
using Penumbra.GameData;
|
using Penumbra.GameData;
|
||||||
using Penumbra.GameData.Files;
|
using Penumbra.GameData.Files;
|
||||||
using Penumbra.Mods;
|
using Penumbra.Meta.Manipulations;
|
||||||
using Penumbra.String.Classes;
|
using Penumbra.String.Classes;
|
||||||
|
|
||||||
namespace Penumbra.UI.AdvancedWindow;
|
namespace Penumbra.UI.AdvancedWindow;
|
||||||
|
|
@ -22,14 +22,13 @@ public partial class ModEditWindow
|
||||||
public bool PendingIo { get; private set; }
|
public bool PendingIo { get; private set; }
|
||||||
public string? IoException { get; private set; }
|
public string? IoException { get; private set; }
|
||||||
|
|
||||||
public MdlTab(ModEditWindow edit, byte[] bytes, string path, IMod? mod)
|
public MdlTab(ModEditWindow edit, byte[] bytes, string path)
|
||||||
{
|
{
|
||||||
_edit = edit;
|
_edit = edit;
|
||||||
|
|
||||||
Initialize(new MdlFile(bytes));
|
Initialize(new MdlFile(bytes));
|
||||||
|
|
||||||
if (mod != null)
|
FindGamePaths(path);
|
||||||
FindGamePaths(path, mod);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[MemberNotNull(nameof(Mdl), nameof(_attributes))]
|
[MemberNotNull(nameof(Mdl), nameof(_attributes))]
|
||||||
|
|
@ -59,9 +58,13 @@ 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>
|
private void FindGamePaths(string path)
|
||||||
private void FindGamePaths(string path, IMod mod)
|
|
||||||
{
|
{
|
||||||
|
// If there's no current mod (somehow), there's nothing to resolve the model within.
|
||||||
|
var mod = _edit._editor.Mod;
|
||||||
|
if (mod == null)
|
||||||
|
return;
|
||||||
|
|
||||||
if (!Path.IsPathRooted(path) && Utf8GamePath.FromString(path, out var p))
|
if (!Path.IsPathRooted(path) && Utf8GamePath.FromString(path, out var p))
|
||||||
{
|
{
|
||||||
GamePaths = [p];
|
GamePaths = [p];
|
||||||
|
|
@ -88,6 +91,48 @@ public partial class ModEditWindow
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private EstManipulation[] GetCurrentEstManipulations()
|
||||||
|
{
|
||||||
|
var mod = _edit._editor.Mod;
|
||||||
|
var option = _edit._editor.Option;
|
||||||
|
if (mod == null || option == null)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
// Filter then prepend the current option to ensure it's chosen first.
|
||||||
|
return mod.AllSubMods
|
||||||
|
.Where(subMod => subMod != option)
|
||||||
|
.Prepend(option)
|
||||||
|
.SelectMany(subMod => subMod.Manipulations)
|
||||||
|
.Where(manipulation => manipulation.ManipulationType is MetaManipulation.Type.Est)
|
||||||
|
.Select(manipulation => manipulation.Est)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Export model to an interchange format. </summary>
|
||||||
|
/// <param name="outputPath"> Disk path to save the resulting file to. </param>
|
||||||
|
public void Export(string outputPath, Utf8GamePath mdlPath)
|
||||||
|
{
|
||||||
|
IEnumerable<SklbFile> skeletons;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var sklbPaths = _edit._models.ResolveSklbsForMdl(mdlPath.ToString(), GetCurrentEstManipulations());
|
||||||
|
skeletons = sklbPaths.Select(ReadSklb).ToArray();
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
IoException = exception.ToString();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PendingIo = true;
|
||||||
|
_edit._models.ExportToGltf(Mdl, skeletons, outputPath)
|
||||||
|
.ContinueWith(task =>
|
||||||
|
{
|
||||||
|
IoException = task.Exception?.ToString();
|
||||||
|
PendingIo = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary> Import a model from an interchange format. </summary>
|
/// <summary> Import a model from an interchange format. </summary>
|
||||||
/// <param name="inputPath"> Disk path to load model data from. </param>
|
/// <param name="inputPath"> Disk path to load model data from. </param>
|
||||||
public void Import(string inputPath)
|
public void Import(string inputPath)
|
||||||
|
|
@ -107,32 +152,6 @@ public partial class ModEditWindow
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Export model to an interchange format. </summary>
|
|
||||||
/// <param name="outputPath"> Disk path to save the resulting file to. </param>
|
|
||||||
/// <param name="mdlPath"> Game path to consider as the canonical .mdl path during export, used for resolution of other files. </param>
|
|
||||||
public void Export(string outputPath, Utf8GamePath mdlPath)
|
|
||||||
{
|
|
||||||
SklbFile? sklb = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var sklbPath = _edit._models.ResolveSklbForMdl(mdlPath.ToString());
|
|
||||||
sklb = sklbPath != null ? ReadSklb(sklbPath) : null;
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
IoException = exception?.ToString();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
PendingIo = true;
|
|
||||||
_edit._models.ExportToGltf(Mdl, sklb, outputPath)
|
|
||||||
.ContinueWith(task =>
|
|
||||||
{
|
|
||||||
IoException = task.Exception?.ToString();
|
|
||||||
PendingIo = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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)
|
||||||
|
|
|
||||||
|
|
@ -600,7 +600,7 @@ public partial class ModEditWindow : Window, IDisposable
|
||||||
(bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable));
|
(bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable));
|
||||||
_modelTab = new FileEditor<MdlTab>(this, gameData, config, _editor.Compactor, _fileDialog, "Models", ".mdl",
|
_modelTab = new FileEditor<MdlTab>(this, gameData, config, _editor.Compactor, _fileDialog, "Models", ".mdl",
|
||||||
() => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty,
|
() => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty,
|
||||||
(bytes, path, _) => new MdlTab(this, bytes, path, _mod));
|
(bytes, path, _) => new MdlTab(this, bytes, path));
|
||||||
_shaderPackageTab = new FileEditor<ShpkTab>(this, gameData, config, _editor.Compactor, _fileDialog, "Shaders", ".shpk",
|
_shaderPackageTab = new FileEditor<ShpkTab>(this, gameData, config, _editor.Compactor, _fileDialog, "Shaders", ".shpk",
|
||||||
() => PopulateIsOnPlayer(_editor.Files.Shpk, ResourceType.Shpk), DrawShaderPackagePanel,
|
() => PopulateIsOnPlayer(_editor.Files.Shpk, ResourceType.Shpk), DrawShaderPackagePanel,
|
||||||
() => _mod?.ModPath.FullName ?? string.Empty,
|
() => _mod?.ModPath.FullName ?? string.Empty,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue