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 OtterGui.Filesystem;
using Luna;
using Penumbra.GameData.Actors;
using Penumbra.GameData.DataContainers.Bases;
using Penumbra.GameData.Enums;

View file

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

View file

@ -1,15 +1,15 @@
using OtterGui.Log;
using Luna;
namespace Penumbra.Import.Models;
public record class IoNotifier
public record IoNotifier(Logger Log)
{
private readonly List<string> _messages = [];
private string _context = "";
/// <summary> Create a new notifier with the specified context appended to any other context already present. </summary>
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>
public void Warning(string content)
@ -34,7 +34,7 @@ public record class IoNotifier
private void SendMessage(string message, Logger.LogLevel type)
{
var fullText = $"{_context}{message}";
Penumbra.Log.Message(type, fullText);
Log.Message(type, fullText);
_messages.Add(fullText);
}
}

View file

@ -1,5 +1,6 @@
using Dalamud.Plugin.Services;
using Lumina.Data.Parsing;
using Luna;
using OtterGui.Tasks;
using Penumbra.Collections.Manager;
using Penumbra.GameData;
@ -22,9 +23,15 @@ namespace Penumbra.Import.Models;
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,
IFramework framework,
MetaFileManager metaFileManager,
ActiveCollections collections,
GamePathParser parser)
: SingleTaskQueue, IDisposable, IService
{
public readonly Logger Log = log;
private readonly IFramework _framework = framework;
private readonly ConcurrentDictionary<IAction, (Task, CancellationTokenSource)> _tasks = new();
@ -48,7 +55,7 @@ public sealed class ModelManager(IFramework framework, MetaFileManager metaFileM
public Task<(MdlFile?, IoNotifier)> ImportGltf(string inputPath)
=> EnqueueWithResult(
new ImportGltfAction(inputPath),
new ImportGltfAction(this, inputPath),
action => (action.Out, action.Notifier)
);
@ -88,8 +95,7 @@ public sealed class ModelManager(IFramework framework, MetaFileManager metaFileM
{
// Try to find an EST entry from the manipulations provided.
var modEst = estManipulations
.FirstOrNull(
est => est.Key.GenderRace == info.GenderRace
.FirstOrNull(est => est.Key.GenderRace == info.GenderRace
&& est.Key.Slot == type
&& est.Key.SetId == info.PrimaryId
);
@ -190,7 +196,7 @@ public sealed class ModelManager(IFramework framework, MetaFileManager metaFileM
string outputPath)
: IAction
{
public readonly IoNotifier Notifier = new();
public readonly IoNotifier Notifier = new(manager.Log);
public void Execute(CancellationToken cancel)
{
@ -292,7 +298,7 @@ public sealed class ModelManager(IFramework framework, MetaFileManager metaFileM
public bool Equals(IAction? other)
{
if (other is not ExportToGltfAction rhs)
if (other is not ExportToGltfAction)
return false;
// TODO: compare configuration and such
@ -300,10 +306,10 @@ public sealed class ModelManager(IFramework framework, MetaFileManager metaFileM
}
}
private partial class ImportGltfAction(string inputPath) : IAction
private class ImportGltfAction(ModelManager manager, string inputPath) : IAction
{
public MdlFile? Out;
public readonly IoNotifier Notifier = new();
public readonly IoNotifier Notifier = new(manager.Log);
public void Execute(CancellationToken cancel)
{
@ -314,7 +320,7 @@ public sealed class ModelManager(IFramework framework, MetaFileManager metaFileM
public bool Equals(IAction? other)
{
if (other is not ImportGltfAction rhs)
if (other is not ImportGltfAction)
return false;
return true;

View file

@ -1,10 +1,9 @@
using Dalamud.Utility;
using Luna;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui.Filesystem;
using Penumbra.Import.Structs;
using Penumbra.Mods;
using Penumbra.Services;
using SharpCompress.Archives;
using SharpCompress.Archives.Rar;
using SharpCompress.Archives.SevenZip;
@ -16,7 +15,7 @@ namespace Penumbra.Import;
public partial class TexToolsImporter
{
private static readonly ExtractionOptions _extractionOptions = new()
private static readonly ExtractionOptions ExtractionOptions = new()
{
ExtractFullPath = true,
Overwrite = true,
@ -79,7 +78,7 @@ public partial class TexToolsImporter
using var t = new StreamReader(s);
using var j = new JsonTextReader(t);
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)
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))
{
case ".mdl":
_migrationManager.MigrateMdlDuringExtraction(reader, _currentModDirectory!.FullName, _extractionOptions);
_migrationManager.MigrateMdlDuringExtraction(reader, _currentModDirectory!.FullName, ExtractionOptions);
break;
case ".mtrl":
_migrationManager.MigrateMtrlDuringExtraction(reader, _currentModDirectory!.FullName, _extractionOptions);
_migrationManager.MigrateMtrlDuringExtraction(reader, _currentModDirectory!.FullName, ExtractionOptions);
break;
case ".tex":
_migrationManager.FixMipMaps(reader, _currentModDirectory!.FullName, _extractionOptions);
_migrationManager.FixMipMaps(reader, _currentModDirectory!.FullName, ExtractionOptions);
break;
default:
reader.WriteEntryToDirectory(_currentModDirectory!.FullName, _extractionOptions);
reader.WriteEntryToDirectory(_currentModDirectory!.FullName, ExtractionOptions);
break;
}
}

View file

@ -1,9 +1,5 @@
using Dalamud.Bindings.ImGui;
using OtterGui.Raii;
using OtterGui;
using Dalamud.Interface.Utility;
using ImSharp;
using Penumbra.UI;
using OtterGui.Text;
using Rgba32 = SixLabors.ImageSharp.PixelFormats.Rgba32;
namespace Penumbra.Import.Textures;
@ -29,20 +25,19 @@ public partial class CombinedTexture
private const float BWeight = 0.0722f;
// @formatter:off
private static readonly IReadOnlyList<(string Label, Matrix4x4 Multiplier, Vector4 Constant)> PredefinedColorTransforms =
new[]
{
("No Transform (Identity)", Matrix4x4.Identity, 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 ),
("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 ),
("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 ),
("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 ),
("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 ),
("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 ),
("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 ),
("Extract Blue", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ),
("Extract Alpha", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f), Vector4.UnitW ),
};
private static readonly IReadOnlyList<(StringU8 Label, Matrix4x4 Multiplier, Vector4 Constant)> PredefinedColorTransforms =
[
(new StringU8("No Transform (Identity)"u8), Matrix4x4.Identity, Vector4.Zero ),
(new StringU8("Grayscale (Average)"u8), new Matrix4x4(OneThird, OneThird, OneThird, 0.0f, OneThird, OneThird, OneThird, 0.0f, OneThird, OneThird, OneThird, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f), Vector4.Zero ),
(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 ),
(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 ),
(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 ),
(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 ),
(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 ),
(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 ),
(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 ),
(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 ),
];
// @formatter:on
private Vector4 DataLeft(int offset)
@ -211,15 +206,15 @@ public partial class CombinedTexture
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;
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(width);
if (ImGui.DragFloat(label, ref tmp, 0.001f, -1f, 1f))
Im.Table.NextColumn();
Im.Item.SetNextWidth(width);
if (Im.Drag(label, ref tmp, speed: 0.001f, min: -1f, max: 1f))
value = tmp;
return ImGui.IsItemDeactivatedAfterEdit();
return Im.Item.DeactivatedAfterEdit;
}
public void DrawMatrixInputLeft(float width)
@ -230,53 +225,69 @@ public partial class CombinedTexture
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)
{
var ret = DrawMatrixInput(ref _multiplierRight, ref _constantRight, width);
ret |= DrawMatrixTools(ref _multiplierRight, ref _constantRight);
ImGui.SetNextItemWidth(75.0f * UiHelpers.Scale);
ImGui.DragInt("##XOffset", ref _offsetX, 0.5f);
ret |= ImGui.IsItemDeactivatedAfterEdit();
Im.Item.SetNextWidthScaled(75);
Im.Drag("##XOffset"u8, ref _offsetX, speed: 0.5f);
ret |= Im.Item.DeactivatedAfterEdit;
Im.Line.Same();
ImGui.SetNextItemWidth(75.0f * UiHelpers.Scale);
ImGui.DragInt("Offsets##YOffset", ref _offsetY, 0.5f);
ret |= ImGui.IsItemDeactivatedAfterEdit();
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(75);
Im.Drag("Offsets##YOffset"u8, ref _offsetY, speed: 0.5f);
ret |= Im.Item.DeactivatedAfterEdit;
Im.Item.SetNextWidthScaled(200);
ret |= _combineCombo.Draw("Combine Operation"u8, ref _combineOp, StringU8.Empty, 200 * Im.Style.GlobalScale);
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,
Enum.GetValues<ResizeOp>().Where(op => (int)op >= 0), op => ResizeOpLabels[(int)op]);
ret |= _resizeCombo.Draw("Resizing Mode"u8, ref _resizeOp, StringU8.Empty, 200 * Im.Style.GlobalScale);
}
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>())
{
Im.Line.Same();
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;
ret = true;
@ -290,62 +301,52 @@ public partial class CombinedTexture
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)
return false;
var changes = false;
ImGui.TableNextColumn();
ImGui.TableNextColumn();
ImGuiUtil.Center("R");
ImGui.TableNextColumn();
ImGuiUtil.Center("G");
ImGui.TableNextColumn();
ImGuiUtil.Center("B");
ImGui.TableNextColumn();
ImGuiUtil.Center("A");
table.NextColumn();
table.NextColumn();
ImEx.TextCentered("R"u8);
table.NextColumn();
ImEx.TextCentered("G"u8);
table.NextColumn();
ImEx.TextCentered("B"u8);
table.NextColumn();
ImEx.TextCentered("A"u8);
var inputWidth = width / 6;
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.Text("R ");
changes |= DragFloat("##RR", inputWidth, ref multiplier.M11);
changes |= DragFloat("##RG", inputWidth, ref multiplier.M12);
changes |= DragFloat("##RB", inputWidth, ref multiplier.M13);
changes |= DragFloat("##RA", inputWidth, ref multiplier.M14);
table.DrawFrameColumn("R "u8);
changes |= DragFloat("##RR"u8, inputWidth, ref multiplier.M11);
changes |= DragFloat("##RG"u8, inputWidth, ref multiplier.M12);
changes |= DragFloat("##RB"u8, inputWidth, ref multiplier.M13);
changes |= DragFloat("##RA"u8, inputWidth, ref multiplier.M14);
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.Text("G ");
changes |= DragFloat("##GR", inputWidth, ref multiplier.M21);
changes |= DragFloat("##GG", inputWidth, ref multiplier.M22);
changes |= DragFloat("##GB", inputWidth, ref multiplier.M23);
changes |= DragFloat("##GA", inputWidth, ref multiplier.M24);
table.DrawFrameColumn("G "u8);
changes |= DragFloat("##GR"u8, inputWidth, ref multiplier.M21);
changes |= DragFloat("##GG"u8, inputWidth, ref multiplier.M22);
changes |= DragFloat("##GB"u8, inputWidth, ref multiplier.M23);
changes |= DragFloat("##GA"u8, inputWidth, ref multiplier.M24);
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.Text("B ");
changes |= DragFloat("##BR", inputWidth, ref multiplier.M31);
changes |= DragFloat("##BG", inputWidth, ref multiplier.M32);
changes |= DragFloat("##BB", inputWidth, ref multiplier.M33);
changes |= DragFloat("##BA", inputWidth, ref multiplier.M34);
table.DrawFrameColumn("B "u8);
changes |= DragFloat("##BR"u8, inputWidth, ref multiplier.M31);
changes |= DragFloat("##BG"u8, inputWidth, ref multiplier.M32);
changes |= DragFloat("##BB"u8, inputWidth, ref multiplier.M33);
changes |= DragFloat("##BA"u8, inputWidth, ref multiplier.M34);
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.Text("A ");
changes |= DragFloat("##AR", inputWidth, ref multiplier.M41);
changes |= DragFloat("##AG", inputWidth, ref multiplier.M42);
changes |= DragFloat("##AB", inputWidth, ref multiplier.M43);
changes |= DragFloat("##AA", inputWidth, ref multiplier.M44);
table.DrawFrameColumn("A "u8);
changes |= DragFloat("##AR"u8, inputWidth, ref multiplier.M41);
changes |= DragFloat("##AG"u8, inputWidth, ref multiplier.M42);
changes |= DragFloat("##AB"u8, inputWidth, ref multiplier.M43);
changes |= DragFloat("##AA"u8, inputWidth, ref multiplier.M44);
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.Text("1 ");
changes |= DragFloat("##1R", inputWidth, ref constant.X);
changes |= DragFloat("##1G", inputWidth, ref constant.Y);
changes |= DragFloat("##1B", inputWidth, ref constant.Z);
changes |= DragFloat("##1A", inputWidth, ref constant.W);
table.DrawFrameColumn("1 "u8);
changes |= DragFloat("##1R"u8, inputWidth, ref constant.X);
changes |= DragFloat("##1G"u8, inputWidth, ref constant.Y);
changes |= DragFloat("##1B"u8, inputWidth, ref constant.Z);
changes |= DragFloat("##1A"u8, inputWidth, ref constant.W);
return changes;
}
@ -354,28 +355,28 @@ public partial class CombinedTexture
{
var changes = PresetCombo(ref multiplier, ref constant);
Im.Line.Same();
ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0));
Im.ScaledDummy(20);
Im.Line.Same();
ImGui.TextUnformatted("Invert");
Im.Text("Invert"u8);
Im.Line.Same();
Channels channels = 0;
if (ImGui.Button("Colors"))
if (Im.Button("Colors"u8))
channels |= Channels.Red | Channels.Green | Channels.Blue;
Im.Line.Same();
if (ImGui.Button("R"))
if (Im.Button("R"u8))
channels |= Channels.Red;
Im.Line.Same();
if (ImGui.Button("G"))
if (Im.Button("G"u8))
channels |= Channels.Green;
Im.Line.Same();
if (ImGui.Button("B"))
if (Im.Button("B"u8))
channels |= Channels.Blue;
Im.Line.Same();
if (ImGui.Button("A"))
if (Im.Button("A"u8))
channels |= Channels.Alpha;
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)
{
using var combo = ImRaii.Combo("Presets", string.Empty, ImGuiComboFlags.NoPreview);
using var combo = Im.Combo.Begin("Presets"u8, StringU8.Empty, ComboFlags.NoPreview);
if (!combo)
return false;
var ret = false;
foreach (var (label, preMultiplier, preConstant) in PredefinedColorTransforms)
{
if (!ImGui.Selectable(label, multiplier == preMultiplier && constant == preConstant))
if (!Im.Selectable(label, multiplier == preMultiplier && constant == preConstant))
continue;
multiplier = preMultiplier;

View file

@ -1,30 +1,54 @@
using Luna.Generators;
namespace Penumbra.Import.Textures;
public partial class CombinedTexture
{
private enum CombineOp
[NamedEnum("ToLabel")]
[TooltipEnum]
public enum CombineOp
{
LeftMultiply = -4,
LeftCopy = -3,
RightCopy = -2,
Invalid = -1,
[Name("Overlay over Input")]
[Tooltip("Standard composition.\nApply the overlay over the input.")]
Over = 0,
[Name("Input over Overlay")]
[Tooltip("Standard composition, reversed.\nApply the input over the overlay ; can be used to fix some wrong imports.")]
Under = 1,
[Name("Replace Input")]
[Tooltip("Completely replace the input with the overlay.\nCan be used to select the destination file as input and the source file as overlay.")]
RightMultiply = 2,
[Name("Copy Channels")]
[Tooltip("Replace some input channels with those from the overlay.\nUseful for Multi maps.")]
CopyChannels = 3,
}
private enum ResizeOp
[NamedEnum("ToLabel")]
public enum ResizeOp
{
LeftOnly = -2,
RightOnly = -1,
[Name("No Resizing")]
None = 0,
[Name("Adjust Overlay to Input")]
ToLeft = 1,
[Name("Adjust Input to Overlay")]
ToRight = 2,
}
[Flags]
private enum Channels : byte
[NamedEnum]
public enum Channels : byte
{
Red = 1,
Green = 2,
@ -32,29 +56,6 @@ public partial class CombinedTexture
Alpha = 8,
}
private static readonly IReadOnlyList<string> CombineOpLabels = new[]
{
"Overlay over Input",
"Input over Overlay",
"Replace Input",
"Copy Channels",
};
private static readonly IReadOnlyList<string> CombineOpTooltips = new[]
{
"Standard composition.\nApply the overlay over the input.",
"Standard composition, reversed.\nApply the input over the overlay ; can be used to fix some wrong imports.",
"Completely replace the input with the overlay.\nCan be used to select the destination file as input and the source file as overlay.",
"Replace some input channels with those from the overlay.\nUseful for Multi maps.",
};
private static readonly IReadOnlyList<string> ResizeOpLabels = new string[]
{
"No Resizing",
"Adjust Overlay to Input",
"Adjust Input to Overlay",
};
private static ResizeOp GetActualResizeOp(ResizeOp resizeOp, CombineOp combineOp)
=> combineOp switch
{

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,16 +1,10 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using ImSharp;
using Lumina.Data.Files;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Widgets;
using Luna;
using OtterTex;
using Penumbra.Mods.Editor;
using Penumbra.String.Classes;
using Penumbra.UI;
using Penumbra.UI.Classes;
using MouseWheelType = OtterGui.Widgets.MouseWheelType;
using VectorExtensions = Luna.VectorExtensions;
namespace Penumbra.Import.Textures;
@ -20,44 +14,42 @@ public static class TextureDrawer
{
if (texture.TextureWrap != null)
{
size = texture.TextureWrap.Size.Contain(size);
size = VectorExtensions.Contain(texture.TextureWrap.Size, size);
ImGui.Image(texture.TextureWrap.Handle, size);
Im.Image.Draw(texture.TextureWrap.Id(), size);
DrawData(texture);
}
else if (texture.LoadError != null)
{
const string link = "https://aka.ms/vcredist";
ImGui.TextUnformatted("Could not load file:");
Im.Text("Could not load file:"u8);
if (texture.LoadError is DllNotFoundException)
{
ImGuiUtil.TextColored(Colors.RegexWarningBorder,
"A texture handling dependency could not be found. Try installing a current Microsoft VC Redistributable.");
if (ImGui.Button("Microsoft VC Redistributables"))
Im.Text("A texture handling dependency could not be found. Try installing a current Microsoft VC Redistributable."u8,
Colors.RegexWarningBorder);
if (Im.Button("Microsoft VC Redistributables"u8))
Dalamud.Utility.Util.OpenLink(link);
ImGuiUtil.HoverTooltip($"Open {link} in your browser.");
Im.Tooltip.OnHover($"Open {link} in your browser.");
}
ImGuiUtil.TextColored(Colors.RegexWarningBorder, texture.LoadError.ToString());
Im.Text($"{texture.LoadError}", Colors.RegexWarningBorder);
}
}
public static void PathInputBox(TextureManager textures, Texture current, ref string? tmpPath, string label, string hint, string tooltip,
public static void PathInputBox(TextureManager textures, Texture current, ref string? tmpPath, ReadOnlySpan<byte> label,
ReadOnlySpan<byte> hint, ReadOnlySpan<byte> tooltip,
string startPath, FileDialogService fileDialog, string defaultModImportPath)
{
tmpPath ??= current.Path;
using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing,
new Vector2(UiHelpers.ScaleX3, ImGui.GetStyle().ItemSpacing.Y));
ImGui.SetNextItemWidth(-2 * ImGui.GetFrameHeight() - 7 * UiHelpers.Scale);
ImGui.InputTextWithHint(label, hint, ref tmpPath, Utf8GamePath.MaxGamePathLength);
if (ImGui.IsItemDeactivatedAfterEdit())
using var spacing = ImStyleDouble.ItemSpacing.PushX(UiHelpers.ScaleX3);
Im.Item.SetNextWidth(-2 * Im.Style.FrameHeight - 7 * Im.Style.GlobalScale);
if (ImEx.InputOnDeactivation.Text(label, tmpPath, out tmpPath, hint))
current.Load(textures, tmpPath);
ImGuiUtil.HoverTooltip(tooltip);
Im.Tooltip.OnHover(tooltip);
Im.Line.Same();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Folder.ToIconString(), new Vector2(ImGui.GetFrameHeight()), string.Empty, false,
true))
if (ImEx.Icon.Button(LunaStyle.FolderIcon))
{
if (defaultModImportPath.Length > 0)
startPath = defaultModImportPath;
@ -72,97 +64,41 @@ public static class TextureDrawer
}
Im.Line.Same();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Recycle.ToIconString(), new Vector2(ImGui.GetFrameHeight()),
"Reload the currently selected path.", false,
true))
if (ImEx.Icon.Button(LunaStyle.RefreshIcon, "Reload the currently selected path."u8))
current.Reload(textures);
}
private static void DrawData(Texture texture)
{
using var table = ImRaii.Table("##data", 2, ImGuiTableFlags.SizingFixedFit);
ImGuiUtil.DrawTableColumn("Width");
ImGuiUtil.DrawTableColumn(texture.TextureWrap!.Width.ToString());
ImGuiUtil.DrawTableColumn("Height");
ImGuiUtil.DrawTableColumn(texture.TextureWrap!.Height.ToString());
ImGuiUtil.DrawTableColumn("File Type");
ImGuiUtil.DrawTableColumn(texture.Type.ToString());
ImGuiUtil.DrawTableColumn("Bitmap Size");
ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(texture.RgbaPixels.Length)} ({texture.RgbaPixels.Length} Bytes)");
using var table = Im.Table.Begin("##data"u8, 2, TableFlags.SizingFixedFit);
table.DrawColumn("Width"u8);
table.DrawColumn($"{texture.TextureWrap!.Width}");
table.DrawColumn("Height"u8);
table.DrawColumn($"{texture.TextureWrap!.Height}");
table.DrawColumn("File Type"u8);
table.DrawColumn($"{texture.Type}");
table.DrawColumn("Bitmap Size"u8);
table.DrawColumn($"{FormattingFunctions.HumanReadableSize(texture.RgbaPixels.Length)} ({texture.RgbaPixels.Length} Bytes)");
switch (texture.BaseImage.Image)
{
case ScratchImage s:
ImGuiUtil.DrawTableColumn("Format");
ImGuiUtil.DrawTableColumn(s.Meta.Format.ToString());
ImGuiUtil.DrawTableColumn("Mip Levels");
ImGuiUtil.DrawTableColumn(s.Meta.MipLevels.ToString());
ImGuiUtil.DrawTableColumn("Data Size");
ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(s.Pixels.Length)} ({s.Pixels.Length} Bytes)");
ImGuiUtil.DrawTableColumn("Number of Images");
ImGuiUtil.DrawTableColumn(s.Images.Length.ToString());
table.DrawColumn("Format"u8);
table.DrawColumn($"{s.Meta.Format}");
table.DrawColumn("Mip Levels"u8);
table.DrawColumn($"{s.Meta.MipLevels}");
table.DrawColumn("Data Size"u8);
table.DrawColumn($"{FormattingFunctions.HumanReadableSize(s.Pixels.Length)} ({s.Pixels.Length} Bytes)");
table.DrawColumn("Number of Images"u8);
table.DrawColumn($"{s.Images.Length}");
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)");
table.DrawColumn("Format"u8);
table.DrawColumn($"{t.Header.Format}");
table.DrawColumn("Mip Levels"u8);
table.DrawColumn($"{t.Header.MipCount}");
table.DrawColumn("Data Size"u8);
table.DrawColumn($"{FormattingFunctions.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 OtterGui.Services;
using Luna;
using Penumbra.Collections;
using Penumbra.CrashHandler.Buffers;
using Penumbra.GameData;

View file

@ -1,5 +1,5 @@
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using OtterGui.Services;
using Luna;
using Penumbra.GameData;
using Penumbra.Interop.PathResolving;

View file

@ -9,6 +9,16 @@ namespace Penumbra.Interop.ResourceTree;
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(
IEnumerable<(ICharacter, ResourceTree)> resourceTrees)
{

View file

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

View file

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

View file

@ -18,7 +18,7 @@ public partial class ModEditWindow
private readonly Texture _left = new();
private readonly Texture _right = new();
private readonly CombinedTexture _center;
private readonly TextureDrawer.PathSelectCombo _textureSelectCombo;
private readonly TextureSelectCombo _textureSelectCombo;
private bool _overlayCollapsed = true;
private bool _addMipMaps = true;
@ -49,13 +49,13 @@ public partial class ModEditWindow
return;
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();
using (ImRaii.Disabled(!_center.SaveTask.IsCompleted))
{
TextureDrawer.PathInputBox(_textures, tex, ref tex.TmpPath, "##input", "Import Image...",
"Can import game paths as well as your own files.", Mod!.ModPath.FullName, _fileDialog, _config.DefaultModImportPath);
TextureDrawer.PathInputBox(_textures, tex, ref tex.TmpPath, "##input"u8, "Import Image..."u8,
"Can import game paths as well as your own files."u8, Mod!.ModPath.FullName, _fileDialog, _config.DefaultModImportPath);
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,
Mod.ModPath.FullName.Length + 1, out var newPath)
@ -200,7 +200,6 @@ public partial class ModEditWindow
case TaskStatus.WaitingToRun:
case TaskStatus.Running:
ImGuiUtil.DrawTextButton("Computing...", -Vector2.UnitX, Colors.PressEnterWarningBg);
break;
case TaskStatus.Canceled:
case TaskStatus.Faulted:
@ -210,9 +209,7 @@ public partial class ModEditWindow
ImGuiUtil.TextWrapped(_center.SaveTask.Exception?.ToString() ?? "Unknown Error");
break;
}
default:
ImGui.Dummy(new Vector2(1, ImGui.GetFrameHeight()));
break;
default: ImGui.Dummy(new Vector2(1, ImGui.GetFrameHeight())); break;
}
ImGui.NewLine();

View file

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

View file

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