Fix some off-by-one errors with the import progress reports, add test implementation for pbd editing.

This commit is contained in:
Ottermandias 2024-10-09 18:47:30 +02:00
parent 2e424a693d
commit 4a0c996ff6
7 changed files with 368 additions and 20 deletions

@ -1 +1 @@
Subproject commit 34c96a55efe1ce1296d9edcd8296f6396998cc6a
Subproject commit 07b01ec9b043e4b8f56d084f5d6cde1ed4ed9a58

View file

@ -9,7 +9,6 @@ using Penumbra.Api.Api;
using Penumbra.Api.Enums;
using Penumbra.Api.IpcSubscribers;
using Penumbra.Collections.Manager;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Services;
@ -28,6 +27,8 @@ public class TemporaryIpcTester(
{
public Guid LastCreatedCollectionId = Guid.Empty;
private readonly bool _debug = Assembly.GetAssembly(typeof(TemporaryIpcTester))?.GetName().Version?.Major >= 9;
private Guid? _tempGuid;
private string _tempCollectionName = string.Empty;
private string _tempCollectionGuidName = string.Empty;
@ -48,9 +49,9 @@ public class TemporaryIpcTester(
ImGui.InputTextWithHint("##tempCollection", "Collection Name...", ref _tempCollectionName, 128);
ImGuiUtil.GuidInput("##guid", "Collection GUID...", string.Empty, ref _tempGuid, ref _tempCollectionGuidName);
ImGui.InputInt("##tempActorIndex", ref _tempActorIndex, 0, 0);
ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32);
ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256);
ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256);
ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32);
ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256);
ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256);
ImUtf8.InputText("##tempManip"u8, ref _tempManipulation, "Manipulation Base64 String..."u8);
ImGui.Checkbox("Force Character Collection Overwrite", ref _forceOverwrite);
@ -102,7 +103,7 @@ public class TemporaryIpcTester(
!collections.Storage.ByName(_tempModName, out var copyCollection))
&& copyCollection is { HasCache: true })
{
var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString());
var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString());
var manips = MetaApi.CompressMetaManipulations(copyCollection);
_lastTempError = new AddTemporaryMod(pi).Invoke(_tempModName, guid, files, manips, 999);
}
@ -124,11 +125,11 @@ public class TemporaryIpcTester(
public void DrawCollections()
{
using var collTree = ImRaii.TreeNode("Temporary Collections##TempCollections");
using var collTree = ImUtf8.TreeNode("Temporary Collections##TempCollections"u8);
if (!collTree)
return;
using var table = ImRaii.Table("##collTree", 6, ImGuiTableFlags.SizingFixedFit);
using var table = ImUtf8.Table("##collTree"u8, 6, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
@ -139,7 +140,7 @@ public class TemporaryIpcTester(
var character = tempCollections.Collections.Where(p => p.Collection == collection).Select(p => p.DisplayName)
.FirstOrDefault()
?? "Unknown";
if (ImGui.Button("Save##Collection"))
if (_debug && ImUtf8.Button("Save##Collection"u8))
TemporaryMod.SaveTempCollection(config, saveService, modManager, collection, character);
using (ImRaii.PushFont(UiBuilder.MonoFont))

View file

@ -46,21 +46,28 @@ public partial class TexToolsImporter
{
ImGui.NewLine();
ImGui.NewLine();
percentage = _currentNumOptions == 0 ? 1f : _currentOptionIdx / (float)_currentNumOptions;
ImGui.ProgressBar(percentage, size, $"Option {_currentOptionIdx + 1} / {_currentNumOptions}");
if (_currentOptionIdx >= _currentNumOptions)
ImGui.ProgressBar(1f, size, $"Extracted {_currentNumOptions} Options");
else
ImGui.ProgressBar(_currentOptionIdx / (float)_currentNumOptions, size,
$"Extracting Option {_currentOptionIdx + 1} / {_currentNumOptions}...");
ImGui.NewLine();
if (State != ImporterState.DeduplicatingFiles)
ImGui.TextUnformatted(
$"Extracting option {(_currentGroupName.Length == 0 ? string.Empty : $"{_currentGroupName} - ")}{_currentOptionName}...");
$"Extracting Option {(_currentGroupName.Length == 0 ? string.Empty : $"{_currentGroupName} - ")}{_currentOptionName}...");
}
ImGui.NewLine();
ImGui.NewLine();
percentage = _currentNumFiles == 0 ? 1f : _currentFileIdx / (float)_currentNumFiles;
ImGui.ProgressBar(percentage, size, $"File {_currentFileIdx + 1} / {_currentNumFiles}");
if (_currentFileIdx >= _currentNumFiles)
ImGui.ProgressBar(1f, size, $"Extracted {_currentNumFiles} Files");
else
ImGui.ProgressBar(_currentFileIdx / (float)_currentNumFiles, size, $"Extracting File {_currentFileIdx + 1} / {_currentNumFiles}...");
ImGui.NewLine();
if (State != ImporterState.DeduplicatingFiles)
ImGui.TextUnformatted($"Extracting file {_currentFileName}...");
ImGui.TextUnformatted($"Extracting File {_currentFileName}...");
return false;
}

View file

@ -151,6 +151,7 @@ public partial class TexToolsImporter
_currentGroupName = string.Empty;
_currentOptionName = "Default";
ExtractSimpleModList(_currentModDirectory, modList.SimpleModsList);
++_currentOptionIdx;
}
// Iterate through all pages
@ -208,6 +209,7 @@ public partial class TexToolsImporter
options.Insert(idx, MultiSubMod.WithoutGroup(option.Name, option.Description, ModPriority.Default));
if (option.IsChecked)
defaultSettings = Setting.Single(idx);
++_currentOptionIdx;
}
}

