diff --git a/Glamourer/Designs/Design.cs b/Glamourer/Designs/Design.cs index 20d85e7..4e1e72e 100644 --- a/Glamourer/Designs/Design.cs +++ b/Glamourer/Designs/Design.cs @@ -72,6 +72,7 @@ public sealed class Design : DesignBase, ISavable ["Equipment"] = SerializeEquipment(), ["Customize"] = SerializeCustomize(), ["Parameters"] = SerializeParameters(), + ["Materials"] = SerializeMaterials(), ["Mods"] = SerializeMods(), ["Links"] = Links.Serialize(), }; @@ -136,6 +137,7 @@ public sealed class Design : DesignBase, ISavable LoadEquip(items, json["Equipment"], design, design.Name, true); LoadMods(json["Mods"], design); LoadParameters(json["Parameters"], design, design.Name); + LoadMaterials(json["Materials"], design, design.Name); LoadLinks(linkLoader, json["Links"], design); design.Color = json["Color"]?.ToObject() ?? string.Empty; return design; diff --git a/Glamourer/Designs/DesignBase.cs b/Glamourer/Designs/DesignBase.cs index 9699bc2..8873339 100644 --- a/Glamourer/Designs/DesignBase.cs +++ b/Glamourer/Designs/DesignBase.cs @@ -14,7 +14,7 @@ public class DesignBase { public const int FileVersion = 1; - private DesignData _designData = new(); + private DesignData _designData = new(); private readonly DesignMaterialManager _materials = new(); /// For read-only information about custom material color changes. @@ -86,9 +86,9 @@ public class DesignBase internal CustomizeFlag ApplyCustomizeRaw => _applyCustomize; - internal EquipFlag ApplyEquip = EquipFlagExtensions.All; - internal CrestFlag ApplyCrest = CrestExtensions.AllRelevant; - internal MetaFlag ApplyMeta = MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState; + internal EquipFlag ApplyEquip = EquipFlagExtensions.All; + internal CrestFlag ApplyCrest = CrestExtensions.AllRelevant; + internal MetaFlag ApplyMeta = MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState; private bool _writeProtected; public bool SetCustomize(CustomizeService customizeService, CustomizeArray customize) @@ -124,7 +124,6 @@ public class DesignBase _writeProtected = value; return true; - } public bool DoApplyEquip(EquipSlot slot) @@ -244,6 +243,7 @@ public class DesignBase ["Equipment"] = SerializeEquipment(), ["Customize"] = SerializeCustomize(), ["Parameters"] = SerializeParameters(), + ["Materials"] = SerializeMaterials(), }; return ret; } @@ -362,6 +362,45 @@ public class DesignBase return ret; } + protected JObject SerializeMaterials() + { + var ret = new JObject(); + foreach (var (key, value) in Materials) + ret[key.ToString("X16")] = JToken.FromObject(value); + return ret; + } + + protected static void LoadMaterials(JToken? materials, DesignBase design, string name) + { + if (materials is not JObject obj) + return; + + design.GetMaterialDataRef().Clear(); + foreach (var (key, value) in obj.Properties().Zip(obj.PropertyValues())) + { + try + { + var k = uint.Parse(key.Name, NumberStyles.HexNumber); + var v = value.ToObject(); + if (!MaterialValueIndex.FromKey(k, out var idx)) + { + Glamourer.Messager.NotificationMessage($"Invalid material value key {k} for design {name}, skipped.", + NotificationType.Warning); + continue; + } + + if (!design.GetMaterialDataRef().TryAddValue(MaterialValueIndex.FromKey(k), v)) + Glamourer.Messager.NotificationMessage($"Duplicate material value key {k} for design {name}, skipped.", + NotificationType.Warning); + } + catch (Exception ex) + { + Glamourer.Messager.NotificationMessage(ex, $"Error parsing material value for design {name}, skipped", + NotificationType.Warning); + } + } + } + #endregion #region Deserialization @@ -382,6 +421,7 @@ public class DesignBase LoadCustomize(customizations, json["Customize"], ret, "Temporary Design", false, true); LoadEquip(items, json["Equipment"], ret, "Temporary Design", true); LoadParameters(json["Parameters"], ret, "Temporary Design"); + LoadMaterials(json["Materials"], ret, "Temporary Design"); return ret; } diff --git a/Glamourer/Designs/DesignConverter.cs b/Glamourer/Designs/DesignConverter.cs index be70672..9b9ebfc 100644 --- a/Glamourer/Designs/DesignConverter.cs +++ b/Glamourer/Designs/DesignConverter.cs @@ -1,4 +1,5 @@ using Glamourer.Designs.Links; +using Glamourer.Interop.Material; using Glamourer.Services; using Glamourer.State; using Glamourer.Utility; @@ -6,6 +7,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; using Penumbra.GameData.Structs; namespace Glamourer.Designs; @@ -38,22 +40,23 @@ public class DesignConverter( => ShareBase64(ShareJObject(design)); public string ShareBase64(ActorState state, in ApplicationRules rules) - => ShareBase64(state.ModelData, rules); + => ShareBase64(state.ModelData, state.Materials, rules); - public string ShareBase64(in DesignData data, in ApplicationRules rules) + public string ShareBase64(in DesignData data, in StateMaterialManager materials, in ApplicationRules rules) { - var design = Convert(data, rules); + var design = Convert(data, materials, rules); return ShareBase64(ShareJObject(design)); } public DesignBase Convert(ActorState state, in ApplicationRules rules) - => Convert(state.ModelData, rules); + => Convert(state.ModelData, state.Materials, rules); - public DesignBase Convert(in DesignData data, in ApplicationRules rules) + public DesignBase Convert(in DesignData data, in StateMaterialManager materials, in ApplicationRules rules) { var design = _designs.CreateTemporary(); rules.Apply(design); design.SetDesignData(_customize, data); + ComputeMaterials(design.GetMaterialDataRef(), materials, rules.Equip); return design; } @@ -181,4 +184,29 @@ public class DesignConverter( yield return (EquipSlot.OffHand, oh, offhand.Stain); } + + private static void ComputeMaterials(DesignMaterialManager manager, in StateMaterialManager materials, + EquipFlag equipFlags = EquipFlagExtensions.All) + { + foreach (var (key, value) in materials.Values) + { + var idx = MaterialValueIndex.FromKey(key); + if (idx.RowIndex >= MtrlFile.ColorTable.NumRows) + continue; + if (idx.MaterialIndex >= MaterialService.MaterialsPerModel) + continue; + + var slot = idx.DrawObject switch + { + MaterialValueIndex.DrawObjectType.Human => idx.SlotIndex < 10 ? ((uint)idx.SlotIndex).ToEquipSlot() : EquipSlot.Unknown, + MaterialValueIndex.DrawObjectType.Mainhand when idx.SlotIndex == 0 => EquipSlot.MainHand, + MaterialValueIndex.DrawObjectType.Offhand when idx.SlotIndex == 0 => EquipSlot.OffHand, + _ => EquipSlot.Unknown, + }; + if (slot is EquipSlot.Unknown || (slot.ToBothFlags() & equipFlags) == 0) + continue; + + manager.AddOrUpdateValue(idx, value.Convert()); + } + } } diff --git a/Glamourer/Designs/DesignEditor.cs b/Glamourer/Designs/DesignEditor.cs index ab258d7..9640c71 100644 --- a/Glamourer/Designs/DesignEditor.cs +++ b/Glamourer/Designs/DesignEditor.cs @@ -1,6 +1,7 @@ using Glamourer.Designs.Links; using Glamourer.Events; using Glamourer.GameData; +using Glamourer.Interop.Material; using Glamourer.Services; using Glamourer.State; using Penumbra.GameData.Enums; @@ -250,6 +251,14 @@ public class DesignEditor( foreach (var parameter in CustomizeParameterExtensions.AllFlags.Where(other.DoApplyParameter)) ChangeCustomizeParameter(design, parameter, other.DesignData.Parameters[parameter]); + + foreach (var (key, value) in other.Materials) + { + if (!value.Enabled) + continue; + + design.GetMaterialDataRef().AddOrUpdateValue(MaterialValueIndex.FromKey(key), value); + } } /// Change a mainhand weapon and either fix or apply appropriate offhand and potentially gauntlets. diff --git a/Glamourer/GameData/CustomizeParameterValue.cs b/Glamourer/GameData/CustomizeParameterValue.cs index 3bfdf99..87ab851 100644 --- a/Glamourer/GameData/CustomizeParameterValue.cs +++ b/Glamourer/GameData/CustomizeParameterValue.cs @@ -62,4 +62,11 @@ public static class VectorExtensions [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static bool NearEqual(this CustomizeParameterValue lhs, CustomizeParameterValue rhs, float eps = 1e-9f) => NearEqual(lhs.InternalQuadruple, rhs.InternalQuadruple, eps); + + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + public static bool NearEqual(this float lhs, float rhs, float eps = 1e-5f) + { + var diff = lhs - rhs; + return diff < 0 ? diff > -eps : diff < eps; + } } diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs b/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs index 3a8e021..e684554 100644 --- a/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs +++ b/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs @@ -207,7 +207,7 @@ public partial class CustomizationDrawer private static bool CaptureMouseWheel(ref int value, int offset, int cap) { - if (!ImGui.IsItemHovered()) + if (!ImGui.IsItemHovered() || !ImGui.GetIO().KeyCtrl) return false; ImGuiInternal.ItemSetUsingMouseWheel(); diff --git a/Glamourer/Gui/Equipment/GlamourerColorCombo.cs b/Glamourer/Gui/Equipment/GlamourerColorCombo.cs index 3a791d0..d3fde9f 100644 --- a/Glamourer/Gui/Equipment/GlamourerColorCombo.cs +++ b/Glamourer/Gui/Equipment/GlamourerColorCombo.cs @@ -10,7 +10,7 @@ using Penumbra.GameData.Structs; namespace Glamourer.Gui.Equipment; public sealed class GlamourerColorCombo(float _comboWidth, DictStain _stains, FavoriteManager _favorites) - : FilterComboColors(_comboWidth, MouseWheelType.Unmodified, CreateFunc(_stains, _favorites), Glamourer.Log) + : FilterComboColors(_comboWidth, MouseWheelType.Control, CreateFunc(_stains, _favorites), Glamourer.Log) { protected override bool DrawSelectable(int globalIdx, bool selected) { diff --git a/Glamourer/Gui/Equipment/ItemCombo.cs b/Glamourer/Gui/Equipment/ItemCombo.cs index a6f22b5..24ff582 100644 --- a/Glamourer/Gui/Equipment/ItemCombo.cs +++ b/Glamourer/Gui/Equipment/ItemCombo.cs @@ -24,7 +24,7 @@ public sealed class ItemCombo : FilterComboCache public Variant CustomVariant { get; private set; } public ItemCombo(IDataManager gameData, ItemManager items, EquipSlot slot, Logger log, FavoriteManager favorites) - : base(() => GetItems(favorites, items, slot), MouseWheelType.Unmodified, log) + : base(() => GetItems(favorites, items, slot), MouseWheelType.Control, log) { _favorites = favorites; Label = GetLabel(gameData, slot); diff --git a/Glamourer/Gui/Equipment/WeaponCombo.cs b/Glamourer/Gui/Equipment/WeaponCombo.cs index b6c3218..17abb24 100644 --- a/Glamourer/Gui/Equipment/WeaponCombo.cs +++ b/Glamourer/Gui/Equipment/WeaponCombo.cs @@ -17,7 +17,7 @@ public sealed class WeaponCombo : FilterComboCache private float _innerWidth; public WeaponCombo(ItemManager items, FullEquipType type, Logger log) - : base(() => GetWeapons(items, type), MouseWheelType.Unmodified, log) + : base(() => GetWeapons(items, type), MouseWheelType.Control, log) { Label = GetLabel(type); SearchByParts = true; diff --git a/Glamourer/Gui/Materials/MaterialDrawer.cs b/Glamourer/Gui/Materials/MaterialDrawer.cs index e4a612d..fb6a4ac 100644 --- a/Glamourer/Gui/Materials/MaterialDrawer.cs +++ b/Glamourer/Gui/Materials/MaterialDrawer.cs @@ -1,18 +1,20 @@ using Dalamud.Interface; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Glamourer.Designs; using Glamourer.Interop.Material; using Glamourer.Interop.Structs; using Glamourer.State; using ImGuiNET; +using OtterGui; using OtterGui.Services; +using Penumbra.GameData.Enums; using Penumbra.GameData.Files; +using Penumbra.GameData.Structs; namespace Glamourer.Gui.Materials; -public unsafe class MaterialDrawer(StateManager _stateManager) : IService +public unsafe class MaterialDrawer(StateManager _stateManager, DesignManager _designManager) : IService { private static readonly IReadOnlyList Types = [ @@ -23,135 +25,94 @@ public unsafe class MaterialDrawer(StateManager _stateManager) : IService private ActorState? _state; - public void DrawPanel(Actor actor) + public void DrawActorPanel(Actor actor) { if (!actor.IsCharacter || !_stateManager.GetOrCreate(actor, out _state)) return; - foreach (var type in Types) - { - var index = new MaterialValueIndex(type, 0, 0, 0, 0); - if (index.TryGetModel(actor, out var model)) - DrawModelType(model, index); - } - } - - private void DrawModelType(Model model, MaterialValueIndex sourceIndex) - { - using var tree = ImRaii.TreeNode(sourceIndex.DrawObject.ToString()); - if (!tree) + var model = actor.Model; + if (!model.IsHuman) return; - var names = model.AsCharacterBase->GetModelType() is CharacterBase.ModelType.Human - ? SlotNamesHuman - : SlotNames; - for (byte i = 0; i < model.AsCharacterBase->SlotCount; ++i) - { - var index = sourceIndex with { SlotIndex = i }; - DrawSlot(model, names, index); - } - } - - private void DrawSlot(Model model, IReadOnlyList names, MaterialValueIndex sourceIndex) - { - using var tree = ImRaii.TreeNode(names[sourceIndex.SlotIndex]); - if (!tree) + if (model.AsCharacterBase->SlotCount < 10) return; - for (byte i = 0; i < MaterialService.MaterialsPerModel; ++i) + // Humans should have at least 10 slots for the equipment types. Technically more. + foreach (var (slot, idx) in EquipSlotExtensions.EqdpSlots.WithIndex()) { - var index = sourceIndex with { MaterialIndex = i }; - var texture = model.AsCharacterBase->ColorTableTextures + index.SlotIndex * MaterialService.MaterialsPerModel + i; + var item = model.GetArmor(slot).ToWeapon(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)); + if (offhand.IsWeapon && offhand.AsCharacterBase->SlotCount > 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; if (*texture == null) continue; if (!DirectXTextureHelper.TryGetColorTable(*texture, out var table)) continue; - DrawMaterial(ref table, index); + using var tree = ImRaii.TreeNode($"{name} Material #{drawnMaterial++}###{name}{materialIndex}"); + if (!tree) + continue; + + DrawMaterial(ref table, drawData, index with { MaterialIndex = materialIndex} ); } } - private void DrawMaterial(ref MtrlFile.ColorTable table, MaterialValueIndex sourceIndex) + private void DrawMaterial(ref MtrlFile.ColorTable table, CharacterWeapon drawData, MaterialValueIndex sourceIndex) { - using var tree = ImRaii.TreeNode($"Material {sourceIndex.MaterialIndex + 1}"); - if (!tree) - return; - for (byte i = 0; i < MtrlFile.ColorTable.NumRows; ++i) { var index = sourceIndex with { RowIndex = i }; ref var row = ref table[i]; - DrawRow(ref row, index); + DrawRow(ref row, drawData, index); } } - private void DrawRow(ref MtrlFile.ColorTable.Row row, MaterialValueIndex sourceIndex) + private void DrawRow(ref MtrlFile.ColorTable.Row row, CharacterWeapon drawData, MaterialValueIndex index) { - var r = _state!.Materials.GetValues( - MaterialValueIndex.Min(sourceIndex.DrawObject, sourceIndex.SlotIndex, sourceIndex.MaterialIndex, sourceIndex.RowIndex), - MaterialValueIndex.Max(sourceIndex.DrawObject, sourceIndex.SlotIndex, sourceIndex.MaterialIndex, sourceIndex.RowIndex)); - - var highlightColor = ColorId.FavoriteStarOn.Value(); - - using var id = ImRaii.PushId(sourceIndex.RowIndex); - var index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.Diffuse }; - var (diffuse, diffuseGame, changed) = MaterialValueManager.GetSpecific(r, index, out var d) - ? (d.Model, d.Game, true) - : (row.Diffuse, row.Diffuse, false); - using (ImRaii.PushColor(ImGuiCol.Text, highlightColor, changed)) + using var id = ImRaii.PushId(index.RowIndex); + var changed = _state!.Materials.TryGetValue(index, out var value); + if (!changed) { - if (ImGui.ColorEdit3("Diffuse", ref diffuse, ImGuiColorEditFlags.NoInputs)) - _stateManager.ChangeMaterialValue(_state!, index, diffuse, diffuseGame, ApplySettings.Manual); + var internalRow = new ColorRow(row); + value = new MaterialValueState(internalRow, internalRow, drawData, StateSource.Manual); } - - index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.Specular }; - (var specular, var specularGame, changed) = MaterialValueManager.GetSpecific(r, index, out var s) - ? (s.Model, s.Game, true) - : (row.Specular, row.Specular, false); + var applied = ImGui.ColorEdit3("Diffuse", ref value.Model.Diffuse, ImGuiColorEditFlags.NoInputs); ImGui.SameLine(); - using (ImRaii.PushColor(ImGuiCol.Text, highlightColor, changed)) - { - if (ImGui.ColorEdit3("Specular", ref specular, ImGuiColorEditFlags.NoInputs)) - _stateManager.ChangeMaterialValue(_state!, index, specular, specularGame, ApplySettings.Manual); - } - - index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.Emissive }; - (var emissive, var emissiveGame, changed) = MaterialValueManager.GetSpecific(r, index, out var e) - ? (e.Model, e.Game, true) - : (row.Emissive, row.Emissive, false); + applied |= ImGui.ColorEdit3("Specular", ref value.Model.Specular, ImGuiColorEditFlags.NoInputs); ImGui.SameLine(); - using (ImRaii.PushColor(ImGuiCol.Text, highlightColor, changed)) - { - if (ImGui.ColorEdit3("Emissive", ref emissive, ImGuiColorEditFlags.NoInputs)) - _stateManager.ChangeMaterialValue(_state!, index, emissive, emissiveGame, ApplySettings.Manual); - } - - index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.GlossStrength }; - (var glossStrength, var glossStrengthGame, changed) = MaterialValueManager.GetSpecific(r, index, out var g) - ? (g.Model.X, g.Game.X, true) - : (row.GlossStrength, row.GlossStrength, false); + applied |= ImGui.ColorEdit3("Emissive", ref value.Model.Emissive, ImGuiColorEditFlags.NoInputs); ImGui.SameLine(); ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); - using (ImRaii.PushColor(ImGuiCol.Text, highlightColor, changed)) - { - if (ImGui.DragFloat("Gloss", ref glossStrength, 0.1f)) - _stateManager.ChangeMaterialValue(_state!, index, new Vector3(glossStrength), new Vector3(glossStrengthGame), - ApplySettings.Manual); - } - - index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.SpecularStrength }; - (var specularStrength, var specularStrengthGame, changed) = MaterialValueManager.GetSpecific(r, index, out var ss) - ? (ss.Model.X, ss.Game.X, true) - : (row.SpecularStrength, row.SpecularStrength, false); + applied |= ImGui.DragFloat("Gloss", ref value.Model.GlossStrength, 0.1f); ImGui.SameLine(); ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); - using (ImRaii.PushColor(ImGuiCol.Text, highlightColor, changed)) + applied |= ImGui.DragFloat("Specular Strength", ref value.Model.SpecularStrength, 0.1f); + if (applied) + _stateManager.ChangeMaterialValue(_state!, index, value, ApplySettings.Manual); + if (changed) { - if (ImGui.DragFloat("Specular Strength", ref specularStrength, 0.1f)) - _stateManager.ChangeMaterialValue(_state!, index, new Vector3(specularStrength), new Vector3(specularStrengthGame), - ApplySettings.Manual); + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.FavoriteStarOn.Value()); + ImGui.TextUnformatted(FontAwesomeIcon.UserEdit.ToIconString()); + } } } diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs index ff6b798..90ff4b7 100644 --- a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs +++ b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs @@ -119,7 +119,7 @@ public class ActorPanel( if (ImGui.CollapsingHeader("Material Shit")) - _materialDrawer.DrawPanel(_actor); + _materialDrawer.DrawActorPanel(_actor); using var disabled = ImRaii.Disabled(transformationId != 0); if (_state.ModelData.IsHuman) DrawHumanPanel(); diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignColorCombo.cs b/Glamourer/Gui/Tabs/DesignTab/DesignColorCombo.cs index 72d717c..e59be09 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignColorCombo.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignColorCombo.cs @@ -10,7 +10,7 @@ public sealed class DesignColorCombo(DesignColors _designColors, bool _skipAutom FilterComboCache(_skipAutomatic ? _designColors.Keys.OrderBy(k => k) : _designColors.Keys.OrderBy(k => k).Prepend(DesignColors.AutomaticName), - MouseWheelType.Shift, Glamourer.Log) + MouseWheelType.Control, Glamourer.Log) { protected override void OnMouseWheel(string preview, ref int current, int steps) { diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs index a957975..13cd293 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs @@ -174,6 +174,25 @@ public class DesignPanel( _parameterDrawer.Draw(_manager, _selector.Selected!); } + private void DrawMaterialValues() + { + if (!_config.UseAdvancedParameters) + 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); + } + } + private void DrawCustomizeApplication() { using var id = ImRaii.PushId("Customizations"); @@ -365,6 +384,7 @@ public class DesignPanel( DrawCustomize(); DrawEquipment(); DrawCustomizeParameters(); + DrawMaterialValues(); _designDetails.Draw(); DrawApplicationRules(); _modAssociations.Draw(); diff --git a/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs b/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs index 77dafa9..eb61580 100644 --- a/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs +++ b/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs @@ -85,7 +85,7 @@ public class NpcPanel( try { var data = ToDesignData(); - var text = _converter.ShareBase64(data, ApplicationRules.NpcFromModifiers()); + var text = _converter.ShareBase64(data, new StateMaterialManager(), ApplicationRules.NpcFromModifiers()); ImGui.SetClipboardText(text); } catch (Exception ex) @@ -101,7 +101,7 @@ public class NpcPanel( ImGui.OpenPopup("Save as Design"); _newName = _selector.Selection.Name; var data = ToDesignData(); - _newDesign = _converter.Convert(data, ApplicationRules.NpcFromModifiers()); + _newDesign = _converter.Convert(data, new StateMaterialManager(), ApplicationRules.NpcFromModifiers()); } private void SaveDesignDrawPopup() @@ -195,7 +195,7 @@ public class NpcPanel( if (_state.GetOrCreate(id, data.Objects[0], out var state)) { - var design = _converter.Convert(ToDesignData(), ApplicationRules.NpcFromModifiers()); + var design = _converter.Convert(ToDesignData(), new StateMaterialManager(), ApplicationRules.NpcFromModifiers()); _state.ApplyDesign(state, design, ApplySettings.Manual); } } @@ -213,7 +213,7 @@ public class NpcPanel( if (_state.GetOrCreate(id, data.Objects[0], out var state)) { - var design = _converter.Convert(ToDesignData(), ApplicationRules.NpcFromModifiers()); + var design = _converter.Convert(ToDesignData(), new StateMaterialManager(), ApplicationRules.NpcFromModifiers()); _state.ApplyDesign(state, design, ApplySettings.Manual); } } @@ -247,7 +247,9 @@ public class NpcPanel( var colorName = color.Length == 0 ? DesignColors.AutomaticName : color; ImGui.TableNextColumn(); if (_colorCombo.Draw("##colorCombo", colorName, - "Associate a color with this NPC appearance. Right-Click to revert to automatic coloring.", + "Associate a color with this NPC appearance.\n" + + "Right-Click to revert to automatic coloring.\n" + + "Hold Control and scroll the mousewheel to scroll.", width - ImGui.GetStyle().ItemSpacing.X - ImGui.GetFrameHeight(), ImGui.GetTextLineHeight()) && _colorCombo.CurrentSelection != null) { diff --git a/Glamourer/Interop/Material/MaterialManager.cs b/Glamourer/Interop/Material/MaterialManager.cs index 8cbe0c5..f35ee8a 100644 --- a/Glamourer/Interop/Material/MaterialManager.cs +++ b/Glamourer/Interop/Material/MaterialManager.cs @@ -6,6 +6,8 @@ using Glamourer.Interop.Structs; using Glamourer.State; using OtterGui.Services; using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; using Penumbra.GameData.Structs; namespace Glamourer.Interop.Material; @@ -19,6 +21,8 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable private int _lastSlot; + private readonly ThreadLocal> _deleteList = new(() => []); + public MaterialManager(PrepareColorSet prepareColorSet, StateManager stateManager, ActorManager actors, PenumbraService penumbra) { _stateManager = stateManager; @@ -39,7 +43,8 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable var (slotId, materialId) = FindMaterial(characterBase, material); if (!validType - || slotId == byte.MaxValue + || slotId > 9 + || type is not MaterialValueIndex.DrawObjectType.Human && slotId > 0 || !actor.Identifier(_actors, out var identifier) || !_stateManager.TryGetValue(identifier, out var state)) return; @@ -53,38 +58,60 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable if (!PrepareColorSet.TryGetColorTable(characterBase, material, stain, out var baseColorSet)) return; - for (var i = 0; i < values.Length; ++i) + var drawData = type switch { - var idx = MaterialValueIndex.FromKey(values[i].key); - var (oldGame, model, source) = values[i].Value; - ref var row = ref baseColorSet[idx.RowIndex]; - if (!idx.DataIndex.TryGetValue(row, out var newGame)) - continue; - - if (newGame == oldGame) - { - idx.DataIndex.SetValue(ref row, model); - } - else - { - switch (source.Base()) - { - case StateSource.Manual: - _stateManager.ChangeMaterialValue(state, idx, Vector3.Zero, Vector3.Zero, ApplySettings.Game); - --i; - break; - case StateSource.Fixed: - idx.DataIndex.SetValue(ref row, model); - state.Materials.UpdateValue(idx, new MaterialValueState(newGame, model, source), out _); - break; - } - } - } + MaterialValueIndex.DrawObjectType.Human => GetTempSlot((Human*)characterBase, slotId), + _ => GetTempSlot((Weapon*)characterBase), + }; + UpdateMaterialValues(state, values, drawData, ref baseColorSet); if (MaterialService.GenerateNewColorTable(baseColorSet, out var texture)) ret = (nint)texture; } + /// Update and apply the glamourer state of an actor according to the application sources when updated by the game. + private void UpdateMaterialValues(ActorState state, ReadOnlySpan<(uint Key, MaterialValueState Value)> values, CharacterWeapon drawData, + ref MtrlFile.ColorTable colorTable) + { + var deleteList = _deleteList.Value!; + deleteList.Clear(); + for (var i = 0; i < values.Length; ++i) + { + var idx = MaterialValueIndex.FromKey(values[i].Key); + var materialValue = values[i].Value; + ref var row = ref colorTable[idx.RowIndex]; + var newGame = new ColorRow(row); + if (materialValue.EqualGame(newGame, drawData)) + materialValue.Model.Apply(ref row); + else + switch (materialValue.Source) + { + case StateSource.Pending: + materialValue.Model.Apply(ref row); + state.Materials.UpdateValue(idx, new MaterialValueState(newGame, materialValue.Model, drawData, StateSource.Manual), + out _); + break; + case StateSource.IpcManual: + case StateSource.Manual: + deleteList.Add(idx); + break; + case StateSource.Fixed: + case StateSource.IpcFixed: + materialValue.Model.Apply(ref row); + state.Materials.UpdateValue(idx, new MaterialValueState(newGame, materialValue.Model, drawData, materialValue.Source), + out _); + break; + } + } + + foreach (var idx in deleteList) + _stateManager.ChangeMaterialValue(state, idx, default, ApplySettings.Game); + } + + /// + /// Find the index of a material by searching through a draw objects pointers. + /// Tries to take shortcuts for consecutive searches like when a character is newly created. + /// [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private (byte SlotId, byte MaterialId) FindMaterial(CharacterBase* characterBase, MaterialResourceHandle* material) { @@ -119,6 +146,7 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable return (byte.MaxValue, byte.MaxValue); } + /// Find the type of the given draw object by checking the actors pointers. private static bool FindType(CharacterBase* characterBase, Actor actor, out MaterialValueIndex.DrawObjectType type) { type = MaterialValueIndex.DrawObjectType.Human; @@ -145,4 +173,26 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable return false; } + + /// 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) + { + if (human->ChangedEquipData == null) + return ((Model)human).GetArmor(((uint)slotId).ToEquipSlot()).ToWeapon(0); + + return ((CharacterArmor*)human->ChangedEquipData + slotId * 3)->ToWeapon(0); + } + + /// + /// 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) + { + var changedData = *(void**)((byte*)weapon + 0x918); + if (changedData == null) + return new CharacterWeapon(weapon->ModelSetId, weapon->SecondaryId, (Variant)weapon->Variant, (StainId)weapon->ModelUnknown); + + return new CharacterWeapon(weapon->ModelSetId, *(SecondaryId*)changedData, ((Variant*)changedData)[2], ((StainId*)changedData)[3]); + } } diff --git a/Glamourer/Interop/Material/MaterialValueIndex.cs b/Glamourer/Interop/Material/MaterialValueIndex.cs index d2d8db1..47ddb7a 100644 --- a/Glamourer/Interop/Material/MaterialValueIndex.cs +++ b/Glamourer/Interop/Material/MaterialValueIndex.cs @@ -11,14 +11,13 @@ public readonly record struct MaterialValueIndex( MaterialValueIndex.DrawObjectType DrawObject, byte SlotIndex, byte MaterialIndex, - byte RowIndex, - MaterialValueIndex.ColorTableIndex DataIndex) + byte RowIndex) { public uint Key - => ToKey(DrawObject, SlotIndex, MaterialIndex, RowIndex, DataIndex); + => ToKey(DrawObject, SlotIndex, MaterialIndex, RowIndex); public bool Valid - => Validate(DrawObject) && ValidateSlot(SlotIndex) && ValidateMaterial(MaterialIndex) && ValidateRow(RowIndex) && Validate(DataIndex); + => Validate(DrawObject) && ValidateSlot(SlotIndex) && ValidateMaterial(MaterialIndex) && ValidateRow(RowIndex); public static bool FromKey(uint key, out MaterialValueIndex index) { @@ -96,7 +95,7 @@ public readonly record struct MaterialValueIndex( public unsafe bool TryGetColorTable(Texture** texture, out MtrlFile.ColorTable table) => DirectXTextureHelper.TryGetColorTable(*texture, out table); - public unsafe bool TryGetColorRow(Actor actor, out MtrlFile.ColorTable.Row row) + public bool TryGetColorRow(Actor actor, out MtrlFile.ColorTable.Row row) { if (!TryGetColorTable(actor, out var table)) { @@ -108,40 +107,16 @@ public readonly record struct MaterialValueIndex( return true; } - public unsafe bool TryGetValue(Actor actor, out Vector3 value) - { - if (!TryGetColorRow(actor, out var row)) - { - value = Vector3.Zero; - return false; - } - - value = DataIndex switch - { - ColorTableIndex.Diffuse => row.Diffuse, - ColorTableIndex.Specular => row.Specular, - ColorTableIndex.SpecularStrength => new Vector3(row.SpecularStrength, 0, 0), - ColorTableIndex.Emissive => row.Emissive, - ColorTableIndex.GlossStrength => new Vector3(row.GlossStrength, 0, 0), - ColorTableIndex.TileSet => new Vector3(row.TileSet), - ColorTableIndex.MaterialRepeat => new Vector3(row.MaterialRepeat, 0), - ColorTableIndex.MaterialSkew => new Vector3(row.MaterialSkew, 0), - _ => new Vector3(float.NaN), - }; - return !float.IsNaN(value.X); - } public static MaterialValueIndex FromKey(uint key) => new(key); - public static MaterialValueIndex Min(DrawObjectType drawObject = 0, byte slotIndex = 0, byte materialIndex = 0, byte rowIndex = 0, - ColorTableIndex dataIndex = 0) - => new(drawObject, slotIndex, materialIndex, rowIndex, dataIndex); + public static MaterialValueIndex Min(DrawObjectType drawObject = 0, byte slotIndex = 0, byte materialIndex = 0, byte rowIndex = 0) + => new(drawObject, slotIndex, materialIndex, rowIndex); public static MaterialValueIndex Max(DrawObjectType drawObject = (DrawObjectType)byte.MaxValue, byte slotIndex = byte.MaxValue, - byte materialIndex = byte.MaxValue, byte rowIndex = byte.MaxValue, - ColorTableIndex dataIndex = (ColorTableIndex)byte.MaxValue) - => new(drawObject, slotIndex, materialIndex, rowIndex, dataIndex); + byte materialIndex = byte.MaxValue, byte rowIndex = byte.MaxValue) + => new(drawObject, slotIndex, materialIndex, rowIndex); public enum DrawObjectType : byte { @@ -150,18 +125,6 @@ public readonly record struct MaterialValueIndex( Offhand, }; - public enum ColorTableIndex : byte - { - Diffuse, - Specular, - SpecularStrength, - Emissive, - GlossStrength, - TileSet, - MaterialRepeat, - MaterialSkew, - } - public static bool Validate(DrawObjectType type) => Enum.IsDefined(type); @@ -174,22 +137,17 @@ public readonly record struct MaterialValueIndex( public static bool ValidateRow(byte rowIndex) => rowIndex < MtrlFile.ColorTable.NumRows; - public static bool Validate(ColorTableIndex dataIndex) - => Enum.IsDefined(dataIndex); - - private static uint ToKey(DrawObjectType type, byte slotIndex, byte materialIndex, byte rowIndex, ColorTableIndex index) + private static uint ToKey(DrawObjectType type, byte slotIndex, byte materialIndex, byte rowIndex) { - var result = (uint)index & 0xFF; - result |= (uint)(rowIndex & 0xFF) << 8; - result |= (uint)(materialIndex & 0xF) << 16; - result |= (uint)(slotIndex & 0xFF) << 20; - result |= (uint)((byte)type & 0xF) << 28; + var result = (uint)rowIndex; + result |= (uint)materialIndex << 8; + result |= (uint)slotIndex << 16; + result |= (uint)((byte)type << 24); return result; } private MaterialValueIndex(uint key) - : this((DrawObjectType)((key >> 28) & 0xF), (byte)(key >> 20), (byte)((key >> 16) & 0xF), (byte)(key >> 8), - (ColorTableIndex)(key & 0xFF)) + : this((DrawObjectType)(key >> 24), (byte)(key >> 16), (byte)(key >> 8), (byte)key) { } private class Converter : JsonConverter @@ -202,81 +160,3 @@ public readonly record struct MaterialValueIndex( => FromKey(serializer.Deserialize(reader), out var value) ? value : throw new Exception($"Invalid material key {value.Key}."); } } - -public static class MaterialExtensions -{ - public static bool TryGetValue(this MaterialValueIndex.ColorTableIndex index, in MtrlFile.ColorTable.Row row, out Vector3 value) - { - value = index switch - { - MaterialValueIndex.ColorTableIndex.Diffuse => row.Diffuse, - MaterialValueIndex.ColorTableIndex.Specular => row.Specular, - MaterialValueIndex.ColorTableIndex.SpecularStrength => new Vector3(row.SpecularStrength, 0, 0), - MaterialValueIndex.ColorTableIndex.Emissive => row.Emissive, - MaterialValueIndex.ColorTableIndex.GlossStrength => new Vector3(row.GlossStrength, 0, 0), - MaterialValueIndex.ColorTableIndex.TileSet => new Vector3(row.TileSet), - MaterialValueIndex.ColorTableIndex.MaterialRepeat => new Vector3(row.MaterialRepeat, 0), - MaterialValueIndex.ColorTableIndex.MaterialSkew => new Vector3(row.MaterialSkew, 0), - _ => new Vector3(float.NaN), - }; - return !float.IsNaN(value.X); - } - - public static bool SetValue(this MaterialValueIndex.ColorTableIndex index, ref MtrlFile.ColorTable.Row row, in Vector3 value) - { - switch (index) - { - case MaterialValueIndex.ColorTableIndex.Diffuse: - if (value == row.Diffuse) - return false; - - row.Diffuse = value; - return true; - - case MaterialValueIndex.ColorTableIndex.Specular: - if (value == row.Specular) - return false; - - row.Specular = value; - return true; - case MaterialValueIndex.ColorTableIndex.SpecularStrength: - if (value.X == row.SpecularStrength) - return false; - - row.SpecularStrength = value.X; - return true; - case MaterialValueIndex.ColorTableIndex.Emissive: - if (value == row.Emissive) - return false; - - row.Emissive = value; - return true; - case MaterialValueIndex.ColorTableIndex.GlossStrength: - if (value.X == row.GlossStrength) - return false; - - row.GlossStrength = value.X; - return true; - case MaterialValueIndex.ColorTableIndex.TileSet: - var @ushort = (ushort)(value.X + 0.5f); - if (@ushort == row.TileSet) - return false; - - row.TileSet = @ushort; - return true; - case MaterialValueIndex.ColorTableIndex.MaterialRepeat: - if (value.X == row.MaterialRepeat.X && value.Y == row.MaterialRepeat.Y) - return false; - - row.MaterialRepeat = new Vector2(value.X, value.Y); - return true; - case MaterialValueIndex.ColorTableIndex.MaterialSkew: - if (value.X == row.MaterialSkew.X && value.Y == row.MaterialSkew.Y) - return false; - - row.MaterialSkew = new Vector2(value.X, value.Y); - return true; - default: return false; - } - } -} diff --git a/Glamourer/Interop/Material/MaterialValueManager.cs b/Glamourer/Interop/Material/MaterialValueManager.cs index 5f7c2dc..c735182 100644 --- a/Glamourer/Interop/Material/MaterialValueManager.cs +++ b/Glamourer/Interop/Material/MaterialValueManager.cs @@ -1,12 +1,242 @@ global using StateMaterialManager = Glamourer.Interop.Material.MaterialValueManager; global using DesignMaterialManager = Glamourer.Interop.Material.MaterialValueManager; +using Glamourer.GameData; using Glamourer.State; +using Penumbra.GameData.Files; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Structs; namespace Glamourer.Interop.Material; -public record struct MaterialValueDesign(Vector3 Value, bool Enabled); -public record struct MaterialValueState(Vector3 Game, Vector3 Model, StateSource Source); +[JsonConverter(typeof(Converter))] +public struct ColorRow(Vector3 diffuse, Vector3 specular, Vector3 emissive, float specularStrength, float glossStrength) +{ + public static readonly ColorRow Empty = new(Vector3.Zero, Vector3.Zero, Vector3.Zero, 0, 0); + + public Vector3 Diffuse = diffuse; + public Vector3 Specular = specular; + public Vector3 Emissive = emissive; + public float SpecularStrength = specularStrength; + public float GlossStrength = glossStrength; + + public ColorRow(in MtrlFile.ColorTable.Row row) + : this(row.Diffuse, row.Specular, row.Emissive, row.SpecularStrength, row.GlossStrength) + { } + + public readonly bool NearEqual(in ColorRow rhs) + => Diffuse.NearEqual(rhs.Diffuse) + && Specular.NearEqual(rhs.Specular) + && Emissive.NearEqual(rhs.Emissive) + && SpecularStrength.NearEqual(rhs.SpecularStrength) + && GlossStrength.NearEqual(rhs.GlossStrength); + + public readonly bool Apply(ref MtrlFile.ColorTable.Row row) + { + var ret = false; + if (!row.Diffuse.NearEqual(Diffuse)) + { + row.Diffuse = Diffuse; + ret = true; + } + + if (!row.Specular.NearEqual(Specular)) + { + row.Specular = Specular; + ret = true; + } + + if (!row.Emissive.NearEqual(Emissive)) + { + row.Emissive = Emissive; + ret = true; + } + + if (!row.SpecularStrength.NearEqual(SpecularStrength)) + { + row.SpecularStrength = SpecularStrength; + ret = true; + } + + if (!row.GlossStrength.NearEqual(GlossStrength)) + { + row.GlossStrength = GlossStrength; + ret = true; + } + + return ret; + } + + private class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, ColorRow value, JsonSerializer serializer) + { + writer.WriteStartObject(); + writer.WritePropertyName("DiffuseR"); + writer.WriteValue(value.Diffuse.X); + writer.WritePropertyName("DiffuseG"); + writer.WriteValue(value.Diffuse.Y); + writer.WritePropertyName("DiffuseB"); + writer.WriteValue(value.Diffuse.Z); + writer.WritePropertyName("SpecularR"); + writer.WriteValue(value.Specular.X); + writer.WritePropertyName("SpecularG"); + writer.WriteValue(value.Specular.Y); + writer.WritePropertyName("SpecularB"); + writer.WriteValue(value.Specular.Z); + writer.WritePropertyName("SpecularA"); + writer.WriteValue(value.SpecularStrength); + writer.WritePropertyName("EmissiveR"); + writer.WriteValue(value.Emissive.X); + writer.WritePropertyName("EmissiveG"); + writer.WriteValue(value.Emissive.Y); + writer.WritePropertyName("EmissiveB"); + writer.WriteValue(value.Emissive.Z); + writer.WritePropertyName("Gloss"); + writer.WriteValue(value.GlossStrength); + writer.WriteEndObject(); + } + + public override ColorRow ReadJson(JsonReader reader, Type objectType, ColorRow existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + var obj = JObject.Load(reader); + Set(ref existingValue.Diffuse.X, obj["DiffuseR"]?.Value()); + Set(ref existingValue.Diffuse.Y, obj["DiffuseG"]?.Value()); + Set(ref existingValue.Diffuse.Z, obj["DiffuseB"]?.Value()); + Set(ref existingValue.Specular.X, obj["SpecularR"]?.Value()); + Set(ref existingValue.Specular.Y, obj["SpecularG"]?.Value()); + Set(ref existingValue.Specular.Z, obj["SpecularB"]?.Value()); + Set(ref existingValue.SpecularStrength, obj["SpecularA"]?.Value()); + Set(ref existingValue.Emissive.X, obj["EmissiveR"]?.Value()); + Set(ref existingValue.Emissive.Y, obj["EmissiveG"]?.Value()); + Set(ref existingValue.Emissive.Z, obj["EmissiveB"]?.Value()); + Set(ref existingValue.GlossStrength, obj["Gloss"]?.Value()); + return existingValue; + + static void Set(ref T target, T? value) + where T : struct + { + if (value.HasValue) + target = value.Value; + } + } + } +} + +[JsonConverter(typeof(Converter))] +public struct MaterialValueDesign(ColorRow value, bool enabled) +{ + public ColorRow Value = value; + public bool Enabled = enabled; + + public readonly bool Apply(ref MaterialValueState state) + { + if (!Enabled) + return false; + + if (state.Model.NearEqual(Value)) + return false; + + state.Model = Value; + return true; + } + + private class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, MaterialValueDesign value, JsonSerializer serializer) + { + writer.WriteStartObject(); + writer.WritePropertyName("DiffuseR"); + writer.WriteValue(value.Value.Diffuse.X); + writer.WritePropertyName("DiffuseG"); + writer.WriteValue(value.Value.Diffuse.Y); + writer.WritePropertyName("DiffuseB"); + writer.WriteValue(value.Value.Diffuse.Z); + writer.WritePropertyName("SpecularR"); + writer.WriteValue(value.Value.Specular.X); + writer.WritePropertyName("SpecularG"); + writer.WriteValue(value.Value.Specular.Y); + writer.WritePropertyName("SpecularB"); + writer.WriteValue(value.Value.Specular.Z); + writer.WritePropertyName("SpecularA"); + writer.WriteValue(value.Value.SpecularStrength); + writer.WritePropertyName("EmissiveR"); + writer.WriteValue(value.Value.Emissive.X); + writer.WritePropertyName("EmissiveG"); + writer.WriteValue(value.Value.Emissive.Y); + writer.WritePropertyName("EmissiveB"); + writer.WriteValue(value.Value.Emissive.Z); + writer.WritePropertyName("Gloss"); + writer.WriteValue(value.Value.GlossStrength); + writer.WritePropertyName("Enabled"); + writer.WriteValue(value.Enabled); + writer.WriteEndObject(); + } + + public override MaterialValueDesign ReadJson(JsonReader reader, Type objectType, MaterialValueDesign existingValue, + bool hasExistingValue, + JsonSerializer serializer) + { + var obj = JObject.Load(reader); + 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()); + Set(ref existingValue.Value.Specular.X, obj["SpecularR"]?.Value()); + Set(ref existingValue.Value.Specular.Y, obj["SpecularG"]?.Value()); + Set(ref existingValue.Value.Specular.Z, obj["SpecularB"]?.Value()); + Set(ref existingValue.Value.SpecularStrength, obj["SpecularA"]?.Value()); + Set(ref existingValue.Value.Emissive.X, obj["EmissiveR"]?.Value()); + Set(ref existingValue.Value.Emissive.Y, obj["EmissiveG"]?.Value()); + Set(ref existingValue.Value.Emissive.Z, obj["EmissiveB"]?.Value()); + Set(ref existingValue.Value.GlossStrength, obj["Gloss"]?.Value()); + existingValue.Enabled = obj["Enabled"]?.Value() ?? false; + return existingValue; + + static void Set(ref T target, T? value) + where T : struct + { + if (value.HasValue) + target = value.Value; + } + } + } +} + +[StructLayout(LayoutKind.Explicit)] +public struct MaterialValueState( + in ColorRow game, + in ColorRow model, + CharacterWeapon drawData, + StateSource source) +{ + public MaterialValueState(in ColorRow gameRow, in ColorRow modelRow, CharacterArmor armor, StateSource source) + : this(gameRow, modelRow, armor.ToWeapon(0), source) + { } + + [FieldOffset(0)] + public ColorRow Game = game; + + [FieldOffset(44)] + public ColorRow Model = model; + + [FieldOffset(88)] + public readonly CharacterWeapon DrawData = drawData; + + [FieldOffset(95)] + public readonly StateSource Source = source; + + public readonly bool EqualGame(in ColorRow rhsRow, CharacterWeapon rhsData) + => DrawData.Skeleton == rhsData.Skeleton + && DrawData.Weapon == rhsData.Weapon + && DrawData.Variant == rhsData.Variant + && DrawData.Stain == rhsData.Stain + && Game.NearEqual(rhsRow); + + public readonly MaterialValueDesign Convert() + => new(Model, true); +} public readonly struct MaterialValueManager { @@ -113,7 +343,7 @@ public readonly struct MaterialValueManager return count; } - public ReadOnlySpan<(uint key, T Value)> GetValues(MaterialValueIndex min, MaterialValueIndex max) + public ReadOnlySpan<(uint Key, T Value)> GetValues(MaterialValueIndex min, MaterialValueIndex max) => MaterialValueManager.Filter(CollectionsMarshal.AsSpan(_values), min, max); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] @@ -187,6 +417,7 @@ public static class MaterialValueManager maxIdx = ~maxIdx + idx; return maxIdx > minIdx ? (minIdx, maxIdx - 1) : (-1, -1); } + maxIdx += idx; while (maxIdx < values.Length - 1 && values[maxIdx + 1].Key <= maxKey) diff --git a/Glamourer/State/InternalStateEditor.cs b/Glamourer/State/InternalStateEditor.cs index 75cee46..35587fe 100644 --- a/Glamourer/State/InternalStateEditor.cs +++ b/Glamourer/State/InternalStateEditor.cs @@ -222,7 +222,7 @@ public class InternalStateEditor( } /// Change the value of a single material color table entry. - public bool ChangeMaterialValue(ActorState state, MaterialValueIndex index, Vector3 value, Vector3 gameValue, StateSource source, out Vector3 oldValue, + public bool ChangeMaterialValue(ActorState state, MaterialValueIndex index, in MaterialValueState newValue, StateSource source, out ColorRow? oldValue, uint key = 0) { // We already have an existing value. @@ -240,18 +240,18 @@ public class InternalStateEditor( } // Update if edited. - state.Materials.UpdateValue(index, new MaterialValueState(gameValue, value, source), out _); + state.Materials.UpdateValue(index, newValue, out _); return true; } // We do not have an existing value. - oldValue = gameValue; + oldValue = null; // Do not do anything if locked or if the game value updates, because then we do not need to add an entry. if (!state.CanUnlock(key) || source is StateSource.Game) return false; - // Only add an entry if it is sufficiently different from the game value. - return !value.NearEqual(gameValue) && state.Materials.TryAddValue(index, new MaterialValueState(gameValue, value, source)); + // Only add an entry if it is different from the game value. + return state.Materials.TryAddValue(index, newValue); } public bool ChangeMetaState(ActorState state, MetaIndex index, bool value, StateSource source, out bool oldValue, diff --git a/Glamourer/State/StateApplier.cs b/Glamourer/State/StateApplier.cs index 19c1f3e..7e38726 100644 --- a/Glamourer/State/StateApplier.cs +++ b/Glamourer/State/StateApplier.cs @@ -276,7 +276,7 @@ public class StateApplier( return data; } - public unsafe void ChangeMaterialValue(ActorData data, MaterialValueIndex index, Vector3? value, bool force) + public unsafe void ChangeMaterialValue(ActorData data, MaterialValueIndex index, ColorRow? value, bool force) { if (!force && !_config.UseAdvancedParameters) return; @@ -289,14 +289,11 @@ public class StateApplier( if (!index.TryGetColorTable(texture, out var table)) continue; - Vector3 actualValue; if (value.HasValue) - actualValue = value.Value; - else if (!PrepareColorSet.TryGetColorTable(actor, index, out var baseTable) - || !index.DataIndex.TryGetValue(baseTable[index.RowIndex], out actualValue)) - continue; - - if (!index.DataIndex.SetValue(ref table[index.RowIndex], actualValue)) + value.Value.Apply(ref table[index.RowIndex]); + else if (PrepareColorSet.TryGetColorTable(actor, index, out var baseTable)) + table[index.RowIndex] = baseTable[index.RowIndex]; + else continue; MaterialService.ReplaceColorTable(texture, table); diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs index e747448..7d0c203 100644 --- a/Glamourer/State/StateEditor.cs +++ b/Glamourer/State/StateEditor.cs @@ -165,15 +165,15 @@ public class StateEditor( StateChanged.Invoke(StateChanged.Type.Parameter, settings.Source, state, actors, (old, @new, flag)); } - public void ChangeMaterialValue(object data, MaterialValueIndex index, Vector3 value, Vector3 gameValue, ApplySettings settings) + public void ChangeMaterialValue(object data, MaterialValueIndex index, in MaterialValueState newValue, ApplySettings settings) { var state = (ActorState)data; - if (!Editor.ChangeMaterialValue(state, index, value, gameValue, settings.Source, out var oldValue, settings.Key)) + if (!Editor.ChangeMaterialValue(state, index, newValue, settings.Source, out var oldValue, settings.Key)) 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 {value}. [Affecting {actors.ToLazyString("nothing")}.]"); - StateChanged.Invoke(StateChanged.Type.MaterialValue, settings.Source, state, actors, (oldValue, value, index)); + 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)); } /// @@ -282,6 +282,20 @@ public class StateEditor( if (!settings.RespectManual || !state.Sources[meta].IsManual()) Editor.ChangeMetaState(state, meta, mergedDesign.Design.DesignData.GetMeta(meta), Source(meta), out _, settings.Key); } + + foreach (var (key, value) in mergedDesign.Design.Materials) + { + 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 actors = settings.Source.RequiresChange()