mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 10:17:22 +01:00
Compare commits
3 commits
cb275f57bf
...
34f067f13d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34f067f13d | ||
|
|
7ed81a9823 | ||
|
|
d88593c500 |
19 changed files with 856 additions and 826 deletions
2
Luna
2
Luna
|
|
@ -1 +1 @@
|
||||||
Subproject commit 7214f079cb9b8eeea6fa1a9fe1c6ca8118049969
|
Subproject commit 78216203f4570a6194fce9422204d8abb536c828
|
||||||
2
OtterGui
2
OtterGui
|
|
@ -1 +1 @@
|
||||||
Subproject commit f354444776591ae423e2d8374aae346308d81424
|
Subproject commit 9af1e5fce4c13ef98842807d4f593dec8ae80c87
|
||||||
|
|
@ -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)}",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using OtterGui.Filesystem;
|
using Luna;
|
||||||
|
|
||||||
namespace Penumbra.Collections;
|
namespace Penumbra.Collections;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
80
Penumbra/Import/Textures/PathSelectCombo.cs
Normal file
80
Penumbra/Import/Textures/PathSelectCombo.cs
Normal 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);
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue