diff --git a/Penumbra/Import/Models/Import/ModelImporter.cs b/Penumbra/Import/Models/Import/ModelImporter.cs
index d02d143c..0d8c029d 100644
--- a/Penumbra/Import/Models/Import/ModelImporter.cs
+++ b/Penumbra/Import/Models/Import/ModelImporter.cs
@@ -4,7 +4,7 @@ using SharpGLTF.Schema2;
namespace Penumbra.Import.Models.Import;
-public partial class ModelImporter(ModelRoot _model)
+public partial class ModelImporter(ModelRoot model)
{
public static MdlFile Import(ModelRoot model)
{
@@ -105,7 +105,7 @@ public partial class ModelImporter(ModelRoot _model)
/// Returns an iterator over sorted, grouped mesh nodes.
private IEnumerable> GroupedMeshNodes()
- => _model.LogicalNodes
+ => model.LogicalNodes
.Where(node => node.Mesh != null)
.Select(node =>
{
@@ -184,6 +184,14 @@ public partial class ModelImporter(ModelRoot _model)
_shapeValues.AddRange(meshShapeKey.ShapeValues);
}
+
+ // The number of shape values in a model is bounded by the count
+ // value, which is stored as a u16.
+ // While technically there are similar bounds on other shape struct
+ // arrays, values is practically guaranteed to be the highest of the
+ // group, so a failure on any of them will be a failure on it.
+ if (_shapeValues.Count > ushort.MaxValue)
+ throw new Exception($"Importing this file would require more than the maximum of {ushort.MaxValue} shape values.\nTry removing or applying shape keys that do not need to be changed at runtime in-game.");
}
private ushort GetMaterialIndex(string materialName)
diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs
index bae9569f..f099a0e0 100644
--- a/Penumbra/Import/Models/ModelManager.cs
+++ b/Penumbra/Import/Models/ModelManager.cs
@@ -37,7 +37,12 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
public Task ImportGltf(string inputPath)
{
var action = new ImportGltfAction(inputPath);
- return Enqueue(action).ContinueWith(_ => action.Out);
+ return Enqueue(action).ContinueWith(task =>
+ {
+ if (task.IsFaulted && task.Exception != null)
+ throw task.Exception;
+ return action.Out;
+ });
}
/// Try to find the .sklb paths for a .mdl file.
/// .mdl file to look up the skeletons for.
diff --git a/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs b/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs
index 81f6d552..2f717491 100644
--- a/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs
+++ b/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs
@@ -1,5 +1,6 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
-using OtterGui.Services;
+using OtterGui.Services;
+using Penumbra.Collections;
using Penumbra.GameData;
using Penumbra.GameData.Structs;
using Penumbra.Interop.PathResolving;
@@ -29,6 +30,7 @@ public sealed unsafe class ChangeCustomize : FastHook
using var decal2 = _metaState.ResolveDecal(_metaState.CustomizeChangeCollection, false);
var ret = Task.Result.Original.Invoke(human, data, skipEquipment);
Penumbra.Log.Excessive($"[Change Customize] Invoked on {(nint)human:X} with {(nint)data:X}, {skipEquipment} -> {ret}.");
+ _metaState.CustomizeChangeCollection = ResolveData.Invalid;
return ret;
}
}
diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs
index bd599133..d38d8d92 100644
--- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs
+++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs
@@ -20,9 +20,9 @@ public partial class ModEditWindow
public List? GamePaths { get; private set; }
public int GamePathIndex;
- private bool _dirty;
- public bool PendingIo { get; private set; }
- public string? IoException { get; private set; }
+ private bool _dirty;
+ public bool PendingIo { get; private set; }
+ public List IoExceptions { get; private set; } = [];
public MdlTab(ModEditWindow edit, byte[] bytes, string path)
{
@@ -87,7 +87,7 @@ public partial class ModEditWindow
task.ContinueWith(t =>
{
- IoException = t.Exception?.ToString();
+ RecordIoExceptions(t.Exception);
GamePaths = t.Result;
PendingIo = false;
});
@@ -123,7 +123,7 @@ public partial class ModEditWindow
}
catch (Exception exception)
{
- IoException = exception.ToString();
+ RecordIoExceptions(exception);
return;
}
@@ -131,7 +131,7 @@ public partial class ModEditWindow
_edit._models.ExportToGltf(Mdl, skeletons, outputPath)
.ContinueWith(task =>
{
- IoException = task.Exception?.ToString();
+ RecordIoExceptions(task.Exception);
PendingIo = false;
});
}
@@ -144,7 +144,7 @@ public partial class ModEditWindow
_edit._models.ImportGltf(inputPath)
.ContinueWith(task =>
{
- IoException = task.Exception?.ToString();
+ RecordIoExceptions(task.Exception);
if (task is { IsCompletedSuccessfully: true, Result: not null })
FinalizeImport(task.Result);
PendingIo = false;
@@ -177,6 +177,15 @@ public partial class ModEditWindow
}
}
+ private void RecordIoExceptions(Exception? exception)
+ {
+ IoExceptions = exception switch {
+ null => [],
+ AggregateException ae => ae.Flatten().InnerExceptions.ToList(),
+ Exception other => [other],
+ };
+ }
+
/// Read a .sklb from the active collection or game.
/// Game path to the .sklb to load.
private SklbFile ReadSklb(string sklbPath)
diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs
index 9a69a4e8..fbdfcc74 100644
--- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs
+++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs
@@ -7,6 +7,7 @@ using Penumbra.GameData;
using Penumbra.GameData.Files;
using Penumbra.Import.Models;
using Penumbra.String.Classes;
+using Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow;
@@ -61,8 +62,7 @@ public partial class ModEditWindow
ImGui.SameLine();
DrawExport(tab, childSize, disabled);
- if (tab.IoException != null)
- ImGuiUtil.TextWrapped(tab.IoException);
+ DrawIoExceptions(tab);
}
private void DrawImport(MdlTab tab, Vector2 size, bool _1)
@@ -100,10 +100,10 @@ public partial class ModEditWindow
if (tab.GamePaths == null)
{
- if (tab.IoException == null)
+ if (tab.IoExceptions.Count == 0)
ImGui.TextUnformatted("Resolving model game paths.");
else
- ImGuiUtil.TextWrapped(tab.IoException);
+ ImGui.TextUnformatted("Failed to resolve model game paths.");
return;
}
@@ -127,6 +127,30 @@ public partial class ModEditWindow
false
);
}
+
+ private void DrawIoExceptions(MdlTab tab)
+ {
+ if (tab.IoExceptions.Count == 0)
+ return;
+
+ var size = new Vector2(ImGui.GetContentRegionAvail().X, 0);
+ using var frame = ImRaii.FramedGroup("Exceptions", size, headerPreIcon: FontAwesomeIcon.TimesCircle, borderColor: Colors.RegexWarningBorder);
+
+ var spaceAvail = ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X - 100;
+ foreach (var exception in tab.IoExceptions)
+ {
+ var message = $"{exception.GetType().Name}: {exception.Message}";
+ var textSize = ImGui.CalcTextSize(message).X;
+ if (textSize > spaceAvail)
+ message = message.Substring(0, (int)Math.Floor(message.Length * (spaceAvail / textSize))) + "...";
+
+ using (var exceptionNode = ImRaii.TreeNode(message))
+ {
+ if (exceptionNode)
+ ImGuiUtil.TextWrapped(exception.ToString());
+ }
+ }
+ }
private void DrawGamePathCombo(MdlTab tab)
{
diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs
index e9facdf4..34d0800c 100644
--- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs
+++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs
@@ -108,11 +108,14 @@ public partial class ModEditWindow
MipMapInput();
var canSaveInPlace = Path.IsPathRooted(_left.Path) && _left.Type is TextureType.Tex or TextureType.Dds or TextureType.Png;
+ var isActive = _config.DeleteModModifier.IsActive();
+ var tt = isActive
+ ? "This saves the texture in place. This is not revertible."
+ : $"This saves the texture in place. This is not revertible. Hold {_config.DeleteModModifier} to save.";
var buttonSize2 = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0);
if (ImGuiUtil.DrawDisabledButton("Save in place", buttonSize2,
- "This saves the texture in place. This is not revertible.",
- !canSaveInPlace || _center.IsLeftCopy && _currentSaveAs == (int)CombinedTexture.TextureSaveType.AsIs))
+ tt, !isActive || !canSaveInPlace || _center.IsLeftCopy && _currentSaveAs == (int)CombinedTexture.TextureSaveType.AsIs))
{
_center.SaveAs(_left.Type, _textures, _left.Path, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps);
AddReloadTask(_left.Path, false);
diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs
index 0e3b9377..93d7e091 100644
--- a/Penumbra/UI/Classes/Colors.cs
+++ b/Penumbra/UI/Classes/Colors.cs
@@ -33,7 +33,6 @@ public enum ColorId
public static class Colors
{
// These are written as 0xAABBGGRR.
-
public const uint PressEnterWarningBg = 0xFF202080;
public const uint RegexWarningBorder = 0xFF0000B0;
public const uint MetaInfoText = 0xAAFFFFFF;
diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs
index d63e42ef..195c07d6 100644
--- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs
+++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs
@@ -209,7 +209,7 @@ public class ModPanelSettingsTab : ITab
{
using var id = ImRaii.PushId(groupIdx);
var selectedOption = _empty ? (int)group.DefaultSettings : (int)_settings.Settings[groupIdx];
- var minWidth = Widget.BeginFramedGroup(group.Name, group.Description);
+ var minWidth = Widget.BeginFramedGroup(group.Name, description:group.Description);
void DrawOptions()
{
@@ -288,7 +288,7 @@ public class ModPanelSettingsTab : ITab
{
using var id = ImRaii.PushId(groupIdx);
var flags = _empty ? group.DefaultSettings : _settings.Settings[groupIdx];
- var minWidth = Widget.BeginFramedGroup(group.Name, group.Description);
+ var minWidth = Widget.BeginFramedGroup(group.Name, description: group.Description);
void DrawOptions()
{