View file

@ -12,6 +12,7 @@ public class ModFileCollection : IDisposable, IService
private readonly List<FileRegistry> _mdl = [];
private readonly List<FileRegistry> _tex = [];
private readonly List<FileRegistry> _shpk = [];
private readonly List<FileRegistry> _pbd = [];
private readonly SortedSet<FullPath> _missing = [];
private readonly HashSet<Utf8GamePath> _usedPaths = [];
@ -23,19 +24,22 @@ public class ModFileCollection : IDisposable, IService
=> Ready ? _usedPaths : [];
public IReadOnlyList<FileRegistry> Available
=> Ready ? _available : Array.Empty<FileRegistry>();
=> Ready ? _available : [];
public IReadOnlyList<FileRegistry> Mtrl
=> Ready ? _mtrl : Array.Empty<FileRegistry>();
=> Ready ? _mtrl : [];
public IReadOnlyList<FileRegistry> Mdl
=> Ready ? _mdl : Array.Empty<FileRegistry>();
=> Ready ? _mdl : [];
public IReadOnlyList<FileRegistry> Tex
=> Ready ? _tex : Array.Empty<FileRegistry>();
=> Ready ? _tex : [];
public IReadOnlyList<FileRegistry> Shpk
=> Ready ? _shpk : Array.Empty<FileRegistry>();
=> Ready ? _shpk : [];
public IReadOnlyList<FileRegistry> Pbd
=> Ready ? _pbd : [];
public bool Ready { get; private set; } = true;
@ -128,6 +132,9 @@ public class ModFileCollection : IDisposable, IService
case ".shpk":
_shpk.Add(registry);
break;
case ".pbd":
_pbd.Add(registry);
break;
}
}
}
@ -139,6 +146,7 @@ public class ModFileCollection : IDisposable, IService
_mdl.Clear();
_tex.Clear();
_shpk.Clear();
_pbd.Clear();
}
private void ClearPaths(bool clearRegistries, CancellationToken tok)

View file

