Compare commits

..

3 commits

Author SHA1 Message Date
Ottermandias
34f067f13d Merge branch 'master' into luna
Some checks failed
.NET Build / build (push) Has been cancelled
2025-10-22 17:53:18 +02:00
Ottermandias
7ed81a9823 Update OtterGui.
Some checks are pending
.NET Build / build (push) Waiting to run
2025-10-22 17:53:02 +02:00
Ottermandias
d88593c500 Current State. 2025-10-22 17:52:28 +02:00
19 changed files with 856 additions and 826 deletions

2
Luna

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

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

View file

@ -1,5 +1,5 @@
using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Enums;
using OtterGui.Filesystem; using Luna;
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 OtterGui.Filesystem; using Luna;
namespace Penumbra.Collections; namespace Penumbra.Collections;

View file

@ -1,40 +1,40 @@
using OtterGui.Log; using Luna;
namespace Penumbra.Import.Models; namespace Penumbra.Import.Models;
public record class IoNotifier public record IoNotifier(Logger Log)
{ {
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}";
Penumbra.Log.Message(type, fullText); Log.Message(type, fullText);
_messages.Add(fullText); _messages.Add(fullText);
} }
} }

View file

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

View file

@ -1,10 +1,9 @@
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;
@ -16,7 +15,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,
@ -79,7 +78,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>()?.RemoveInvalidPathSymbols() ?? string.Empty; name = obj[nameof(Mod.Name)]?.Value<string>()?.RemoveInvalidFileNameSymbols() ?? 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.");
@ -142,16 +141,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,9 +1,5 @@
using Dalamud.Bindings.ImGui;
using OtterGui.Raii;
using OtterGui;
using Dalamud.Interface.Utility;
using ImSharp; using ImSharp;
using Penumbra.UI; using OtterGui.Text;
using Rgba32 = SixLabors.ImageSharp.PixelFormats.Rgba32; using Rgba32 = SixLabors.ImageSharp.PixelFormats.Rgba32;
namespace Penumbra.Import.Textures; namespace Penumbra.Import.Textures;
@ -29,20 +25,19 @@ public partial class CombinedTexture
private const float BWeight = 0.0722f; private const float BWeight = 0.0722f;
// @formatter:off // @formatter:off
private static readonly IReadOnlyList<(string Label, Matrix4x4 Multiplier, Vector4 Constant)> PredefinedColorTransforms = private static readonly IReadOnlyList<(StringU8 Label, Matrix4x4 Multiplier, Vector4 Constant)> PredefinedColorTransforms =
new[] [
{ (new StringU8("No Transform (Identity)"u8), Matrix4x4.Identity, Vector4.Zero ),
("No Transform (Identity)", 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 ),
("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 (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 (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 (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 (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("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 (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("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 ),
("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 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 ),
("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 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 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 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 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 ), (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 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)
@ -211,15 +206,15 @@ public partial class CombinedTexture
return transformed; return transformed;
} }
private static bool DragFloat(string label, float width, ref float value) private static bool DragFloat(Utf8StringHandler<LabelStringHandlerBuffer> label, float width, ref float value)
{ {
var tmp = value; var tmp = value;
ImGui.TableNextColumn(); Im.Table.NextColumn();
ImGui.SetNextItemWidth(width); Im.Item.SetNextWidth(width);
if (ImGui.DragFloat(label, ref tmp, 0.001f, -1f, 1f)) if (Im.Drag(label, ref tmp, speed: 0.001f, min: -1f, max: 1f))
value = tmp; value = tmp;
return ImGui.IsItemDeactivatedAfterEdit(); return Im.Item.DeactivatedAfterEdit;
} }
public void DrawMatrixInputLeft(float width) public void DrawMatrixInputLeft(float width)
@ -230,53 +225,69 @@ 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);
ImGui.SetNextItemWidth(75.0f * UiHelpers.Scale); Im.Item.SetNextWidthScaled(75);
ImGui.DragInt("##XOffset", ref _offsetX, 0.5f); Im.Drag("##XOffset"u8, ref _offsetX, speed: 0.5f);
ret |= ImGui.IsItemDeactivatedAfterEdit(); ret |= Im.Item.DeactivatedAfterEdit;
Im.Line.Same(); Im.Line.Same();
ImGui.SetNextItemWidth(75.0f * UiHelpers.Scale); Im.Item.SetNextWidthScaled(75);
ImGui.DragInt("Offsets##YOffset", ref _offsetY, 0.5f); Im.Drag("Offsets##YOffset"u8, ref _offsetY, speed: 0.5f);
ret |= ImGui.IsItemDeactivatedAfterEdit(); ret |= Im.Item.DeactivatedAfterEdit;
ImGui.SetNextItemWidth(200.0f * UiHelpers.Scale);
using (var c = ImRaii.Combo("Combine Operation", CombineOpLabels[(int)_combineOp]))
{
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]);
}
}
Im.Item.SetNextWidthScaled(200);
ret |= _combineCombo.Draw("Combine Operation"u8, ref _combineOp, StringU8.Empty, 200 * Im.Style.GlobalScale);
var resizeOp = GetActualResizeOp(_resizeOp, _combineOp); var resizeOp = GetActualResizeOp(_resizeOp, _combineOp);
using (var dis = ImRaii.Disabled((int)resizeOp < 0)) using (Im.Disabled((int)resizeOp < 0))
{ {
ret |= ImGuiUtil.GenericEnumCombo("Resizing Mode", 200.0f * UiHelpers.Scale, _resizeOp, out _resizeOp, ret |= _resizeCombo.Draw("Resizing Mode"u8, ref _resizeOp, StringU8.Empty, 200 * Im.Style.GlobalScale);
Enum.GetValues<ResizeOp>().Where(op => (int)op >= 0), op => ResizeOpLabels[(int)op]);
} }
using (var dis = ImRaii.Disabled(_combineOp != CombineOp.CopyChannels)) using (Im.Disabled(_combineOp != CombineOp.CopyChannels))
{ {
ImGui.TextUnformatted("Copy"); Im.Text("Copy"u8);
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 (ImGui.Checkbox(channel.ToString(), ref copy)) if (Im.Checkbox(channel.ToString(), ref copy))
{ {
_copyChannels = copy ? _copyChannels | channel : _copyChannels & ~channel; _copyChannels = copy ? _copyChannels | channel : _copyChannels & ~channel;
ret = true; ret = true;
@ -290,62 +301,52 @@ 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 = ImRaii.Table(string.Empty, 5, ImGuiTableFlags.BordersInner | ImGuiTableFlags.SizingFixedFit); using var table = Im.Table.Begin(StringU8.Empty, 5, TableFlags.BordersInner | TableFlags.SizingFixedFit);
if (!table) if (!table)
return false; return false;
var changes = false; var changes = false;
ImGui.TableNextColumn(); table.NextColumn();
ImGui.TableNextColumn(); table.NextColumn();
ImGuiUtil.Center("R"); ImEx.TextCentered("R"u8);
ImGui.TableNextColumn(); table.NextColumn();
ImGuiUtil.Center("G"); ImEx.TextCentered("G"u8);
ImGui.TableNextColumn(); table.NextColumn();
ImGuiUtil.Center("B"); ImEx.TextCentered("B"u8);
ImGui.TableNextColumn(); table.NextColumn();
ImGuiUtil.Center("A"); ImEx.TextCentered("A"u8);
var inputWidth = width / 6; var inputWidth = width / 6;
ImGui.TableNextColumn(); table.DrawFrameColumn("R "u8);
ImGui.AlignTextToFramePadding(); changes |= DragFloat("##RR"u8, inputWidth, ref multiplier.M11);
ImGui.Text("R "); changes |= DragFloat("##RG"u8, inputWidth, ref multiplier.M12);
changes |= DragFloat("##RR", inputWidth, ref multiplier.M11); changes |= DragFloat("##RB"u8, inputWidth, ref multiplier.M13);
changes |= DragFloat("##RG", inputWidth, ref multiplier.M12); changes |= DragFloat("##RA"u8, inputWidth, ref multiplier.M14);
changes |= DragFloat("##RB", inputWidth, ref multiplier.M13);
changes |= DragFloat("##RA", inputWidth, ref multiplier.M14);
ImGui.TableNextColumn(); table.DrawFrameColumn("G "u8);
ImGui.AlignTextToFramePadding(); changes |= DragFloat("##GR"u8, inputWidth, ref multiplier.M21);
ImGui.Text("G "); changes |= DragFloat("##GG"u8, inputWidth, ref multiplier.M22);
changes |= DragFloat("##GR", inputWidth, ref multiplier.M21); changes |= DragFloat("##GB"u8, inputWidth, ref multiplier.M23);
changes |= DragFloat("##GG", inputWidth, ref multiplier.M22); changes |= DragFloat("##GA"u8, inputWidth, ref multiplier.M24);
changes |= DragFloat("##GB", inputWidth, ref multiplier.M23);
changes |= DragFloat("##GA", inputWidth, ref multiplier.M24);
ImGui.TableNextColumn(); table.DrawFrameColumn("B "u8);
ImGui.AlignTextToFramePadding(); changes |= DragFloat("##BR"u8, inputWidth, ref multiplier.M31);
ImGui.Text("B "); changes |= DragFloat("##BG"u8, inputWidth, ref multiplier.M32);
changes |= DragFloat("##BR", inputWidth, ref multiplier.M31); changes |= DragFloat("##BB"u8, inputWidth, ref multiplier.M33);
changes |= DragFloat("##BG", inputWidth, ref multiplier.M32); changes |= DragFloat("##BA"u8, inputWidth, ref multiplier.M34);
changes |= DragFloat("##BB", inputWidth, ref multiplier.M33);
changes |= DragFloat("##BA", inputWidth, ref multiplier.M34);
ImGui.TableNextColumn(); table.DrawFrameColumn("A "u8);
ImGui.AlignTextToFramePadding(); changes |= DragFloat("##AR"u8, inputWidth, ref multiplier.M41);
ImGui.Text("A "); changes |= DragFloat("##AG"u8, inputWidth, ref multiplier.M42);
changes |= DragFloat("##AR", inputWidth, ref multiplier.M41); changes |= DragFloat("##AB"u8, inputWidth, ref multiplier.M43);
changes |= DragFloat("##AG", inputWidth, ref multiplier.M42); changes |= DragFloat("##AA"u8, inputWidth, ref multiplier.M44);
changes |= DragFloat("##AB", inputWidth, ref multiplier.M43);
changes |= DragFloat("##AA", inputWidth, ref multiplier.M44);
ImGui.TableNextColumn(); table.DrawFrameColumn("1 "u8);
ImGui.AlignTextToFramePadding(); changes |= DragFloat("##1R"u8, inputWidth, ref constant.X);
ImGui.Text("1 "); changes |= DragFloat("##1G"u8, inputWidth, ref constant.Y);
changes |= DragFloat("##1R", inputWidth, ref constant.X); changes |= DragFloat("##1B"u8, inputWidth, ref constant.Z);
changes |= DragFloat("##1G", inputWidth, ref constant.Y); changes |= DragFloat("##1A"u8, inputWidth, ref constant.W);
changes |= DragFloat("##1B", inputWidth, ref constant.Z);
changes |= DragFloat("##1A", inputWidth, ref constant.W);
return changes; return changes;
} }
@ -354,28 +355,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();
ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); Im.ScaledDummy(20);
Im.Line.Same(); Im.Line.Same();
ImGui.TextUnformatted("Invert"); Im.Text("Invert"u8);
Im.Line.Same(); Im.Line.Same();
Channels channels = 0; Channels channels = 0;
if (ImGui.Button("Colors")) if (Im.Button("Colors"u8))
channels |= Channels.Red | Channels.Green | Channels.Blue; channels |= Channels.Red | Channels.Green | Channels.Blue;
Im.Line.Same(); Im.Line.Same();
if (ImGui.Button("R")) if (Im.Button("R"u8))
channels |= Channels.Red; channels |= Channels.Red;
Im.Line.Same(); Im.Line.Same();
if (ImGui.Button("G")) if (Im.Button("G"u8))
channels |= Channels.Green; channels |= Channels.Green;
Im.Line.Same(); Im.Line.Same();
if (ImGui.Button("B")) if (Im.Button("B"u8))
channels |= Channels.Blue; channels |= Channels.Blue;
Im.Line.Same(); Im.Line.Same();
if (ImGui.Button("A")) if (Im.Button("A"u8))
channels |= Channels.Alpha; channels |= Channels.Alpha;
changes |= InvertChannels(channels, ref multiplier, ref constant); changes |= InvertChannels(channels, ref multiplier, ref constant);
@ -384,14 +385,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 = ImRaii.Combo("Presets", string.Empty, ImGuiComboFlags.NoPreview); using var combo = Im.Combo.Begin("Presets"u8, StringU8.Empty, ComboFlags.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 (!ImGui.Selectable(label, multiplier == preMultiplier && constant == preConstant)) if (!Im.Selectable(label, multiplier == preMultiplier && constant == preConstant))
continue; continue;
multiplier = preMultiplier; multiplier = preMultiplier;

View file

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

View file

@ -0,0 +1,80 @@
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,168 +1,104 @@
using Dalamud.Bindings.ImGui; using ImSharp;
using Dalamud.Interface; using Lumina.Data.Files;
using ImSharp; using Luna;
using Lumina.Data.Files; using OtterTex;
using OtterGui; using Penumbra.UI;
using OtterGui.Raii; using Penumbra.UI.Classes;
using OtterGui.Widgets; using VectorExtensions = Luna.VectorExtensions;
using OtterTex;
using Penumbra.Mods.Editor; namespace Penumbra.Import.Textures;
using Penumbra.String.Classes;
using Penumbra.UI; public static class TextureDrawer
using Penumbra.UI.Classes; {
using MouseWheelType = OtterGui.Widgets.MouseWheelType; public static void Draw(Texture texture, Vector2 size)
{
namespace Penumbra.Import.Textures; if (texture.TextureWrap != null)
{
public static class TextureDrawer size = VectorExtensions.Contain(texture.TextureWrap.Size, size);
{
public static void Draw(Texture texture, Vector2 size) Im.Image.Draw(texture.TextureWrap.Id(), 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";
ImGui.Image(texture.TextureWrap.Handle, size); Im.Text("Could not load file:"u8);
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,
const string link = "https://aka.ms/vcredist"; Colors.RegexWarningBorder);
ImGui.TextUnformatted("Could not load file:"); if (Im.Button("Microsoft VC Redistributables"u8))
Dalamud.Utility.Util.OpenLink(link);
if (texture.LoadError is DllNotFoundException) Im.Tooltip.OnHover($"Open {link} in your browser.");
{ }
ImGuiUtil.TextColored(Colors.RegexWarningBorder,
"A texture handling dependency could not be found. Try installing a current Microsoft VC Redistributable."); Im.Text($"{texture.LoadError}", Colors.RegexWarningBorder);
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,
ImGuiUtil.TextColored(Colors.RegexWarningBorder, texture.LoadError.ToString()); string startPath, FileDialogService fileDialog, string defaultModImportPath)
} {
} tmpPath ??= current.Path;
using var spacing = ImStyleDouble.ItemSpacing.PushX(UiHelpers.ScaleX3);
public static void PathInputBox(TextureManager textures, Texture current, ref string? tmpPath, string label, string hint, string tooltip, Im.Item.SetNextWidth(-2 * Im.Style.FrameHeight - 7 * Im.Style.GlobalScale);
string startPath, FileDialogService fileDialog, string defaultModImportPath) if (ImEx.InputOnDeactivation.Text(label, tmpPath, out tmpPath, hint))
{ current.Load(textures, tmpPath);
tmpPath ??= current.Path;
using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Im.Tooltip.OnHover(tooltip);
new Vector2(UiHelpers.ScaleX3, ImGui.GetStyle().ItemSpacing.Y)); Im.Line.Same();
ImGui.SetNextItemWidth(-2 * ImGui.GetFrameHeight() - 7 * UiHelpers.Scale); if (ImEx.Icon.Button(LunaStyle.FolderIcon))
ImGui.InputTextWithHint(label, hint, ref tmpPath, Utf8GamePath.MaxGamePathLength); {
if (ImGui.IsItemDeactivatedAfterEdit()) if (defaultModImportPath.Length > 0)
current.Load(textures, tmpPath); startPath = defaultModImportPath;
ImGuiUtil.HoverTooltip(tooltip); void UpdatePath(bool success, List<string> paths)
Im.Line.Same(); {
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Folder.ToIconString(), new Vector2(ImGui.GetFrameHeight()), string.Empty, false, if (success && paths.Count > 0)
true)) current.Load(textures, paths[0]);
{ }
if (defaultModImportPath.Length > 0)
startPath = defaultModImportPath; fileDialog.OpenFilePicker("Open Image...", "Textures{.png,.dds,.tex,.tga}", UpdatePath, 1, startPath, false);
}
void UpdatePath(bool success, List<string> paths)
{ Im.Line.Same();
if (success && paths.Count > 0) if (ImEx.Icon.Button(LunaStyle.RefreshIcon, "Reload the currently selected path."u8))
current.Load(textures, paths[0]); current.Reload(textures);
} }
fileDialog.OpenFilePicker("Open Image...", "Textures{.png,.dds,.tex,.tga}", UpdatePath, 1, startPath, false); private static void DrawData(Texture texture)
} {
using var table = Im.Table.Begin("##data"u8, 2, TableFlags.SizingFixedFit);
Im.Line.Same(); table.DrawColumn("Width"u8);
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Recycle.ToIconString(), new Vector2(ImGui.GetFrameHeight()), table.DrawColumn($"{texture.TextureWrap!.Width}");
"Reload the currently selected path.", false, table.DrawColumn("Height"u8);
true)) table.DrawColumn($"{texture.TextureWrap!.Height}");
current.Reload(textures); table.DrawColumn("File Type"u8);
} table.DrawColumn($"{texture.Type}");
table.DrawColumn("Bitmap Size"u8);
private static void DrawData(Texture texture) table.DrawColumn($"{FormattingFunctions.HumanReadableSize(texture.RgbaPixels.Length)} ({texture.RgbaPixels.Length} Bytes)");
{ switch (texture.BaseImage.Image)
using var table = ImRaii.Table("##data", 2, ImGuiTableFlags.SizingFixedFit); {
ImGuiUtil.DrawTableColumn("Width"); case ScratchImage s:
ImGuiUtil.DrawTableColumn(texture.TextureWrap!.Width.ToString()); table.DrawColumn("Format"u8);
ImGuiUtil.DrawTableColumn("Height"); table.DrawColumn($"{s.Meta.Format}");
ImGuiUtil.DrawTableColumn(texture.TextureWrap!.Height.ToString()); table.DrawColumn("Mip Levels"u8);
ImGuiUtil.DrawTableColumn("File Type"); table.DrawColumn($"{s.Meta.MipLevels}");
ImGuiUtil.DrawTableColumn(texture.Type.ToString()); table.DrawColumn("Data Size"u8);
ImGuiUtil.DrawTableColumn("Bitmap Size"); table.DrawColumn($"{FormattingFunctions.HumanReadableSize(s.Pixels.Length)} ({s.Pixels.Length} Bytes)");
ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(texture.RgbaPixels.Length)} ({texture.RgbaPixels.Length} Bytes)"); table.DrawColumn("Number of Images"u8);
switch (texture.BaseImage.Image) table.DrawColumn($"{s.Images.Length}");
{ break;
case ScratchImage s: case TexFile t:
ImGuiUtil.DrawTableColumn("Format"); table.DrawColumn("Format"u8);
ImGuiUtil.DrawTableColumn(s.Meta.Format.ToString()); table.DrawColumn($"{t.Header.Format}");
ImGuiUtil.DrawTableColumn("Mip Levels"); table.DrawColumn("Mip Levels"u8);
ImGuiUtil.DrawTableColumn(s.Meta.MipLevels.ToString()); table.DrawColumn($"{t.Header.MipCount}");
ImGuiUtil.DrawTableColumn("Data Size"); table.DrawColumn("Data Size"u8);
ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(s.Pixels.Length)} ({s.Pixels.Length} Bytes)"); table.DrawColumn($"{FormattingFunctions.HumanReadableSize(t.ImageData.Length)} ({t.ImageData.Length} Bytes)");
ImGuiUtil.DrawTableColumn("Number of Images"); break;
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 OtterGui.Services; using Luna;
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 OtterGui.Services; using Luna;
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,6 +9,16 @@ 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,6 +71,8 @@
<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,7 +1,5 @@
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;
@ -20,7 +18,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();
private HashSet<string> GetPlayerResourcesOfType(ResourceType type) public 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 TextureDrawer.PathSelectCombo _textureSelectCombo; private readonly TextureSelectCombo _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, new Vector2(-1, 0), ImGuiColor.FrameBackground.Get()); ImEx.TextFramed(label, Im.ContentRegion.Available with { Y = 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", "Import Image...", TextureDrawer.PathInputBox(_textures, tex, ref tex.TmpPath, "##input"u8, "Import Image..."u8,
"Can import game paths as well as your own files.", Mod!.ModPath.FullName, _fileDialog, _config.DefaultModImportPath); "Can import game paths as well as your own files."u8, 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,7 +200,6 @@ 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:
@ -210,9 +209,7 @@ public partial class ModEditWindow
ImGuiUtil.TextWrapped(_center.SaveTask.Exception?.ToString() ?? "Unknown Error"); ImGuiUtil.TextWrapped(_center.SaveTask.Exception?.ToString() ?? "Unknown Error");
break; break;
} }
default: default: ImGui.Dummy(new Vector2(1, ImGui.GetFrameHeight())); break;
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 TextureDrawer.PathSelectCombo(textures, editor, () => GetPlayerResourcesOfType(ResourceType.Tex)); _textureSelectCombo = new TextureSelectCombo(resourceTreeFactory, editor, gameData);
_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) : Luna.IService FileCompactor compactor) : 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,