diff --git a/Glamourer/Configuration.cs b/Glamourer/Configuration.cs
index 1d24bf3..676cacc 100644
--- a/Glamourer/Configuration.cs
+++ b/Glamourer/Configuration.cs
@@ -35,6 +35,7 @@ public class Configuration : IPluginConfiguration, ISavable
public bool ShowQuickBarInTabs { get; set; } = true;
public bool OpenWindowAtStart { get; set; } = false;
public bool UseAdvancedParameters { get; set; } = true;
+ public bool UseAdvancedDyes { get; set; } = false;
public bool ShowRevertAdvancedParametersButton { get; set; } = true;
public bool ShowPalettePlusImport { get; set; } = true;
public bool UseFloatForColors { get; set; } = true;
diff --git a/Glamourer/Designs/DesignEditor.cs b/Glamourer/Designs/DesignEditor.cs
index 9640c71..91050ec 100644
--- a/Glamourer/Designs/DesignEditor.cs
+++ b/Glamourer/Designs/DesignEditor.cs
@@ -33,7 +33,7 @@ public class DesignEditor(
///
public void ChangeCustomize(object data, CustomizeIndex idx, CustomizeValue value, ApplySettings _ = default)
{
- var design = (Design)data;
+ var design = (Design)data;
var oldValue = design.DesignData.Customize[idx];
switch (idx)
{
@@ -96,7 +96,7 @@ public class DesignEditor(
public void ChangeCustomizeParameter(object data, CustomizeParameterFlag flag, CustomizeParameterValue value, ApplySettings _ = default)
{
var design = (Design)data;
- var old = design.DesignData.Parameters[flag];
+ var old = design.DesignData.Parameters[flag];
if (!design.GetDesignDataRef().Parameters.Set(flag, value))
return;
@@ -219,6 +219,68 @@ public class DesignEditor(
DesignChanged.Invoke(DesignChanged.Type.Other, design, (metaIndex, false, value));
}
+ public void ChangeMaterialRevert(Design design, MaterialValueIndex index, bool revert)
+ {
+ var materials = design.GetMaterialDataRef();
+ if (!materials.TryGetValue(index, out var oldValue))
+ return;
+
+ materials.AddOrUpdateValue(index, oldValue with { Revert = revert });
+ Glamourer.Log.Debug($"Changed advanced dye value for {index} to {(revert ? "Revert." : "no longer Revert.")}");
+ design.LastEdit = DateTimeOffset.UtcNow;
+ SaveService.QueueSave(design);
+ DesignChanged.Invoke(DesignChanged.Type.MaterialRevert, design, index);
+ }
+
+ public void ChangeMaterialValue(Design design, MaterialValueIndex index, ColorRow? row)
+ {
+ var materials = design.GetMaterialDataRef();
+ if (materials.TryGetValue(index, out var oldValue))
+ {
+ if (!row.HasValue)
+ {
+ materials.RemoveValue(index);
+ Glamourer.Log.Debug($"Removed advanced dye value for {index}.");
+ }
+ else if (!row.Value.NearEqual(oldValue.Value))
+ {
+ materials.UpdateValue(index, new MaterialValueDesign(row.Value, oldValue.Enabled, oldValue.Revert), out _);
+ Glamourer.Log.Debug($"Updated advanced dye value for {index} to new value.");
+ }
+ else
+ {
+ return;
+ }
+ }
+ else
+ {
+ if (!row.HasValue)
+ return;
+ if (!materials.TryAddValue(index, new MaterialValueDesign(row.Value, true, false)))
+ return;
+
+ Glamourer.Log.Debug($"Added new advanced dye value for {index}.");
+ }
+
+ design.LastEdit = DateTimeOffset.UtcNow;
+ SaveService.DelaySave(design);
+ DesignChanged.Invoke(DesignChanged.Type.Material, design, (oldValue, row, index));
+ }
+
+ public void ChangeApplyMaterialValue(Design design, MaterialValueIndex index, bool value)
+ {
+ var materials = design.GetMaterialDataRef();
+ if (!materials.TryGetValue(index, out var oldValue) || oldValue.Enabled == value)
+ return;
+
+ materials.AddOrUpdateValue(index, oldValue with { Enabled = value });
+ Glamourer.Log.Debug($"Changed application of advanced dye for {index} to {value}.");
+ design.LastEdit = DateTimeOffset.UtcNow;
+ SaveService.QueueSave(design);
+ DesignChanged.Invoke(DesignChanged.Type.ApplyMaterial, design, index);
+ }
+
+
///
public void ApplyDesign(object data, MergedDesign other, ApplySettings _ = default)
=> ApplyDesign(data, other.Design);
@@ -262,7 +324,8 @@ public class DesignEditor(
}
/// Change a mainhand weapon and either fix or apply appropriate offhand and potentially gauntlets.
- private bool ChangeMainhandPeriphery(DesignBase design, EquipItem currentMain, EquipItem currentOff, EquipItem newMain, out EquipItem? newOff,
+ private bool ChangeMainhandPeriphery(DesignBase design, EquipItem currentMain, EquipItem currentOff, EquipItem newMain,
+ out EquipItem? newOff,
out EquipItem? newGauntlets)
{
newOff = null;
diff --git a/Glamourer/Designs/Links/DesignMerger.cs b/Glamourer/Designs/Links/DesignMerger.cs
index 4b29582..7aa0fdf 100644
--- a/Glamourer/Designs/Links/DesignMerger.cs
+++ b/Glamourer/Designs/Links/DesignMerger.cs
@@ -1,5 +1,6 @@
using Glamourer.Automation;
using Glamourer.GameData;
+using Glamourer.Interop.Material;
using Glamourer.Services;
using Glamourer.State;
using Glamourer.Unlocks;
@@ -46,6 +47,7 @@ public class DesignMerger(
ReduceCrests(data, crestFlags, ret, source);
ReduceParameters(data, parameterFlags, ret, source);
ReduceMods(design as Design, ret, modAssociations);
+ ReduceMaterials(design, ret);
}
ApplyFixFlags(ret, fixFlags);
@@ -53,6 +55,15 @@ public class DesignMerger(
}
+ private static void ReduceMaterials(DesignBase? design, MergedDesign ret)
+ {
+ if (design == null)
+ return;
+ var materials = ret.Design.GetMaterialDataRef();
+ foreach (var (key, value) in design.Materials.Where(p => p.Item2.Enabled))
+ materials.TryAddValue(MaterialValueIndex.FromKey(key), value);
+ }
+
private static void ReduceMods(Design? design, MergedDesign ret, bool modAssociations)
{
if (design == null || !modAssociations)
diff --git a/Glamourer/Events/DesignChanged.cs b/Glamourer/Events/DesignChanged.cs
index 81d56e4..70e9aa6 100644
--- a/Glamourer/Events/DesignChanged.cs
+++ b/Glamourer/Events/DesignChanged.cs
@@ -74,10 +74,16 @@ public sealed class DesignChanged()
/// An existing design had a customize parameter changed. Data is the old value, the new value and the flag [(CustomizeParameterValue, CustomizeParameterValue, CustomizeParameterFlag)].
Parameter,
+ /// An existing design had an advanced dye row added, changed, or deleted. Data is the old value, the new value and the index [(ColorRow?, ColorRow?, MaterialValueIndex)].
+ Material,
+
+ /// An existing design had an advanced dye rows Revert state changed. Data is the index [MaterialValueIndex].
+ MaterialRevert,
+
/// An existing design changed whether a specific customization is applied. Data is the type of customization [CustomizeIndex].
ApplyCustomize,
- /// An existing design changed whether a specific equipment is applied. Data is the slot of the equipment [EquipSlot].
+ /// An existing design changed whether a specific equipment piece is applied. Data is the slot of the equipment [EquipSlot].
ApplyEquip,
/// An existing design changed whether a specific stain is applied. Data is the slot of the equipment [EquipSlot].
@@ -89,6 +95,9 @@ public sealed class DesignChanged()
/// An existing design changed whether a specific customize parameter is applied. Data is the flag for the parameter [CustomizeParameterFlag].
ApplyParameter,
+ /// An existing design changed whether an advanced dye row is applied. Data is the index [MaterialValueIndex].
+ ApplyMaterial,
+
/// An existing design changed its write protection status. Data is the new value [bool].
WriteProtection,
diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs b/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs
index ff6e0c5..fb50f55 100644
--- a/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs
+++ b/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs
@@ -80,7 +80,7 @@ public partial class CustomizationDrawer
{
var size = ImGui.GetItemRectSize();
ImGui.GetWindowDrawList()
- .AddCircleFilled(ImGui.GetItemRectMin() + size / 2, size.X / 4, ImGuiUtil.ContrastColorBW(custom.Color));
+ .AddCircleFilled(ImGui.GetItemRectMin() + size / 2, size.X / 4, ImGuiUtil.ContrastColorBw(custom.Color));
}
if (i % 8 != 7)
diff --git a/Glamourer/Gui/DesignQuickBar.cs b/Glamourer/Gui/DesignQuickBar.cs
index a1a748f..2521765 100644
--- a/Glamourer/Gui/DesignQuickBar.cs
+++ b/Glamourer/Gui/DesignQuickBar.cs
@@ -240,7 +240,7 @@ public sealed class DesignQuickBar : Window, IDisposable
if (_playerIdentifier.IsValid && _playerState is { IsLocked: false } && _playerData.Valid)
{
available |= 1;
- tooltip = "Left-Click: Revert the advanced customizations of the player character to their game state.";
+ tooltip = "Left-Click: Revert the advanced customizations and dyes of the player character to their game state.";
}
if (_targetIdentifier.IsValid && _targetState is { IsLocked: false } && _targetData.Valid)
@@ -248,7 +248,7 @@ public sealed class DesignQuickBar : Window, IDisposable
if (available != 0)
tooltip += '\n';
available |= 2;
- tooltip += $"Right-Click: Revert the advanced customizations of {_targetIdentifier} to their game state.";
+ tooltip += $"Right-Click: Revert the advanced customizations and dyes of {_targetIdentifier} to their game state.";
}
if (available == 0)
diff --git a/Glamourer/Gui/Materials/MaterialDrawer.cs b/Glamourer/Gui/Materials/MaterialDrawer.cs
index fb6a4ac..2840dc5 100644
--- a/Glamourer/Gui/Materials/MaterialDrawer.cs
+++ b/Glamourer/Gui/Materials/MaterialDrawer.cs
@@ -10,20 +10,135 @@ using OtterGui;
using OtterGui.Services;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Files;
+using Penumbra.GameData.Gui;
using Penumbra.GameData.Structs;
+using Vortice.DXGI;
namespace Glamourer.Gui.Materials;
-public unsafe class MaterialDrawer(StateManager _stateManager, DesignManager _designManager) : IService
+public unsafe class MaterialDrawer(StateManager _stateManager, DesignManager _designManager, Configuration _config) : IService
{
- private static readonly IReadOnlyList Types =
- [
- MaterialValueIndex.DrawObjectType.Human,
- MaterialValueIndex.DrawObjectType.Mainhand,
- MaterialValueIndex.DrawObjectType.Offhand,
- ];
+ private ActorState? _state;
+ private EquipSlot _newSlot = EquipSlot.Head;
+ private int _newMaterialIdx;
+ private int _newRowIdx;
+ private MaterialValueIndex _newKey = MaterialValueIndex.Min();
- private ActorState? _state;
+ public void DrawDesignPanel(Design design)
+ {
+ var buttonSize = new Vector2(ImGui.GetFrameHeight());
+ using (var table = ImRaii.Table("table", 5, ImGuiTableFlags.RowBg))
+ {
+ if (!table)
+ return;
+
+
+ ImGui.TableSetupColumn("button", ImGuiTableColumnFlags.WidthFixed, buttonSize.X);
+ ImGui.TableSetupColumn("enabled", ImGuiTableColumnFlags.WidthFixed, buttonSize.X);
+ ImGui.TableSetupColumn("values", ImGuiTableColumnFlags.WidthFixed, ImGui.GetStyle().ItemInnerSpacing.X * 4 + 3 * buttonSize.X + 220 * ImGuiHelpers.GlobalScale);
+ ImGui.TableSetupColumn("revert", ImGuiTableColumnFlags.WidthFixed, buttonSize.X + ImGui.CalcTextSize("Revertm").X);
+ ImGui.TableSetupColumn("slot", ImGuiTableColumnFlags.WidthStretch);
+
+
+
+ for (var i = 0; i < design.Materials.Count; ++i)
+ {
+ var (idx, value) = design.Materials[i];
+ var key = MaterialValueIndex.FromKey(idx);
+ var name = key.DrawObject switch
+ {
+ MaterialValueIndex.DrawObjectType.Human => ((uint)key.SlotIndex).ToEquipSlot().ToName(),
+ MaterialValueIndex.DrawObjectType.Mainhand => EquipSlot.MainHand.ToName(),
+ MaterialValueIndex.DrawObjectType.Offhand => EquipSlot.OffHand.ToName(),
+ _ => string.Empty,
+ };
+ if (name.Length == 0)
+ continue;
+
+ name = $"{name} Material #{key.MaterialIndex + 1} Row #{key.RowIndex + 1}";
+ using var id = ImRaii.PushId((int)idx);
+ var deleteEnabled = _config.DeleteDesignModifier.IsActive();
+ ImGui.TableNextColumn();
+ if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), buttonSize,
+ $"Delete this color row.{(deleteEnabled ? string.Empty : $"\nHold {_config.DeleteDesignModifier} to delete.")}",
+ !deleteEnabled, true))
+ {
+ _designManager.ChangeMaterialValue(design, key, null);
+ --i;
+ }
+
+ ImGui.TableNextColumn();
+ var enabled = value.Enabled;
+ if (ImGui.Checkbox("Enabled", ref enabled))
+ _designManager.ChangeApplyMaterialValue(design, key, enabled);
+
+ ImGui.TableNextColumn();
+ var revert = value.Revert;
+ using (ImRaii.Disabled(revert))
+ {
+ var row = value.Value;
+ DrawRow(design, key, row);
+ }
+
+ ImGui.TableNextColumn();
+
+ if (ImGui.Checkbox("Revert", ref revert))
+ _designManager.ChangeMaterialRevert(design, key, revert);
+ ImGuiUtil.HoverTooltip(
+ "If this is checked, Glamourer will try to revert the advanced dye row to its game state instead of applying a specific row.");
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(name);
+ }
+ }
+
+ var exists = design.GetMaterialDataRef().TryGetValue(_newKey, out _);
+ if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), buttonSize,
+ exists ? "The selected advanced dye row already exists." : "Add the selected advanced dye row.", exists, true))
+ _designManager.ChangeMaterialValue(design, _newKey, ColorRow.Empty);
+
+ ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
+ if (EquipSlotCombo.Draw("##slot", "Choose a slot for an advanced dye row.", ref _newSlot))
+ _newKey = _newSlot switch
+ {
+ EquipSlot.MainHand => new MaterialValueIndex(MaterialValueIndex.DrawObjectType.Mainhand, 0, (byte)_newMaterialIdx,
+ (byte)_newRowIdx),
+ EquipSlot.OffHand => new MaterialValueIndex(MaterialValueIndex.DrawObjectType.Offhand, 0, (byte)_newMaterialIdx,
+ (byte)_newRowIdx),
+ _ => new MaterialValueIndex(MaterialValueIndex.DrawObjectType.Human, (byte)_newSlot.ToIndex(), (byte)_newMaterialIdx,
+ (byte)_newRowIdx),
+ };
+ ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
+ DrawMaterialIdxDrag();
+ ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
+ DrawRowIdxDrag();
+ }
+
+ private void DrawMaterialIdxDrag()
+ {
+ _newMaterialIdx += 1;
+ ImGui.SetNextItemWidth(ImGui.CalcTextSize("Material #000").X);
+ if (ImGui.DragInt("##Material", ref _newMaterialIdx, 0.01f, 1, MaterialService.MaterialsPerModel, "Material #%i"))
+ {
+ _newMaterialIdx = Math.Clamp(_newMaterialIdx, 1, MaterialService.MaterialsPerModel);
+ _newKey = _newKey with { MaterialIndex = (byte)(_newMaterialIdx - 1) };
+ }
+
+ _newMaterialIdx -= 1;
+ }
+
+ private void DrawRowIdxDrag()
+ {
+ _newRowIdx += 1;
+ ImGui.SetNextItemWidth(ImGui.CalcTextSize("Row #0000").X);
+ if (ImGui.DragInt("##Row", ref _newRowIdx, 0.01f, 1, MtrlFile.ColorTable.NumRows, "Row #%i"))
+ {
+ _newRowIdx = Math.Clamp(_newRowIdx, 1, MtrlFile.ColorTable.NumRows);
+ _newKey = _newKey with { RowIndex = (byte)(_newRowIdx - 1) };
+ }
+
+ _newRowIdx -= 1;
+ }
public void DrawActorPanel(Actor actor)
{
@@ -41,20 +156,21 @@ public unsafe class MaterialDrawer(StateManager _stateManager, DesignManager _de
foreach (var (slot, idx) in EquipSlotExtensions.EqdpSlots.WithIndex())
{
var item = model.GetArmor(slot).ToWeapon(0);
- DrawSlotMaterials(model, slot.ToName(), item, new MaterialValueIndex(MaterialValueIndex.DrawObjectType.Human, (byte) idx, 0, 0));
+ DrawSlotMaterials(model, slot.ToName(), item, new MaterialValueIndex(MaterialValueIndex.DrawObjectType.Human, (byte)idx, 0, 0));
}
var (mainhand, offhand, mh, oh) = actor.Model.GetWeapons(actor);
if (mainhand.IsWeapon && mainhand.AsCharacterBase->SlotCount > 0)
- DrawSlotMaterials(mainhand, EquipSlot.MainHand.ToName(), mh, new MaterialValueIndex(MaterialValueIndex.DrawObjectType.Mainhand, 0, 0, 0));
+ DrawSlotMaterials(mainhand, EquipSlot.MainHand.ToName(), mh,
+ new MaterialValueIndex(MaterialValueIndex.DrawObjectType.Mainhand, 0, 0, 0));
if (offhand.IsWeapon && offhand.AsCharacterBase->SlotCount > 0)
- DrawSlotMaterials(offhand, EquipSlot.OffHand.ToName(), oh, new MaterialValueIndex(MaterialValueIndex.DrawObjectType.Offhand, 0, 0, 0));
+ DrawSlotMaterials(offhand, EquipSlot.OffHand.ToName(), oh,
+ new MaterialValueIndex(MaterialValueIndex.DrawObjectType.Offhand, 0, 0, 0));
}
private void DrawSlotMaterials(Model model, string name, CharacterWeapon drawData, MaterialValueIndex index)
{
- var drawnMaterial = 1;
for (byte materialIndex = 0; materialIndex < MaterialService.MaterialsPerModel; ++materialIndex)
{
var texture = model.AsCharacterBase->ColorTableTextures + index.SlotIndex * MaterialService.MaterialsPerModel + materialIndex;
@@ -64,11 +180,11 @@ public unsafe class MaterialDrawer(StateManager _stateManager, DesignManager _de
if (!DirectXTextureHelper.TryGetColorTable(*texture, out var table))
continue;
- using var tree = ImRaii.TreeNode($"{name} Material #{drawnMaterial++}###{name}{materialIndex}");
+ using var tree = ImRaii.TreeNode($"{name} Material #{materialIndex + 1}###{name}{materialIndex}");
if (!tree)
continue;
- DrawMaterial(ref table, drawData, index with { MaterialIndex = materialIndex} );
+ DrawMaterial(ref table, drawData, index with { MaterialIndex = materialIndex });
}
}
@@ -82,6 +198,27 @@ public unsafe class MaterialDrawer(StateManager _stateManager, DesignManager _de
}
}
+ private void DrawRow(Design design, MaterialValueIndex index, in ColorRow row)
+ {
+ var spacing = ImGui.GetStyle().ItemInnerSpacing;
+ var tmp = row;
+ var applied = ImGuiUtil.ColorPicker("##diffuse", "Change the diffuse value for this row.", row.Diffuse, v => tmp.Diffuse = v, "D");
+ ImGui.SameLine(0, spacing.X);
+ applied |= ImGuiUtil.ColorPicker("##specular", "Change the specular value for this row.", row.Specular, v => tmp.Specular = v, "S");
+ ImGui.SameLine(0, spacing.X);
+ applied |= ImGuiUtil.ColorPicker("##emissive", "Change the emissive value for this row.", row.Emissive, v => tmp.Emissive = v, "E");
+ ImGui.SameLine(0, spacing.X);
+ ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale);
+ applied |= ImGui.DragFloat("##Gloss", ref tmp.GlossStrength, 0.01f, 0.001f, float.MaxValue, "%.3f G");
+ ImGuiUtil.HoverTooltip("Change the gloss strength for this row.");
+ ImGui.SameLine(0, spacing.X);
+ ImGui.SetNextItemWidth(120 * ImGuiHelpers.GlobalScale);
+ applied |= ImGui.DragFloat("##Specular Strength", ref tmp.SpecularStrength, 0.01f, float.MinValue, float.MaxValue, "%.3f SS");
+ ImGuiUtil.HoverTooltip("Change the specular strength for this row.");
+ if (applied)
+ _designManager.ChangeMaterialValue(design, index, tmp);
+ }
+
private void DrawRow(ref MtrlFile.ColorTable.Row row, CharacterWeapon drawData, MaterialValueIndex index)
{
using var id = ImRaii.PushId(index.RowIndex);
@@ -92,22 +229,33 @@ public unsafe class MaterialDrawer(StateManager _stateManager, DesignManager _de
value = new MaterialValueState(internalRow, internalRow, drawData, StateSource.Manual);
}
- var applied = ImGui.ColorEdit3("Diffuse", ref value.Model.Diffuse, ImGuiColorEditFlags.NoInputs);
- ImGui.SameLine();
- applied |= ImGui.ColorEdit3("Specular", ref value.Model.Specular, ImGuiColorEditFlags.NoInputs);
- ImGui.SameLine();
- applied |= ImGui.ColorEdit3("Emissive", ref value.Model.Emissive, ImGuiColorEditFlags.NoInputs);
- ImGui.SameLine();
+ ImGui.AlignTextToFramePadding();
+ using (ImRaii.PushFont(UiBuilder.MonoFont))
+ {
+ ImGui.TextUnformatted($"Row {index.RowIndex + 1:D2}");
+ }
+
+ ImGui.SameLine(0, ImGui.GetStyle().ItemSpacing.X * 2);
+ var applied = ImGuiUtil.ColorPicker("##diffuse", "Change the diffuse value for this row.", value.Model.Diffuse, v => value.Model.Diffuse = v, "D");
+
+ var spacing = ImGui.GetStyle().ItemInnerSpacing;
+ ImGui.SameLine(0, spacing.X);
+ applied |= ImGuiUtil.ColorPicker("##specular", "Change the specular value for this row.", value.Model.Specular, v => value.Model.Specular = v, "S");
+ ImGui.SameLine(0, spacing.X);
+ applied |= ImGuiUtil.ColorPicker("##emissive", "Change the emissive value for this row.", value.Model.Emissive, v => value.Model.Emissive = v, "E");
+ ImGui.SameLine(0, spacing.X);
ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale);
- applied |= ImGui.DragFloat("Gloss", ref value.Model.GlossStrength, 0.1f);
- ImGui.SameLine();
+ applied |= ImGui.DragFloat("##Gloss", ref value.Model.GlossStrength, 0.01f, 0.001f, float.MaxValue, "%.3f G") && value.Model.GlossStrength > 0;
+ ImGuiUtil.HoverTooltip("Change the gloss strength for this row.");
+ ImGui.SameLine(0, spacing.X);
ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale);
- applied |= ImGui.DragFloat("Specular Strength", ref value.Model.SpecularStrength, 0.1f);
+ applied |= ImGui.DragFloat("##Specular Strength", ref value.Model.SpecularStrength, 0.01f, float.MinValue, float.MaxValue, "%.3f SS");
+ ImGuiUtil.HoverTooltip("Change the specular strength for this row.");
if (applied)
_stateManager.ChangeMaterialValue(_state!, index, value, ApplySettings.Manual);
if (changed)
{
- ImGui.SameLine();
+ ImGui.SameLine(0, spacing.X);
using (ImRaii.PushFont(UiBuilder.IconFont))
{
using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.FavoriteStarOn.Value());
@@ -115,52 +263,4 @@ public unsafe class MaterialDrawer(StateManager _stateManager, DesignManager _de
}
}
}
-
- private static readonly IReadOnlyList SlotNames =
- [
- "Slot 1",
- "Slot 2",
- "Slot 3",
- "Slot 4",
- "Slot 5",
- "Slot 6",
- "Slot 7",
- "Slot 8",
- "Slot 9",
- "Slot 10",
- "Slot 11",
- "Slot 12",
- "Slot 13",
- "Slot 14",
- "Slot 15",
- "Slot 16",
- "Slot 17",
- "Slot 18",
- "Slot 19",
- "Slot 20",
- ];
-
- private static readonly IReadOnlyList SlotNamesHuman =
- [
- "Head",
- "Body",
- "Hands",
- "Legs",
- "Feet",
- "Earrings",
- "Neck",
- "Wrists",
- "Right Finger",
- "Left Finger",
- "Slot 11",
- "Slot 12",
- "Slot 13",
- "Slot 14",
- "Slot 15",
- "Slot 16",
- "Slot 17",
- "Slot 18",
- "Slot 19",
- "Slot 20",
- ];
}
diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs
index 968b88c..e1c8356 100644
--- a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs
+++ b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs
@@ -117,9 +117,8 @@ public class ActorPanel(
RevertButtons();
- // TODO Materials
- //if (ImGui.CollapsingHeader("Material Shit"))
- // _materialDrawer.DrawActorPanel(_actor);
+ if (_config.UseAdvancedDyes && ImGui.CollapsingHeader("Material Shit"))
+ _materialDrawer.DrawActorPanel(_actor);
using var disabled = ImRaii.Disabled(transformationId != 0);
if (_state.ModelData.IsHuman)
DrawHumanPanel();
diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs
index 3cf46fe..168f994 100644
--- a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs
+++ b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs
@@ -7,6 +7,7 @@ using Glamourer.Designs;
using Glamourer.GameData;
using Glamourer.Gui.Customization;
using Glamourer.Gui.Equipment;
+using Glamourer.Gui.Materials;
using Glamourer.Interop;
using Glamourer.State;
using ImGuiNET;
@@ -31,7 +32,8 @@ public class DesignPanel(
ImportService _importService,
MultiDesignPanel _multiDesignPanel,
CustomizeParameterDrawer _parameterDrawer,
- DesignLinkDrawer _designLinkDrawer)
+ DesignLinkDrawer _designLinkDrawer,
+ MaterialDrawer _materials)
{
private readonly FileDialogManager _fileDialog = new();
@@ -176,21 +178,14 @@ public class DesignPanel(
private void DrawMaterialValues()
{
- if (!_config.UseAdvancedParameters)
+ if (!_config.UseAdvancedDyes)
return;
using var h = ImRaii.CollapsingHeader("Advanced Dyes");
if (!h)
return;
- foreach (var ((key, value), i) in _selector.Selected!.Materials.WithIndex())
- {
- using var id = ImRaii.PushId(i);
- ImGui.TextUnformatted($"{key:X16}");
- ImGui.SameLine();
- var enabled = value.Enabled;
- ImGui.Checkbox("Enabled", ref enabled);
- }
+ _materials.DrawDesignPanel(_selector.Selected!);
}
private void DrawCustomizeApplication()
@@ -384,7 +379,7 @@ public class DesignPanel(
DrawCustomize();
DrawEquipment();
DrawCustomizeParameters();
- //DrawMaterialValues(); TODO Materials
+ DrawMaterialValues();
_designDetails.Draw();
DrawApplicationRules();
_modAssociations.Draw();
diff --git a/Glamourer/Interop/Material/MaterialManager.cs b/Glamourer/Interop/Material/MaterialManager.cs
index b8c33c8..38e45fe 100644
--- a/Glamourer/Interop/Material/MaterialManager.cs
+++ b/Glamourer/Interop/Material/MaterialManager.cs
@@ -18,20 +18,21 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable
private readonly StateManager _stateManager;
private readonly PenumbraService _penumbra;
private readonly ActorManager _actors;
+ private readonly Configuration _config;
private int _lastSlot;
private readonly ThreadLocal> _deleteList = new(() => []);
- public MaterialManager(PrepareColorSet prepareColorSet, StateManager stateManager, ActorManager actors, PenumbraService penumbra)
+ public MaterialManager(PrepareColorSet prepareColorSet, StateManager stateManager, ActorManager actors, PenumbraService penumbra,
+ Configuration config)
{
_stateManager = stateManager;
_actors = actors;
_penumbra = penumbra;
+ _config = config;
_event = prepareColorSet;
-
- // TODO Material
- //_event.Subscribe(OnPrepareColorSet, PrepareColorSet.Priority.MaterialManager);
+ _event.Subscribe(OnPrepareColorSet, PrepareColorSet.Priority.MaterialManager);
}
public void Dispose()
@@ -39,6 +40,9 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable
private void OnPrepareColorSet(CharacterBase* characterBase, MaterialResourceHandle* material, ref StainId stain, ref nint ret)
{
+ if (!_config.UseAdvancedDyes)
+ return;
+
var actor = _penumbra.GameObjectFromDrawObject(characterBase);
var validType = FindType(characterBase, actor, out var type);
var (slotId, materialId) = FindMaterial(characterBase, material);
@@ -176,7 +180,7 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable
}
/// We need to get the temporary set, variant and stain that is currently being set if it is available.
- private CharacterWeapon GetTempSlot(Human* human, byte slotId)
+ private static CharacterWeapon GetTempSlot(Human* human, byte slotId)
{
if (human->ChangedEquipData == null)
return ((Model)human).GetArmor(((uint)slotId).ToEquipSlot()).ToWeapon(0);
@@ -188,7 +192,7 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable
/// We need to get the temporary set, variant and stain that is currently being set if it is available.
/// Weapons do not change in skeleton id without being reconstructed, so this is not changeable data.
///
- private CharacterWeapon GetTempSlot(Weapon* weapon)
+ private static CharacterWeapon GetTempSlot(Weapon* weapon)
{
var changedData = *(void**)((byte*)weapon + 0x918);
if (changedData == null)
diff --git a/Glamourer/Interop/Material/MaterialValueIndex.cs b/Glamourer/Interop/Material/MaterialValueIndex.cs
index 47ddb7a..0facba4 100644
--- a/Glamourer/Interop/Material/MaterialValueIndex.cs
+++ b/Glamourer/Interop/Material/MaterialValueIndex.cs
@@ -2,6 +2,7 @@
using FFXIVClientStructs.Interop;
using Glamourer.Interop.Structs;
using Newtonsoft.Json;
+using Penumbra.GameData.Enums;
using Penumbra.GameData.Files;
namespace Glamourer.Interop.Material;
@@ -150,6 +151,16 @@ public readonly record struct MaterialValueIndex(
: this((DrawObjectType)(key >> 24), (byte)(key >> 16), (byte)(key >> 8), (byte)key)
{ }
+ public override string ToString()
+ => DrawObject switch
+ {
+ DrawObjectType.Human when SlotIndex < 10 =>
+ $"{((uint)SlotIndex).ToEquipSlot().ToName()} Material #{MaterialIndex + 1} Row #{RowIndex + 1}",
+ DrawObjectType.Mainhand when SlotIndex == 0 => $"{EquipSlot.MainHand.ToName()} Material #{MaterialIndex + 1} Row #{RowIndex + 1}",
+ DrawObjectType.Offhand when SlotIndex == 0 => $"{EquipSlot.OffHand.ToName()} Material #{MaterialIndex + 1} Row #{RowIndex + 1}",
+ _ => $"{DrawObject} Slot {SlotIndex} Material #{MaterialIndex + 1} Row #{RowIndex + 1}",
+ };
+
private class Converter : JsonConverter
{
public override void WriteJson(JsonWriter writer, MaterialValueIndex value, JsonSerializer serializer)
diff --git a/Glamourer/Interop/Material/MaterialValueManager.cs b/Glamourer/Interop/Material/MaterialValueManager.cs
index c735182..35b61fc 100644
--- a/Glamourer/Interop/Material/MaterialValueManager.cs
+++ b/Glamourer/Interop/Material/MaterialValueManager.cs
@@ -126,16 +126,26 @@ public struct ColorRow(Vector3 diffuse, Vector3 specular, Vector3 emissive, floa
}
[JsonConverter(typeof(Converter))]
-public struct MaterialValueDesign(ColorRow value, bool enabled)
+public struct MaterialValueDesign(ColorRow value, bool enabled, bool revert)
{
public ColorRow Value = value;
public bool Enabled = enabled;
+ public bool Revert = revert;
public readonly bool Apply(ref MaterialValueState state)
{
if (!Enabled)
return false;
+ if (revert)
+ {
+ if (state.Model.NearEqual(state.Game))
+ return false;
+
+ state.Model = state.Game;
+ return true;
+ }
+
if (state.Model.NearEqual(Value))
return false;
@@ -148,6 +158,8 @@ public struct MaterialValueDesign(ColorRow value, bool enabled)
public override void WriteJson(JsonWriter writer, MaterialValueDesign value, JsonSerializer serializer)
{
writer.WriteStartObject();
+ writer.WritePropertyName("Revert");
+ writer.WriteValue(value.Revert);
writer.WritePropertyName("DiffuseR");
writer.WriteValue(value.Value.Diffuse.X);
writer.WritePropertyName("DiffuseG");
@@ -180,6 +192,7 @@ public struct MaterialValueDesign(ColorRow value, bool enabled)
JsonSerializer serializer)
{
var obj = JObject.Load(reader);
+ Set(ref existingValue.Revert, obj["Revert"]?.Value());
Set(ref existingValue.Value.Diffuse.X, obj["DiffuseR"]?.Value());
Set(ref existingValue.Value.Diffuse.Y, obj["DiffuseG"]?.Value());
Set(ref existingValue.Value.Diffuse.Z, obj["DiffuseB"]?.Value());
@@ -235,7 +248,7 @@ public struct MaterialValueState(
&& Game.NearEqual(rhsRow);
public readonly MaterialValueDesign Convert()
- => new(Model, true);
+ => new(Model, true, false);
}
public readonly struct MaterialValueManager
diff --git a/Glamourer/State/StateApplier.cs b/Glamourer/State/StateApplier.cs
index 7e38726..1b9c69a 100644
--- a/Glamourer/State/StateApplier.cs
+++ b/Glamourer/State/StateApplier.cs
@@ -278,7 +278,7 @@ public class StateApplier(
public unsafe void ChangeMaterialValue(ActorData data, MaterialValueIndex index, ColorRow? value, bool force)
{
- if (!force && !_config.UseAdvancedParameters)
+ if (!force && !_config.UseAdvancedDyes)
return;
foreach (var actor in data.Objects.Where(a => a is { IsCharacter: true, Model.IsHuman: true }))
diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs
index 3705d86..f77e75a 100644
--- a/Glamourer/State/StateEditor.cs
+++ b/Glamourer/State/StateEditor.cs
@@ -172,7 +172,8 @@ public class StateEditor(
return;
var actors = Applier.ChangeMaterialValue(state, index, settings.Source.RequiresChange());
- Glamourer.Log.Verbose($"Set material value in state {state.Identifier.Incognito(null)} from {oldValue} to {newValue.Game}. [Affecting {actors.ToLazyString("nothing")}.]");
+ Glamourer.Log.Verbose(
+ $"Set material value in state {state.Identifier.Incognito(null)} from {oldValue} to {newValue.Game}. [Affecting {actors.ToLazyString("nothing")}.]");
StateChanged.Invoke(StateChanged.Type.MaterialValue, settings.Source, state, actors, (oldValue, newValue.Game, index));
}
@@ -288,13 +289,25 @@ public class StateEditor(
if (!value.Enabled)
continue;
- var idx = MaterialValueIndex.FromKey(key);
- // TODO
- //if (state.Materials.TryGetValue(idx, out var materialState))
- //{
- // if (!settings.RespectManual || materialState.Source.IsManual())
- // Editor.ChangeMaterialValue(state, idx, new MaterialValueState(materialState.Game, value.Value, materialState.DrawData));
- //}
+ var idx = MaterialValueIndex.FromKey(key);
+ var source = settings.Source.SetPending();
+ if (state.Materials.TryGetValue(idx, out var materialState))
+ {
+ if (settings.RespectManual && !materialState.Source.IsManual())
+ continue;
+
+ if (value.Revert)
+ Editor.ChangeMaterialValue(state, idx, default, StateSource.Game, out _, settings.Key);
+ else
+ Editor.ChangeMaterialValue(state, idx,
+ new MaterialValueState(materialState.Game, value.Value, materialState.DrawData, source), settings.Source, out _,
+ settings.Key);
+ }
+ else if (!value.Revert)
+ {
+ Editor.ChangeMaterialValue(state, idx, new MaterialValueState(ColorRow.Empty, value.Value, CharacterWeapon.Empty, source),
+ settings.Source, out _, settings.Key);
+ }
}
}
@@ -321,7 +334,8 @@ public class StateEditor(
public void ApplyDesign(object data, DesignBase design, ApplySettings settings)
{
var merged = settings.MergeLinks && design is Design d
- ? merger.Merge(d.AllLinks, ((ActorState)data).ModelData.Customize, ((ActorState)data).BaseData, false, Config.AlwaysApplyAssociatedMods)
+ ? merger.Merge(d.AllLinks, ((ActorState)data).ModelData.Customize, ((ActorState)data).BaseData, false,
+ Config.AlwaysApplyAssociatedMods)
: new MergedDesign(design);
ApplyDesign(data, merged, settings with
diff --git a/Glamourer/State/StateManager.cs b/Glamourer/State/StateManager.cs
index 32d1237..d731a66 100644
--- a/Glamourer/State/StateManager.cs
+++ b/Glamourer/State/StateManager.cs
@@ -4,6 +4,7 @@ using Glamourer.Designs.Links;
using Glamourer.Events;
using Glamourer.GameData;
using Glamourer.Interop;
+using Glamourer.Interop.Material;
using Glamourer.Interop.Penumbra;
using Glamourer.Interop.Structs;
using Glamourer.Services;
@@ -11,6 +12,7 @@ using Penumbra.GameData.Actors;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
+using System;
namespace Glamourer.State;
@@ -265,9 +267,16 @@ public sealed class StateManager(
var actors = ActorData.Invalid;
if (source is not StateSource.Game)
+ {
actors = Applier.ChangeParameters(state, CustomizeParameterExtensions.All, true);
+ foreach (var (idx, mat) in state.Materials.Values)
+ Applier.ChangeMaterialValue(actors, MaterialValueIndex.FromKey(idx), mat.Game, true);
+ }
+
+ state.Materials.Clear();
+
Glamourer.Log.Verbose(
- $"Reset advanced customization state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]");
+ $"Reset advanced customization and dye state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]");
StateChanged.Invoke(StateChanged.Type.Reset, source, state, actors, null);
}
diff --git a/OtterGui b/OtterGui
index 1a187f7..75c5a7b 160000
--- a/OtterGui
+++ b/OtterGui
@@ -1 +1 @@
-Subproject commit 1a187f756f2e8823197bd43db1c3383231f5eaff
+Subproject commit 75c5a7b220e7f799f85741288e3de4a20af9bcf4