@ -0,0 +1,324 @@
using Dalamud.Interface;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.Utility.Raii;
using ImGuiNET;
using OtterGui;
using OtterGui.Text;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Files;
using Penumbra.UI.Classes;
using Notification = OtterGui.Classes.Notification;
namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{
private readonly FileEditor<PbdTab> _pbdTab;
private readonly PbdData _pbdData = new();
private bool DrawDeformerPanel(PbdTab tab, bool disabled)
{
_pbdData.Update(tab.File);
DrawGenderRaceSelector(tab);
ImGui.SameLine();
DrawBoneSelector();
ImGui.SameLine();
return DrawBoneData(tab, disabled);
}
private void DrawGenderRaceSelector(PbdTab tab)
{
using var group = ImUtf8.Group();
var width = ImUtf8.CalcTextSize("Hellsguard - Female (Child)____0000"u8).X + 2 * ImGui.GetStyle().WindowPadding.X;
using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0)
.Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero))
{
ImGui.SetNextItemWidth(width);
ImUtf8.InputText("##grFilter"u8, ref _pbdData.RaceCodeFilter, "Filter..."u8);
}
using var child = ImUtf8.Child("GenderRace"u8, new Vector2(width, ImGui.GetContentRegionMax().Y), true);
if (!child)
return;
var metaColor = ColorId.ItemId.Value();
foreach (var (deformer, index) in tab.File.Deformers.WithIndex())
{
var name = deformer.GenderRace.ToName();
var raceCode = deformer.GenderRace.ToRaceCode();
// No clipping necessary since this are not that many objects anyway.
if (!name.Contains(_pbdData.RaceCodeFilter) && !raceCode.Contains(_pbdData.RaceCodeFilter))
continue;
using var id = ImUtf8.PushId(index);
using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), deformer.RacialDeformer.IsEmpty);
if (ImUtf8.Selectable(name, deformer.GenderRace == _pbdData.SelectedRaceCode))
{
_pbdData.SelectedRaceCode = deformer.GenderRace;
_pbdData.SelectedDeformer = deformer.RacialDeformer;
}
ImGui.SameLine();
color.Push(ImGuiCol.Text, metaColor);
ImUtf8.TextRightAligned(raceCode);
}
}
private void DrawBoneSelector()
{
using var group = ImUtf8.Group();
var width = 200 * ImUtf8.GlobalScale;
using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0)
.Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero))
{
ImGui.SetNextItemWidth(width);
ImUtf8.InputText("##boneFilter"u8, ref _pbdData.BoneFilter, "Filter..."u8);
}
using var child = ImUtf8.Child("Bone"u8, new Vector2(width, ImGui.GetContentRegionMax().Y), true);
if (!child)
return;
if (_pbdData.SelectedDeformer == null)
return;
if (_pbdData.SelectedDeformer.IsEmpty)
{
ImUtf8.Text("<Empty>"u8);
}
else
{
var height = ImGui.GetTextLineHeightWithSpacing();
var skips = ImGuiClip.GetNecessarySkips(height);
var remainder = ImGuiClip.FilteredClippedDraw(_pbdData.SelectedDeformer.DeformMatrices.Keys, skips,
b => b.Contains(_pbdData.BoneFilter), bone
=>
{
if (ImUtf8.Selectable(bone, bone == _pbdData.SelectedBone))
_pbdData.SelectedBone = bone;
});
ImGuiClip.DrawEndDummy(remainder, height);
}
}
private bool DrawBoneData(PbdTab tab, bool disabled)
{
using var child = ImUtf8.Child("Data"u8, ImGui.GetContentRegionMax() with { X = ImGui.GetContentRegionAvail().X}, true);
if (!child)
return false;
if (_pbdData.SelectedBone == null)
return false;
if (!_pbdData.SelectedDeformer!.DeformMatrices.TryGetValue(_pbdData.SelectedBone, out var matrix))
return false;
var width = UiBuilder.MonoFont.GetCharAdvance('0') * 12 + ImGui.GetStyle().FramePadding.X * 2;
var dummyHeight = ImGui.GetTextLineHeight() / 2;
var ret = DrawAddNewBone(tab, disabled, width);
ImUtf8.Dummy(0, dummyHeight);
ImGui.Separator();
ImUtf8.Dummy(0, dummyHeight);
ret |= DrawDeformerMatrix(disabled, matrix, width);
ImUtf8.Dummy(0, dummyHeight);
ret |= DrawCopyPasteButtons(disabled, matrix, width);
ImUtf8.Dummy(0, dummyHeight);
ImGui.Separator();
ImUtf8.Dummy(0, dummyHeight);
ret |= DrawDecomposedData(disabled, matrix, width);
return ret;
}
private bool DrawAddNewBone(PbdTab tab, bool disabled, float width)
{
var ret = false;
ImUtf8.TextFrameAligned("Copy the values of the bone "u8);
ImGui.SameLine(0, 0);
using (ImRaii.PushColor(ImGuiCol.Text, ColorId.NewMod.Value()))
{
ImUtf8.TextFrameAligned(_pbdData.SelectedBone);
}
ImGui.SameLine(0, 0);
ImUtf8.TextFrameAligned(" to a new bone of name"u8);
var fullWidth = width * 4 + ImGui.GetStyle().ItemSpacing.X * 3;
ImGui.SetNextItemWidth(fullWidth);
ImUtf8.InputText("##newBone"u8, ref _pbdData.NewBoneName, "New Bone Name..."u8);
ImUtf8.TextFrameAligned("for all races that have a corresponding bone."u8);
ImGui.SameLine(0, fullWidth - width - ImGui.GetItemRectSize().X);
if (!ImUtf8.ButtonEx("Apply"u8, ""u8, new Vector2(width, 0),
disabled || _pbdData.NewBoneName.Length == 0 || _pbdData.SelectedBone == null))
return ret;
foreach (var deformer in tab.File.Deformers)
{
if (!deformer.RacialDeformer.DeformMatrices.TryGetValue(_pbdData.SelectedBone!, out var existingMatrix))
continue;
if (!deformer.RacialDeformer.DeformMatrices.TryAdd(_pbdData.NewBoneName, existingMatrix)
&& deformer.RacialDeformer.DeformMatrices.TryGetValue(_pbdData.NewBoneName, out var newBoneMatrix)
&& !newBoneMatrix.Equals(existingMatrix))
Penumbra.Messager.AddMessage(new Notification(
$"Could not add deformer matrix to {deformer.GenderRace.ToName()}, Bone {_pbdData.NewBoneName} because it already has a deformer that differs from the intended one.",
NotificationType.Warning));
else
ret = true;
}
_pbdData.NewBoneName = string.Empty;
return ret;
}
private bool DrawDeformerMatrix(bool disabled, in TransformMatrix matrix, float width)
{
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
using var _ = ImRaii.Disabled(disabled);
var ret = false;
for (var i = 0; i < 3; ++i)
{
for (var j = 0; j < 4; ++j)
{
using var id = ImUtf8.PushId(i * 4 + j);
ImGui.SetNextItemWidth(width);
var tmp = matrix[i, j];
if (ImUtf8.InputScalar(""u8, ref tmp, "% 12.8f"u8))
{
ret = true;
_pbdData.SelectedDeformer!.DeformMatrices[_pbdData.SelectedBone!] = matrix.ChangeValue(i, j, tmp);
}
ImGui.SameLine();
}
ImGui.NewLine();
}
return ret;
}
private bool DrawCopyPasteButtons(bool disabled, in TransformMatrix matrix, float width)
{
var size = new Vector2(width, 0);
if (ImUtf8.Button("Copy Values"u8, size))
_pbdData.CopiedMatrix = matrix;
ImGui.SameLine();
if (ImUtf8.ButtonEx("Paste Values"u8, ""u8, size, disabled || !_pbdData.CopiedMatrix.HasValue))
{
_pbdData.SelectedDeformer!.DeformMatrices[_pbdData.SelectedBone!] = _pbdData.CopiedMatrix!.Value;
return true;
}
return false;
}
private bool DrawDecomposedData(bool disabled, in TransformMatrix matrix, float width)
{
var ret = false;
if (!matrix.TryDecompose(out var scale, out var rotation, out var translation))
return false;
using (ImUtf8.Group())
{
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
using var _ = ImRaii.Disabled(disabled);
ImGui.SetNextItemWidth(width);
ret |= ImUtf8.InputScalar("##ScaleX"u8, ref scale.X, "% 12.8f"u8);
ImGui.SameLine();
ImGui.SetNextItemWidth(width);
ret |= ImUtf8.InputScalar("##ScaleY"u8, ref scale.Y, "% 12.8f"u8);
ImGui.SameLine();
ImGui.SetNextItemWidth(width);
ret |= ImUtf8.InputScalar("##ScaleZ"u8, ref scale.Z, "% 12.8f"u8);
ImGui.SetNextItemWidth(width);
ret |= ImUtf8.InputScalar("##TranslationX"u8, ref translation.X, "% 12.8f"u8);
ImGui.SameLine();
ImGui.SetNextItemWidth(width);
ret |= ImUtf8.InputScalar("##TranslationY"u8, ref translation.Y, "% 12.8f"u8);
ImGui.SameLine();
ImGui.SetNextItemWidth(width);
ret |= ImUtf8.InputScalar("##TranslationZ"u8, ref translation.Z, "% 12.8f"u8);
ImGui.SetNextItemWidth(width);
ret |= ImUtf8.InputScalar("##RotationR"u8, ref rotation.W, "% 12.8f"u8);
ImGui.SameLine();
ImGui.SetNextItemWidth(width);
ret |= ImUtf8.InputScalar("##RotationI"u8, ref rotation.X, "% 12.8f"u8);
ImGui.SameLine();
ImGui.SetNextItemWidth(width);
ret |= ImUtf8.InputScalar("##RotationJ"u8, ref rotation.Y, "% 12.8f"u8);
ImGui.SameLine();
ImGui.SetNextItemWidth(width);
ret |= ImUtf8.InputScalar("##RotationK"u8, ref rotation.Z, "% 12.8f"u8);
}
ImGui.SameLine();
using (ImUtf8.Group())
{
ImUtf8.TextFrameAligned("Scale"u8);
ImUtf8.TextFrameAligned("Translation"u8);
ImUtf8.TextFrameAligned("Rotation (Quaternion, rijk)"u8);
}
if (ret)
_pbdData.SelectedDeformer!.DeformMatrices[_pbdData.SelectedBone!] = TransformMatrix.Compose(scale, rotation, translation);
return ret;
}
public class PbdTab(byte[] data, string filePath) : IWritable
{
public readonly string FilePath = filePath;
public readonly PbdFile File = new(data);
public bool Valid
=> File.Valid;
public byte[] Write()
=> File.Write();
}
private class PbdData
{
public GenderRace SelectedRaceCode = GenderRace.Unknown;
public RacialDeformer? SelectedDeformer;
public string? SelectedBone;
public string NewBoneName = string.Empty;
public string BoneFilter = string.Empty;
public string RaceCodeFilter = string.Empty;
public TransformMatrix? CopiedMatrix;
public void Update(PbdFile file)
{
if (SelectedRaceCode is GenderRace.Unknown)
{
SelectedDeformer = null;
}
else
{
SelectedDeformer = file.Deformers.FirstOrDefault(p => p.GenderRace == SelectedRaceCode).RacialDeformer;
if (SelectedDeformer is null)
SelectedRaceCode = GenderRace.Unknown;
}
}
}
}

View file

@ -236,6 +236,8 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
_itemSwapTab.DrawContent();
}
_pbdTab.Draw();
DrawMissingFilesTab();
DrawMaterialReassignmentTab();
}
@ -665,6 +667,10 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
() => PopulateIsOnPlayer(_editor.Files.Shpk, ResourceType.Shpk), DrawShaderPackagePanel,
() => Mod?.ModPath.FullName ?? string.Empty,
(bytes, path, _) => new ShpkTab(_fileDialog, bytes, path));
_pbdTab = new FileEditor<PbdTab>(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Deformers", ".pbd",
() => _editor.Files.Pbd, DrawDeformerPanel,
() => 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));
_resourceTreeFactory = resourceTreeFactory;