Compare commits

..

No commits in common. "34f067f13d68eb310fce111e39d94a2bb131d5a9" and "cb275f57bfb41c7d7fd7ba43c736028bfcdec52c" have entirely different histories.

19 changed files with 827 additions and 857 deletions

2
Luna

@ -1 +1 @@
Subproject commit 78216203f4570a6194fce9422204d8abb536c828 Subproject commit 7214f079cb9b8eeea6fa1a9fe1c6ca8118049969

@ -1 +1 @@
Subproject commit 9af1e5fce4c13ef98842807d4f593dec8ae80c87 Subproject commit f354444776591ae423e2d8374aae346308d81424

View file

@ -1,5 +1,5 @@
using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Enums;
using Luna; using OtterGui.Filesystem;
using Penumbra.GameData.Actors; using Penumbra.GameData.Actors;
using Penumbra.GameData.DataContainers.Bases; using Penumbra.GameData.DataContainers.Bases;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
@ -224,7 +224,7 @@ public sealed partial class IndividualCollections
{ {
return identifier.Type switch return identifier.Type switch
{ {
IdentifierType.Player => $"{identifier.PlayerName} ({_actors.Data.ToWorldName(identifier.HomeWorld)})", IdentifierType.Player => $"{identifier.PlayerName} ({_actors.Data.ToWorldName(identifier.HomeWorld)})",
IdentifierType.Retainer => $"{identifier.PlayerName} (Retainer)", IdentifierType.Retainer => $"{identifier.PlayerName} (Retainer)",
IdentifierType.Owned => IdentifierType.Owned =>
$"{identifier.PlayerName} ({_actors.Data.ToWorldName(identifier.HomeWorld)})'s {_actors.Data.ToName(identifier.Kind, identifier.DataId)}", $"{identifier.PlayerName} ({_actors.Data.ToWorldName(identifier.HomeWorld)})'s {_actors.Data.ToName(identifier.Kind, identifier.DataId)}",

View file

@ -1,4 +1,4 @@
using Luna; using OtterGui.Filesystem;
namespace Penumbra.Collections; namespace Penumbra.Collections;

View file

@ -1,40 +1,40 @@
using Luna; using OtterGui.Log;
namespace Penumbra.Import.Models; namespace Penumbra.Import.Models;
public record IoNotifier(Logger Log) public record class IoNotifier
{ {
private readonly List<string> _messages = []; private readonly List<string> _messages = [];
private string _context = ""; private string _context = "";
/// <summary> Create a new notifier with the specified context appended to any other context already present. </summary> /// <summary> Create a new notifier with the specified context appended to any other context already present. </summary>
public IoNotifier WithContext(string context) public IoNotifier WithContext(string context)
=> this with { _context = $"{_context}{context}: " }; => this with { _context = $"{_context}{context}: "};
/// <summary> Send a warning with any current context to notification channels. </summary> /// <summary> Send a warning with any current context to notification channels. </summary>
public void Warning(string content) public void Warning(string content)
=> SendMessage(content, Logger.LogLevel.Warning); => SendMessage(content, Logger.LogLevel.Warning);
/// <summary> Get the current warnings for this notifier. </summary> /// <summary> Get the current warnings for this notifier. </summary>
/// <remarks> This does not currently filter to notifications with the current notifier's context - it will return all IO notifications from all notifiers. </remarks> /// <remarks> This does not currently filter to notifications with the current notifier's context - it will return all IO notifications from all notifiers. </remarks>
public IEnumerable<string> GetWarnings() public IEnumerable<string> GetWarnings()
=> _messages; => _messages;
/// <summary> Create an exception with any current context. </summary> /// <summary> Create an exception with any current context. </summary>
[StackTraceHidden] [StackTraceHidden]
public Exception Exception(string message) public Exception Exception(string message)
=> Exception<Exception>(message); => Exception<Exception>(message);
/// <summary> Create an exception of the provided type with any current context. </summary> /// <summary> Create an exception of the provided type with any current context. </summary>
[StackTraceHidden] [StackTraceHidden]
public TException Exception<TException>(string message) public TException Exception<TException>(string message)
where TException : Exception, new() where TException : Exception, new()
=> (TException)Activator.CreateInstance(typeof(TException), $"{_context}{message}")!; => (TException)Activator.CreateInstance(typeof(TException), $"{_context}{message}")!;
private void SendMessage(string message, Logger.LogLevel type) private void SendMessage(string message, Logger.LogLevel type)
{ {
var fullText = $"{_context}{message}"; var fullText = $"{_context}{message}";
Log.Message(type, fullText); Penumbra.Log.Message(type, fullText);
_messages.Add(fullText); _messages.Add(fullText);
} }
} }

View file

@ -1,329 +1,323 @@
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Lumina.Data.Parsing; using Lumina.Data.Parsing;
using Luna; using OtterGui.Tasks;
using OtterGui.Tasks; using Penumbra.Collections.Manager;
using Penumbra.Collections.Manager; using Penumbra.GameData;
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.GameData.Structs; using Penumbra.Import.Models.Export;
using Penumbra.Import.Models.Export; using Penumbra.Import.Models.Import;
using Penumbra.Import.Models.Import; using Penumbra.Import.Textures;
using Penumbra.Import.Textures; using Penumbra.Meta;
using Penumbra.Meta; using Penumbra.Meta.Files;
using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations;
using Penumbra.Meta.Manipulations; using SharpGLTF.Scenes;
using SharpGLTF.Scenes; using SixLabors.ImageSharp;
using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.PixelFormats;
namespace Penumbra.Import.Models;
namespace Penumbra.Import.Models;
using Schema2 = SharpGLTF.Schema2;
using Schema2 = SharpGLTF.Schema2; using LuminaMaterial = Lumina.Models.Materials.Material;
using LuminaMaterial = Lumina.Models.Materials.Material;
public sealed class ModelManager(IFramework framework, MetaFileManager metaFileManager, ActiveCollections collections, GamePathParser parser)
public sealed class ModelManager( : SingleTaskQueue, IDisposable, Luna.IService
Logger log, {
IFramework framework, private readonly IFramework _framework = framework;
MetaFileManager metaFileManager,
ActiveCollections collections, private readonly ConcurrentDictionary<IAction, (Task, CancellationTokenSource)> _tasks = new();
GamePathParser parser)
: SingleTaskQueue, IDisposable, IService private bool _disposed;
{
public readonly Logger Log = log; public void Dispose()
private readonly IFramework _framework = framework; {
_disposed = true;
private readonly ConcurrentDictionary<IAction, (Task, CancellationTokenSource)> _tasks = new(); foreach (var (_, cancel) in _tasks.Values.ToArray())
cancel.Cancel();
private bool _disposed; _tasks.Clear();
}
public void Dispose()
{ public Task<IoNotifier> ExportToGltf(in ExportConfig config, MdlFile mdl, IEnumerable<string> sklbPaths, Func<string, byte[]?> read,
_disposed = true; string outputPath)
foreach (var (_, cancel) in _tasks.Values.ToArray()) => EnqueueWithResult(
cancel.Cancel(); new ExportToGltfAction(this, config, mdl, sklbPaths, read, outputPath),
_tasks.Clear(); action => action.Notifier
} );
public Task<IoNotifier> ExportToGltf(in ExportConfig config, MdlFile mdl, IEnumerable<string> sklbPaths, Func<string, byte[]?> read, public Task<(MdlFile?, IoNotifier)> ImportGltf(string inputPath)
string outputPath) => EnqueueWithResult(
=> EnqueueWithResult( new ImportGltfAction(inputPath),
new ExportToGltfAction(this, config, mdl, sklbPaths, read, outputPath), action => (action.Out, action.Notifier)
action => action.Notifier );
);
/// <summary> Try to find the .sklb paths for a .mdl file. </summary>
public Task<(MdlFile?, IoNotifier)> ImportGltf(string inputPath) /// <param name="mdlPath"> .mdl file to look up the skeletons for. </param>
=> EnqueueWithResult( /// <param name="estManipulations"> Modified extra skeleton template parameters. </param>
new ImportGltfAction(this, inputPath), public string[] ResolveSklbsForMdl(string mdlPath, KeyValuePair<EstIdentifier, EstEntry>[] estManipulations)
action => (action.Out, action.Notifier) {
); var info = parser.GetFileInfo(mdlPath);
if (info.FileType is not FileType.Model)
/// <summary> Try to find the .sklb paths for a .mdl file. </summary> return [];
/// <param name="mdlPath"> .mdl file to look up the skeletons for. </param>
/// <param name="estManipulations"> Modified extra skeleton template parameters. </param> var baseSkeleton = GamePaths.Sklb.Customization(info.GenderRace, "base", 1);
public string[] ResolveSklbsForMdl(string mdlPath, KeyValuePair<EstIdentifier, EstEntry>[] estManipulations)
{ return info.ObjectType switch
var info = parser.GetFileInfo(mdlPath); {
if (info.FileType is not FileType.Model) ObjectType.Equipment when info.EquipSlot.ToSlot() is EquipSlot.Body
return []; => [baseSkeleton, ..ResolveEstSkeleton(EstType.Body, info, estManipulations)],
ObjectType.Equipment when info.EquipSlot.ToSlot() is EquipSlot.Head
var baseSkeleton = GamePaths.Sklb.Customization(info.GenderRace, "base", 1); => [baseSkeleton, ..ResolveEstSkeleton(EstType.Head, info, estManipulations)],
ObjectType.Equipment => [baseSkeleton],
return info.ObjectType switch ObjectType.Accessory => [baseSkeleton],
{ ObjectType.Character when info.BodySlot is BodySlot.Body or BodySlot.Tail => [baseSkeleton],
ObjectType.Equipment when info.EquipSlot.ToSlot() is EquipSlot.Body ObjectType.Character when info.BodySlot is BodySlot.Hair
=> [baseSkeleton, ..ResolveEstSkeleton(EstType.Body, info, estManipulations)], => [baseSkeleton, ..ResolveEstSkeleton(EstType.Hair, info, estManipulations)],
ObjectType.Equipment when info.EquipSlot.ToSlot() is EquipSlot.Head ObjectType.Character when info.BodySlot is BodySlot.Face or BodySlot.Ear
=> [baseSkeleton, ..ResolveEstSkeleton(EstType.Head, info, estManipulations)], => [baseSkeleton, ..ResolveEstSkeleton(EstType.Face, info, estManipulations)],
ObjectType.Equipment => [baseSkeleton], ObjectType.Character => throw new Exception($"Currently unsupported human model type \"{info.BodySlot}\"."),
ObjectType.Accessory => [baseSkeleton], ObjectType.DemiHuman => [GamePaths.Sklb.DemiHuman(info.PrimaryId)],
ObjectType.Character when info.BodySlot is BodySlot.Body or BodySlot.Tail => [baseSkeleton], ObjectType.Monster => [GamePaths.Sklb.Monster(info.PrimaryId)],
ObjectType.Character when info.BodySlot is BodySlot.Hair ObjectType.Weapon => [GamePaths.Sklb.Weapon(info.PrimaryId)],
=> [baseSkeleton, ..ResolveEstSkeleton(EstType.Hair, info, estManipulations)], _ => [],
ObjectType.Character when info.BodySlot is BodySlot.Face or BodySlot.Ear };
=> [baseSkeleton, ..ResolveEstSkeleton(EstType.Face, info, estManipulations)], }
ObjectType.Character => throw new Exception($"Currently unsupported human model type \"{info.BodySlot}\"."),
ObjectType.DemiHuman => [GamePaths.Sklb.DemiHuman(info.PrimaryId)], private string[] ResolveEstSkeleton(EstType type, GameObjectInfo info, KeyValuePair<EstIdentifier, EstEntry>[] estManipulations)
ObjectType.Monster => [GamePaths.Sklb.Monster(info.PrimaryId)], {
ObjectType.Weapon => [GamePaths.Sklb.Weapon(info.PrimaryId)], // Try to find an EST entry from the manipulations provided.
_ => [], var modEst = estManipulations
}; .FirstOrNull(
} est => est.Key.GenderRace == info.GenderRace
&& est.Key.Slot == type
private string[] ResolveEstSkeleton(EstType type, GameObjectInfo info, KeyValuePair<EstIdentifier, EstEntry>[] estManipulations) && est.Key.SetId == info.PrimaryId
{ );
// Try to find an EST entry from the manipulations provided.
var modEst = estManipulations // Try to use an entry from provided manipulations, falling back to the current collection.
.FirstOrNull(est => est.Key.GenderRace == info.GenderRace var targetId = modEst?.Value
&& est.Key.Slot == type ?? collections.Current.MetaCache?.GetEstEntry(type, info.GenderRace, info.PrimaryId)
&& est.Key.SetId == info.PrimaryId ?? EstFile.GetDefault(metaFileManager, type, info.GenderRace, info.PrimaryId);
);
// If there's no entries, we can assume that there's no additional skeleton.
// Try to use an entry from provided manipulations, falling back to the current collection. if (targetId == EstEntry.Zero)
var targetId = modEst?.Value return [];
?? collections.Current.MetaCache?.GetEstEntry(type, info.GenderRace, info.PrimaryId)
?? EstFile.GetDefault(metaFileManager, type, info.GenderRace, info.PrimaryId); return [GamePaths.Sklb.Customization(info.GenderRace, type.ToName(), targetId.AsId)];
}
// If there's no entries, we can assume that there's no additional skeleton.
if (targetId == EstEntry.Zero) /// <summary> Try to resolve the absolute path to a .mtrl from the potentially-partial path provided by a model. </summary>
return []; private string? ResolveMtrlPath(string rawPath, IoNotifier notifier)
{
return [GamePaths.Sklb.Customization(info.GenderRace, type.ToName(), targetId.AsId)]; // TODO: this should probably be chosen in the export settings
} var variantId = 1;
/// <summary> Try to resolve the absolute path to a .mtrl from the potentially-partial path provided by a model. </summary> // Get standardised paths
private string? ResolveMtrlPath(string rawPath, IoNotifier notifier) var absolutePath = rawPath.StartsWith('/')
{ ? LuminaMaterial.ResolveRelativeMaterialPath(rawPath, variantId)
// TODO: this should probably be chosen in the export settings : rawPath;
var variantId = 1; var relativePath = rawPath.StartsWith('/')
? rawPath
// Get standardised paths : '/' + Path.GetFileName(rawPath);
var absolutePath = rawPath.StartsWith('/')
? LuminaMaterial.ResolveRelativeMaterialPath(rawPath, variantId) if (absolutePath == null)
: rawPath; {
var relativePath = rawPath.StartsWith('/') notifier.Warning($"Material path \"{rawPath}\" could not be resolved.");
? rawPath return null;
: '/' + Path.GetFileName(rawPath); }
if (absolutePath == null) var info = parser.GetFileInfo(absolutePath);
{ if (info.FileType is not FileType.Material)
notifier.Warning($"Material path \"{rawPath}\" could not be resolved."); {
return null; notifier.Warning($"Material path {rawPath} does not conform to material conventions.");
} return null;
}
var info = parser.GetFileInfo(absolutePath);
if (info.FileType is not FileType.Material) var resolvedPath = info.ObjectType switch
{ {
notifier.Warning($"Material path {rawPath} does not conform to material conventions."); ObjectType.Character => GamePaths.Mtrl.Customization(
return null; info.GenderRace, info.BodySlot, info.PrimaryId, relativePath, out _, out _, info.Variant),
} _ => absolutePath,
};
var resolvedPath = info.ObjectType switch
{ Penumbra.Log.Debug($"Resolved material {rawPath} to {resolvedPath}");
ObjectType.Character => GamePaths.Mtrl.Customization(
info.GenderRace, info.BodySlot, info.PrimaryId, relativePath, out _, out _, info.Variant), return resolvedPath;
_ => absolutePath, }
};
private Task Enqueue(IAction action)
Penumbra.Log.Debug($"Resolved material {rawPath} to {resolvedPath}"); {
if (_disposed)
return resolvedPath; return Task.FromException(new ObjectDisposedException(nameof(ModelManager)));
}
Task task;
private Task Enqueue(IAction action) lock (_tasks)
{ {
if (_disposed) task = _tasks.GetOrAdd(action, a =>
return Task.FromException(new ObjectDisposedException(nameof(ModelManager))); {
var token = new CancellationTokenSource();
Task task; var t = Enqueue(a, token.Token);
lock (_tasks) t.ContinueWith(_ =>
{ {
task = _tasks.GetOrAdd(action, a => lock (_tasks)
{ {
var token = new CancellationTokenSource(); return _tasks.TryRemove(a, out var unused);
var t = Enqueue(a, token.Token); }
t.ContinueWith(_ => }, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default);
{ return (t, token);
lock (_tasks) }).Item1;
{ }
return _tasks.TryRemove(a, out var unused);
} return task;
}, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default); }
return (t, token);
}).Item1; private Task<TOut> EnqueueWithResult<TAction, TOut>(TAction action, Func<TAction, TOut> process)
} where TAction : IAction
=> Enqueue(action).ContinueWith(task =>
return task; {
} if (task is { IsFaulted: true, Exception: not null })
throw task.Exception;
private Task<TOut> EnqueueWithResult<TAction, TOut>(TAction action, Func<TAction, TOut> process)
where TAction : IAction return process(action);
=> Enqueue(action).ContinueWith(task => }, TaskScheduler.Default);
{
if (task is { IsFaulted: true, Exception: not null }) private class ExportToGltfAction(
throw task.Exception; ModelManager manager,
ExportConfig config,
return process(action); MdlFile mdl,
}, TaskScheduler.Default); IEnumerable<string> sklbPaths,
Func<string, byte[]?> read,
private class ExportToGltfAction( string outputPath)
ModelManager manager, : IAction
ExportConfig config, {
MdlFile mdl, public readonly IoNotifier Notifier = new();
IEnumerable<string> sklbPaths,
Func<string, byte[]?> read, public void Execute(CancellationToken cancel)
string outputPath) {
: IAction Penumbra.Log.Debug($"[GLTF Export] Exporting model to {outputPath}...");
{
public readonly IoNotifier Notifier = new(manager.Log); Penumbra.Log.Debug("[GLTF Export] Reading skeletons...");
var xivSkeletons = BuildSkeletons(cancel);
public void Execute(CancellationToken cancel)
{ Penumbra.Log.Debug("[GLTF Export] Reading materials...");
Penumbra.Log.Debug($"[GLTF Export] Exporting model to {outputPath}..."); var materials = mdl.Materials
.Select(path => (path, material: BuildMaterial(path, Notifier, cancel)))
Penumbra.Log.Debug("[GLTF Export] Reading skeletons..."); .Where(pair => pair.material != null)
var xivSkeletons = BuildSkeletons(cancel); .ToDictionary(pair => pair.path, pair => pair.material!.Value);
Penumbra.Log.Debug("[GLTF Export] Reading materials..."); Penumbra.Log.Debug("[GLTF Export] Converting model...");
var materials = mdl.Materials var model = ModelExporter.Export(config, mdl, xivSkeletons, materials, Notifier);
.Select(path => (path, material: BuildMaterial(path, Notifier, cancel)))
.Where(pair => pair.material != null) Penumbra.Log.Debug("[GLTF Export] Building scene...");
.ToDictionary(pair => pair.path, pair => pair.material!.Value); var scene = new SceneBuilder();
model.AddToScene(scene);
Penumbra.Log.Debug("[GLTF Export] Converting model...");
var model = ModelExporter.Export(config, mdl, xivSkeletons, materials, Notifier); Penumbra.Log.Debug("[GLTF Export] Saving...");
var gltfModel = scene.ToGltf2();
Penumbra.Log.Debug("[GLTF Export] Building scene..."); gltfModel.Save(outputPath);
var scene = new SceneBuilder(); Penumbra.Log.Debug("[GLTF Export] Done.");
model.AddToScene(scene); }
Penumbra.Log.Debug("[GLTF Export] Saving..."); /// <summary> Attempt to read out the pertinent information from the sklb file paths provided. </summary>
var gltfModel = scene.ToGltf2(); private IEnumerable<XivSkeleton> BuildSkeletons(CancellationToken cancel)
gltfModel.Save(outputPath); {
Penumbra.Log.Debug("[GLTF Export] Done."); // We're intentionally filtering failed reads here - the failure will
} // be picked up, if relevant, when the model tries to create mappings
// for a bone in the failed sklb.
/// <summary> Attempt to read out the pertinent information from the sklb file paths provided. </summary> var havokTasks = sklbPaths
private IEnumerable<XivSkeleton> BuildSkeletons(CancellationToken cancel) .Select(read)
{ .Where(bytes => bytes != null)
// We're intentionally filtering failed reads here - the failure will .Select(bytes => new SklbFile(bytes!))
// be picked up, if relevant, when the model tries to create mappings .Index()
// for a bone in the failed sklb. .Select(CreateHavokTask)
var havokTasks = sklbPaths .ToArray();
.Select(read)
.Where(bytes => bytes != null) // Result waits automatically.
.Select(bytes => new SklbFile(bytes!)) return havokTasks.Select(task => SkeletonConverter.FromXml(task.Result));
.Index()
.Select(CreateHavokTask) // The havok methods we're relying on for this conversion are a bit
.ToArray(); // 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
// Result waits automatically. // this consistently non-crashy across my testing.
return havokTasks.Select(task => SkeletonConverter.FromXml(task.Result)); Task<string> CreateHavokTask((int Index, SklbFile Sklb) pair)
=> manager._framework.RunOnTick(
// The havok methods we're relying on for this conversion are a bit () => HavokConverter.HkxToXml(pair.Sklb.Skeleton),
// finicky at the best of times, and can outright cause a CTD if they delayTicks: pair.Index, cancellationToken: cancel);
// get upset. Running each conversion on its own tick seems to make }
// this consistently non-crashy across my testing.
Task<string> CreateHavokTask((int Index, SklbFile Sklb) pair) /// <summary> Read a .mtrl and populate its textures. </summary>
=> manager._framework.RunOnTick( private MaterialExporter.Material? BuildMaterial(string relativePath, IoNotifier notifier, CancellationToken cancel)
() => HavokConverter.HkxToXml(pair.Sklb.Skeleton), {
delayTicks: pair.Index, cancellationToken: cancel); var path = manager.ResolveMtrlPath(relativePath, notifier);
} if (path == null)
return null;
/// <summary> Read a .mtrl and populate its textures. </summary>
private MaterialExporter.Material? BuildMaterial(string relativePath, IoNotifier notifier, CancellationToken cancel) var bytes = read(path);
{ if (bytes == null)
var path = manager.ResolveMtrlPath(relativePath, notifier); return null;
if (path == null)
return null; var mtrl = new MtrlFile(bytes);
var bytes = read(path); return new MaterialExporter.Material
if (bytes == null) {
return null; Mtrl = mtrl,
Textures = mtrl.ShaderPackage.Samplers.ToDictionary(
var mtrl = new MtrlFile(bytes); sampler => (TextureUsage)sampler.SamplerId,
sampler => ConvertImage(mtrl.Textures[sampler.TextureIndex], cancel)
return new MaterialExporter.Material ),
{ };
Mtrl = mtrl, }
Textures = mtrl.ShaderPackage.Samplers.ToDictionary(
sampler => (TextureUsage)sampler.SamplerId, /// <summary> Read a texture referenced by a .mtrl and convert it into an ImageSharp image. </summary>
sampler => ConvertImage(mtrl.Textures[sampler.TextureIndex], cancel) private Image<Rgba32> ConvertImage(MtrlFile.Texture texture, CancellationToken cancel)
), {
}; // Work out the texture's path - the DX11 material flag controls a file name prefix.
} GamePaths.Tex.HandleDx11Path(texture, out var texturePath);
var bytes = read(texturePath);
/// <summary> Read a texture referenced by a .mtrl and convert it into an ImageSharp image. </summary> if (bytes == null)
private Image<Rgba32> ConvertImage(MtrlFile.Texture texture, CancellationToken cancel) return CreateDummyImage();
{
// Work out the texture's path - the DX11 material flag controls a file name prefix. using var textureData = new MemoryStream(bytes);
GamePaths.Tex.HandleDx11Path(texture, out var texturePath); var image = TexFileParser.Parse(textureData);
var bytes = read(texturePath); var pngImage = TextureManager.ConvertToPng(image, cancel).AsPng;
if (bytes == null) return pngImage ?? throw new Exception("Failed to convert texture to png.");
return CreateDummyImage(); }
using var textureData = new MemoryStream(bytes); private static Image<Rgba32> CreateDummyImage()
var image = TexFileParser.Parse(textureData); {
var pngImage = TextureManager.ConvertToPng(image, cancel).AsPng; var image = new Image<Rgba32>(1, 1);
return pngImage ?? throw new Exception("Failed to convert texture to png."); image[0, 0] = Color.White;
} return image;
}
private static Image<Rgba32> CreateDummyImage()
{ public bool Equals(IAction? other)
var image = new Image<Rgba32>(1, 1); {
image[0, 0] = Color.White; if (other is not ExportToGltfAction rhs)
return image; return false;
}
// TODO: compare configuration and such
public bool Equals(IAction? other) return true;
{ }
if (other is not ExportToGltfAction) }
return false;
private partial class ImportGltfAction(string inputPath) : IAction
// TODO: compare configuration and such {
return true; public MdlFile? Out;
} public readonly IoNotifier Notifier = new();
}
public void Execute(CancellationToken cancel)
private class ImportGltfAction(ModelManager manager, string inputPath) : IAction {
{ var model = Schema2.ModelRoot.Load(inputPath);
public MdlFile? Out;
public readonly IoNotifier Notifier = new(manager.Log); Out = ModelImporter.Import(model, Notifier);
}
public void Execute(CancellationToken cancel)
{ public bool Equals(IAction? other)
var model = Schema2.ModelRoot.Load(inputPath); {
if (other is not ImportGltfAction rhs)
Out = ModelImporter.Import(model, Notifier); return false;
}
return true;
public bool Equals(IAction? other) }
{ }
if (other is not ImportGltfAction) }
return false;
return true;
}
}
}

View file

@ -1,9 +1,10 @@
using Dalamud.Utility; using Dalamud.Utility;
using Luna;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using OtterGui.Filesystem;
using Penumbra.Import.Structs; using Penumbra.Import.Structs;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Services;
using SharpCompress.Archives; using SharpCompress.Archives;
using SharpCompress.Archives.Rar; using SharpCompress.Archives.Rar;
using SharpCompress.Archives.SevenZip; using SharpCompress.Archives.SevenZip;
@ -15,7 +16,7 @@ namespace Penumbra.Import;
public partial class TexToolsImporter public partial class TexToolsImporter
{ {
private static readonly ExtractionOptions ExtractionOptions = new() private static readonly ExtractionOptions _extractionOptions = new()
{ {
ExtractFullPath = true, ExtractFullPath = true,
Overwrite = true, Overwrite = true,
@ -78,7 +79,7 @@ public partial class TexToolsImporter
using var t = new StreamReader(s); using var t = new StreamReader(s);
using var j = new JsonTextReader(t); using var j = new JsonTextReader(t);
var obj = JObject.Load(j); var obj = JObject.Load(j);
name = obj[nameof(Mod.Name)]?.Value<string>()?.RemoveInvalidFileNameSymbols() ?? string.Empty; name = obj[nameof(Mod.Name)]?.Value<string>()?.RemoveInvalidPathSymbols() ?? string.Empty;
if (name.Length == 0) if (name.Length == 0)
throw new Exception("Invalid mod archive: mod meta has no name."); throw new Exception("Invalid mod archive: mod meta has no name.");
@ -141,16 +142,16 @@ public partial class TexToolsImporter
switch (Path.GetExtension(reader.Entry.Key)) switch (Path.GetExtension(reader.Entry.Key))
{ {
case ".mdl": case ".mdl":
_migrationManager.MigrateMdlDuringExtraction(reader, _currentModDirectory!.FullName, ExtractionOptions); _migrationManager.MigrateMdlDuringExtraction(reader, _currentModDirectory!.FullName, _extractionOptions);
break; break;
case ".mtrl": case ".mtrl":
_migrationManager.MigrateMtrlDuringExtraction(reader, _currentModDirectory!.FullName, ExtractionOptions); _migrationManager.MigrateMtrlDuringExtraction(reader, _currentModDirectory!.FullName, _extractionOptions);
break; break;
case ".tex": case ".tex":
_migrationManager.FixMipMaps(reader, _currentModDirectory!.FullName, ExtractionOptions); _migrationManager.FixMipMaps(reader, _currentModDirectory!.FullName, _extractionOptions);
break; break;
default: default:
reader.WriteEntryToDirectory(_currentModDirectory!.FullName, ExtractionOptions); reader.WriteEntryToDirectory(_currentModDirectory!.FullName, _extractionOptions);
break; break;
} }
} }

View file

@ -1,5 +1,9 @@
using Dalamud.Bindings.ImGui;
using OtterGui.Raii;
using OtterGui;
using Dalamud.Interface.Utility;
using ImSharp; using ImSharp;
using OtterGui.Text; using Penumbra.UI;
using Rgba32 = SixLabors.ImageSharp.PixelFormats.Rgba32; using Rgba32 = SixLabors.ImageSharp.PixelFormats.Rgba32;
namespace Penumbra.Import.Textures; namespace Penumbra.Import.Textures;
@ -25,19 +29,20 @@ public partial class CombinedTexture
private const float BWeight = 0.0722f; private const float BWeight = 0.0722f;
// @formatter:off // @formatter:off
private static readonly IReadOnlyList<(StringU8 Label, Matrix4x4 Multiplier, Vector4 Constant)> PredefinedColorTransforms = private static readonly IReadOnlyList<(string Label, Matrix4x4 Multiplier, Vector4 Constant)> PredefinedColorTransforms =
[ new[]
(new StringU8("No Transform (Identity)"u8), Matrix4x4.Identity, Vector4.Zero ), {
(new StringU8("Grayscale (Average)"u8), new Matrix4x4(OneThird, OneThird, OneThird, 0.0f, OneThird, OneThird, OneThird, 0.0f, OneThird, OneThird, OneThird, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f), Vector4.Zero ), ("No Transform (Identity)", Matrix4x4.Identity, Vector4.Zero ),
(new StringU8("Grayscale (Weighted)"u8), new Matrix4x4(RWeight, RWeight, RWeight, 0.0f, GWeight, GWeight, GWeight, 0.0f, BWeight, BWeight, BWeight, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f), Vector4.Zero ), ("Grayscale (Average)", new Matrix4x4(OneThird, OneThird, OneThird, 0.0f, OneThird, OneThird, OneThird, 0.0f, OneThird, OneThird, OneThird, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f), Vector4.Zero ),
(new StringU8("Grayscale (Average) to Alpha"u8), new Matrix4x4(OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.Zero ), ("Grayscale (Weighted)", new Matrix4x4(RWeight, RWeight, RWeight, 0.0f, GWeight, GWeight, GWeight, 0.0f, BWeight, BWeight, BWeight, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f), Vector4.Zero ),
(new StringU8("Grayscale (Weighted) to Alpha"u8), new Matrix4x4(RWeight, RWeight, RWeight, RWeight, GWeight, GWeight, GWeight, GWeight, BWeight, BWeight, BWeight, BWeight, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.Zero ), ("Grayscale (Average) to Alpha", new Matrix4x4(OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.Zero ),
(new StringU8("Make Opaque (Drop Alpha)"u8), new Matrix4x4(1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), ("Grayscale (Weighted) to Alpha", new Matrix4x4(RWeight, RWeight, RWeight, RWeight, GWeight, GWeight, GWeight, GWeight, BWeight, BWeight, BWeight, BWeight, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.Zero ),
(new StringU8("Extract Red"u8), new Matrix4x4(1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), ("Make Opaque (Drop Alpha)", new Matrix4x4(1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ),
(new StringU8("Extract Green"u8), new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), ("Extract Red", new Matrix4x4(1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ),
(new StringU8("Extract Blue"u8), new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), ("Extract Green", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ),
(new StringU8("Extract Alpha"u8), new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f), Vector4.UnitW ), ("Extract Blue", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ),
]; ("Extract Alpha", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f), Vector4.UnitW ),
};
// @formatter:on // @formatter:on
private Vector4 DataLeft(int offset) private Vector4 DataLeft(int offset)
@ -206,15 +211,15 @@ public partial class CombinedTexture
return transformed; return transformed;
} }
private static bool DragFloat(Utf8StringHandler<LabelStringHandlerBuffer> label, float width, ref float value) private static bool DragFloat(string label, float width, ref float value)
{ {
var tmp = value; var tmp = value;
Im.Table.NextColumn(); ImGui.TableNextColumn();
Im.Item.SetNextWidth(width); ImGui.SetNextItemWidth(width);
if (Im.Drag(label, ref tmp, speed: 0.001f, min: -1f, max: 1f)) if (ImGui.DragFloat(label, ref tmp, 0.001f, -1f, 1f))
value = tmp; value = tmp;
return Im.Item.DeactivatedAfterEdit; return ImGui.IsItemDeactivatedAfterEdit();
} }
public void DrawMatrixInputLeft(float width) public void DrawMatrixInputLeft(float width)
@ -225,69 +230,53 @@ public partial class CombinedTexture
Update(); Update();
} }
private sealed class CombineOperationCombo() : SimpleFilterCombo<CombineOp>(SimpleFilterType.None)
{
private static readonly CombineOp[] UserValues = Enum.GetValues<CombineOp>().Where(c => (int)c >= 0).ToArray();
public override StringU8 DisplayString(in CombineOp value)
=> new(value.ToLabelU8());
public override string FilterString(in CombineOp value)
=> value.ToLabel();
public override IEnumerable<CombineOp> GetBaseItems()
=> UserValues;
public override StringU8 Tooltip(in CombineOp value)
=> new(value.Tooltip());
}
private sealed class ResizeOperationCombo() : SimpleFilterCombo<ResizeOp>(SimpleFilterType.None)
{
private static readonly ResizeOp[] UserValues = Enum.GetValues<ResizeOp>().Where(c => (int)c >= 0).ToArray();
public override StringU8 DisplayString(in ResizeOp value)
=> new(value.ToLabelU8());
public override string FilterString(in ResizeOp value)
=> value.ToLabel();
public override IEnumerable<ResizeOp> GetBaseItems()
=> UserValues;
}
private readonly CombineOperationCombo _combineCombo = new();
private readonly ResizeOperationCombo _resizeCombo = new();
public void DrawMatrixInputRight(float width) public void DrawMatrixInputRight(float width)
{ {
var ret = DrawMatrixInput(ref _multiplierRight, ref _constantRight, width); var ret = DrawMatrixInput(ref _multiplierRight, ref _constantRight, width);
ret |= DrawMatrixTools(ref _multiplierRight, ref _constantRight); ret |= DrawMatrixTools(ref _multiplierRight, ref _constantRight);
Im.Item.SetNextWidthScaled(75); ImGui.SetNextItemWidth(75.0f * UiHelpers.Scale);
Im.Drag("##XOffset"u8, ref _offsetX, speed: 0.5f); ImGui.DragInt("##XOffset", ref _offsetX, 0.5f);
ret |= Im.Item.DeactivatedAfterEdit; ret |= ImGui.IsItemDeactivatedAfterEdit();
Im.Line.Same(); Im.Line.Same();
Im.Item.SetNextWidthScaled(75); ImGui.SetNextItemWidth(75.0f * UiHelpers.Scale);
Im.Drag("Offsets##YOffset"u8, ref _offsetY, speed: 0.5f); ImGui.DragInt("Offsets##YOffset", ref _offsetY, 0.5f);
ret |= Im.Item.DeactivatedAfterEdit; ret |= ImGui.IsItemDeactivatedAfterEdit();
Im.Item.SetNextWidthScaled(200); ImGui.SetNextItemWidth(200.0f * UiHelpers.Scale);
ret |= _combineCombo.Draw("Combine Operation"u8, ref _combineOp, StringU8.Empty, 200 * Im.Style.GlobalScale); using (var c = ImRaii.Combo("Combine Operation", CombineOpLabels[(int)_combineOp]))
var resizeOp = GetActualResizeOp(_resizeOp, _combineOp);
using (Im.Disabled((int)resizeOp < 0))
{ {
ret |= _resizeCombo.Draw("Resizing Mode"u8, ref _resizeOp, StringU8.Empty, 200 * Im.Style.GlobalScale); if (c)
foreach (var op in Enum.GetValues<CombineOp>())
{
if ((int)op < 0) // Negative codes are for internal use only.
continue;
if (ImGui.Selectable(CombineOpLabels[(int)op], op == _combineOp))
{
_combineOp = op;
ret = true;
}
ImGuiUtil.SelectableHelpMarker(CombineOpTooltips[(int)op]);
}
} }
using (Im.Disabled(_combineOp != CombineOp.CopyChannels)) var resizeOp = GetActualResizeOp(_resizeOp, _combineOp);
using (var dis = ImRaii.Disabled((int)resizeOp < 0))
{ {
Im.Text("Copy"u8); ret |= ImGuiUtil.GenericEnumCombo("Resizing Mode", 200.0f * UiHelpers.Scale, _resizeOp, out _resizeOp,
Enum.GetValues<ResizeOp>().Where(op => (int)op >= 0), op => ResizeOpLabels[(int)op]);
}
using (var dis = ImRaii.Disabled(_combineOp != CombineOp.CopyChannels))
{
ImGui.TextUnformatted("Copy");
foreach (var channel in Enum.GetValues<Channels>()) foreach (var channel in Enum.GetValues<Channels>())
{ {
Im.Line.Same(); Im.Line.Same();
var copy = (_copyChannels & channel) != 0; var copy = (_copyChannels & channel) != 0;
if (Im.Checkbox(channel.ToString(), ref copy)) if (ImGui.Checkbox(channel.ToString(), ref copy))
{ {
_copyChannels = copy ? _copyChannels | channel : _copyChannels & ~channel; _copyChannels = copy ? _copyChannels | channel : _copyChannels & ~channel;
ret = true; ret = true;
@ -301,52 +290,62 @@ public partial class CombinedTexture
private static bool DrawMatrixInput(ref Matrix4x4 multiplier, ref Vector4 constant, float width) private static bool DrawMatrixInput(ref Matrix4x4 multiplier, ref Vector4 constant, float width)
{ {
using var table = Im.Table.Begin(StringU8.Empty, 5, TableFlags.BordersInner | TableFlags.SizingFixedFit); using var table = ImRaii.Table(string.Empty, 5, ImGuiTableFlags.BordersInner | ImGuiTableFlags.SizingFixedFit);
if (!table) if (!table)
return false; return false;
var changes = false; var changes = false;
table.NextColumn(); ImGui.TableNextColumn();
table.NextColumn(); ImGui.TableNextColumn();
ImEx.TextCentered("R"u8); ImGuiUtil.Center("R");
table.NextColumn(); ImGui.TableNextColumn();
ImEx.TextCentered("G"u8); ImGuiUtil.Center("G");
table.NextColumn(); ImGui.TableNextColumn();
ImEx.TextCentered("B"u8); ImGuiUtil.Center("B");
table.NextColumn(); ImGui.TableNextColumn();
ImEx.TextCentered("A"u8); ImGuiUtil.Center("A");
var inputWidth = width / 6; var inputWidth = width / 6;
table.DrawFrameColumn("R "u8); ImGui.TableNextColumn();
changes |= DragFloat("##RR"u8, inputWidth, ref multiplier.M11); ImGui.AlignTextToFramePadding();
changes |= DragFloat("##RG"u8, inputWidth, ref multiplier.M12); ImGui.Text("R ");
changes |= DragFloat("##RB"u8, inputWidth, ref multiplier.M13); changes |= DragFloat("##RR", inputWidth, ref multiplier.M11);
changes |= DragFloat("##RA"u8, inputWidth, ref multiplier.M14); changes |= DragFloat("##RG", inputWidth, ref multiplier.M12);
changes |= DragFloat("##RB", inputWidth, ref multiplier.M13);
changes |= DragFloat("##RA", inputWidth, ref multiplier.M14);
table.DrawFrameColumn("G "u8); ImGui.TableNextColumn();
changes |= DragFloat("##GR"u8, inputWidth, ref multiplier.M21); ImGui.AlignTextToFramePadding();
changes |= DragFloat("##GG"u8, inputWidth, ref multiplier.M22); ImGui.Text("G ");
changes |= DragFloat("##GB"u8, inputWidth, ref multiplier.M23); changes |= DragFloat("##GR", inputWidth, ref multiplier.M21);
changes |= DragFloat("##GA"u8, inputWidth, ref multiplier.M24); changes |= DragFloat("##GG", inputWidth, ref multiplier.M22);
changes |= DragFloat("##GB", inputWidth, ref multiplier.M23);
changes |= DragFloat("##GA", inputWidth, ref multiplier.M24);
table.DrawFrameColumn("B "u8); ImGui.TableNextColumn();
changes |= DragFloat("##BR"u8, inputWidth, ref multiplier.M31); ImGui.AlignTextToFramePadding();
changes |= DragFloat("##BG"u8, inputWidth, ref multiplier.M32); ImGui.Text("B ");
changes |= DragFloat("##BB"u8, inputWidth, ref multiplier.M33); changes |= DragFloat("##BR", inputWidth, ref multiplier.M31);
changes |= DragFloat("##BA"u8, inputWidth, ref multiplier.M34); changes |= DragFloat("##BG", inputWidth, ref multiplier.M32);
changes |= DragFloat("##BB", inputWidth, ref multiplier.M33);
changes |= DragFloat("##BA", inputWidth, ref multiplier.M34);
table.DrawFrameColumn("A "u8); ImGui.TableNextColumn();
changes |= DragFloat("##AR"u8, inputWidth, ref multiplier.M41); ImGui.AlignTextToFramePadding();
changes |= DragFloat("##AG"u8, inputWidth, ref multiplier.M42); ImGui.Text("A ");
changes |= DragFloat("##AB"u8, inputWidth, ref multiplier.M43); changes |= DragFloat("##AR", inputWidth, ref multiplier.M41);
changes |= DragFloat("##AA"u8, inputWidth, ref multiplier.M44); changes |= DragFloat("##AG", inputWidth, ref multiplier.M42);
changes |= DragFloat("##AB", inputWidth, ref multiplier.M43);
changes |= DragFloat("##AA", inputWidth, ref multiplier.M44);
table.DrawFrameColumn("1 "u8); ImGui.TableNextColumn();
changes |= DragFloat("##1R"u8, inputWidth, ref constant.X); ImGui.AlignTextToFramePadding();
changes |= DragFloat("##1G"u8, inputWidth, ref constant.Y); ImGui.Text("1 ");
changes |= DragFloat("##1B"u8, inputWidth, ref constant.Z); changes |= DragFloat("##1R", inputWidth, ref constant.X);
changes |= DragFloat("##1A"u8, inputWidth, ref constant.W); changes |= DragFloat("##1G", inputWidth, ref constant.Y);
changes |= DragFloat("##1B", inputWidth, ref constant.Z);
changes |= DragFloat("##1A", inputWidth, ref constant.W);
return changes; return changes;
} }
@ -355,28 +354,28 @@ public partial class CombinedTexture
{ {
var changes = PresetCombo(ref multiplier, ref constant); var changes = PresetCombo(ref multiplier, ref constant);
Im.Line.Same(); Im.Line.Same();
Im.ScaledDummy(20); ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0));
Im.Line.Same(); Im.Line.Same();
Im.Text("Invert"u8); ImGui.TextUnformatted("Invert");
Im.Line.Same(); Im.Line.Same();
Channels channels = 0; Channels channels = 0;
if (Im.Button("Colors"u8)) if (ImGui.Button("Colors"))
channels |= Channels.Red | Channels.Green | Channels.Blue; channels |= Channels.Red | Channels.Green | Channels.Blue;
Im.Line.Same(); Im.Line.Same();
if (Im.Button("R"u8)) if (ImGui.Button("R"))
channels |= Channels.Red; channels |= Channels.Red;
Im.Line.Same(); Im.Line.Same();
if (Im.Button("G"u8)) if (ImGui.Button("G"))
channels |= Channels.Green; channels |= Channels.Green;
Im.Line.Same(); Im.Line.Same();
if (Im.Button("B"u8)) if (ImGui.Button("B"))
channels |= Channels.Blue; channels |= Channels.Blue;
Im.Line.Same(); Im.Line.Same();
if (Im.Button("A"u8)) if (ImGui.Button("A"))
channels |= Channels.Alpha; channels |= Channels.Alpha;
changes |= InvertChannels(channels, ref multiplier, ref constant); changes |= InvertChannels(channels, ref multiplier, ref constant);
@ -385,14 +384,14 @@ public partial class CombinedTexture
private static bool PresetCombo(ref Matrix4x4 multiplier, ref Vector4 constant) private static bool PresetCombo(ref Matrix4x4 multiplier, ref Vector4 constant)
{ {
using var combo = Im.Combo.Begin("Presets"u8, StringU8.Empty, ComboFlags.NoPreview); using var combo = ImRaii.Combo("Presets", string.Empty, ImGuiComboFlags.NoPreview);
if (!combo) if (!combo)
return false; return false;
var ret = false; var ret = false;
foreach (var (label, preMultiplier, preConstant) in PredefinedColorTransforms) foreach (var (label, preMultiplier, preConstant) in PredefinedColorTransforms)
{ {
if (!Im.Selectable(label, multiplier == preMultiplier && constant == preConstant)) if (!ImGui.Selectable(label, multiplier == preMultiplier && constant == preConstant))
continue; continue;
multiplier = preMultiplier; multiplier = preMultiplier;

View file

@ -1,147 +1,146 @@
using Luna.Generators; namespace Penumbra.Import.Textures;
namespace Penumbra.Import.Textures; public partial class CombinedTexture
{
public partial class CombinedTexture private enum CombineOp
{ {
[NamedEnum("ToLabel")] LeftMultiply = -4,
[TooltipEnum] LeftCopy = -3,
public enum CombineOp RightCopy = -2,
{ Invalid = -1,
LeftMultiply = -4, Over = 0,
LeftCopy = -3, Under = 1,
RightCopy = -2, RightMultiply = 2,
Invalid = -1, CopyChannels = 3,
}
[Name("Overlay over Input")]
[Tooltip("Standard composition.\nApply the overlay over the input.")] private enum ResizeOp
Over = 0, {
LeftOnly = -2,
[Name("Input over Overlay")] RightOnly = -1,
[Tooltip("Standard composition, reversed.\nApply the input over the overlay ; can be used to fix some wrong imports.")] None = 0,
Under = 1, ToLeft = 1,
ToRight = 2,
[Name("Replace Input")] }
[Tooltip("Completely replace the input with the overlay.\nCan be used to select the destination file as input and the source file as overlay.")]
RightMultiply = 2, [Flags]
private enum Channels : byte
[Name("Copy Channels")] {
[Tooltip("Replace some input channels with those from the overlay.\nUseful for Multi maps.")] Red = 1,
CopyChannels = 3, Green = 2,
} Blue = 4,
Alpha = 8,
[NamedEnum("ToLabel")] }
public enum ResizeOp
{ private static readonly IReadOnlyList<string> CombineOpLabels = new[]
LeftOnly = -2, {
RightOnly = -1, "Overlay over Input",
"Input over Overlay",
[Name("No Resizing")] "Replace Input",
None = 0, "Copy Channels",
};
[Name("Adjust Overlay to Input")]
ToLeft = 1, private static readonly IReadOnlyList<string> CombineOpTooltips = new[]
{
[Name("Adjust Input to Overlay")] "Standard composition.\nApply the overlay over the input.",
ToRight = 2, "Standard composition, reversed.\nApply the input over the overlay ; can be used to fix some wrong imports.",
} "Completely replace the input with the overlay.\nCan be used to select the destination file as input and the source file as overlay.",
"Replace some input channels with those from the overlay.\nUseful for Multi maps.",
[Flags] };
[NamedEnum]
public enum Channels : byte private static readonly IReadOnlyList<string> ResizeOpLabels = new string[]
{ {
Red = 1, "No Resizing",
Green = 2, "Adjust Overlay to Input",
Blue = 4, "Adjust Input to Overlay",
Alpha = 8, };
}
private static ResizeOp GetActualResizeOp(ResizeOp resizeOp, CombineOp combineOp)
private static ResizeOp GetActualResizeOp(ResizeOp resizeOp, CombineOp combineOp) => combineOp switch
=> combineOp switch {
{ CombineOp.LeftCopy => ResizeOp.LeftOnly,
CombineOp.LeftCopy => ResizeOp.LeftOnly, CombineOp.LeftMultiply => ResizeOp.LeftOnly,
CombineOp.LeftMultiply => ResizeOp.LeftOnly, CombineOp.RightCopy => ResizeOp.RightOnly,
CombineOp.RightCopy => ResizeOp.RightOnly, CombineOp.RightMultiply => ResizeOp.RightOnly,
CombineOp.RightMultiply => ResizeOp.RightOnly, CombineOp.Over => resizeOp,
CombineOp.Over => resizeOp, CombineOp.Under => resizeOp,
CombineOp.Under => resizeOp, CombineOp.CopyChannels => resizeOp,
CombineOp.CopyChannels => resizeOp, _ => throw new ArgumentException($"Invalid combine operation {combineOp}"),
_ => throw new ArgumentException($"Invalid combine operation {combineOp}"), };
};
private CombineOp GetActualCombineOp()
private CombineOp GetActualCombineOp() {
{ var combineOp = (_left.IsLoaded, _right.IsLoaded) switch
var combineOp = (_left.IsLoaded, _right.IsLoaded) switch {
{ (true, true) => _combineOp,
(true, true) => _combineOp, (true, false) => CombineOp.LeftMultiply,
(true, false) => CombineOp.LeftMultiply, (false, true) => CombineOp.RightMultiply,
(false, true) => CombineOp.RightMultiply, (false, false) => CombineOp.Invalid,
(false, false) => CombineOp.Invalid, };
};
if (combineOp == CombineOp.CopyChannels)
if (combineOp == CombineOp.CopyChannels) {
{ if (_copyChannels == 0)
if (_copyChannels == 0) combineOp = CombineOp.LeftMultiply;
combineOp = CombineOp.LeftMultiply; else if (_copyChannels == (Channels.Red | Channels.Green | Channels.Blue | Channels.Alpha))
else if (_copyChannels == (Channels.Red | Channels.Green | Channels.Blue | Channels.Alpha)) combineOp = CombineOp.RightMultiply;
combineOp = CombineOp.RightMultiply; }
}
return combineOp switch
return combineOp switch {
{ CombineOp.LeftMultiply when _multiplierLeft.IsIdentity && _constantLeft == Vector4.Zero => CombineOp.LeftCopy,
CombineOp.LeftMultiply when _multiplierLeft.IsIdentity && _constantLeft == Vector4.Zero => CombineOp.LeftCopy, CombineOp.RightMultiply when _multiplierRight.IsIdentity && _constantRight == Vector4.Zero => CombineOp.RightCopy,
CombineOp.RightMultiply when _multiplierRight.IsIdentity && _constantRight == Vector4.Zero => CombineOp.RightCopy, _ => combineOp,
_ => combineOp, };
}; }
}
private static bool InvertChannels(Channels channels, ref Matrix4x4 multiplier, ref Vector4 constant)
private static bool InvertChannels(Channels channels, ref Matrix4x4 multiplier, ref Vector4 constant) {
{ if (channels.HasFlag(Channels.Red))
if (channels.HasFlag(Channels.Red)) InvertRed(ref multiplier, ref constant);
InvertRed(ref multiplier, ref constant); if (channels.HasFlag(Channels.Green))
if (channels.HasFlag(Channels.Green)) InvertGreen(ref multiplier, ref constant);
InvertGreen(ref multiplier, ref constant); if (channels.HasFlag(Channels.Blue))
if (channels.HasFlag(Channels.Blue)) InvertBlue(ref multiplier, ref constant);
InvertBlue(ref multiplier, ref constant); if (channels.HasFlag(Channels.Alpha))
if (channels.HasFlag(Channels.Alpha)) InvertAlpha(ref multiplier, ref constant);
InvertAlpha(ref multiplier, ref constant); return channels != 0;
return channels != 0; }
}
private static void InvertRed(ref Matrix4x4 multiplier, ref Vector4 constant)
private static void InvertRed(ref Matrix4x4 multiplier, ref Vector4 constant) {
{ multiplier.M11 = -multiplier.M11;
multiplier.M11 = -multiplier.M11; multiplier.M21 = -multiplier.M21;
multiplier.M21 = -multiplier.M21; multiplier.M31 = -multiplier.M31;
multiplier.M31 = -multiplier.M31; multiplier.M41 = -multiplier.M41;
multiplier.M41 = -multiplier.M41; constant.X = 1.0f - constant.X;
constant.X = 1.0f - constant.X; }
}
private static void InvertGreen(ref Matrix4x4 multiplier, ref Vector4 constant)
private static void InvertGreen(ref Matrix4x4 multiplier, ref Vector4 constant) {
{ multiplier.M12 = -multiplier.M12;
multiplier.M12 = -multiplier.M12; multiplier.M22 = -multiplier.M22;
multiplier.M22 = -multiplier.M22; multiplier.M32 = -multiplier.M32;
multiplier.M32 = -multiplier.M32; multiplier.M42 = -multiplier.M42;
multiplier.M42 = -multiplier.M42; constant.Y = 1.0f - constant.Y;
constant.Y = 1.0f - constant.Y; }
}
private static void InvertBlue(ref Matrix4x4 multiplier, ref Vector4 constant)
private static void InvertBlue(ref Matrix4x4 multiplier, ref Vector4 constant) {
{ multiplier.M13 = -multiplier.M13;
multiplier.M13 = -multiplier.M13; multiplier.M23 = -multiplier.M23;
multiplier.M23 = -multiplier.M23; multiplier.M33 = -multiplier.M33;
multiplier.M33 = -multiplier.M33; multiplier.M43 = -multiplier.M43;
multiplier.M43 = -multiplier.M43; constant.Z = 1.0f - constant.Z;
constant.Z = 1.0f - constant.Z; }
}
private static void InvertAlpha(ref Matrix4x4 multiplier, ref Vector4 constant)
private static void InvertAlpha(ref Matrix4x4 multiplier, ref Vector4 constant) {
{ multiplier.M14 = -multiplier.M14;
multiplier.M14 = -multiplier.M14; multiplier.M24 = -multiplier.M24;
multiplier.M24 = -multiplier.M24; multiplier.M34 = -multiplier.M34;
multiplier.M34 = -multiplier.M34; multiplier.M44 = -multiplier.M44;
multiplier.M44 = -multiplier.M44; constant.W = 1.0f - constant.W;
constant.W = 1.0f - constant.W; }
} }
}

View file

@ -1,80 +0,0 @@
using Dalamud.Plugin.Services;
using ImSharp;
using Penumbra.Api.Enums;
using Penumbra.Interop.ResourceTree;
using Penumbra.Mods.Editor;
using Penumbra.UI.Classes;
namespace Penumbra.Import.Textures;
public abstract class PathSelectCombo(IDataManager dataManager) : FilterComboBase<PathSelectCombo.PathData>
{
public bool Draw(Utf8StringHandler<LabelStringHandlerBuffer> label, Utf8StringHandler<HintStringHandlerBuffer> tooltip, string current,
int skipPrefix, out string newPath)
{
_skipPrefix = skipPrefix;
_selected = current;
if (!base.Draw(label, current.Length > 0 ? current : "Choose a modded texture from this mod here..."u8, tooltip,
Im.ContentRegion.Available.X, out var ret))
{
newPath = string.Empty;
return false;
}
newPath = ret.SearchPath;
return true;
}
public record PathData(StringU8 Path, string SearchPath, bool IsOnPlayer, bool IsGame);
private int _skipPrefix;
private string _selected = string.Empty;
protected abstract IEnumerable<FileRegistry> GetFiles();
protected abstract ISet<string> GetPlayerResources();
protected override IEnumerable<PathData> GetItems()
{
var playerResources = GetPlayerResources();
var files = GetFiles();
foreach (var (file, game) in files.SelectMany(f => f.SubModUsage.Select(p => (p.Item2.ToString(), true))
.Prepend((f.File.FullName, false)))
.Where(p => p.Item2 ? dataManager.FileExists(p.Item1) : File.Exists(p.Item1)))
{
var onPlayer = playerResources.Contains(file);
var displayString = game ? new StringU8($"--> {file}") : new StringU8(file.AsSpan(_skipPrefix));
yield return new PathData(displayString, file, onPlayer, game);
}
}
protected override float ItemHeight
=> Im.Style.TextHeightWithSpacing;
protected override bool DrawItem(in PathData item, int globalIndex, bool selected)
{
var textColor = item.IsOnPlayer ? ColorId.HandledConflictMod.Value() :
item.IsGame ? ColorId.FolderExpanded.Value() : ColorParameter.Default;
bool ret;
using (ImGuiColor.Text.Push(textColor))
{
ret = Im.Selectable(item.Path, selected);
}
Im.Tooltip.OnHover(item.IsGame
? "This is a game path and refers to an unmanipulated file from your game data."u8
: "This is a path to a modded file on your file system."u8);
return ret;
}
protected override bool IsSelected(PathData item, int globalIndex)
=> string.Equals(_selected, item.SearchPath, StringComparison.OrdinalIgnoreCase);
}
public sealed class TextureSelectCombo(ResourceTreeFactory resources, ModEditor editor, IDataManager dataManager) : PathSelectCombo(dataManager)
{
protected override IEnumerable<FileRegistry> GetFiles()
=> editor.Files.Tex;
protected override ISet<string> GetPlayerResources()
=> ResourceTreeApiHelper.GetPlayerResourcesOfType(resources, ResourceType.Tex);
}

View file

@ -1,104 +1,168 @@
using ImSharp; using Dalamud.Bindings.ImGui;
using Lumina.Data.Files; using Dalamud.Interface;
using Luna; using ImSharp;
using OtterTex; using Lumina.Data.Files;
using Penumbra.UI; using OtterGui;
using Penumbra.UI.Classes; using OtterGui.Raii;
using VectorExtensions = Luna.VectorExtensions; using OtterGui.Widgets;
using OtterTex;
namespace Penumbra.Import.Textures; using Penumbra.Mods.Editor;
using Penumbra.String.Classes;
public static class TextureDrawer using Penumbra.UI;
{ using Penumbra.UI.Classes;
public static void Draw(Texture texture, Vector2 size) using MouseWheelType = OtterGui.Widgets.MouseWheelType;
{
if (texture.TextureWrap != null) namespace Penumbra.Import.Textures;
{
size = VectorExtensions.Contain(texture.TextureWrap.Size, size); public static class TextureDrawer
{
Im.Image.Draw(texture.TextureWrap.Id(), size); public static void Draw(Texture texture, Vector2 size)
DrawData(texture); {
} if (texture.TextureWrap != null)
else if (texture.LoadError != null) {
{ size = texture.TextureWrap.Size.Contain(size);
const string link = "https://aka.ms/vcredist";
Im.Text("Could not load file:"u8); ImGui.Image(texture.TextureWrap.Handle, size);
DrawData(texture);
if (texture.LoadError is DllNotFoundException) }
{ else if (texture.LoadError != null)
Im.Text("A texture handling dependency could not be found. Try installing a current Microsoft VC Redistributable."u8, {
Colors.RegexWarningBorder); const string link = "https://aka.ms/vcredist";
if (Im.Button("Microsoft VC Redistributables"u8)) ImGui.TextUnformatted("Could not load file:");
Dalamud.Utility.Util.OpenLink(link);
Im.Tooltip.OnHover($"Open {link} in your browser."); if (texture.LoadError is DllNotFoundException)
} {
ImGuiUtil.TextColored(Colors.RegexWarningBorder,
Im.Text($"{texture.LoadError}", Colors.RegexWarningBorder); "A texture handling dependency could not be found. Try installing a current Microsoft VC Redistributable.");
} if (ImGui.Button("Microsoft VC Redistributables"))
} Dalamud.Utility.Util.OpenLink(link);
ImGuiUtil.HoverTooltip($"Open {link} in your browser.");
public static void PathInputBox(TextureManager textures, Texture current, ref string? tmpPath, ReadOnlySpan<byte> label, }
ReadOnlySpan<byte> hint, ReadOnlySpan<byte> tooltip,
string startPath, FileDialogService fileDialog, string defaultModImportPath) ImGuiUtil.TextColored(Colors.RegexWarningBorder, texture.LoadError.ToString());
{ }
tmpPath ??= current.Path; }
using var spacing = ImStyleDouble.ItemSpacing.PushX(UiHelpers.ScaleX3);
Im.Item.SetNextWidth(-2 * Im.Style.FrameHeight - 7 * Im.Style.GlobalScale); public static void PathInputBox(TextureManager textures, Texture current, ref string? tmpPath, string label, string hint, string tooltip,
if (ImEx.InputOnDeactivation.Text(label, tmpPath, out tmpPath, hint)) string startPath, FileDialogService fileDialog, string defaultModImportPath)
current.Load(textures, tmpPath); {
tmpPath ??= current.Path;
Im.Tooltip.OnHover(tooltip); using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing,
Im.Line.Same(); new Vector2(UiHelpers.ScaleX3, ImGui.GetStyle().ItemSpacing.Y));
if (ImEx.Icon.Button(LunaStyle.FolderIcon)) ImGui.SetNextItemWidth(-2 * ImGui.GetFrameHeight() - 7 * UiHelpers.Scale);
{ ImGui.InputTextWithHint(label, hint, ref tmpPath, Utf8GamePath.MaxGamePathLength);
if (defaultModImportPath.Length > 0) if (ImGui.IsItemDeactivatedAfterEdit())
startPath = defaultModImportPath; current.Load(textures, tmpPath);
void UpdatePath(bool success, List<string> paths) ImGuiUtil.HoverTooltip(tooltip);
{ Im.Line.Same();
if (success && paths.Count > 0) if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Folder.ToIconString(), new Vector2(ImGui.GetFrameHeight()), string.Empty, false,
current.Load(textures, paths[0]); true))
} {
if (defaultModImportPath.Length > 0)
fileDialog.OpenFilePicker("Open Image...", "Textures{.png,.dds,.tex,.tga}", UpdatePath, 1, startPath, false); startPath = defaultModImportPath;
}
void UpdatePath(bool success, List<string> paths)
Im.Line.Same(); {
if (ImEx.Icon.Button(LunaStyle.RefreshIcon, "Reload the currently selected path."u8)) if (success && paths.Count > 0)
current.Reload(textures); current.Load(textures, paths[0]);
} }
private static void DrawData(Texture texture) fileDialog.OpenFilePicker("Open Image...", "Textures{.png,.dds,.tex,.tga}", UpdatePath, 1, startPath, false);
{ }
using var table = Im.Table.Begin("##data"u8, 2, TableFlags.SizingFixedFit);
table.DrawColumn("Width"u8); Im.Line.Same();
table.DrawColumn($"{texture.TextureWrap!.Width}"); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Recycle.ToIconString(), new Vector2(ImGui.GetFrameHeight()),
table.DrawColumn("Height"u8); "Reload the currently selected path.", false,
table.DrawColumn($"{texture.TextureWrap!.Height}"); true))
table.DrawColumn("File Type"u8); current.Reload(textures);
table.DrawColumn($"{texture.Type}"); }
table.DrawColumn("Bitmap Size"u8);
table.DrawColumn($"{FormattingFunctions.HumanReadableSize(texture.RgbaPixels.Length)} ({texture.RgbaPixels.Length} Bytes)"); private static void DrawData(Texture texture)
switch (texture.BaseImage.Image) {
{ using var table = ImRaii.Table("##data", 2, ImGuiTableFlags.SizingFixedFit);
case ScratchImage s: ImGuiUtil.DrawTableColumn("Width");
table.DrawColumn("Format"u8); ImGuiUtil.DrawTableColumn(texture.TextureWrap!.Width.ToString());
table.DrawColumn($"{s.Meta.Format}"); ImGuiUtil.DrawTableColumn("Height");
table.DrawColumn("Mip Levels"u8); ImGuiUtil.DrawTableColumn(texture.TextureWrap!.Height.ToString());
table.DrawColumn($"{s.Meta.MipLevels}"); ImGuiUtil.DrawTableColumn("File Type");
table.DrawColumn("Data Size"u8); ImGuiUtil.DrawTableColumn(texture.Type.ToString());
table.DrawColumn($"{FormattingFunctions.HumanReadableSize(s.Pixels.Length)} ({s.Pixels.Length} Bytes)"); ImGuiUtil.DrawTableColumn("Bitmap Size");
table.DrawColumn("Number of Images"u8); ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(texture.RgbaPixels.Length)} ({texture.RgbaPixels.Length} Bytes)");
table.DrawColumn($"{s.Images.Length}"); switch (texture.BaseImage.Image)
break; {
case TexFile t: case ScratchImage s:
table.DrawColumn("Format"u8); ImGuiUtil.DrawTableColumn("Format");
table.DrawColumn($"{t.Header.Format}"); ImGuiUtil.DrawTableColumn(s.Meta.Format.ToString());
table.DrawColumn("Mip Levels"u8); ImGuiUtil.DrawTableColumn("Mip Levels");
table.DrawColumn($"{t.Header.MipCount}"); ImGuiUtil.DrawTableColumn(s.Meta.MipLevels.ToString());
table.DrawColumn("Data Size"u8); ImGuiUtil.DrawTableColumn("Data Size");
table.DrawColumn($"{FormattingFunctions.HumanReadableSize(t.ImageData.Length)} ({t.ImageData.Length} Bytes)"); ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(s.Pixels.Length)} ({s.Pixels.Length} Bytes)");
break; ImGuiUtil.DrawTableColumn("Number of Images");
} ImGuiUtil.DrawTableColumn(s.Images.Length.ToString());
} break;
} case TexFile t:
ImGuiUtil.DrawTableColumn("Format");
ImGuiUtil.DrawTableColumn(t.Header.Format.ToString());
ImGuiUtil.DrawTableColumn("Mip Levels");
ImGuiUtil.DrawTableColumn(t.Header.MipCount.ToString());
ImGuiUtil.DrawTableColumn("Data Size");
ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(t.ImageData.Length)} ({t.ImageData.Length} Bytes)");
break;
}
}
public sealed class PathSelectCombo(TextureManager textures, ModEditor editor, Func<ISet<string>> getPlayerResources)
: FilterComboCache<(string Path, bool Game, bool IsOnPlayer)>(() => CreateFiles(textures, editor, getPlayerResources),
MouseWheelType.None, Penumbra.Log)
{
private int _skipPrefix = 0;
protected override string ToString((string Path, bool Game, bool IsOnPlayer) obj)
=> obj.Path;
protected override bool DrawSelectable(int globalIdx, bool selected)
{
var (path, game, isOnPlayer) = Items[globalIdx];
bool ret;
using (var color = ImGuiColor.Text.Push(ColorId.FolderExpanded.Value(), game))
{
color.Push(ImGuiColor.Text, ColorId.HandledConflictMod.Value(), isOnPlayer);
var equals = string.Equals(CurrentSelection.Path, path, StringComparison.OrdinalIgnoreCase);
var p = game ? $"--> {path}" : path[_skipPrefix..];
ret = ImGui.Selectable(p, selected) && !equals;
}
ImGuiUtil.HoverTooltip(game
? "This is a game path and refers to an unmanipulated file from your game data."
: "This is a path to a modded file on your file system.");
return ret;
}
private static IReadOnlyList<(string Path, bool Game, bool IsOnPlayer)> CreateFiles(TextureManager textures, ModEditor editor,
Func<ISet<string>> getPlayerResources)
{
var playerResources = getPlayerResources();
return editor.Files.Tex.SelectMany(f => f.SubModUsage.Select(p => (p.Item2.ToString(), true))
.Prepend((f.File.FullName, false)))
.Where(p => p.Item2 ? textures.GameFileExists(p.Item1) : File.Exists(p.Item1))
.Select(p => (p.Item1, p.Item2, playerResources.Contains(p.Item1)))
.ToList();
}
public bool Draw(string label, string tooltip, string current, int skipPrefix, out string newPath)
{
_skipPrefix = skipPrefix;
var startPath = current.Length > 0 ? current : "Choose a modded texture from this mod here...";
if (!Draw(label, startPath, tooltip, -0.0001f, ImGui.GetTextLineHeightWithSpacing()))
{
newPath = current;
return false;
}
newPath = CurrentSelection.Item1;
return true;
}
}
}

View file

@ -1,5 +1,5 @@
using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Luna; using OtterGui.Services;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.CrashHandler.Buffers; using Penumbra.CrashHandler.Buffers;
using Penumbra.GameData; using Penumbra.GameData;

View file

@ -1,10 +1,10 @@
using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Luna; using OtterGui.Services;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.Interop.PathResolving; using Penumbra.Interop.PathResolving;
namespace Penumbra.Interop.Hooks.Animation; namespace Penumbra.Interop.Hooks.Animation;
public sealed unsafe class PlayFootstep : FastHook<PlayFootstep.Delegate> public sealed unsafe class PlayFootstep : FastHook<PlayFootstep.Delegate>
{ {
private readonly GameState _state; private readonly GameState _state;
@ -12,9 +12,9 @@ public sealed unsafe class PlayFootstep : FastHook<PlayFootstep.Delegate>
public PlayFootstep(HookManager hooks, GameState state, CollectionResolver collectionResolver) public PlayFootstep(HookManager hooks, GameState state, CollectionResolver collectionResolver)
{ {
_state = state; _state = state;
_collectionResolver = collectionResolver; _collectionResolver = collectionResolver;
Task = hooks.CreateHook<Delegate>("Play Footstep", Sigs.FootStepSound, Detour, !HookOverrides.Instance.Animation.PlayFootstep); Task = hooks.CreateHook<Delegate>("Play Footstep", Sigs.FootStepSound, Detour, !HookOverrides.Instance.Animation.PlayFootstep);
} }
public delegate void Delegate(GameObject* gameObject, int id, int unk); public delegate void Delegate(GameObject* gameObject, int id, int unk);
@ -27,4 +27,4 @@ public sealed unsafe class PlayFootstep : FastHook<PlayFootstep.Delegate>
Task.Result.Original(gameObject, id, unk); Task.Result.Original(gameObject, id, unk);
_state.RestoreAnimationData(last); _state.RestoreAnimationData(last);
} }
} }

View file

@ -9,16 +9,6 @@ namespace Penumbra.Interop.ResourceTree;
internal static class ResourceTreeApiHelper internal static class ResourceTreeApiHelper
{ {
public static HashSet<string> GetPlayerResourcesOfType(ResourceTreeFactory factory, ResourceType type)
{
var resources = GetResourcesOfType(factory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly), type)
.Values
.SelectMany(r => r.Values)
.Select(r => r.Item1);
return new HashSet<string>(resources, StringComparer.OrdinalIgnoreCase);
}
public static Dictionary<ushort, Dictionary<string, HashSet<string>>> GetResourcePathDictionaries( public static Dictionary<ushort, Dictionary<string, HashSet<string>>> GetResourcePathDictionaries(
IEnumerable<(ICharacter, ResourceTree)> resourceTrees) IEnumerable<(ICharacter, ResourceTree)> resourceTrees)
{ {

View file

@ -71,8 +71,6 @@
<ProjectReference Include="..\Penumbra.GameData\Penumbra.GameData.csproj" /> <ProjectReference Include="..\Penumbra.GameData\Penumbra.GameData.csproj" />
<ProjectReference Include="..\Penumbra.Api\Penumbra.Api.csproj" /> <ProjectReference Include="..\Penumbra.Api\Penumbra.Api.csproj" />
<ProjectReference Include="..\Penumbra.String\Penumbra.String.csproj" /> <ProjectReference Include="..\Penumbra.String\Penumbra.String.csproj" />
<ProjectReference Include="..\Luna\Luna.Generators\Luna.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup> </ItemGroup>
<Target Name="GetGitHash" BeforeTargets="GetAssemblyVersion" Returns="InformationalVersion"> <Target Name="GetGitHash" BeforeTargets="GetAssemblyVersion" Returns="InformationalVersion">

View file

@ -1,5 +1,7 @@
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Bindings.ImGui;
using ImSharp; using ImSharp;
using Lumina.Data;
using OtterGui.Text; using OtterGui.Text;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
@ -18,7 +20,7 @@ public partial class ModEditWindow
private readonly ResourceTreeViewer _quickImportViewer; private readonly ResourceTreeViewer _quickImportViewer;
private readonly Dictionary<(Utf8GamePath, IWritable?), QuickImportAction> _quickImportActions = new(); private readonly Dictionary<(Utf8GamePath, IWritable?), QuickImportAction> _quickImportActions = new();
public HashSet<string> GetPlayerResourcesOfType(ResourceType type) private HashSet<string> GetPlayerResourcesOfType(ResourceType type)
{ {
var resources = ResourceTreeApiHelper var resources = ResourceTreeApiHelper
.GetResourcesOfType(_resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly), type) .GetResourcesOfType(_resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly), type)

View file

@ -15,10 +15,10 @@ public partial class ModEditWindow
{ {
private readonly TextureManager _textures; private readonly TextureManager _textures;
private readonly Texture _left = new(); private readonly Texture _left = new();
private readonly Texture _right = new(); private readonly Texture _right = new();
private readonly CombinedTexture _center; private readonly CombinedTexture _center;
private readonly TextureSelectCombo _textureSelectCombo; private readonly TextureDrawer.PathSelectCombo _textureSelectCombo;
private bool _overlayCollapsed = true; private bool _overlayCollapsed = true;
private bool _addMipMaps = true; private bool _addMipMaps = true;
@ -49,13 +49,13 @@ public partial class ModEditWindow
return; return;
using var id = ImRaii.PushId(label); using var id = ImRaii.PushId(label);
ImEx.TextFramed(label, Im.ContentRegion.Available with { Y = 0 }, ImGuiColor.FrameBackground.Get()); ImEx.TextFramed(label, new Vector2(-1, 0), ImGuiColor.FrameBackground.Get());
ImGui.NewLine(); ImGui.NewLine();
using (ImRaii.Disabled(!_center.SaveTask.IsCompleted)) using (ImRaii.Disabled(!_center.SaveTask.IsCompleted))
{ {
TextureDrawer.PathInputBox(_textures, tex, ref tex.TmpPath, "##input"u8, "Import Image..."u8, TextureDrawer.PathInputBox(_textures, tex, ref tex.TmpPath, "##input", "Import Image...",
"Can import game paths as well as your own files."u8, Mod!.ModPath.FullName, _fileDialog, _config.DefaultModImportPath); "Can import game paths as well as your own files.", Mod!.ModPath.FullName, _fileDialog, _config.DefaultModImportPath);
if (_textureSelectCombo.Draw("##combo", if (_textureSelectCombo.Draw("##combo",
"Select the textures included in this mod on your drive or the ones they replace from the game files.", tex.Path, "Select the textures included in this mod on your drive or the ones they replace from the game files.", tex.Path,
Mod.ModPath.FullName.Length + 1, out var newPath) Mod.ModPath.FullName.Length + 1, out var newPath)
@ -200,6 +200,7 @@ public partial class ModEditWindow
case TaskStatus.WaitingToRun: case TaskStatus.WaitingToRun:
case TaskStatus.Running: case TaskStatus.Running:
ImGuiUtil.DrawTextButton("Computing...", -Vector2.UnitX, Colors.PressEnterWarningBg); ImGuiUtil.DrawTextButton("Computing...", -Vector2.UnitX, Colors.PressEnterWarningBg);
break; break;
case TaskStatus.Canceled: case TaskStatus.Canceled:
case TaskStatus.Faulted: case TaskStatus.Faulted:
@ -209,7 +210,9 @@ public partial class ModEditWindow
ImGuiUtil.TextWrapped(_center.SaveTask.Exception?.ToString() ?? "Unknown Error"); ImGuiUtil.TextWrapped(_center.SaveTask.Exception?.ToString() ?? "Unknown Error");
break; break;
} }
default: ImGui.Dummy(new Vector2(1, ImGui.GetFrameHeight())); break; default:
ImGui.Dummy(new Vector2(1, ImGui.GetFrameHeight()));
break;
} }
ImGui.NewLine(); ImGui.NewLine();

View file

@ -656,7 +656,7 @@ public partial class ModEditWindow : IndexedWindow, IDisposable
() => Mod?.ModPath.FullName ?? string.Empty, () => Mod?.ModPath.FullName ?? string.Empty,
(bytes, path, _) => new PbdTab(bytes, path)); (bytes, path, _) => new PbdTab(bytes, path));
_center = new CombinedTexture(_left, _right); _center = new CombinedTexture(_left, _right);
_textureSelectCombo = new TextureSelectCombo(resourceTreeFactory, editor, gameData); _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor, () => GetPlayerResourcesOfType(ResourceType.Tex));
_resourceTreeFactory = resourceTreeFactory; _resourceTreeFactory = resourceTreeFactory;
_quickImportViewer = resourceTreeViewerFactory.Create(1, OnQuickImportRefresh, DrawQuickImportActions); _quickImportViewer = resourceTreeViewerFactory.Create(1, OnQuickImportRefresh, DrawQuickImportActions);
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModEditWindow); _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModEditWindow);

View file

@ -15,7 +15,7 @@ public class ResourceTreeViewerFactory(
PcpService pcpService, PcpService pcpService,
IDataManager gameData, IDataManager gameData,
FileDialogService fileDialog, FileDialogService fileDialog,
FileCompactor compactor) : IService FileCompactor compactor) : Luna.IService
{ {
public ResourceTreeViewer Create(int actionCapacity, Action onRefresh, Action<ResourceNode, IWritable?, Vector2> drawActions) public ResourceTreeViewer Create(int actionCapacity, Action onRefresh, Action<ResourceNode, IWritable?, Vector2> drawActions)
=> new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions, communicator, pcpService, gameData, => new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions, communicator, pcpService, gameData,