Merge branch 'colortable'

This commit is contained in:
Ottermandias 2024-02-06 16:51:44 +01:00
commit 73266811a0
66 changed files with 2458 additions and 286 deletions

View file

@ -11,7 +11,9 @@ namespace Glamourer.Api;
public partial class GlamourerIpc
{
public const string LabelApplyAll = "Glamourer.ApplyAll";
public const string LabelApplyAllOnce = "Glamourer.ApplyAllOnce";
public const string LabelApplyAllToCharacter = "Glamourer.ApplyAllToCharacter";
public const string LabelApplyAllOnceToCharacter = "Glamourer.ApplyAllOnceToCharacter";
public const string LabelApplyOnlyEquipment = "Glamourer.ApplyOnlyEquipment";
public const string LabelApplyOnlyEquipmentToCharacter = "Glamourer.ApplyOnlyEquipmentToCharacter";
public const string LabelApplyOnlyCustomization = "Glamourer.ApplyOnlyCustomization";
@ -24,11 +26,15 @@ public partial class GlamourerIpc
public const string LabelApplyOnlyCustomizationLock = "Glamourer.ApplyOnlyCustomizationLock";
public const string LabelApplyOnlyCustomizationToCharacterLock = "Glamourer.ApplyOnlyCustomizationToCharacterLock";
public const string LabelApplyByGuid = "Glamourer.ApplyByGuid";
public const string LabelApplyByGuidToCharacter = "Glamourer.ApplyByGuidToCharacter";
public const string LabelApplyByGuid = "Glamourer.ApplyByGuid";
public const string LabelApplyByGuidOnce = "Glamourer.ApplyByGuidOnce";
public const string LabelApplyByGuidToCharacter = "Glamourer.ApplyByGuidToCharacter";
public const string LabelApplyByGuidOnceToCharacter = "Glamourer.ApplyByGuidOnceToCharacter";
private readonly ActionProvider<string, string> _applyAllProvider;
private readonly ActionProvider<string, string> _applyAllOnceProvider;
private readonly ActionProvider<string, Character?> _applyAllToCharacterProvider;
private readonly ActionProvider<string, Character?> _applyAllOnceToCharacterProvider;
private readonly ActionProvider<string, string> _applyOnlyEquipmentProvider;
private readonly ActionProvider<string, Character?> _applyOnlyEquipmentToCharacterProvider;
private readonly ActionProvider<string, string> _applyOnlyCustomizationProvider;
@ -42,14 +48,22 @@ public partial class GlamourerIpc
private readonly ActionProvider<string, Character?, uint> _applyOnlyCustomizationToCharacterProviderLock;
private readonly ActionProvider<Guid, string> _applyByGuidProvider;
private readonly ActionProvider<Guid, string> _applyByGuidOnceProvider;
private readonly ActionProvider<Guid, Character?> _applyByGuidToCharacterProvider;
private readonly ActionProvider<Guid, Character?> _applyByGuidOnceToCharacterProvider;
public static ActionSubscriber<string, string> ApplyAllSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelApplyAll);
public static ActionSubscriber<string, string> ApplyAllOnceSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelApplyAllOnce);
public static ActionSubscriber<string, Character?> ApplyAllToCharacterSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelApplyAllToCharacter);
public static ActionSubscriber<string, Character?> ApplyAllOnceToCharacterSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelApplyAllOnceToCharacter);
public static ActionSubscriber<string, string> ApplyOnlyEquipmentSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelApplyOnlyEquipment);
@ -65,15 +79,27 @@ public partial class GlamourerIpc
public static ActionSubscriber<Guid, string> ApplyByGuidSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelApplyByGuid);
public static ActionSubscriber<Guid, string> ApplyByGuidOnceSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelApplyByGuidOnce);
public static ActionSubscriber<Guid, Character?> ApplyByGuidToCharacterSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelApplyByGuidToCharacter);
public static ActionSubscriber<Guid, Character?> ApplyByGuidOnceToCharacterSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelApplyByGuidOnceToCharacter);
public void ApplyAll(string base64, string characterName)
=> ApplyDesign(_designConverter.FromBase64(base64, true, true, out var version), FindActors(characterName), version, 0);
public void ApplyAllOnce(string base64, string characterName)
=> ApplyDesign(_designConverter.FromBase64(base64, true, true, out var version), FindActors(characterName), version, 0, true);
public void ApplyAllToCharacter(string base64, Character? character)
=> ApplyDesign(_designConverter.FromBase64(base64, true, true, out var version), FindActors(character), version, 0);
public void ApplyAllOnceToCharacter(string base64, Character? character)
=> ApplyDesign(_designConverter.FromBase64(base64, true, true, out var version), FindActors(character), version, 0, true);
public void ApplyOnlyEquipment(string base64, string characterName)
=> ApplyDesign(_designConverter.FromBase64(base64, false, true, out var version), FindActors(characterName), version, 0);
@ -107,12 +133,18 @@ public partial class GlamourerIpc
public void ApplyByGuid(Guid identifier, string characterName)
=> ApplyDesignByGuid(identifier, FindActors(characterName), 0);
=> ApplyDesignByGuid(identifier, FindActors(characterName), 0, false);
public void ApplyByGuidOnce(Guid identifier, string characterName)
=> ApplyDesignByGuid(identifier, FindActors(characterName), 0, true);
public void ApplyByGuidToCharacter(Guid identifier, Character? character)
=> ApplyDesignByGuid(identifier, FindActors(character), 0);
=> ApplyDesignByGuid(identifier, FindActors(character), 0, false);
private void ApplyDesign(DesignBase? design, IEnumerable<ActorIdentifier> actors, byte version, uint lockCode)
public void ApplyByGuidOnceToCharacter(Guid identifier, Character? character)
=> ApplyDesignByGuid(identifier, FindActors(character), 0, true);
private void ApplyDesign(DesignBase? design, IEnumerable<ActorIdentifier> actors, byte version, uint lockCode, bool once = false)
{
if (design == null)
return;
@ -130,12 +162,13 @@ public partial class GlamourerIpc
if ((hasModelId || state.ModelData.ModelId == 0) && state.CanUnlock(lockCode))
{
_stateManager.ApplyDesign(state, design, new ApplySettings(Source:StateSource.Ipc, Key:lockCode));
_stateManager.ApplyDesign(state, design,
new ApplySettings(Source: once ? StateSource.IpcManual : StateSource.IpcFixed, Key: lockCode));
state.Lock(lockCode);
}
}
}
private void ApplyDesignByGuid(Guid identifier, IEnumerable<ActorIdentifier> actors, uint lockCode)
=> ApplyDesign(_designManager.Designs.ByIdentifier(identifier), actors, DesignConverter.Version, lockCode);
private void ApplyDesignByGuid(Guid identifier, IEnumerable<ActorIdentifier> actors, uint lockCode, bool once)
=> ApplyDesign(_designManager.Designs.ByIdentifier(identifier), actors, DesignConverter.Version, lockCode, once);
}

View file

@ -1,4 +1,5 @@
using Glamourer.Events;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Interop.Structs;
using Glamourer.State;
using Penumbra.Api.Helpers;
@ -18,7 +19,7 @@ public partial class GlamourerIpc
private void OnStateChanged(StateChanged.Type type, StateSource source, ActorState state, ActorData actors, object? data = null)
{
foreach (var actor in actors.Objects)
_stateChangedProvider.Invoke(type, actor.Address, new Lazy<string>(() => _designConverter.ShareBase64(state)));
_stateChangedProvider.Invoke(type, actor.Address, new Lazy<string>(() => _designConverter.ShareBase64(state, ApplicationRules.AllButParameters(state))));
}
private void OnGPoseChanged(bool value)

View file

@ -1,5 +1,6 @@
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin;
using Glamourer.Designs;
using Penumbra.Api.Helpers;
using Penumbra.GameData.Actors;
@ -40,6 +41,6 @@ public partial class GlamourerIpc
return null;
}
return _designConverter.ShareBase64(state);
return _designConverter.ShareBase64(state, ApplicationRules.AllButParameters(state));
}
}

View file

@ -1,6 +1,5 @@
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin;
using Glamourer.Events;
using Glamourer.State;
using Penumbra.Api.Helpers;
using Penumbra.GameData.Actors;
@ -83,7 +82,7 @@ public partial class GlamourerIpc
foreach (var id in actors)
{
if (_stateManager.TryGetValue(id, out var state))
_stateManager.ResetState(state, StateSource.Ipc, lockCode);
_stateManager.ResetState(state, StateSource.IpcFixed, lockCode);
}
}

View file

@ -20,20 +20,30 @@ public partial class GlamourerIpc
ItemInvalid,
}
public const string LabelSetItem = "Glamourer.SetItem";
public const string LabelSetItemByActorName = "Glamourer.SetItemByActorName";
public const string LabelSetItem = "Glamourer.SetItem";
public const string LabelSetItemOnce = "Glamourer.SetItemOnce";
public const string LabelSetItemByActorName = "Glamourer.SetItemByActorName";
public const string LabelSetItemOnceByActorName = "Glamourer.SetItemOnceByActorName";
private readonly FuncProvider<Character?, byte, ulong, byte, uint, int> _setItemProvider;
private readonly FuncProvider<Character?, byte, ulong, byte, uint, int> _setItemOnceProvider;
private readonly FuncProvider<string, byte, ulong, byte, uint, int> _setItemByActorNameProvider;
private readonly FuncProvider<string, byte, ulong, byte, uint, int> _setItemOnceByActorNameProvider;
public static FuncSubscriber<Character?, byte, ulong, byte, uint, int> SetItemSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelSetItem);
public static FuncSubscriber<Character?, byte, ulong, byte, uint, int> SetItemOnceSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelSetItemOnce);
public static FuncSubscriber<string, byte, ulong, byte, uint, int> SetItemByActorNameSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelSetItemByActorName);
private GlamourerErrorCode SetItem(Character? character, EquipSlot slot, CustomItemId itemId, StainId stainId, uint key)
public static FuncSubscriber<string, byte, ulong, byte, uint, int> SetItemOnceByActorNameSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelSetItemOnceByActorName);
private GlamourerErrorCode SetItem(Character? character, EquipSlot slot, CustomItemId itemId, StainId stainId, uint key, bool once)
{
if (itemId.Id == 0)
itemId = ItemManager.NothingId(slot);
@ -57,11 +67,12 @@ public partial class GlamourerIpc
if (!state.ModelData.IsHuman)
return GlamourerErrorCode.ActorNotHuman;
_stateManager.ChangeEquip(state, slot, item, stainId, new ApplySettings(Source: StateSource.Ipc, Key:key));
_stateManager.ChangeEquip(state, slot, item, stainId,
new ApplySettings(Source: once ? StateSource.IpcManual : StateSource.IpcFixed, Key: key));
return GlamourerErrorCode.Success;
}
private GlamourerErrorCode SetItemByActorName(string name, EquipSlot slot, CustomItemId itemId, StainId stainId, uint key)
private GlamourerErrorCode SetItemByActorName(string name, EquipSlot slot, CustomItemId itemId, StainId stainId, uint key, bool once)
{
if (itemId.Id == 0)
itemId = ItemManager.NothingId(slot);
@ -84,7 +95,8 @@ public partial class GlamourerIpc
if (!state.ModelData.IsHuman)
return GlamourerErrorCode.ActorNotHuman;
_stateManager.ChangeEquip(state, slot, item, stainId, new ApplySettings(Source: StateSource.Ipc, Key: key));
_stateManager.ChangeEquip(state, slot, item, stainId,
new ApplySettings(Source: once ? StateSource.IpcManual : StateSource.IpcFixed, Key: key));
found = true;
}

View file

@ -46,9 +46,11 @@ public sealed partial class GlamourerIpc : IDisposable
_getAllCustomizationFromCharacterProvider =
new FuncProvider<Character?, string?>(pi, LabelGetAllCustomizationFromCharacter, GetAllCustomizationFromCharacter);
_applyAllProvider = new ActionProvider<string, string>(pi, LabelApplyAll, ApplyAll);
_applyAllToCharacterProvider = new ActionProvider<string, Character?>(pi, LabelApplyAllToCharacter, ApplyAllToCharacter);
_applyOnlyEquipmentProvider = new ActionProvider<string, string>(pi, LabelApplyOnlyEquipment, ApplyOnlyEquipment);
_applyAllProvider = new ActionProvider<string, string>(pi, LabelApplyAll, ApplyAll);
_applyAllOnceProvider = new ActionProvider<string, string>(pi, LabelApplyAll, ApplyAllOnce);
_applyAllToCharacterProvider = new ActionProvider<string, Character?>(pi, LabelApplyAllToCharacter, ApplyAllToCharacter);
_applyAllOnceToCharacterProvider = new ActionProvider<string, Character?>(pi, LabelApplyAllToCharacter, ApplyAllOnceToCharacter);
_applyOnlyEquipmentProvider = new ActionProvider<string, string>(pi, LabelApplyOnlyEquipment, ApplyOnlyEquipment);
_applyOnlyEquipmentToCharacterProvider =
new ActionProvider<string, Character?>(pi, LabelApplyOnlyEquipmentToCharacter, ApplyOnlyEquipmentToCharacter);
_applyOnlyCustomizationProvider = new ActionProvider<string, string>(pi, LabelApplyOnlyCustomization, ApplyOnlyCustomization);
@ -66,8 +68,11 @@ public sealed partial class GlamourerIpc : IDisposable
_applyOnlyCustomizationToCharacterProviderLock =
new ActionProvider<string, Character?, uint>(pi, LabelApplyOnlyCustomizationToCharacterLock, ApplyOnlyCustomizationToCharacterLock);
_applyByGuidProvider = new ActionProvider<Guid, string>(pi, LabelApplyByGuid, ApplyByGuid);
_applyByGuidProvider = new ActionProvider<Guid, string>(pi, LabelApplyByGuid, ApplyByGuid);
_applyByGuidOnceProvider = new ActionProvider<Guid, string>(pi, LabelApplyByGuidOnce, ApplyByGuidOnce);
_applyByGuidToCharacterProvider = new ActionProvider<Guid, Character?>(pi, LabelApplyByGuidToCharacter, ApplyByGuidToCharacter);
_applyByGuidOnceToCharacterProvider =
new ActionProvider<Guid, Character?>(pi, LabelApplyByGuidOnceToCharacter, ApplyByGuidOnceToCharacter);
_revertProvider = new ActionProvider<string>(pi, LabelRevert, Revert);
_revertCharacterProvider = new ActionProvider<Character?>(pi, LabelRevertCharacter, RevertCharacter);
@ -83,9 +88,14 @@ public sealed partial class GlamourerIpc : IDisposable
_gPoseChangedProvider = new EventProvider<bool>(pi, LabelGPoseChanged);
_setItemProvider = new FuncProvider<Character?, byte, ulong, byte, uint, int>(pi, LabelSetItem,
(idx, slot, item, stain, key) => (int)SetItem(idx, (EquipSlot)slot, item, stain, key));
_setItemByActorNameProvider = new FuncProvider<string, byte, ulong, byte, uint, int>(pi, LabelSetItemByActorName,
(name, slot, item, stain, key) => (int)SetItemByActorName(name, (EquipSlot)slot, item, stain, key));
(idx, slot, item, stain, key) => (int)SetItem(idx, (EquipSlot)slot, item, stain, key, false));
_setItemOnceProvider = new FuncProvider<Character?, byte, ulong, byte, uint, int>(pi, LabelSetItem,
(idx, slot, item, stain, key) => (int)SetItem(idx, (EquipSlot)slot, item, stain, key, true));
_setItemByActorNameProvider = new FuncProvider<string, byte, ulong, byte, uint, int>(pi, LabelSetItemOnceByActorName,
(name, slot, item, stain, key) => (int)SetItemByActorName(name, (EquipSlot)slot, item, stain, key, false));
_setItemOnceByActorNameProvider = new FuncProvider<string, byte, ulong, byte, uint, int>(pi, LabelSetItemOnceByActorName,
(name, slot, item, stain, key) => (int)SetItemByActorName(name, (EquipSlot)slot, item, stain, key, true));
_stateChangedEvent.Subscribe(OnStateChanged, StateChanged.Priority.GlamourerIpc);
_gPose.Subscribe(OnGPoseChanged, GPoseService.Priority.GlamourerIpc);
@ -102,7 +112,9 @@ public sealed partial class GlamourerIpc : IDisposable
_getAllCustomizationFromCharacterProvider.Dispose();
_applyAllProvider.Dispose();
_applyAllOnceProvider.Dispose();
_applyAllToCharacterProvider.Dispose();
_applyAllOnceToCharacterProvider.Dispose();
_applyOnlyEquipmentProvider.Dispose();
_applyOnlyEquipmentToCharacterProvider.Dispose();
_applyOnlyCustomizationProvider.Dispose();
@ -113,8 +125,11 @@ public sealed partial class GlamourerIpc : IDisposable
_applyOnlyEquipmentToCharacterProviderLock.Dispose();
_applyOnlyCustomizationProviderLock.Dispose();
_applyOnlyCustomizationToCharacterProviderLock.Dispose();
_applyByGuidProvider.Dispose();
_applyByGuidOnceProvider.Dispose();
_applyByGuidToCharacterProvider.Dispose();
_applyByGuidOnceToCharacterProvider.Dispose();
_revertProvider.Dispose();
_revertCharacterProvider.Dispose();
@ -133,7 +148,9 @@ public sealed partial class GlamourerIpc : IDisposable
_getDesignListProvider.Dispose();
_setItemProvider.Dispose();
_setItemOnceProvider.Dispose();
_setItemByActorNameProvider.Dispose();
_setItemOnceByActorNameProvider.Dispose();
}
private IEnumerable<ActorIdentifier> FindActors(string actorName)

View file

@ -264,7 +264,7 @@ public sealed class AutoDesignApplier : IDisposable
var mergedDesign = _designMerger.Merge(
set.Designs.Where(d => d.IsActive(actor)).SelectMany(d => d.Design?.AllLinks.Select(l => (l.Design, l.Flags & d.Type)) ?? [(d.Design, d.Type)]),
state.ModelData, true, false);
state.ModelData, true, _config.AlwaysApplyAssociatedMods);
_state.ApplyDesign(state, mergedDesign, new ApplySettings(0, StateSource.Fixed, respectManual, fromJobChange, false));
}

View file

@ -42,6 +42,7 @@ public class Configuration : IPluginConfiguration, ISavable
public bool UseRgbForColors { get; set; } = true;
public bool ShowColorConfig { get; set; } = true;
public bool ChangeEntireItem { get; set; } = false;
public bool AlwaysApplyAssociatedMods { get; set; } = false;
public ModifiableHotkey ToggleQuickDesignBar { get; set; } = new(VirtualKey.NO_KEY);
public DoubleModifier DeleteDesignModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift);
public ChangeLogDisplayType ChangeLogDisplayType { get; set; } = ChangeLogDisplayType.New;

View file

@ -0,0 +1,84 @@
using Glamourer.GameData;
using Glamourer.State;
using ImGuiNET;
using Penumbra.GameData.Enums;
namespace Glamourer.Designs;
public readonly struct ApplicationRules(
EquipFlag equip,
CustomizeFlag customize,
CrestFlag crest,
CustomizeParameterFlag parameters,
MetaFlag meta)
{
public static readonly ApplicationRules All = new(EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant,
CrestExtensions.AllRelevant, CustomizeParameterExtensions.All, MetaExtensions.All);
public static ApplicationRules FromModifiers(ActorState state)
=> FromModifiers(state, ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift);
public static ApplicationRules NpcFromModifiers()
=> NpcFromModifiers(ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift);
public static ApplicationRules AllButParameters(ActorState state)
=> new(All.Equip, All.Customize, All.Crest, ComputeParameters(state.ModelData, state.BaseData, All.Parameters), All.Meta);
public static ApplicationRules NpcFromModifiers(bool ctrl, bool shift)
=> new(ctrl || !shift ? EquipFlagExtensions.All : 0,
!ctrl || shift ? CustomizeFlagExtensions.AllRelevant : 0,
0,
0,
ctrl || !shift ? MetaFlag.VisorState : 0);
public static ApplicationRules FromModifiers(ActorState state, bool ctrl, bool shift)
{
var equip = ctrl || !shift ? EquipFlagExtensions.All : 0;
var customize = !ctrl || shift ? CustomizeFlagExtensions.AllRelevant : 0;
var crest = equip == 0 ? 0 : CrestExtensions.AllRelevant;
var parameters = customize == 0 ? 0 : CustomizeParameterExtensions.All;
var meta = state.ModelData.IsWet() ? MetaFlag.Wetness : 0;
if (equip != 0)
meta |= MetaFlag.HatState | MetaFlag.WeaponState | MetaFlag.VisorState;
return new ApplicationRules(equip, customize, crest, ComputeParameters(state.ModelData, state.BaseData, parameters), meta);
}
public void Apply(DesignBase design)
{
design.ApplyEquip = Equip;
design.ApplyCustomize = Customize;
design.ApplyCrest = Crest;
design.ApplyParameters = Parameters;
design.ApplyMeta = Meta;
}
public EquipFlag Equip
=> equip & EquipFlagExtensions.All;
public CustomizeFlag Customize
=> customize & CustomizeFlagExtensions.AllRelevant;
public CrestFlag Crest
=> crest & CrestExtensions.AllRelevant;
public CustomizeParameterFlag Parameters
=> parameters & CustomizeParameterExtensions.All;
public MetaFlag Meta
=> meta & MetaExtensions.All;
public static CustomizeParameterFlag ComputeParameters(in DesignData model, in DesignData game,
CustomizeParameterFlag baseFlags = CustomizeParameterExtensions.All)
{
foreach (var flag in baseFlags.Iterate())
{
var modelValue = model.Parameters[flag];
var gameValue = game.Parameters[flag];
if (modelValue.NearEqual(gameValue))
baseFlags &= ~flag;
}
return baseFlags;
}
}

View file

@ -26,6 +26,7 @@ public sealed class Design : DesignBase, ISavable
{
Tags = [.. other.Tags];
Description = other.Description;
QuickDesign = other.QuickDesign;
AssociatedMods = new SortedList<Mod, ModSettings>(other.AssociatedMods);
}
@ -39,6 +40,7 @@ public sealed class Design : DesignBase, ISavable
public string Description { get; internal set; } = string.Empty;
public string[] Tags { get; internal set; } = [];
public int Index { get; internal set; }
public bool QuickDesign { get; internal set; } = true;
public string Color { get; internal set; } = string.Empty;
public SortedList<Mod, ModSettings> AssociatedMods { get; private set; } = [];
public LinkContainer Links { get; private set; } = [];
@ -64,11 +66,13 @@ public sealed class Design : DesignBase, ISavable
["Name"] = Name.Text,
["Description"] = Description,
["Color"] = Color,
["QuickDesign"] = QuickDesign,
["Tags"] = JArray.FromObject(Tags),
["WriteProtected"] = WriteProtected(),
["Equipment"] = SerializeEquipment(),
["Customize"] = SerializeCustomize(),
["Parameters"] = SerializeParameters(),
["Materials"] = SerializeMaterials(),
["Mods"] = SerializeMods(),
["Links"] = Links.Serialize(),
};
@ -124,6 +128,7 @@ public sealed class Design : DesignBase, ISavable
Description = json["Description"]?.ToObject<string>() ?? string.Empty,
Tags = ParseTags(json),
LastEdit = json["LastEdit"]?.ToObject<DateTimeOffset>() ?? creationDate,
QuickDesign = json["QuickDesign"]?.ToObject<bool>() ?? true,
};
if (design.LastEdit < creationDate)
design.LastEdit = creationDate;
@ -132,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>() ?? string.Empty;
return design;

View file

@ -1,7 +1,7 @@
using Dalamud.Interface.Internal.Notifications;
using Glamourer.GameData;
using Glamourer.Interop.Material;
using Glamourer.Services;
using Glamourer.State;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
@ -14,7 +14,16 @@ public class DesignBase
{
public const int FileVersion = 1;
private DesignData _designData = new();
private DesignData _designData = new();
private readonly DesignMaterialManager _materials = new();
/// <summary> For read-only information about custom material color changes. </summary>
public IReadOnlyList<(uint, MaterialValueDesign)> Materials
=> _materials.Values;
/// <summary> To make it clear something is edited here. </summary>
public DesignMaterialManager GetMaterialDataRef()
=> _materials;
/// <summary> For read-only information about the actual design. </summary>
public ref readonly DesignData DesignData
@ -30,6 +39,7 @@ public class DesignBase
CustomizeSet = SetCustomizationSet(customize);
}
/// <summary> Used when importing .cma or .chara files. </summary>
internal DesignBase(CustomizeService customize, in DesignData designData, EquipFlag equipFlags, CustomizeFlag customizeFlags)
{
_designData = designData;
@ -42,6 +52,7 @@ public class DesignBase
internal DesignBase(DesignBase clone)
{
_designData = clone._designData;
_materials = clone._materials.Clone();
CustomizeSet = clone.CustomizeSet;
ApplyCustomize = clone.ApplyCustomizeRaw;
ApplyEquip = clone.ApplyEquip & EquipFlagExtensions.All;
@ -75,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)
@ -113,7 +124,6 @@ public class DesignBase
_writeProtected = value;
return true;
}
public bool DoApplyEquip(EquipSlot slot)
@ -233,6 +243,7 @@ public class DesignBase
["Equipment"] = SerializeEquipment(),
["Customize"] = SerializeCustomize(),
["Parameters"] = SerializeParameters(),
["Materials"] = SerializeMaterials(),
};
return ret;
}
@ -351,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<MaterialValueDesign>();
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
@ -371,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;
}

View file

@ -1,5 +1,5 @@
using Glamourer.Designs.Links;
using Glamourer.GameData;
using Glamourer.Interop.Material;
using Glamourer.Services;
using Glamourer.State;
using Glamourer.Utility;
@ -7,11 +7,17 @@ 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;
public class DesignConverter(ItemManager _items, DesignManager _designs, CustomizeService _customize, HumanModelList _humans, DesignLinkLoader _linkLoader)
public class DesignConverter(
ItemManager _items,
DesignManager _designs,
CustomizeService _customize,
HumanModelList _humans,
DesignLinkLoader _linkLoader)
{
public const byte Version = 6;
@ -21,9 +27,9 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi
public JObject ShareJObject(Design design)
=> design.JsonSerialize();
public JObject ShareJObject(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags, CustomizeParameterFlag parameterFlags)
public JObject ShareJObject(ActorState state, in ApplicationRules rules)
{
var design = Convert(state, equipFlags, customizeFlags, crestFlags, parameterFlags);
var design = Convert(state, rules);
return ShareJObject(design);
}
@ -33,33 +39,24 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi
public string ShareBase64(DesignBase design)
=> ShareBase64(ShareJObject(design));
public string ShareBase64(ActorState state)
=> ShareBase64(state, EquipFlagExtensions.All, CustomizeFlagExtensions.All, CrestExtensions.All, CustomizeParameterExtensions.All);
public string ShareBase64(ActorState state, in ApplicationRules rules)
=> ShareBase64(state.ModelData, state.Materials, rules);
public string ShareBase64(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags, CustomizeParameterFlag parameterFlags)
=> ShareBase64(state.ModelData, equipFlags, customizeFlags, crestFlags, parameterFlags);
public string ShareBase64(in DesignData data, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags, CustomizeParameterFlag parameterFlags)
public string ShareBase64(in DesignData data, in StateMaterialManager materials, in ApplicationRules rules)
{
var design = Convert(data, equipFlags, customizeFlags, crestFlags, parameterFlags);
var design = Convert(data, materials, rules);
return ShareBase64(ShareJObject(design));
}
public DesignBase Convert(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags, CustomizeParameterFlag parameterFlags)
=> Convert(state.ModelData, equipFlags, customizeFlags, crestFlags, parameterFlags);
public DesignBase Convert(ActorState state, in ApplicationRules rules)
=> Convert(state.ModelData, state.Materials, rules);
public DesignBase Convert(in DesignData data, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags, CustomizeParameterFlag parameterFlags)
public DesignBase Convert(in DesignData data, in StateMaterialManager materials, in ApplicationRules rules)
{
var design = _designs.CreateTemporary();
design.ApplyEquip = equipFlags & EquipFlagExtensions.All;
design.ApplyCustomize = customizeFlags & CustomizeFlagExtensions.AllRelevant;
design.ApplyCrest = crestFlags & CrestExtensions.All;
design.ApplyParameters = parameterFlags & CustomizeParameterExtensions.All;
design.SetApplyMeta(MetaIndex.HatState, design.DoApplyEquip(EquipSlot.Head));
design.SetApplyMeta(MetaIndex.VisorState, design.DoApplyEquip(EquipSlot.Head));
design.SetApplyMeta(MetaIndex.WeaponState, design.DoApplyEquip(EquipSlot.MainHand) || design.DoApplyEquip(EquipSlot.OffHand));
design.SetApplyMeta(MetaIndex.Wetness, true);
rules.Apply(design);
design.SetDesignData(_customize, data);
ComputeMaterials(design.GetMaterialDataRef(), materials, rules.Equip);
return design;
}
@ -139,7 +136,7 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi
return ret;
}
private static string ShareBase64(JObject jObject)
private static string ShareBase64(JToken jObject)
{
var json = jObject.ToString(Formatting.None);
var compressed = json.Compress(Version);
@ -187,4 +184,29 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi
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());
}
}
}

View file

@ -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);
}
}
/// <summary> Change a mainhand weapon and either fix or apply appropriate offhand and potentially gauntlets. </summary>

View file

@ -284,6 +284,18 @@ public sealed class DesignManager : DesignEditor
DesignChanged.Invoke(DesignChanged.Type.WriteProtection, design, value);
}
/// <summary> Set the quick design bar display status of a design. </summary>
public void SetQuickDesign(Design design, bool value)
{
if (value == design.QuickDesign)
return;
design.QuickDesign = value;
SaveService.QueueSave(design);
Glamourer.Log.Debug($"Set design {design.Identifier} to {(!value ? "no longer be " : string.Empty)} displayed in the quick design bar.");
DesignChanged.Invoke(DesignChanged.Type.QuickDesignBar, design, value);
}
#endregion
#region Edit Application Rules

View file

@ -92,6 +92,9 @@ public sealed class DesignChanged()
/// <summary> An existing design changed its write protection status. Data is the new value [bool]. </summary>
WriteProtection,
/// <summary> An existing design changed its display status for the quick design bar. Data is the new value [bool]. </summary>
QuickDesignBar,
/// <summary> An existing design changed one of the meta flags. Data is the flag, whether it was about their applying and the new value [(MetaFlag, bool, bool)]. </summary>
Other,
}

View file

@ -42,6 +42,9 @@ namespace Glamourer.Events
/// <summary> A characters saved state had its customize parameter changed. Data is the old value, the new value and the type [(CustomizeParameterValue, CustomizeParameterValue, CustomizeParameterFlag)]. </summary>
Parameter,
/// <summary> A characters saved state had a material color table value changed. Data is the old value, the new value and the index [(Vector3, Vector3, MaterialValueIndex)]. </summary>
MaterialValue,
/// <summary> A characters saved state had a design applied. This means everything may have changed. Data is the applied design. [DesignBase] </summary>
Design,

View file

@ -1,6 +1,4 @@
using Newtonsoft.Json;
namespace Glamourer.GameData;
namespace Glamourer.GameData;
public readonly struct CustomizeParameterValue
{
@ -50,3 +48,25 @@ public readonly struct CustomizeParameterValue
public override string ToString()
=> _data.ToString();
}
public static class VectorExtensions
{
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
public static bool NearEqual(this Vector3 lhs, Vector3 rhs, float eps = 1e-9f)
=> (lhs - rhs).LengthSquared() < eps;
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
public static bool NearEqual(this Vector4 lhs, Vector4 rhs, float eps = 1e-9f)
=> (lhs - rhs).LengthSquared() < eps;
[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;
}
}

View file

@ -89,6 +89,7 @@
<ProjectReference Include="..\Penumbra.GameData\Penumbra.GameData.csproj" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Dalamud.ContextMenu" Version="1.3.1" />
<PackageReference Include="Vortice.Direct3D11" Version="3.4.2-beta" />
</ItemGroup>
<ItemGroup>

View file

@ -23,6 +23,11 @@ public partial class CustomizationDrawer
{
if (ImGui.ColorButton($"{_customize[index].Value}##color", color, ImGuiColorEditFlags.None, _framedIconSize))
ImGui.OpenPopup(ColorPickerPopupName);
else if (current >= 0 && CaptureMouseWheel(ref current, 0, _currentCount))
{
var data = _set.Data(_currentIndex, current, _customize.Face);
UpdateValue(data.Value);
}
}
var npc = false;

View file

@ -18,8 +18,9 @@ public partial class CustomizationDrawer
using var bigGroup = ImRaii.Group();
var label = _currentOption;
var current = _set.DataByValue(index, _currentByte, out var custom, _customize.Face);
var npc = false;
var current = _set.DataByValue(index, _currentByte, out var custom, _customize.Face);
var originalCurrent = current;
var npc = false;
if (current < 0)
{
label = $"{_currentOption} (NPC)";
@ -32,7 +33,14 @@ public partial class CustomizationDrawer
using (_ = ImRaii.Disabled(_locked || _currentIndex is CustomizeIndex.Face && _lockedRedraw))
{
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize))
{
ImGui.OpenPopup(IconSelectorPopup);
}
else if (originalCurrent >= 0 && CaptureMouseWheel(ref current, 0, _currentCount))
{
var data = _set.Data(_currentIndex, current, _customize.Face);
UpdateValue(data.Value);
}
}
ImGuiUtil.HoverIconTooltip(icon, _iconSize);

View file

@ -1,6 +1,7 @@
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGuiInternal;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -34,7 +35,8 @@ public partial class CustomizationDrawer
{
var tmp = (int)_currentByte.Value;
ImGui.SetNextItemWidth(_comboSelectorSize);
if (ImGui.SliderInt("##slider", ref tmp, 0, _currentCount - 1, "%i", ImGuiSliderFlags.AlwaysClamp))
if (ImGui.SliderInt("##slider", ref tmp, 0, _currentCount - 1, "%i", ImGuiSliderFlags.AlwaysClamp)
|| CaptureMouseWheel(ref tmp, 0, _currentCount - 1))
UpdateValue((CustomizeValue)tmp);
}
@ -42,11 +44,10 @@ public partial class CustomizationDrawer
{
var tmp = (int)_currentByte.Value;
ImGui.SetNextItemWidth(_inputIntSize);
var cap = ImGui.GetIO().KeyCtrl ? byte.MaxValue : _currentCount - 1;
if (ImGui.InputInt("##text", ref tmp, 1, 1))
{
var newValue = (CustomizeValue)(ImGui.GetIO().KeyCtrl
? Math.Clamp(tmp, 0, byte.MaxValue)
: Math.Clamp(tmp, 0, _currentCount - 1));
var newValue = (CustomizeValue)Math.Clamp(tmp, 0, cap);
UpdateValue(newValue);
}
@ -73,6 +74,10 @@ public partial class CustomizationDrawer
else if (ImGui.GetIO().KeyCtrl)
UpdateValue((CustomizeValue)value);
}
else
{
CheckWheel();
}
if (!_withApply)
ImGuiUtil.HoverTooltip("Hold Control to force updates with invalid/unknown options at your own risk.");
@ -81,15 +86,29 @@ public partial class CustomizationDrawer
if (ImGuiUtil.DrawDisabledButton("-", new Vector2(ImGui.GetFrameHeight()), "Select the previous available option in order.",
currentIndex <= 0))
UpdateValue(_set.Data(_currentIndex, currentIndex - 1, _customize.Face).Value);
else
CheckWheel();
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton("+", new Vector2(ImGui.GetFrameHeight()), "Select the next available option in order.",
currentIndex >= _currentCount - 1 || npc))
UpdateValue(_set.Data(_currentIndex, currentIndex + 1, _customize.Face).Value);
else
CheckWheel();
return;
void CheckWheel()
{
if (currentIndex < 0 || !CaptureMouseWheel(ref currentIndex, 0, _currentCount))
return;
var data = _set.Data(_currentIndex, currentIndex, _customize.Face);
UpdateValue(data.Value);
}
}
private void DrawListSelector(CustomizeIndex index, bool indexedBy1)
{
using var id = SetId(index);
using var id = SetId(index);
using var bigGroup = ImRaii.Group();
using (_ = ImRaii.Disabled(_locked))
@ -122,29 +141,31 @@ public partial class CustomizationDrawer
private void ListCombo0()
{
ImGui.SetNextItemWidth(_comboSelectorSize * ImGui.GetIO().FontGlobalScale);
var current = _currentByte.Value;
using var combo = ImRaii.Combo("##combo", $"{_currentOption} #{current + 1}");
if (!combo)
return;
for (var i = 0; i < _currentCount; ++i)
var current = (int)_currentByte.Value;
using (var combo = ImRaii.Combo("##combo", $"{_currentOption} #{current + 1}"))
{
if (ImGui.Selectable($"{_currentOption} #{i + 1}##combo", i == current))
UpdateValue((CustomizeValue)i);
if (combo)
for (var i = 0; i < _currentCount; ++i)
{
if (ImGui.Selectable($"{_currentOption} #{i + 1}##combo", i == current))
UpdateValue((CustomizeValue)i);
}
}
if (CaptureMouseWheel(ref current, 0, _currentCount))
UpdateValue((CustomizeValue)current);
}
private void ListInputInt0()
{
var tmp = _currentByte.Value + 1;
ImGui.SetNextItemWidth(_inputIntSize);
var cap = ImGui.GetIO().KeyCtrl ? byte.MaxValue + 1 : _currentCount;
if (ImGui.InputInt("##text", ref tmp, 1, 1))
{
var newValue = (CustomizeValue)(ImGui.GetIO().KeyCtrl
? Math.Clamp(tmp, 1, byte.MaxValue + 1)
: Math.Clamp(tmp, 1, _currentCount));
UpdateValue(newValue - 1);
var newValue = Math.Clamp(tmp, 1, cap);
UpdateValue((CustomizeValue)(newValue - 1));
}
ImGuiUtil.HoverTooltip($"Input Range: [1, {_currentCount}]\n"
@ -154,28 +175,29 @@ public partial class CustomizationDrawer
private void ListCombo1()
{
ImGui.SetNextItemWidth(_comboSelectorSize * ImGui.GetIO().FontGlobalScale);
var current = _currentByte.Value;
using var combo = ImRaii.Combo("##combo", $"{_currentOption} #{current}");
if (!combo)
return;
for (var i = 1; i <= _currentCount; ++i)
var current = (int)_currentByte.Value;
using (var combo = ImRaii.Combo("##combo", $"{_currentOption} #{current}"))
{
if (ImGui.Selectable($"{_currentOption} #{i}##combo", i == current))
UpdateValue((CustomizeValue)i);
if (combo)
for (var i = 1; i <= _currentCount; ++i)
{
if (ImGui.Selectable($"{_currentOption} #{i}##combo", i == current))
UpdateValue((CustomizeValue)i);
}
}
if (CaptureMouseWheel(ref current, 1, _currentCount))
UpdateValue((CustomizeValue)current);
}
private void ListInputInt1()
{
var tmp = (int)_currentByte.Value;
ImGui.SetNextItemWidth(_inputIntSize);
var (offset, cap) = ImGui.GetIO().KeyCtrl ? (0, byte.MaxValue) : (1, _currentCount);
if (ImGui.InputInt("##text", ref tmp, 1, 1))
{
var newValue = (CustomizeValue)(ImGui.GetIO().KeyCtrl
? Math.Clamp(tmp, 0, byte.MaxValue)
: Math.Clamp(tmp, 1, _currentCount));
var newValue = (CustomizeValue)Math.Clamp(tmp, offset, cap);
UpdateValue(newValue);
}
@ -183,6 +205,26 @@ public partial class CustomizationDrawer
+ "Hold Control to force updates with invalid/unknown options at your own risk.");
}
private static bool CaptureMouseWheel(ref int value, int offset, int cap)
{
if (!ImGui.IsItemHovered() || !ImGui.GetIO().KeyCtrl)
return false;
ImGuiInternal.ItemSetUsingMouseWheel();
var mw = (int)ImGui.GetIO().MouseWheel;
if (mw == 0)
return false;
value -= offset;
value = mw switch
{
< 0 => offset + (value + cap + mw) % cap,
_ => offset + (value + mw) % cap,
};
return true;
}
// Draw a customize checkbox.
private void DrawCheckbox(CustomizeIndex idx)
{

View file

@ -1,7 +1,6 @@
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Glamourer.Automation;
using Glamourer.GameData;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Services;
@ -25,7 +24,7 @@ public abstract class DesignComboBase : FilterComboCache<Tuple<Design, string>>,
protected DesignComboBase(Func<IReadOnlyList<Tuple<Design, string>>> generator, Logger log, DesignChanged designChanged,
TabSelected tabSelected, EphemeralConfig config, DesignColors designColors)
: base(generator, log)
: base(generator, MouseWheelType.Unmodified, log)
{
_designChanged = designChanged;
TabSelected = tabSelected;
@ -38,7 +37,10 @@ public abstract class DesignComboBase : FilterComboCache<Tuple<Design, string>>,
=> _config.IncognitoMode;
void IDisposable.Dispose()
=> _designChanged.Unsubscribe(OnDesignChange);
{
_designChanged.Unsubscribe(OnDesignChange);
GC.SuppressFinalize(this);
}
protected override bool DrawSelectable(int globalIdx, bool selected)
{
@ -118,63 +120,87 @@ public abstract class DesignComboBase : FilterComboCache<Tuple<Design, string>>,
{
case DesignChanged.Type.Created:
case DesignChanged.Type.Renamed:
Cleanup();
break;
case DesignChanged.Type.ChangedColor:
case DesignChanged.Type.Deleted:
Cleanup();
if (CurrentSelection?.Item1 == design)
case DesignChanged.Type.QuickDesignBar:
var priorState = IsInitialized;
if (priorState)
Cleanup();
CurrentSelectionIdx = Items.IndexOf(s => ReferenceEquals(s.Item1, CurrentSelection?.Item1));
if (CurrentSelectionIdx >= 0)
{
CurrentSelectionIdx = Items.Count > 0 ? 0 : -1;
CurrentSelection = Items[CurrentSelectionIdx];
CurrentSelection = Items[CurrentSelectionIdx];
}
else if (Items.Count > 0)
{
CurrentSelectionIdx = 0;
CurrentSelection = Items[0];
}
else
{
CurrentSelection = null;
}
if (!priorState)
Cleanup();
break;
}
}
}
public sealed class DesignCombo : DesignComboBase
public abstract class DesignCombo : DesignComboBase
{
private readonly DesignManager _manager;
public DesignCombo(DesignManager designs, DesignFileSystem fileSystem, Logger log, DesignChanged designChanged, TabSelected tabSelected,
EphemeralConfig config, DesignColors designColors)
: base(() => designs.Designs
.Select(d => new Tuple<Design, string>(d, fileSystem.FindLeaf(d, out var l) ? l.FullName() : string.Empty))
.OrderBy(d => d.Item2)
.ToList(), log, designChanged, tabSelected, config, designColors)
protected DesignCombo(Logger log, DesignChanged designChanged, TabSelected tabSelected,
EphemeralConfig config, DesignColors designColors, Func<IReadOnlyList<Tuple<Design, string>>> generator)
: base(generator, log, designChanged, tabSelected, config, designColors)
{
_manager = designs;
if (designs.Designs.Count == 0)
if (Items.Count == 0)
return;
CurrentSelection = Items[0];
CurrentSelectionIdx = 0;
base.Cleanup();
}
public Design? Design
=> CurrentSelection?.Item1;
public void Draw(float width)
{
Draw(Design, (Incognito ? Design?.Incognito : Design?.Name.Text) ?? string.Empty, width);
if (ImGui.IsItemHovered() && _manager.Designs.Count > 1)
{
var mouseWheel = -(int)ImGui.GetIO().MouseWheel % _manager.Designs.Count;
CurrentSelectionIdx = mouseWheel switch
{
< 0 when CurrentSelectionIdx < 0 => _manager.Designs.Count - 1 + mouseWheel,
< 0 => (CurrentSelectionIdx + _manager.Designs.Count + mouseWheel) % _manager.Designs.Count,
> 0 when CurrentSelectionIdx < 0 => mouseWheel,
> 0 => (CurrentSelectionIdx + mouseWheel) % _manager.Designs.Count,
_ => CurrentSelectionIdx,
};
CurrentSelection = Items[CurrentSelectionIdx];
}
}
=> Draw(Design, (Incognito ? Design?.Incognito : Design?.Name.Text) ?? string.Empty, width);
}
public sealed class RevertDesignCombo : DesignComboBase, IDisposable
public sealed class QuickDesignCombo(
DesignManager designs,
DesignFileSystem fileSystem,
Logger log,
DesignChanged designChanged,
TabSelected tabSelected,
EphemeralConfig config,
DesignColors designColors)
: DesignCombo(log, designChanged, tabSelected, config, designColors, () =>
[
.. designs.Designs
.Where(d => d.QuickDesign)
.Select(d => new Tuple<Design, string>(d, fileSystem.FindLeaf(d, out var l) ? l.FullName() : string.Empty))
.OrderBy(d => d.Item2),
]);
public sealed class LinkDesignCombo(
DesignManager designs,
DesignFileSystem fileSystem,
Logger log,
DesignChanged designChanged,
TabSelected tabSelected,
EphemeralConfig config,
DesignColors designColors)
: DesignCombo(log, designChanged, tabSelected, config, designColors, () =>
[
.. designs.Designs
.Select(d => new Tuple<Design, string>(d, fileSystem.FindLeaf(d, out var l) ? l.FullName() : string.Empty))
.OrderBy(d => d.Item2),
]);
public sealed class RevertDesignCombo : DesignComboBase
{
public const int RevertDesignIndex = -1228;
public readonly Design RevertDesign;

View file

@ -24,7 +24,7 @@ public sealed class DesignQuickBar : Window, IDisposable
: ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoFocusOnAppearing;
private readonly Configuration _config;
private readonly DesignCombo _designCombo;
private readonly QuickDesignCombo _designCombo;
private readonly StateManager _stateManager;
private readonly AutoDesignApplier _autoDesignApplier;
private readonly ObjectManager _objects;
@ -34,7 +34,7 @@ public sealed class DesignQuickBar : Window, IDisposable
private DateTime _keyboardToggle = DateTime.UnixEpoch;
private int _numButtons;
public DesignQuickBar(Configuration config, DesignCombo designCombo, StateManager stateManager, IKeyState keyState,
public DesignQuickBar(Configuration config, QuickDesignCombo designCombo, StateManager stateManager, IKeyState keyState,
ObjectManager objects, AutoDesignApplier autoDesignApplier)
: base("Glamourer Quick Bar", ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDocking)
{
@ -299,7 +299,8 @@ public sealed class DesignQuickBar : Window, IDisposable
(true, false) => 3,
(false, false) => 2,
};
Size = new Vector2((7 + _numButtons) * ImGui.GetFrameHeight() + _numButtons * ImGui.GetStyle().ItemInnerSpacing.X, ImGui.GetFrameHeight());
Size = new Vector2((7 + _numButtons) * ImGui.GetFrameHeight() + _numButtons * ImGui.GetStyle().ItemInnerSpacing.X,
ImGui.GetFrameHeight());
return Size.Value.X;
}
}

View file

@ -10,7 +10,7 @@ using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Equipment;
public sealed class GlamourerColorCombo(float _comboWidth, DictStain _stains, FavoriteManager _favorites)
: FilterComboColors(_comboWidth, CreateFunc(_stains, _favorites), Glamourer.Log)
: FilterComboColors(_comboWidth, MouseWheelType.Control, CreateFunc(_stains, _favorites), Glamourer.Log)
{
protected override bool DrawSelectable(int globalIdx, bool selected)
{
@ -36,6 +36,9 @@ public sealed class GlamourerColorCombo(float _comboWidth, DictStain _stains, Fa
return base.DrawSelectable(globalIdx, selected);
}
public override bool Draw(string label, uint color, string name, bool found, bool gloss, float previewWidth)
=> base.Draw(label, color, name, found, gloss, previewWidth);
private static Func<IReadOnlyList<KeyValuePair<byte, (string Name, uint Color, bool Gloss)>>> CreateFunc(DictStain stains,
FavoriteManager favorites)
=> () => stains.Select(kvp => (kvp, favorites.Contains(kvp.Key))).OrderBy(p => !p.Item2).Select(p => p.kvp)

View file

@ -24,7 +24,7 @@ public sealed class ItemCombo : FilterComboCache<EquipItem>
public Variant CustomVariant { get; private set; }
public ItemCombo(IDataManager gameData, ItemManager items, EquipSlot slot, Logger log, FavoriteManager favorites)
: base(() => GetItems(favorites, items, slot), log)
: base(() => GetItems(favorites, items, slot), MouseWheelType.Control, log)
{
_favorites = favorites;
Label = GetLabel(gameData, slot);

View file

@ -17,7 +17,7 @@ public sealed class WeaponCombo : FilterComboCache<EquipItem>
private float _innerWidth;
public WeaponCombo(ItemManager items, FullEquipType type, Logger log)
: base(() => GetWeapons(items, type), log)
: base(() => GetWeapons(items, type), MouseWheelType.Control, log)
{
Label = GetLabel(type);
SearchByParts = true;

View file

@ -0,0 +1,166 @@
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
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, DesignManager _designManager) : IService
{
private static readonly IReadOnlyList<MaterialValueIndex.DrawObjectType> Types =
[
MaterialValueIndex.DrawObjectType.Human,
MaterialValueIndex.DrawObjectType.Mainhand,
MaterialValueIndex.DrawObjectType.Offhand,
];
private ActorState? _state;
public void DrawActorPanel(Actor actor)
{
if (!actor.IsCharacter || !_stateManager.GetOrCreate(actor, out _state))
return;
var model = actor.Model;
if (!model.IsHuman)
return;
if (model.AsCharacterBase->SlotCount < 10)
return;
// Humans should have at least 10 slots for the equipment types. Technically more.
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));
}
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;
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, CharacterWeapon drawData, MaterialValueIndex sourceIndex)
{
for (byte i = 0; i < MtrlFile.ColorTable.NumRows; ++i)
{
var index = sourceIndex with { RowIndex = i };
ref var row = ref table[i];
DrawRow(ref row, drawData, index);
}
}
private void DrawRow(ref MtrlFile.ColorTable.Row row, CharacterWeapon drawData, MaterialValueIndex index)
{
using var id = ImRaii.PushId(index.RowIndex);
var changed = _state!.Materials.TryGetValue(index, out var value);
if (!changed)
{
var internalRow = new ColorRow(row);
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.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale);
applied |= ImGui.DragFloat("Gloss", ref value.Model.GlossStrength, 0.1f);
ImGui.SameLine();
ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale);
applied |= ImGui.DragFloat("Specular Strength", ref value.Model.SpecularStrength, 0.1f);
if (applied)
_stateManager.ChangeMaterialValue(_state!, index, value, ApplySettings.Manual);
if (changed)
{
ImGui.SameLine();
using (ImRaii.PushFont(UiBuilder.IconFont))
{
using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.FavoriteStarOn.Value());
ImGui.TextUnformatted(FontAwesomeIcon.UserEdit.ToIconString());
}
}
}
private static readonly IReadOnlyList<string> 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<string> 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",
];
}

View file

@ -7,7 +7,9 @@ using Glamourer.Automation;
using Glamourer.Designs;
using Glamourer.Gui.Customization;
using Glamourer.Gui.Equipment;
using Glamourer.Gui.Materials;
using Glamourer.Interop;
using Glamourer.Interop.Material;
using Glamourer.Interop.Structs;
using Glamourer.State;
using ImGuiNET;
@ -33,7 +35,8 @@ public class ActorPanel(
ImportService _importService,
ICondition _conditions,
DictModelChara _modelChara,
CustomizeParameterDrawer _parameterDrawer)
CustomizeParameterDrawer _parameterDrawer,
MaterialDrawer _materialDrawer)
{
private ActorIdentifier _identifier;
private string _actorName = string.Empty;
@ -114,6 +117,9 @@ public class ActorPanel(
RevertButtons();
// TODO Materials
//if (ImGui.CollapsingHeader("Material Shit"))
// _materialDrawer.DrawActorPanel(_actor);
using var disabled = ImRaii.Disabled(transformationId != 0);
if (_state.ModelData.IsHuman)
DrawHumanPanel();
@ -316,8 +322,7 @@ public class ActorPanel(
{
ImGui.OpenPopup("Save as Design");
_newName = _state!.Identifier.ToName();
var (applyGear, applyCustomize, applyCrest, applyParameters) = UiHelpers.ConvertKeysToFlags();
_newDesign = _converter.Convert(_state, applyGear, applyCustomize, applyCrest, applyParameters);
_newDesign = _converter.Convert(_state, ApplicationRules.FromModifiers(_state));
}
private void SaveDesignDrawPopup()
@ -352,8 +357,7 @@ public class ActorPanel(
{
try
{
var (applyGear, applyCustomize, applyCrest, applyParameters) = UiHelpers.ConvertKeysToFlags();
var text = _converter.ShareBase64(_state!, applyGear, applyCustomize, applyCrest, applyParameters);
var text = _converter.ShareBase64(_state!, ApplicationRules.FromModifiers(_state!));
ImGui.SetClipboardText(text);
}
catch (Exception ex)
@ -392,9 +396,8 @@ public class ActorPanel(
!data.Valid || id == _identifier || _state!.ModelData.ModelId != 0))
return;
var (applyGear, applyCustomize, applyCrest, applyParameters) = UiHelpers.ConvertKeysToFlags();
if (_stateManager.GetOrCreate(id, data.Objects[0], out var state))
_stateManager.ApplyDesign(state, _converter.Convert(_state!, applyGear, applyCustomize, applyCrest, applyParameters),
_stateManager.ApplyDesign(state, _converter.Convert(_state!, ApplicationRules.FromModifiers(_state!)),
ApplySettings.Manual);
}
@ -410,9 +413,8 @@ public class ActorPanel(
!data.Valid || id == _identifier || _state!.ModelData.ModelId != 0))
return;
var (applyGear, applyCustomize, applyCrest, applyParameters) = UiHelpers.ConvertKeysToFlags();
if (_stateManager.GetOrCreate(id, data.Objects[0], out var state))
_stateManager.ApplyDesign(state, _converter.Convert(_state!, applyGear, applyCustomize, applyCrest, applyParameters),
_stateManager.ApplyDesign(state, _converter.Convert(_state!, ApplicationRules.FromModifiers(_state!)),
ApplySettings.Manual);
}
}

View file

@ -16,7 +16,7 @@ public sealed class HumanNpcCombo(
DictBNpc bNpcs,
HumanModelList humans,
Logger log)
: FilterComboCache<(string Name, ObjectKind Kind, uint[] Ids)>(() => CreateList(modelCharaDict, bNpcNames, bNpcs, humans), log)
: FilterComboCache<(string Name, ObjectKind Kind, uint[] Ids)>(() => CreateList(modelCharaDict, bNpcNames, bNpcs, humans), MouseWheelType.None, log)
{
protected override string ToString((string Name, ObjectKind Kind, uint[] Ids) obj)
=> obj.Name;

View file

@ -429,7 +429,7 @@ public class SetPanel(
}
private sealed class JobGroupCombo(AutoDesignManager manager, JobService jobs, Logger log)
: FilterComboCache<JobGroup>(() => jobs.JobGroups.Values.ToList(), log)
: FilterComboCache<JobGroup>(() => jobs.JobGroups.Values.ToList(), MouseWheelType.None, log)
{
public void Draw(AutoDesignSet set, AutoDesign design, int autoDesignIndex)
{

View file

@ -1,7 +1,6 @@
using Dalamud.Interface;
using Glamourer.GameData;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Interop;
using Glamourer.Interop.Structs;
using Glamourer.State;

View file

@ -37,4 +37,4 @@ public class DatFilePanel(ImportService _importService) : IGameDataDrawer
ImGui.TextUnformatted(_datFile.Value.Description);
}
}
}
}

View file

@ -36,7 +36,8 @@ public class DebugTabHeader(string label, params IGameDataDrawer[] subTrees)
provider.GetRequiredService<ObjectManagerPanel>(),
provider.GetRequiredService<PenumbraPanel>(),
provider.GetRequiredService<IpcTesterPanel>(),
provider.GetRequiredService<DatFilePanel>()
provider.GetRequiredService<DatFilePanel>(),
provider.GetRequiredService<GlamourPlatePanel>()
);
public static DebugTabHeader CreateGameData(IServiceProvider provider)

View file

@ -0,0 +1,135 @@
using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game;
using Glamourer.Designs;
using Glamourer.Interop;
using Glamourer.Services;
using Glamourer.State;
using ImGuiNET;
using OtterGui;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Gui.Debug;
namespace Glamourer.Gui.Tabs.DebugTab;
public unsafe class GlamourPlatePanel : IGameDataDrawer
{
private readonly DesignManager _design;
private readonly ItemManager _items;
private readonly StateManager _state;
private readonly ObjectManager _objects;
public string Label
=> "Glamour Plates";
public bool Disabled
=> false;
public GlamourPlatePanel(IGameInteropProvider interop, ItemManager items, DesignManager design, StateManager state, ObjectManager objects)
{
_items = items;
_design = design;
_state = state;
_objects = objects;
interop.InitializeFromAttributes(this);
}
public void Draw()
{
var manager = MirageManager.Instance();
using (ImRaii.Group())
{
ImGui.TextUnformatted("Address:");
ImGui.TextUnformatted("Number of Glamour Plates:");
ImGui.TextUnformatted("Glamour Plates Requested:");
ImGui.TextUnformatted("Glamour Plates Loaded:");
ImGui.TextUnformatted("Is Applying Glamour Plates:");
}
ImGui.SameLine();
using (ImRaii.Group())
{
ImGuiUtil.CopyOnClickSelectable($"0x{(ulong)manager:X}");
ImGui.TextUnformatted(manager == null ? "-" : manager->GlamourPlatesSpan.Length.ToString());
ImGui.TextUnformatted(manager == null ? "-" : manager->GlamourPlatesRequested.ToString());
ImGui.SameLine();
if (ImGui.SmallButton("Request Update"))
RequestGlamour();
ImGui.TextUnformatted(manager == null ? "-" : manager->GlamourPlatesLoaded.ToString());
ImGui.TextUnformatted(manager == null ? "-" : manager->IsApplyingGlamourPlate.ToString());
}
if (manager == null)
return;
ActorState? state = null;
var (identifier, data) = _objects.PlayerData;
var enabled = data.Valid && _state.GetOrCreate(identifier, data.Objects[0], out state);
for (var i = 0; i < manager->GlamourPlatesSpan.Length; ++i)
{
using var tree = ImRaii.TreeNode($"Plate #{i + 1:D2}");
if (!tree)
continue;
ref var plate = ref manager->GlamourPlatesSpan[i];
if (ImGuiUtil.DrawDisabledButton("Apply to Player", Vector2.Zero, string.Empty, !enabled))
{
var design = CreateDesign(plate);
_state.ApplyDesign(state!, design, ApplySettings.Manual);
}
using (ImRaii.Group())
{
foreach (var slot in EquipSlotExtensions.FullSlots)
ImGui.TextUnformatted(slot.ToName());
}
ImGui.SameLine();
using (ImRaii.Group())
{
foreach (var (_, index) in EquipSlotExtensions.FullSlots.WithIndex())
ImGui.TextUnformatted($"{plate.ItemIds[index]:D6}, {plate.StainIds[index]:D3}");
}
}
}
[Signature("E8 ?? ?? ?? ?? 32 C0 48 8B 5C 24 ?? 48 8B 6C 24 ?? 48 83 C4 ?? 5F")]
private readonly delegate* unmanaged<MirageManager*, void> _requestUpdate = null!;
public void RequestGlamour()
{
var manager = MirageManager.Instance();
if (manager == null)
return;
_requestUpdate(manager);
}
public DesignBase CreateDesign(in MirageManager.GlamourPlate plate)
{
var design = _design.CreateTemporary();
design.ApplyCustomize = 0;
design.ApplyCrest = 0;
design.ApplyMeta = 0;
design.ApplyParameters = 0;
design.ApplyEquip = 0;
foreach (var (slot, index) in EquipSlotExtensions.FullSlots.WithIndex())
{
var itemId = plate.ItemIds[index];
if (itemId == 0)
continue;
var item = _items.Resolve(slot, itemId);
if (!item.Valid)
continue;
design.GetDesignDataRef().SetItem(slot, item);
design.GetDesignDataRef().SetStain(slot, plate.StainIds[index]);
design.ApplyEquip |= slot.ToBothFlags();
}
return design;
}
}

View file

@ -28,7 +28,9 @@ public class IpcTesterPanel(DalamudPluginInterface _pluginInterface, ObjectManag
private string _base64Apply = string.Empty;
private string _designIdentifier = string.Empty;
private GlamourerIpc.GlamourerErrorCode _setItemEc;
private GlamourerIpc.GlamourerErrorCode _setItemOnceEc;
private GlamourerIpc.GlamourerErrorCode _setItemByActorNameEc;
private GlamourerIpc.GlamourerErrorCode _setItemOnceByActorNameEc;
public unsafe void Draw()
{
@ -77,12 +79,23 @@ public class IpcTesterPanel(DalamudPluginInterface _pluginInterface, ObjectManag
if (ImGui.Button("Apply##AllName"))
GlamourerIpc.ApplyAllSubscriber(_pluginInterface).Invoke(_base64Apply, _gameObjectName);
ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyAllOnce);
ImGui.TableNextColumn();
if (ImGui.Button("Apply Once##AllName"))
GlamourerIpc.ApplyAllOnceSubscriber(_pluginInterface).Invoke(_base64Apply, _gameObjectName);
ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyAllToCharacter);
ImGui.TableNextColumn();
if (ImGui.Button("Apply##AllCharacter"))
GlamourerIpc.ApplyAllToCharacterSubscriber(_pluginInterface)
.Invoke(_base64Apply, _objectManager.Objects[_gameObjectIndex] as Character);
ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyAllOnceToCharacter);
ImGui.TableNextColumn();
if (ImGui.Button("Apply Once##AllCharacter"))
GlamourerIpc.ApplyAllOnceToCharacterSubscriber(_pluginInterface)
.Invoke(_base64Apply, _objectManager.Objects[_gameObjectIndex] as Character);
ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyOnlyEquipment);
ImGui.TableNextColumn();
if (ImGui.Button("Apply##EquipName"))
@ -111,12 +124,23 @@ public class IpcTesterPanel(DalamudPluginInterface _pluginInterface, ObjectManag
if (ImGui.Button("Apply##ByGuidName") && Guid.TryParse(_designIdentifier, out var guid1))
GlamourerIpc.ApplyByGuidSubscriber(_pluginInterface).Invoke(guid1, _gameObjectName);
ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyByGuidOnce);
ImGui.TableNextColumn();
if (ImGui.Button("Apply Once##ByGuidName") && Guid.TryParse(_designIdentifier, out var guid1Once))
GlamourerIpc.ApplyByGuidOnceSubscriber(_pluginInterface).Invoke(guid1Once, _gameObjectName);
ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyByGuidToCharacter);
ImGui.TableNextColumn();
if (ImGui.Button("Apply##ByGuidCharacter") && Guid.TryParse(_designIdentifier, out var guid2))
GlamourerIpc.ApplyByGuidToCharacterSubscriber(_pluginInterface)
.Invoke(guid2, _objectManager.Objects[_gameObjectIndex] as Character);
ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyByGuidOnceToCharacter);
ImGui.TableNextColumn();
if (ImGui.Button("Apply Once##ByGuidCharacter") && Guid.TryParse(_designIdentifier, out var guid2Once))
GlamourerIpc.ApplyByGuidOnceToCharacterSubscriber(_pluginInterface)
.Invoke(guid2Once, _objectManager.Objects[_gameObjectIndex] as Character);
ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelUnlock);
ImGui.TableNextColumn();
@ -149,6 +173,17 @@ public class IpcTesterPanel(DalamudPluginInterface _pluginInterface, ObjectManag
ImGui.TextUnformatted(_setItemEc.ToString());
}
ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelSetItemOnce);
ImGui.TableNextColumn();
if (ImGui.Button("Set Once##SetItem"))
_setItemOnceEc = (GlamourerIpc.GlamourerErrorCode)GlamourerIpc.SetItemOnceSubscriber(_pluginInterface)
.Invoke(_objectManager.Objects[_gameObjectIndex] as Character, (byte)_slot, _customItemId.Id, _stainId.Id, 1337);
if (_setItemOnceEc != GlamourerIpc.GlamourerErrorCode.Success)
{
ImGui.SameLine();
ImGui.TextUnformatted(_setItemOnceEc.ToString());
}
ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelSetItemByActorName);
ImGui.TableNextColumn();
if (ImGui.Button("Set##SetItemByActorName"))
@ -159,6 +194,17 @@ public class IpcTesterPanel(DalamudPluginInterface _pluginInterface, ObjectManag
ImGui.SameLine();
ImGui.TextUnformatted(_setItemByActorNameEc.ToString());
}
ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelSetItemOnceByActorName);
ImGui.TableNextColumn();
if (ImGui.Button("Set Once##SetItemByActorName"))
_setItemOnceByActorNameEc = (GlamourerIpc.GlamourerErrorCode)GlamourerIpc.SetItemOnceByActorNameSubscriber(_pluginInterface)
.Invoke(_gameObjectName, (byte)_slot, _customItemId.Id, _stainId.Id, 1337);
if (_setItemOnceByActorNameEc != GlamourerIpc.GlamourerErrorCode.Success)
{
ImGui.SameLine();
ImGui.TextUnformatted(_setItemOnceByActorNameEc.ToString());
}
}
private void DrawItemInput()

View file

@ -10,8 +10,15 @@ public sealed class DesignColorCombo(DesignColors _designColors, bool _skipAutom
FilterComboCache<string>(_skipAutomatic
? _designColors.Keys.OrderBy(k => k)
: _designColors.Keys.OrderBy(k => k).Prepend(DesignColors.AutomaticName),
Glamourer.Log)
MouseWheelType.Control, Glamourer.Log)
{
protected override void OnMouseWheel(string preview, ref int current, int steps)
{
if (CurrentSelectionIdx < 0)
CurrentSelectionIdx = Items.IndexOf(preview);
base.OnMouseWheel(preview, ref current, steps);
}
protected override bool DrawSelectable(int globalIdx, bool selected)
{
var isAutomatic = !_skipAutomatic && globalIdx == 0;

View file

@ -95,9 +95,13 @@ public class DesignDetailTab
Glamourer.Messager.NotificationMessage(ex, $"Could not open file {fileName}.", $"Could not open file {fileName}",
NotificationType.Warning);
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
ImGui.SetClipboardText(identifier);
}
ImGuiUtil.HoverTooltip($"Open the file\n\t{fileName}\ncontaining this design in the .json-editor of your choice.");
ImGuiUtil.HoverTooltip(
$"Open the file\n\t{fileName}\ncontaining this design in the .json-editor of your choice.\n\nRight-Click to copy identifier to clipboard.");
ImGuiUtil.DrawFrameColumn("Full Selector Path");
ImGui.TableNextColumn();
@ -121,19 +125,33 @@ public class DesignDetailTab
Glamourer.Messager.NotificationMessage(ex, ex.Message, "Could not rename or move design", NotificationType.Error);
}
ImGuiUtil.DrawFrameColumn("Quick Design Bar");
ImGui.TableNextColumn();
if (ImGui.RadioButton("Display##qdb", _selector.Selected.QuickDesign))
_manager.SetQuickDesign(_selector.Selected!, true);
var hovered = ImGui.IsItemHovered();
ImGui.SameLine();
if (ImGui.RadioButton("Hide##qdb", !_selector.Selected.QuickDesign))
_manager.SetQuickDesign(_selector.Selected!, false);
if (hovered || ImGui.IsItemHovered())
ImGui.SetTooltip("Display or hide this design in your quick design bar.");
ImGuiUtil.DrawFrameColumn("Color");
var colorName = _selector.Selected!.Color.Length == 0 ? DesignColors.AutomaticName : _selector.Selected!.Color;
ImGui.TableNextColumn();
if (_colorCombo.Draw("##colorCombo", colorName, "Associate a color with this design. Right-Click to revert to automatic coloring.",
if (_colorCombo.Draw("##colorCombo", colorName, "Associate a color with this design.\n"
+ "Right-Click to revert to automatic coloring.\n"
+ "Hold Control and scroll the mousewheel to scroll.",
width.X - ImGui.GetStyle().ItemSpacing.X - ImGui.GetFrameHeight(), ImGui.GetTextLineHeight())
&& _colorCombo.CurrentSelection != null)
{
colorName = _colorCombo.CurrentSelection is DesignColors.AutomaticName ? string.Empty : _colorCombo.CurrentSelection;
_manager.ChangeColor(_selector.Selected!, colorName);
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
_manager.ChangeColor(_selector.Selected!, string.Empty);
if (_colors.TryGetValue(_selector.Selected!.Color, out var currentColor))
{
ImGui.SameLine();

View file

@ -10,7 +10,7 @@ using OtterGui.Services;
namespace Glamourer.Gui.Tabs.DesignTab;
public class DesignLinkDrawer(DesignLinkManager _linkManager, DesignFileSystemSelector _selector, DesignCombo _combo) : IUiService
public class DesignLinkDrawer(DesignLinkManager _linkManager, DesignFileSystemSelector _selector, LinkDesignCombo _combo) : IUiService
{
private int _dragDropIndex = -1;
private LinkOrder _dragDropOrder = LinkOrder.None;

View file

@ -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");
@ -293,10 +312,10 @@ public class DesignPanel(
var labels = new[]
{
"Apply Wetness",
"Apply Hat Visibility",
"Apply Visor State",
"Apply Weapon Visibility",
"Apply Wetness",
};
foreach (var (index, label) in MetaExtensions.AllRelevant.Zip(labels))
@ -365,6 +384,7 @@ public class DesignPanel(
DrawCustomize();
DrawEquipment();
DrawCustomizeParameters();
//DrawMaterialValues(); TODO Materials
_designDetails.Draw();
DrawApplicationRules();
_modAssociations.Draw();

View file

@ -11,7 +11,7 @@ namespace Glamourer.Gui.Tabs.DesignTab;
public sealed class ModCombo : FilterComboCache<(Mod Mod, ModSettings Settings)>
{
public ModCombo(PenumbraService penumbra, Logger log)
: base(penumbra.GetMods, log)
: base(penumbra.GetMods, MouseWheelType.None, log)
{
SearchByParts = false;
}

View file

@ -3,6 +3,7 @@ using Dalamud.Interface.Utility;
using Glamourer.Designs;
using ImGuiNET;
using OtterGui;
using OtterGui.Filesystem;
using OtterGui.Raii;
namespace Glamourer.Gui.Tabs.DesignTab;
@ -21,6 +22,7 @@ public class MultiDesignPanel(DesignFileSystemSelector _selector, DesignManager
DrawDesignList();
var offset = DrawMultiTagger(width);
DrawMultiColor(width, offset);
DrawMultiQuickDesignBar(offset);
}
private void DrawDesignList()
@ -35,6 +37,8 @@ public class MultiDesignPanel(DesignFileSystemSelector _selector, DesignManager
var sizeMods = availableSizePercent * 35;
var sizeFolders = availableSizePercent * 65;
_numQuickDesignEnabled = 0;
_numDesigns = 0;
using (var table = ImRaii.Table("mods", 3, ImGuiTableFlags.RowBg))
{
if (!table)
@ -61,15 +65,24 @@ public class MultiDesignPanel(DesignFileSystemSelector _selector, DesignManager
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(fullName);
if (path is not DesignFileSystem.Leaf l2)
continue;
++_numDesigns;
if (l2.Value.QuickDesign)
++_numQuickDesignEnabled;
}
}
ImGui.Separator();
}
private string _tag = string.Empty;
private readonly List<Design> _addDesigns = [];
private readonly List<(Design, int)> _removeDesigns = [];
private string _tag = string.Empty;
private int _numQuickDesignEnabled;
private int _numDesigns;
private readonly List<Design> _addDesigns = [];
private readonly List<(Design, int)> _removeDesigns = [];
private float DrawMultiTagger(Vector2 width)
{
@ -110,6 +123,30 @@ public class MultiDesignPanel(DesignFileSystemSelector _selector, DesignManager
return offset;
}
private void DrawMultiQuickDesignBar(float offset)
{
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Multi QDB:");
ImGui.SameLine(offset, ImGui.GetStyle().ItemSpacing.X);
var buttonWidth = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0);
var diff = _numDesigns - _numQuickDesignEnabled;
var tt = diff == 0
? $"All {_numDesigns} selected designs are already displayed in the quick design bar."
: $"Display all {_numDesigns} selected designs in the quick design bar. Changes {diff} designs.";
if (ImGuiUtil.DrawDisabledButton("Display Selected Designs in QDB", buttonWidth, tt, diff == 0))
foreach(var design in _selector.SelectedPaths.OfType<DesignFileSystem.Leaf>())
_editor.SetQuickDesign(design.Value, true);
ImGui.SameLine();
tt = _numQuickDesignEnabled == 0
? $"All {_numDesigns} selected designs are already hidden in the quick design bar."
: $"Hide all {_numDesigns} selected designs in the quick design bar. Changes {_numQuickDesignEnabled} designs.";
if (ImGuiUtil.DrawDisabledButton("Hide Selected Designs in QDB", buttonWidth, tt, _numQuickDesignEnabled == 0))
foreach (var design in _selector.SelectedPaths.OfType<DesignFileSystem.Leaf>())
_editor.SetQuickDesign(design.Value, false);
ImGui.Separator();
}
private void DrawMultiColor(Vector2 width, float offset)
{
ImGui.AlignTextToFramePadding();

View file

@ -4,7 +4,7 @@ using OtterGui.Widgets;
namespace Glamourer.Gui.Tabs;
public class NpcCombo(NpcCustomizeSet npcCustomizeSet)
: FilterComboCache<NpcData>(npcCustomizeSet, Glamourer.Log)
: FilterComboCache<NpcData>(npcCustomizeSet, MouseWheelType.None, Glamourer.Log)
{
protected override string ToString(NpcData obj)
=> obj.Name;

View file

@ -84,9 +84,8 @@ public class NpcPanel(
{
try
{
var (applyGear, applyCustomize, applyCrest, applyParameters) = UiHelpers.ConvertKeysToFlags();
var data = ToDesignData();
var text = _converter.ShareBase64(data, applyGear, applyCustomize, applyCrest, applyParameters);
var text = _converter.ShareBase64(data, new StateMaterialManager(), ApplicationRules.NpcFromModifiers());
ImGui.SetClipboardText(text);
}
catch (Exception ex)
@ -100,11 +99,9 @@ public class NpcPanel(
private void SaveDesignOpen()
{
ImGui.OpenPopup("Save as Design");
_newName = _selector.Selection.Name;
var (applyGear, applyCustomize, applyCrest, applyParameters) = UiHelpers.ConvertKeysToFlags();
_newName = _selector.Selection.Name;
var data = ToDesignData();
_newDesign = _converter.Convert(data, applyGear, applyCustomize, applyCrest, applyParameters);
_newDesign = _converter.Convert(data, new StateMaterialManager(), ApplicationRules.NpcFromModifiers());
}
private void SaveDesignDrawPopup()
@ -198,8 +195,7 @@ public class NpcPanel(
if (_state.GetOrCreate(id, data.Objects[0], out var state))
{
var (applyGear, applyCustomize, _, _) = UiHelpers.ConvertKeysToFlags();
var design = _converter.Convert(ToDesignData(), applyGear, applyCustomize, 0, 0);
var design = _converter.Convert(ToDesignData(), new StateMaterialManager(), ApplicationRules.NpcFromModifiers());
_state.ApplyDesign(state, design, ApplySettings.Manual);
}
}
@ -217,8 +213,7 @@ public class NpcPanel(
if (_state.GetOrCreate(id, data.Objects[0], out var state))
{
var (applyGear, applyCustomize, _, _) = UiHelpers.ConvertKeysToFlags();
var design = _converter.Convert(ToDesignData(), applyGear, applyCustomize, 0, 0);
var design = _converter.Convert(ToDesignData(), new StateMaterialManager(), ApplicationRules.NpcFromModifiers());
_state.ApplyDesign(state, design, ApplySettings.Manual);
}
}
@ -252,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)
{

View file

@ -90,6 +90,11 @@ public class SettingsTab(
"Enable the display and editing of advanced customization options like arbitrary colors.",
config.UseAdvancedParameters, paletteChecker.SetAdvancedParameters);
PaletteImportButton();
Checkbox("Always Apply Associated Mods",
"Whenever a design is applied to a character (including via automation), Glamourer will try to apply its associated mod settings to the collection currently associated with that character, if it is available.\n\n"
+ "Glamourer will NOT revert these applied settings automatically. This may mess up your collection and configuration.\n\n"
+ "If you enable this setting, you are aware that any resulting misconfiguration is your own fault.",
config.AlwaysApplyAssociatedMods, v => config.AlwaysApplyAssociatedMods = v);
ImGui.NewLine();
}

View file

@ -0,0 +1,116 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using Penumbra.GameData.Files;
using Penumbra.String.Functions;
using SharpGen.Runtime;
using Vortice.Direct3D11;
using Vortice.DXGI;
using MapFlags = Vortice.Direct3D11.MapFlags;
namespace Glamourer.Interop.Material;
public static unsafe class DirectXTextureHelper
{
/// <summary> Try to turn a color table GPU-loaded texture (R16G16B16A16Float, 4 Width, 16 Height) into an actual color table. </summary>
/// <param name="texture"> A pointer to the internal texture struct containing the GPU handle. </param>
/// <param name="table"> The returned color table. </param>
/// <returns> Whether the table could be fetched. </returns>
public static bool TryGetColorTable(Texture* texture, out MtrlFile.ColorTable table)
{
if (texture == null)
{
table = default;
return false;
}
try
{
// Create direct x resource and ensure that it is kept alive.
using var tex = new ID3D11Texture2D1((nint)texture->D3D11Texture2D);
tex.AddRef();
table = GetResourceData(tex, CreateStagedClone, GetTextureData);
return true;
}
catch
{
return false;
}
}
/// <summary> Create a staging clone of the existing texture handle for stability reasons. </summary>
private static ID3D11Texture2D1 CreateStagedClone(ID3D11Texture2D1 resource)
{
var desc = resource.Description1 with
{
Usage = ResourceUsage.Staging,
BindFlags = 0,
CPUAccessFlags = CpuAccessFlags.Read,
MiscFlags = 0,
};
return resource.Device.As<ID3D11Device3>().CreateTexture2D1(desc);
}
/// <summary> Turn a mapped texture into a color table. </summary>
private static MtrlFile.ColorTable GetTextureData(ID3D11Texture2D1 resource, MappedSubresource map)
{
var desc = resource.Description1;
if (desc.Format is not Format.R16G16B16A16_Float
|| desc.Width != MaterialService.TextureWidth
|| desc.Height != MaterialService.TextureHeight
|| map.DepthPitch != map.RowPitch * desc.Height)
throw new InvalidDataException("The texture was not a valid color table texture.");
return ReadTexture(map.DataPointer, map.DepthPitch, desc.Height, map.RowPitch);
}
/// <summary> Transform the GPU data into the color table. </summary>
/// <param name="data"> The pointer to the raw texture data. </param>
/// <param name="length"> The size of the raw texture data. </param>
/// <param name="height"> The height of the texture. (Needs to be 16).</param>
/// <param name="pitch"> The stride in the texture data. </param>
/// <returns></returns>
private static MtrlFile.ColorTable ReadTexture(nint data, int length, int height, int pitch)
{
// Check that the data has sufficient dimension and size.
var expectedSize = sizeof(Half) * MaterialService.TextureWidth * height * 4;
if (length < expectedSize || sizeof(MtrlFile.ColorTable) != expectedSize || height != MaterialService.TextureHeight)
return default;
var ret = new MtrlFile.ColorTable();
var target = (byte*)&ret;
// If the stride is the same as in the table, just copy.
if (pitch == MaterialService.TextureWidth)
MemoryUtility.MemCpyUnchecked(target, (void*)data, length);
// Otherwise, adapt the stride.
else
for (var y = 0; y < height; ++y)
{
MemoryUtility.MemCpyUnchecked(target + y * MaterialService.TextureWidth * sizeof(Half) * 4, (byte*)data + y * pitch,
MaterialService.TextureWidth * sizeof(Half) * 4);
}
return ret;
}
/// <summary> Get resources of a texture. </summary>
private static TRet GetResourceData<T, TRet>(T res, Func<T, T> cloneResource, Func<T, MappedSubresource, TRet> getData)
where T : ID3D11Resource
{
using var stagingRes = cloneResource(res);
res.Device.ImmediateContext.CopyResource(stagingRes, res);
stagingRes.Device.ImmediateContext.Map(stagingRes, 0, MapMode.Read, MapFlags.None, out var mapInfo).CheckError();
try
{
return getData(stagingRes, mapInfo);
}
finally
{
stagingRes.Device.ImmediateContext.Unmap(stagingRes, 0);
}
}
}

View file

@ -0,0 +1,199 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using Glamourer.Designs;
using Glamourer.Interop.Penumbra;
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;
public sealed unsafe class MaterialManager : IRequiredService, IDisposable
{
private readonly PrepareColorSet _event;
private readonly StateManager _stateManager;
private readonly PenumbraService _penumbra;
private readonly ActorManager _actors;
private int _lastSlot;
private readonly ThreadLocal<List<MaterialValueIndex>> _deleteList = new(() => []);
public MaterialManager(PrepareColorSet prepareColorSet, StateManager stateManager, ActorManager actors, PenumbraService penumbra)
{
_stateManager = stateManager;
_actors = actors;
_penumbra = penumbra;
_event = prepareColorSet;
// TODO Material
//_event.Subscribe(OnPrepareColorSet, PrepareColorSet.Priority.MaterialManager);
}
public void Dispose()
=> _event.Unsubscribe(OnPrepareColorSet);
private void OnPrepareColorSet(CharacterBase* characterBase, MaterialResourceHandle* material, ref StainId stain, ref nint ret)
{
var actor = _penumbra.GameObjectFromDrawObject(characterBase);
var validType = FindType(characterBase, actor, out var type);
var (slotId, materialId) = FindMaterial(characterBase, material);
if (!validType
|| slotId > 9
|| type is not MaterialValueIndex.DrawObjectType.Human && slotId > 0
|| !actor.Identifier(_actors, out var identifier)
|| !_stateManager.TryGetValue(identifier, out var state))
return;
var min = MaterialValueIndex.Min(type, slotId, materialId);
var max = MaterialValueIndex.Max(type, slotId, materialId);
var values = state.Materials.GetValues(min, max);
if (values.Length == 0)
return;
if (!PrepareColorSet.TryGetColorTable(characterBase, material, stain, out var baseColorSet))
return;
var drawData = type switch
{
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;
}
/// <summary> Update and apply the glamourer state of an actor according to the application sources when updated by the game. </summary>
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);
}
/// <summary>
/// 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.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private (byte SlotId, byte MaterialId) FindMaterial(CharacterBase* characterBase, MaterialResourceHandle* material)
{
for (var i = _lastSlot; i < characterBase->SlotCount; ++i)
{
var idx = MaterialService.MaterialsPerModel * i;
for (var j = 0; j < MaterialService.MaterialsPerModel; ++j)
{
var mat = (nint)characterBase->Materials[idx++];
if (mat != (nint)material)
continue;
_lastSlot = i;
return ((byte)i, (byte)j);
}
}
for (var i = 0; i < _lastSlot; ++i)
{
var idx = MaterialService.MaterialsPerModel * i;
for (var j = 0; j < MaterialService.MaterialsPerModel; ++j)
{
var mat = (nint)characterBase->Materials[idx++];
if (mat != (nint)material)
continue;
_lastSlot = i;
return ((byte)i, (byte)j);
}
}
return (byte.MaxValue, byte.MaxValue);
}
/// <summary> Find the type of the given draw object by checking the actors pointers. </summary>
private static bool FindType(CharacterBase* characterBase, Actor actor, out MaterialValueIndex.DrawObjectType type)
{
type = MaterialValueIndex.DrawObjectType.Human;
if (!actor.Valid)
return false;
if (actor.Model.AsCharacterBase == characterBase)
return true;
if (!actor.AsObject->IsCharacter())
return false;
if (actor.AsCharacter->DrawData.WeaponDataSpan[0].DrawObject == characterBase)
{
type = MaterialValueIndex.DrawObjectType.Mainhand;
return true;
}
if (actor.AsCharacter->DrawData.WeaponDataSpan[1].DrawObject == characterBase)
{
type = MaterialValueIndex.DrawObjectType.Offhand;
return true;
}
return false;
}
/// <summary> We need to get the temporary set, variant and stain that is currently being set if it is available. </summary>
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);
}
/// <summary>
/// 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.
/// </summary>
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]);
}
}

View file

@ -0,0 +1,99 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using Glamourer.Interop.Structs;
using Lumina.Data.Files;
using static Penumbra.GameData.Files.MtrlFile;
using Texture = FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.Texture;
namespace Glamourer.Interop.Material;
public static unsafe class MaterialService
{
public const int TextureWidth = 4;
public const int TextureHeight = ColorTable.NumRows;
public const int MaterialsPerModel = 4;
/// <summary> Generate a color table the way the game does inside the original texture, and release the original. </summary>
/// <param name="original"> The original texture that will be replaced with a new one. </param>
/// <param name="colorTable"> The input color table. </param>
/// <returns> Success or failure. </returns>
public static bool ReplaceColorTable(Texture** original, in ColorTable colorTable)
{
if (original == null)
return false;
var textureSize = stackalloc int[2];
textureSize[0] = TextureWidth;
textureSize[1] = TextureHeight;
using var texture = new SafeTextureHandle(Device.Instance()->CreateTexture2D(textureSize, 1, (uint)TexFile.TextureFormat.R16G16B16A16F,
(uint)(TexFile.Attribute.TextureType2D | TexFile.Attribute.Managed | TexFile.Attribute.Immutable), 7), false);
if (texture.IsInvalid)
return false;
fixed (ColorTable* ptr = &colorTable)
{
if (!texture.Texture->InitializeContents(ptr))
return false;
}
texture.Exchange(ref *(nint*)original);
return true;
}
public static bool GenerateNewColorTable(in ColorTable colorTable, out Texture* texture)
{
var textureSize = stackalloc int[2];
textureSize[0] = TextureWidth;
textureSize[1] = TextureHeight;
texture = Device.Instance()->CreateTexture2D(textureSize, 1, (uint)TexFile.TextureFormat.R16G16B16A16F,
(uint)(TexFile.Attribute.TextureType2D | TexFile.Attribute.Managed | TexFile.Attribute.Immutable), 7);
if (texture == null)
return false;
fixed (ColorTable* ptr = &colorTable)
{
return texture->InitializeContents(ptr);
}
}
/// <summary> Obtain a pointer to the models pointer to a specific color table texture. </summary>
/// <param name="model"></param>
/// <param name="modelSlot"></param>
/// <param name="materialSlot"></param>
/// <returns></returns>
public static Texture** GetColorTableTexture(Model model, int modelSlot, byte materialSlot)
{
if (!model.IsCharacterBase)
return null;
var index = modelSlot * MaterialsPerModel + materialSlot;
if (index < 0 || index >= model.AsCharacterBase->ColorTableTexturesSpan.Length)
return null;
var texture = (Texture**)Unsafe.AsPointer(ref model.AsCharacterBase->ColorTableTexturesSpan[index]);
return texture;
}
/// <summary> Obtain a pointer to the color table of a certain material from a model. </summary>
/// <param name="model"> The draw object. </param>
/// <param name="modelSlot"> The model slot. </param>
/// <param name="materialSlot"> The material slot in the model. </param>
/// <returns> A pointer to the color table or null. </returns>
public static ColorTable* GetMaterialColorTable(Model model, int modelSlot, byte materialSlot)
{
if (!model.IsCharacterBase)
return null;
var index = modelSlot * MaterialsPerModel + materialSlot;
if (index < 0 || index >= model.AsCharacterBase->MaterialsSpan.Length)
return null;
var material = (MaterialResourceHandle*)model.AsCharacterBase->MaterialsSpan[index].Value;
if (material == null || material->ColorTable == null)
return null;
return (ColorTable*)material->ColorTable;
}
}

View file

@ -0,0 +1,162 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.Interop;
using Glamourer.Interop.Structs;
using Newtonsoft.Json;
using Penumbra.GameData.Files;
namespace Glamourer.Interop.Material;
[JsonConverter(typeof(Converter))]
public readonly record struct MaterialValueIndex(
MaterialValueIndex.DrawObjectType DrawObject,
byte SlotIndex,
byte MaterialIndex,
byte RowIndex)
{
public uint Key
=> ToKey(DrawObject, SlotIndex, MaterialIndex, RowIndex);
public bool Valid
=> Validate(DrawObject) && ValidateSlot(SlotIndex) && ValidateMaterial(MaterialIndex) && ValidateRow(RowIndex);
public static bool FromKey(uint key, out MaterialValueIndex index)
{
index = new MaterialValueIndex(key);
return index.Valid;
}
public unsafe bool TryGetModel(Actor actor, out Model model)
{
if (!actor.Valid)
{
model = Model.Null;
return false;
}
model = DrawObject switch
{
DrawObjectType.Human => actor.Model,
DrawObjectType.Mainhand => actor.IsCharacter ? actor.AsCharacter->DrawData.WeaponDataSpan[0].DrawObject : Model.Null,
DrawObjectType.Offhand => actor.IsCharacter ? actor.AsCharacter->DrawData.WeaponDataSpan[1].DrawObject : Model.Null,
_ => Model.Null,
};
return model.IsCharacterBase;
}
public unsafe bool TryGetTextures(Actor actor, out ReadOnlySpan<Pointer<Texture>> textures)
{
if (!TryGetModel(actor, out var model)
|| SlotIndex >= model.AsCharacterBase->SlotCount
|| model.AsCharacterBase->ColorTableTexturesSpan.Length < (SlotIndex + 1) * MaterialService.MaterialsPerModel)
{
textures = [];
return false;
}
textures = model.AsCharacterBase->ColorTableTexturesSpan.Slice(SlotIndex * MaterialService.MaterialsPerModel,
MaterialService.MaterialsPerModel);
return true;
}
public unsafe bool TryGetTexture(Actor actor, out Texture** texture)
{
if (TryGetTextures(actor, out var textures))
return TryGetTexture(textures, out texture);
texture = null;
return false;
}
public unsafe bool TryGetTexture(ReadOnlySpan<Pointer<Texture>> textures, out Texture** texture)
{
if (MaterialIndex >= textures.Length || textures[MaterialIndex].Value == null)
{
texture = null;
return false;
}
fixed (Pointer<Texture>* ptr = textures)
{
texture = (Texture**)ptr + MaterialIndex;
}
return true;
}
public unsafe bool TryGetColorTable(Actor actor, out MtrlFile.ColorTable table)
{
if (TryGetTexture(actor, out var texture))
return TryGetColorTable(texture, out table);
table = default;
return false;
}
public unsafe bool TryGetColorTable(Texture** texture, out MtrlFile.ColorTable table)
=> DirectXTextureHelper.TryGetColorTable(*texture, out table);
public bool TryGetColorRow(Actor actor, out MtrlFile.ColorTable.Row row)
{
if (!TryGetColorTable(actor, out var table))
{
row = default;
return false;
}
row = table[RowIndex];
return true;
}
public static MaterialValueIndex FromKey(uint key)
=> new(key);
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)
=> new(drawObject, slotIndex, materialIndex, rowIndex);
public enum DrawObjectType : byte
{
Human,
Mainhand,
Offhand,
};
public static bool Validate(DrawObjectType type)
=> Enum.IsDefined(type);
public static bool ValidateSlot(byte slotIndex)
=> slotIndex < 10;
public static bool ValidateMaterial(byte materialIndex)
=> materialIndex < MaterialService.MaterialsPerModel;
public static bool ValidateRow(byte rowIndex)
=> rowIndex < MtrlFile.ColorTable.NumRows;
private static uint ToKey(DrawObjectType type, byte slotIndex, byte materialIndex, byte rowIndex)
{
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 >> 24), (byte)(key >> 16), (byte)(key >> 8), (byte)key)
{ }
private class Converter : JsonConverter<MaterialValueIndex>
{
public override void WriteJson(JsonWriter writer, MaterialValueIndex value, JsonSerializer serializer)
=> serializer.Serialize(writer, value.Key);
public override MaterialValueIndex ReadJson(JsonReader reader, Type objectType, MaterialValueIndex existingValue, bool hasExistingValue,
JsonSerializer serializer)
=> FromKey(serializer.Deserialize<uint>(reader), out var value) ? value : throw new Exception($"Invalid material key {value.Key}.");
}
}

View file

@ -0,0 +1,431 @@
global using StateMaterialManager = Glamourer.Interop.Material.MaterialValueManager<Glamourer.Interop.Material.MaterialValueState>;
global using DesignMaterialManager = Glamourer.Interop.Material.MaterialValueManager<Glamourer.Interop.Material.MaterialValueDesign>;
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;
[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<ColorRow>
{
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<float>());
Set(ref existingValue.Diffuse.Y, obj["DiffuseG"]?.Value<float>());
Set(ref existingValue.Diffuse.Z, obj["DiffuseB"]?.Value<float>());
Set(ref existingValue.Specular.X, obj["SpecularR"]?.Value<float>());
Set(ref existingValue.Specular.Y, obj["SpecularG"]?.Value<float>());
Set(ref existingValue.Specular.Z, obj["SpecularB"]?.Value<float>());
Set(ref existingValue.SpecularStrength, obj["SpecularA"]?.Value<float>());
Set(ref existingValue.Emissive.X, obj["EmissiveR"]?.Value<float>());
Set(ref existingValue.Emissive.Y, obj["EmissiveG"]?.Value<float>());
Set(ref existingValue.Emissive.Z, obj["EmissiveB"]?.Value<float>());
Set(ref existingValue.GlossStrength, obj["Gloss"]?.Value<float>());
return existingValue;
static void Set<T>(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<MaterialValueDesign>
{
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<float>());
Set(ref existingValue.Value.Diffuse.Y, obj["DiffuseG"]?.Value<float>());
Set(ref existingValue.Value.Diffuse.Z, obj["DiffuseB"]?.Value<float>());
Set(ref existingValue.Value.Specular.X, obj["SpecularR"]?.Value<float>());
Set(ref existingValue.Value.Specular.Y, obj["SpecularG"]?.Value<float>());
Set(ref existingValue.Value.Specular.Z, obj["SpecularB"]?.Value<float>());
Set(ref existingValue.Value.SpecularStrength, obj["SpecularA"]?.Value<float>());
Set(ref existingValue.Value.Emissive.X, obj["EmissiveR"]?.Value<float>());
Set(ref existingValue.Value.Emissive.Y, obj["EmissiveG"]?.Value<float>());
Set(ref existingValue.Value.Emissive.Z, obj["EmissiveB"]?.Value<float>());
Set(ref existingValue.Value.GlossStrength, obj["Gloss"]?.Value<float>());
existingValue.Enabled = obj["Enabled"]?.Value<bool>() ?? false;
return existingValue;
static void Set<T>(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<T>
{
private readonly List<(uint Key, T Value)> _values = [];
public MaterialValueManager()
{ }
public void Clear()
=> _values.Clear();
public MaterialValueManager<T> Clone()
{
var ret = new MaterialValueManager<T>();
ret._values.AddRange(_values);
return ret;
}
public bool TryGetValue(MaterialValueIndex index, out T value)
{
if (_values.Count == 0)
{
value = default!;
return false;
}
var idx = Search(index.Key);
if (idx >= 0)
{
value = _values[idx].Value;
return true;
}
value = default!;
return false;
}
public bool TryAddValue(MaterialValueIndex index, in T value)
{
var key = index.Key;
var idx = Search(key);
if (idx >= 0)
return false;
_values.Insert(~idx, (key, value));
return true;
}
public bool RemoveValue(MaterialValueIndex index)
{
if (_values.Count == 0)
return false;
var idx = Search(index.Key);
if (idx < 0)
return false;
_values.RemoveAt(idx);
return true;
}
public void AddOrUpdateValue(MaterialValueIndex index, in T value)
{
var key = index.Key;
var idx = Search(key);
if (idx < 0)
_values.Insert(~idx, (key, value));
else
_values[idx] = (key, value);
}
public bool UpdateValue(MaterialValueIndex index, in T value, out T oldValue)
{
if (_values.Count == 0)
{
oldValue = default!;
return false;
}
var key = index.Key;
var idx = Search(key);
if (idx < 0)
{
oldValue = default!;
return false;
}
oldValue = _values[idx].Value;
_values[idx] = (key, value);
return true;
}
public IReadOnlyList<(uint Key, T Value)> Values
=> _values;
public int RemoveValues(MaterialValueIndex min, MaterialValueIndex max)
{
var (minIdx, maxIdx) = MaterialValueManager.GetMinMax<T>(CollectionsMarshal.AsSpan(_values), min.Key, max.Key);
if (minIdx < 0)
return 0;
var count = maxIdx - minIdx;
_values.RemoveRange(minIdx, count);
return count;
}
public ReadOnlySpan<(uint Key, T Value)> GetValues(MaterialValueIndex min, MaterialValueIndex max)
=> MaterialValueManager.Filter<T>(CollectionsMarshal.AsSpan(_values), min, max);
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private int Search(uint key)
=> _values.BinarySearch((key, default!), MaterialValueManager.Comparer<T>.Instance);
}
public static class MaterialValueManager
{
internal class Comparer<T> : IComparer<(uint Key, T Value)>
{
public static readonly Comparer<T> Instance = new();
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
int IComparer<(uint Key, T Value)>.Compare((uint Key, T Value) x, (uint Key, T Value) y)
=> x.Key.CompareTo(y.Key);
}
public static bool GetSpecific<T>(ReadOnlySpan<(uint Key, T Value)> values, MaterialValueIndex index, out T ret)
{
var idx = values.BinarySearch((index.Key, default!), Comparer<T>.Instance);
if (idx < 0)
{
ret = default!;
return false;
}
ret = values[idx].Value;
return true;
}
public static ReadOnlySpan<(uint Key, T Value)> Filter<T>(ReadOnlySpan<(uint Key, T Value)> values, MaterialValueIndex min,
MaterialValueIndex max)
{
var (minIdx, maxIdx) = GetMinMax(values, min.Key, max.Key);
return minIdx < 0 ? [] : values[minIdx..(maxIdx + 1)];
}
/// <summary> Obtain the minimum index and maximum index for a minimum and maximum key. </summary>
internal static (int MinIdx, int MaxIdx) GetMinMax<T>(ReadOnlySpan<(uint Key, T Value)> values, uint minKey, uint maxKey)
{
// Find the minimum index by binary search.
var idx = values.BinarySearch((minKey, default!), Comparer<T>.Instance);
var minIdx = idx;
// If the key does not exist, check if it is an invalid range or set it correctly.
if (minIdx < 0)
{
minIdx = ~minIdx;
if (minIdx == values.Length || values[minIdx].Key > maxKey)
return (-1, -1);
idx = minIdx;
}
else
{
// If it does exist, go upwards until the first key is reached that is actually smaller.
while (minIdx > 0 && values[minIdx - 1].Key >= minKey)
--minIdx;
}
// Check if the range can be valid.
if (values[minIdx].Key < minKey || values[minIdx].Key > maxKey)
return (-1, -1);
// Do pretty much the same but in the other direction with the maximum key.
var maxIdx = values[idx..].BinarySearch((maxKey, default!), Comparer<T>.Instance);
if (maxIdx < 0)
{
maxIdx = ~maxIdx + idx;
return maxIdx > minIdx ? (minIdx, maxIdx - 1) : (-1, -1);
}
maxIdx += idx;
while (maxIdx < values.Length - 1 && values[maxIdx + 1].Key <= maxKey)
++maxIdx;
if (values[maxIdx].Key < minKey || values[maxIdx].Key > maxKey)
return (-1, -1);
return (minIdx, maxIdx);
}
}

View file

@ -0,0 +1,103 @@
using Dalamud.Hooking;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using Glamourer.Interop.Structs;
using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Files;
using Penumbra.GameData.Structs;
namespace Glamourer.Interop.Material;
public sealed unsafe class PrepareColorSet
: EventWrapperPtr12Ref34<CharacterBase, MaterialResourceHandle, StainId, nint, PrepareColorSet.Priority>, IHookService
{
public enum Priority
{
/// <seealso cref="MaterialManager.OnPrepareColorSet"/>
MaterialManager = 0,
}
public PrepareColorSet(HookManager hooks)
: base("Prepare Color Set ")
=> _task = hooks.CreateHook<Delegate>(Name, "40 55 56 41 56 48 83 EC ?? 80 BA", Detour, true);
private readonly Task<Hook<Delegate>> _task;
public nint Address
=> (nint)CharacterBase.MemberFunctionPointers.Destroy;
public void Enable()
=> _task.Result.Enable();
public void Disable()
=> _task.Result.Disable();
public Task Awaiter
=> _task;
public bool Finished
=> _task.IsCompletedSuccessfully;
private delegate Texture* Delegate(CharacterBase* characterBase, MaterialResourceHandle* material, StainId stainId);
private Texture* Detour(CharacterBase* characterBase, MaterialResourceHandle* material, StainId stainId)
{
Glamourer.Log.Excessive($"[{Name}] Triggered with 0x{(nint)characterBase:X} 0x{(nint)material:X} {stainId.Id}.");
var ret = nint.Zero;
Invoke(characterBase, material, ref stainId, ref ret);
if (ret != nint.Zero)
return (Texture*)ret;
return _task.Result.Original(characterBase, material, stainId);
}
public static bool TryGetColorTable(CharacterBase* characterBase, MaterialResourceHandle* material, StainId stainId,
out MtrlFile.ColorTable table)
{
if (material->ColorTable == null)
{
table = default;
return false;
}
var newTable = *(MtrlFile.ColorTable*)material->ColorTable;
if (stainId.Id != 0)
characterBase->ReadStainingTemplate(material, stainId.Id, (Half*)(&newTable));
table = newTable;
return true;
}
/// <summary> Assumes the actor is valid. </summary>
public static bool TryGetColorTable(Actor actor, MaterialValueIndex index, out MtrlFile.ColorTable table)
{
var idx = index.SlotIndex * MaterialService.MaterialsPerModel + index.MaterialIndex;
var model = actor.Model.AsCharacterBase;
var handle = (MaterialResourceHandle*)model->Materials[idx];
if (handle == null)
{
table = default;
return false;
}
return TryGetColorTable(model, handle, GetStain(), out table);
StainId GetStain()
{
switch (index.DrawObject)
{
case MaterialValueIndex.DrawObjectType.Human:
return index.SlotIndex < 10 ? actor.Model.GetArmor(((uint)index.SlotIndex).ToEquipSlot()).Stain : 0;
case MaterialValueIndex.DrawObjectType.Mainhand:
var mainhand = (Model)actor.AsCharacter->DrawData.WeaponDataSpan[1].DrawObject;
return mainhand.IsWeapon ? (StainId)mainhand.AsWeapon->ModelUnknown : 0;
case MaterialValueIndex.DrawObjectType.Offhand:
var offhand = (Model)actor.AsCharacter->DrawData.WeaponDataSpan[1].DrawObject;
return offhand.IsWeapon ? (StainId)offhand.AsWeapon->ModelUnknown : 0;
default: return 0;
}
}
}
}

View file

@ -0,0 +1,49 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
namespace Glamourer.Interop.Material;
public unsafe class SafeTextureHandle : SafeHandle
{
public Texture* Texture
=> (Texture*)handle;
public override bool IsInvalid
=> handle == 0;
public SafeTextureHandle(Texture* handle, bool incRef, bool ownsHandle = true)
: base(0, ownsHandle)
{
if (incRef && !ownsHandle)
throw new ArgumentException("Non-owning SafeTextureHandle with IncRef is unsupported");
if (incRef && handle != null)
handle->IncRef();
SetHandle((nint)handle);
}
public void Exchange(ref nint ppTexture)
{
lock (this)
{
handle = Interlocked.Exchange(ref ppTexture, handle);
}
}
public static SafeTextureHandle CreateInvalid()
=> new(null, false);
protected override bool ReleaseHandle()
{
nint handle;
lock (this)
{
handle = this.handle;
this.handle = 0;
}
if (handle != 0)
((Texture*)handle)->DecRef();
return true;
}
}

View file

@ -0,0 +1,67 @@
using Glamourer.Designs.Links;
using Glamourer.Interop.Structs;
using Glamourer.State;
using OtterGui.Services;
namespace Glamourer.Interop.Penumbra;
public class ModSettingApplier(PenumbraService penumbra, Configuration config, ObjectManager objects) : IService
{
public void HandleStateApplication(ActorState state, MergedDesign design)
{
if (!config.AlwaysApplyAssociatedMods || design.AssociatedMods.Count == 0)
return;
objects.Update();
if (!objects.TryGetValue(state.Identifier, out var data))
{
Glamourer.Log.Verbose(
$"[Mod Applier] No mod settings applied because no actor for {state.Identifier} could be found to associate collection.");
return;
}
var collections = new HashSet<string>();
foreach (var actor in data.Objects)
{
var collection = penumbra.GetActorCollection(actor);
if (collection.Length == 0)
{
Glamourer.Log.Verbose($"[Mod Applier] Could not obtain associated collection for {actor.Utf8Name}.");
continue;
}
if (!collections.Add(collection))
continue;
foreach (var (mod, setting) in design.AssociatedMods)
{
var message = penumbra.SetMod(mod, setting, collection);
if (message.Length > 0)
Glamourer.Log.Verbose($"[Mod Applier] Error applying mod settings: {message}");
else
Glamourer.Log.Verbose($"[Mod Applier] Set mod settings for {mod.DirectoryName} in {collection}.");
}
}
}
public (List<string> Messages, int Applied, string Collection) ApplyModSettings(IReadOnlyDictionary<Mod, ModSettings> settings, Actor actor)
{
var collection = penumbra.GetActorCollection(actor);
if (collection.Length <= 0)
return ([$"Could not obtain associated collection for {actor.Utf8Name}."], 0, string.Empty);
var messages = new List<string>();
var appliedMods = 0;
foreach (var (mod, setting) in settings)
{
var message = penumbra.SetMod(mod, setting, collection);
if (message.Length > 0)
messages.Add($"Error applying mod settings: {message}");
else
++appliedMods;
}
return (messages, appliedMods, collection);
}
}

View file

@ -29,10 +29,11 @@ public class CodeService
World = 0x010000,
Elephants = 0x020000,
Crown = 0x040000,
Dolphins = 0x080000,
}
public const CodeFlag DyeCodes = CodeFlag.Clown | CodeFlag.World | CodeFlag.Elephants;
public const CodeFlag GearCodes = CodeFlag.Emperor | CodeFlag.World | CodeFlag.Elephants;
public const CodeFlag DyeCodes = CodeFlag.Clown | CodeFlag.World | CodeFlag.Elephants | CodeFlag.Dolphins;
public const CodeFlag GearCodes = CodeFlag.Emperor | CodeFlag.World | CodeFlag.Elephants | CodeFlag.Dolphins;
public const CodeFlag RaceCodes = CodeFlag.OopsHyur
| CodeFlag.OopsElezen
@ -114,7 +115,9 @@ public class CodeService
return null;
var badFlags = ~GetMutuallyExclusive(flag);
return v => _enabled = v ? (_enabled | flag) & badFlags : _enabled & ~flag;;
return v => _enabled = v ? (_enabled | flag) & badFlags : _enabled & ~flag;
;
}
public CodeFlag GetCode(string name)
@ -173,6 +176,7 @@ public class CodeService
CodeFlag.World => (DyeCodes | GearCodes) & ~CodeFlag.World,
CodeFlag.Elephants => (DyeCodes | GearCodes) & ~CodeFlag.Elephants,
CodeFlag.Crown => 0,
CodeFlag.Dolphins => (DyeCodes | GearCodes) & ~CodeFlag.Dolphins,
_ => 0,
};
@ -198,6 +202,7 @@ public class CodeService
CodeFlag.World => [ 0xFD, 0xA2, 0xD2, 0xBC, 0xD9, 0x8A, 0x7E, 0x2B, 0x52, 0xCB, 0x57, 0x6E, 0x3A, 0x2E, 0x30, 0xBA, 0x4E, 0xAE, 0x42, 0xEA, 0x5C, 0x57, 0xDF, 0x17, 0x37, 0x3C, 0xCE, 0x17, 0x42, 0x43, 0xAE, 0xD0 ],
CodeFlag.Elephants => [ 0x9F, 0x4C, 0xCF, 0x6D, 0xC4, 0x01, 0x31, 0x46, 0x02, 0x05, 0x31, 0xED, 0xED, 0xB2, 0x66, 0x29, 0x31, 0x09, 0x1E, 0xE7, 0x47, 0xDE, 0x7B, 0x03, 0xB0, 0x3C, 0x06, 0x76, 0x26, 0x91, 0xDF, 0xB2 ],
CodeFlag.Crown => [ 0x43, 0x8E, 0x34, 0x56, 0x24, 0xC9, 0xC6, 0xDE, 0x2A, 0x68, 0x3A, 0x5D, 0xF5, 0x8E, 0xCB, 0xEF, 0x0D, 0x4D, 0x5B, 0xDC, 0x23, 0xF9, 0xF9, 0xBD, 0xD9, 0x60, 0xAD, 0x53, 0xC5, 0xA0, 0x33, 0xC4 ],
CodeFlag.Dolphins => [ 0x64, 0xC6, 0x2E, 0x7C, 0x22, 0x3A, 0x42, 0xF5, 0xC3, 0x93, 0x4F, 0x70, 0x1F, 0xFD, 0xFA, 0x3C, 0x98, 0xD2, 0x7C, 0xD8, 0x88, 0xA7, 0x3D, 0x1D, 0x0D, 0xD6, 0x70, 0x15, 0x28, 0x2E, 0x79, 0xE7 ],
_ => [],
};
}

View file

@ -3,8 +3,6 @@ using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Services;
using Glamourer.Automation;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.GameData;
using Glamourer.Gui;
using Glamourer.Interop;
using Glamourer.Interop.Penumbra;
@ -35,11 +33,11 @@ public class CommandService : IDisposable
private readonly DesignConverter _converter;
private readonly DesignFileSystem _designFileSystem;
private readonly Configuration _config;
private readonly PenumbraService _penumbra;
private readonly ModSettingApplier _modApplier;
public CommandService(ICommandManager commands, MainWindow mainWindow, IChatGui chat, ActorManager actors, ObjectManager objects,
AutoDesignApplier autoDesignApplier, StateManager stateManager, DesignManager designManager, DesignConverter converter,
DesignFileSystem designFileSystem, AutoDesignManager autoDesignManager, Configuration config, PenumbraService penumbra)
DesignFileSystem designFileSystem, AutoDesignManager autoDesignManager, Configuration config, ModSettingApplier modApplier)
{
_commands = commands;
_mainWindow = mainWindow;
@ -53,7 +51,7 @@ public class CommandService : IDisposable
_designFileSystem = designFileSystem;
_autoDesignManager = autoDesignManager;
_config = config;
_penumbra = penumbra;
_modApplier = modApplier;
_commands.AddHandler(MainCommandString, new CommandInfo(OnGlamourer) { HelpMessage = "Open or close the Glamourer window." });
_commands.AddHandler(ApplyCommandString,
@ -442,19 +440,10 @@ public class CommandService : IDisposable
if (!applyMods || design is not Design d)
return;
var collection = _penumbra.GetActorCollection(actor);
if (collection.Length <= 0)
return;
var (messages, appliedMods, collection) = _modApplier.ApplyModSettings(d.AssociatedMods, actor);
var appliedMods = 0;
foreach (var (mod, setting) in d.AssociatedMods)
{
var message = _penumbra.SetMod(mod, setting, collection);
if (message.Length > 0)
Glamourer.Messager.Chat.Print($"Error applying mod settings: {message}");
else
++appliedMods;
}
foreach (var message in messages)
Glamourer.Messager.Chat.Print($"Error applying mod settings: {message}");
if (appliedMods > 0)
Glamourer.Messager.Chat.Print($"Applied {appliedMods} mod settings to {collection}.");
@ -509,7 +498,7 @@ public class CommandService : IDisposable
try
{
var text = _converter.ShareBase64(state);
var text = _converter.ShareBase64(state, ApplicationRules.AllButParameters(state));
ImGui.SetClipboardText(text);
return true;
}
@ -548,8 +537,7 @@ public class CommandService : IDisposable
&& _stateManager.GetOrCreate(identifier, data.Objects[0], out state)))
continue;
var design = _converter.Convert(state, EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant, CrestExtensions.All,
CustomizeParameterExtensions.All);
var design = _converter.Convert(state, ApplicationRules.FromModifiers(state));
_designManager.CreateClone(design, split[0], true);
return true;
}

View file

@ -20,6 +20,7 @@ using Glamourer.Unlocks;
using Microsoft.Extensions.DependencyInjection;
using OtterGui.Classes;
using OtterGui.Log;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Data;
@ -47,7 +48,7 @@ public static class ServiceManagerA
DalamudServices.AddServices(services, pi);
services.AddIServices(typeof(EquipItem).Assembly);
services.AddIServices(typeof(Glamourer).Assembly);
services.AddIServices(typeof(EquipFlag).Assembly);
services.AddIServices(typeof(ImRaii).Assembly);
services.CreateProvider();
return services;
}
@ -145,7 +146,8 @@ public static class ServiceManagerA
.AddSingleton<MultiDesignPanel>()
.AddSingleton<DesignPanel>()
.AddSingleton<DesignTab>()
.AddSingleton<DesignCombo>()
.AddSingleton<QuickDesignCombo>()
.AddSingleton<LinkDesignCombo>()
.AddSingleton<RevertDesignCombo>()
.AddSingleton<ModAssociationsTab>()
.AddSingleton<DesignDetailTab>()

View file

@ -30,6 +30,9 @@ public class ActorState
/// <summary> The territory the draw object was created last. </summary>
public ushort LastTerritory;
/// <summary> State for specific material values. </summary>
public readonly StateMaterialManager Materials = new();
/// <summary> Whether the State is locked at all. </summary>
public bool IsLocked
=> Combination != 0;
@ -84,4 +87,4 @@ public class ActorState
LastTerritory = territory;
return true;
}
}
}

View file

@ -77,7 +77,8 @@ internal class FunEquipSet
new Group(0000, 0, 0137, 2, 0000, 0, 0000, 0, 0000, 0), // Wailing Spirit
new Group(0232, 1, 0232, 1, 0279, 1, 0232, 1, 0232, 1), // Eerie Attire
new Group(0232, 1, 6036, 1, 0279, 1, 0232, 1, 0232, 1), // Vampire
new Group(0505, 6, 0505, 6, 0505, 6, 0505, 6, 0505, 6) // Manusya Casting
new Group(0505, 6, 0505, 6, 0505, 6, 0505, 6, 0505, 6), // Manusya Casting
new Group(6147, 1, 6147, 1, 6147, 1, 6147, 1, 6147, 1) // Tonberry
);
public static readonly FunEquipSet AprilFirst = new
@ -94,7 +95,13 @@ internal class FunEquipSet
new Group(0159, 1, 0000, 0, 0000, 0, 0000, 0, 0000, 0), // Slime Crown
new Group(6117, 1, 6117, 1, 6117, 1, 6117, 1, 6117, 1), // Clown
new Group(6169, 3, 6169, 3, 0279, 1, 6169, 3, 6169, 3), // Chocobo Pajama
new Group(6169, 2, 6169, 2, 0279, 2, 6169, 2, 6169, 2) // Cactuar Pajama
new Group(6169, 2, 6169, 2, 0279, 2, 6169, 2, 6169, 2), // Cactuar Pajama
new Group(6023, 1, 6023, 1, 0000, 0, 0000, 0, 0000, 0), // Swine
new Group(5040, 1, 0000, 0, 0000, 0, 0000, 0, 0000, 0), // Namazu only
new Group(5040, 1, 6023, 1, 0000, 0, 0000, 0, 0000, 0), // Namazu lean
new Group(5040, 1, 6023, 1, 0000, 0, 0000, 0, 0000, 0), // Namazu chonk
new Group(6182, 1, 6182, 1, 0000, 0, 0000, 0, 0000, 0), // Imp
new Group(6147, 1, 6147, 1, 6147, 1, 6147, 1, 6147, 1) // Tonberry
);
private FunEquipSet(params Group[] groups)

View file

@ -116,6 +116,7 @@ public unsafe class FunModule : IDisposable
SetRandomItem(slot, ref armor);
break;
case CodeService.CodeFlag.Elephants:
case CodeService.CodeFlag.Dolphins:
case CodeService.CodeFlag.World when actor.Index != 0:
KeepOldArmor(actor, slot, ref armor);
break;
@ -168,6 +169,10 @@ public unsafe class FunModule : IDisposable
SetElephant(EquipSlot.Body, ref armor[1], stainId);
SetElephant(EquipSlot.Head, ref armor[0], stainId);
break;
case CodeService.CodeFlag.Dolphins:
SetDolphin(EquipSlot.Body, ref armor[1]);
SetDolphin(EquipSlot.Head, ref armor[0]);
break;
case CodeService.CodeFlag.World when actor.Index != 0:
_worldSets.Apply(actor, _rng, armor);
break;
@ -227,6 +232,32 @@ public unsafe class FunModule : IDisposable
7, // Rose Pink
];
private static IReadOnlyList<CharacterArmor> DolphinBodies
=>
[
new CharacterArmor(6089, 1, 4), // Toad
new CharacterArmor(6089, 1, 4), // Toad
new CharacterArmor(6089, 1, 4), // Toad
new CharacterArmor(6023, 1, 4), // Swine
new CharacterArmor(6023, 1, 4), // Swine
new CharacterArmor(6023, 1, 4), // Swine
new CharacterArmor(6133, 1, 4), // Gaja
new CharacterArmor(6182, 1, 3), // Imp
new CharacterArmor(6182, 1, 3), // Imp
new CharacterArmor(6182, 1, 4), // Imp
new CharacterArmor(6182, 1, 4), // Imp
];
private void SetDolphin(EquipSlot slot, ref CharacterArmor armor)
{
armor = slot switch
{
EquipSlot.Body => DolphinBodies[_rng.Next(0, DolphinBodies.Count - 1)],
EquipSlot.Head => new CharacterArmor(5040, 1, 0),
_ => armor,
};
}
private void SetElephant(EquipSlot slot, ref CharacterArmor armor, StainId stainId)
{
armor = slot switch

View file

@ -2,6 +2,7 @@
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.GameData;
using Glamourer.Interop.Material;
using Glamourer.Services;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums;
@ -220,6 +221,39 @@ public class InternalStateEditor(
return true;
}
/// <summary> Change the value of a single material color table entry. </summary>
public bool ChangeMaterialValue(ActorState state, MaterialValueIndex index, in MaterialValueState newValue, StateSource source, out ColorRow? oldValue,
uint key = 0)
{
// We already have an existing value.
if (state.Materials.TryGetValue(index, out var old))
{
oldValue = old.Model;
if (!state.CanUnlock(key))
return false;
// Remove if overwritten by a game value.
if (source is StateSource.Game)
{
state.Materials.RemoveValue(index);
return true;
}
// Update if edited.
state.Materials.UpdateValue(index, newValue, out _);
return true;
}
// We do not have an existing value.
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 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,
uint key = 0)
{

View file

@ -1,6 +1,7 @@
using Glamourer.Designs;
using Glamourer.GameData;
using Glamourer.Interop;
using Glamourer.Interop.Material;
using Glamourer.Interop.Penumbra;
using Glamourer.Interop.Structs;
using Glamourer.Services;
@ -118,8 +119,7 @@ public class StateApplier(
// If the source is not IPC we do not want to apply restrictions.
var data = GetData(state);
if (apply)
ChangeArmor(data, slot, state.ModelData.Armor(slot), state.Sources[slot, false] is not StateSource.Ipc,
state.ModelData.IsHatVisible());
ChangeArmor(data, slot, state.ModelData.Armor(slot), !state.Sources[slot, false].IsIpc(), state.ModelData.IsHatVisible());
return data;
}
@ -267,7 +267,7 @@ public class StateApplier(
actor.Model.ApplyParameterData(flags, values);
}
/// <inheritdoc cref="ChangeParameters(ActorData,CustomizeParameterFlag,in CustomizeParameterData)"/>
/// <inheritdoc cref="ChangeParameters(ActorData,CustomizeParameterFlag,in CustomizeParameterData,bool)"/>
public ActorData ChangeParameters(ActorState state, CustomizeParameterFlag flags, bool apply)
{
var data = GetData(state);
@ -276,6 +276,38 @@ public class StateApplier(
return data;
}
public unsafe void ChangeMaterialValue(ActorData data, MaterialValueIndex index, ColorRow? value, bool force)
{
if (!force && !_config.UseAdvancedParameters)
return;
foreach (var actor in data.Objects.Where(a => a is { IsCharacter: true, Model.IsHuman: true }))
{
if (!index.TryGetTexture(actor, out var texture))
continue;
if (!index.TryGetColorTable(texture, out var table))
continue;
if (value.HasValue)
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);
}
}
public ActorData ChangeMaterialValue(ActorState state, MaterialValueIndex index, bool apply)
{
var data = GetData(state);
if (apply)
ChangeMaterialValue(data, index, state.Materials.TryGetValue(index, out var v) ? v.Model : null, state.IsLocked);
return data;
}
/// <summary> Apply the entire state of an actor to all relevant actors, either via immediate redraw or piecewise. </summary>
/// <param name="state"> The state to apply. </param>
/// <param name="redraw"> Whether a redraw should be forced. </param>
@ -294,10 +326,7 @@ public class StateApplier(
{
ChangeCustomize(actors, state.ModelData.Customize);
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
ChangeArmor(actors, slot, state.ModelData.Armor(slot), state.Sources[slot, false] is not StateSource.Ipc,
state.ModelData.IsHatVisible());
}
ChangeArmor(actors, slot, state.ModelData.Armor(slot), !state.Sources[slot, false].IsIpc(), state.ModelData.IsHatVisible());
var mainhandActors = state.ModelData.MainhandType != state.BaseData.MainhandType ? actors.OnlyGPose() : actors;
ChangeMainhand(mainhandActors, state.ModelData.Item(EquipSlot.MainHand), state.ModelData.Stain(EquipSlot.MainHand));

View file

@ -2,6 +2,8 @@ using Glamourer.Designs;
using Glamourer.Designs.Links;
using Glamourer.Events;
using Glamourer.GameData;
using Glamourer.Interop.Material;
using Glamourer.Interop.Penumbra;
using Glamourer.Interop.Structs;
using Glamourer.Services;
using Penumbra.GameData.Enums;
@ -16,7 +18,8 @@ public class StateEditor(
JobChangeState jobChange,
Configuration config,
ItemManager items,
DesignMerger merger) : IDesignEditor
DesignMerger merger,
ModSettingApplier modApplier) : IDesignEditor
{
protected readonly InternalStateEditor Editor = editor;
protected readonly StateApplier Applier = applier;
@ -31,7 +34,7 @@ public class StateEditor(
if (!Editor.ChangeModelId(state, modelId, customize, equipData, source, out var old, key))
return;
var actors = Applier.ForceRedraw(state, source is StateSource.Manual or StateSource.Ipc);
var actors = Applier.ForceRedraw(state, source.RequiresChange());
Glamourer.Log.Verbose(
$"Set model id in state {state.Identifier.Incognito(null)} from {old} to {modelId}. [Affecting {actors.ToLazyString("nothing")}.]");
StateChanged.Invoke(StateChanged.Type.Model, source, state, actors, (old, modelId));
@ -44,7 +47,7 @@ public class StateEditor(
if (!Editor.ChangeCustomize(state, idx, value, settings.Source, out var old, settings.Key))
return;
var actors = Applier.ChangeCustomize(state, settings.Source is StateSource.Manual or StateSource.Ipc);
var actors = Applier.ChangeCustomize(state, settings.Source.RequiresChange());
Glamourer.Log.Verbose(
$"Set {idx.ToDefaultName()} customizations in state {state.Identifier.Incognito(null)} from {old.Value} to {value.Value}. [Affecting {actors.ToLazyString("nothing")}.]");
StateChanged.Invoke(StateChanged.Type.Customize, settings.Source, state, actors, (old, value, idx));
@ -57,7 +60,7 @@ public class StateEditor(
if (!Editor.ChangeHumanCustomize(state, customizeInput, apply, _ => settings.Source, out var old, out var applied, settings.Key))
return;
var actors = Applier.ChangeCustomize(state, settings.Source is StateSource.Manual or StateSource.Ipc);
var actors = Applier.ChangeCustomize(state, settings.Source.RequiresChange());
Glamourer.Log.Verbose(
$"Set {applied} customizations in state {state.Identifier.Incognito(null)} from {old} to {customizeInput}. [Affecting {actors.ToLazyString("nothing")}.]");
StateChanged.Invoke(StateChanged.Type.EntireCustomize, settings.Source, state, actors, (old, applied));
@ -72,8 +75,8 @@ public class StateEditor(
var type = slot.ToIndex() < 10 ? StateChanged.Type.Equip : StateChanged.Type.Weapon;
var actors = type is StateChanged.Type.Equip
? Applier.ChangeArmor(state, slot, settings.Source is StateSource.Manual or StateSource.Ipc)
: Applier.ChangeWeapon(state, slot, settings.Source is StateSource.Manual or StateSource.Ipc,
? Applier.ChangeArmor(state, slot, settings.Source.RequiresChange())
: Applier.ChangeWeapon(state, slot, settings.Source.RequiresChange(),
item.Type != (slot is EquipSlot.MainHand ? state.BaseData.MainhandType : state.BaseData.OffhandType));
if (slot is EquipSlot.MainHand)
@ -105,8 +108,8 @@ public class StateEditor(
var type = slot.ToIndex() < 10 ? StateChanged.Type.Equip : StateChanged.Type.Weapon;
var actors = type is StateChanged.Type.Equip
? Applier.ChangeArmor(state, slot, settings.Source is StateSource.Manual or StateSource.Ipc)
: Applier.ChangeWeapon(state, slot, settings.Source is StateSource.Manual or StateSource.Ipc,
? Applier.ChangeArmor(state, slot, settings.Source.RequiresChange())
: Applier.ChangeWeapon(state, slot, settings.Source.RequiresChange(),
item!.Value.Type != (slot is EquipSlot.MainHand ? state.BaseData.MainhandType : state.BaseData.OffhandType));
if (slot is EquipSlot.MainHand)
@ -125,7 +128,7 @@ public class StateEditor(
if (!Editor.ChangeStain(state, slot, stain, settings.Source, out var old, settings.Key))
return;
var actors = Applier.ChangeStain(state, slot, settings.Source is StateSource.Manual or StateSource.Ipc);
var actors = Applier.ChangeStain(state, slot, settings.Source.RequiresChange());
Glamourer.Log.Verbose(
$"Set {slot.ToName()} stain in state {state.Identifier.Incognito(null)} from {old.Id} to {stain.Id}. [Affecting {actors.ToLazyString("nothing")}.]");
StateChanged.Invoke(StateChanged.Type.Stain, settings.Source, state, actors, (old, stain, slot));
@ -138,7 +141,7 @@ public class StateEditor(
if (!Editor.ChangeCrest(state, slot, crest, settings.Source, out var old, settings.Key))
return;
var actors = Applier.ChangeCrests(state, settings.Source is StateSource.Manual or StateSource.Ipc);
var actors = Applier.ChangeCrests(state, settings.Source.RequiresChange());
Glamourer.Log.Verbose(
$"Set {slot.ToLabel()} crest in state {state.Identifier.Incognito(null)} from {old} to {crest}. [Affecting {actors.ToLazyString("nothing")}.]");
StateChanged.Invoke(StateChanged.Type.Crest, settings.Source, state, actors, (old, crest, slot));
@ -147,9 +150,7 @@ public class StateEditor(
/// <inheritdoc/>
public void ChangeCustomizeParameter(object data, CustomizeParameterFlag flag, CustomizeParameterValue value, ApplySettings settings)
{
if (data is not ActorState state)
return;
var state = (ActorState)data;
// Also apply main color to highlights when highlights is off.
if (!state.ModelData.Customize.Highlights && flag is CustomizeParameterFlag.HairDiffuse)
ChangeCustomizeParameter(state, CustomizeParameterFlag.HairHighlight, value, settings);
@ -158,12 +159,23 @@ public class StateEditor(
return;
var @new = state.ModelData.Parameters[flag];
var actors = Applier.ChangeParameters(state, flag, settings.Source is StateSource.Manual or StateSource.Ipc);
var actors = Applier.ChangeParameters(state, flag, settings.Source.RequiresChange());
Glamourer.Log.Verbose(
$"Set {flag} crest in state {state.Identifier.Incognito(null)} from {old} to {@new}. [Affecting {actors.ToLazyString("nothing")}.]");
StateChanged.Invoke(StateChanged.Type.Parameter, settings.Source, state, actors, (old, @new, flag));
}
public void ChangeMaterialValue(object data, MaterialValueIndex index, in MaterialValueState newValue, ApplySettings settings)
{
var state = (ActorState)data;
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 {newValue.Game}. [Affecting {actors.ToLazyString("nothing")}.]");
StateChanged.Invoke(StateChanged.Type.MaterialValue, settings.Source, state, actors, (oldValue, newValue.Game, index));
}
/// <inheritdoc/>
public void ChangeMetaState(object data, MetaIndex index, bool value, ApplySettings settings)
{
@ -171,7 +183,7 @@ public class StateEditor(
if (!Editor.ChangeMetaState(state, index, value, settings.Source, out var old, settings.Key))
return;
var actors = Applier.ChangeMetaState(state, index, settings.Source is StateSource.Manual or StateSource.Ipc);
var actors = Applier.ChangeMetaState(state, index, settings.Source.RequiresChange());
Glamourer.Log.Verbose(
$"Set Head Gear Visibility in state {state.Identifier.Incognito(null)} from {old} to {value}. [Affecting {actors.ToLazyString("nothing")}.]");
StateChanged.Invoke(StateChanged.Type.Other, settings.Source, state, actors, (old, value, MetaIndex.HatState));
@ -181,6 +193,7 @@ public class StateEditor(
public void ApplyDesign(object data, MergedDesign mergedDesign, ApplySettings settings)
{
var state = (ActorState)data;
modApplier.HandleStateApplication(state, mergedDesign);
if (!Editor.ChangeModelId(state, mergedDesign.Design.DesignData.ModelId, mergedDesign.Design.DesignData.Customize,
mergedDesign.Design.GetDesignDataRef().GetEquipmentPtr(), settings.Source, out var oldModelId, settings.Key))
return;
@ -191,7 +204,7 @@ public class StateEditor(
{
foreach (var slot in CrestExtensions.AllRelevantSet.Where(mergedDesign.Design.DoApplyCrest))
{
if (!settings.RespectManual || state.Sources[slot] is not StateSource.Manual)
if (!settings.RespectManual || !state.Sources[slot].IsManual())
Editor.ChangeCrest(state, slot, mergedDesign.Design.DesignData.Crest(slot), Source(slot),
out _, settings.Key);
}
@ -201,7 +214,7 @@ public class StateEditor(
customizeFlags |= CustomizeFlag.Race;
Func<CustomizeIndex, bool> applyWhich = settings.RespectManual
? i => customizeFlags.HasFlag(i.ToFlag()) && state.Sources[i] is not StateSource.Manual
? i => customizeFlags.HasFlag(i.ToFlag()) && !state.Sources[i].IsManual()
: i => customizeFlags.HasFlag(i.ToFlag());
if (Editor.ChangeHumanCustomize(state, mergedDesign.Design.DesignData.Customize, applyWhich, i => Source(i), out _, out var changed,
@ -210,12 +223,10 @@ public class StateEditor(
foreach (var parameter in mergedDesign.Design.ApplyParameters.Iterate())
{
if (settings.RespectManual && state.Sources[parameter] is StateSource.Manual or StateSource.Pending)
if (settings.RespectManual && state.Sources[parameter].IsManual())
continue;
var source = Source(parameter);
if (source is StateSource.Manual)
source = StateSource.Pending;
var source = Source(parameter).SetPending();
Editor.ChangeParameter(state, parameter, mergedDesign.Design.DesignData.Parameters[parameter], source, out _, settings.Key);
}
@ -228,12 +239,12 @@ public class StateEditor(
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
if (mergedDesign.Design.DoApplyEquip(slot))
if (!settings.RespectManual || state.Sources[slot, false] is not StateSource.Manual)
if (!settings.RespectManual || !state.Sources[slot, false].IsManual())
Editor.ChangeItem(state, slot, mergedDesign.Design.DesignData.Item(slot),
Source(slot.ToState()), out _, settings.Key);
if (mergedDesign.Design.DoApplyStain(slot))
if (!settings.RespectManual || state.Sources[slot, true] is not StateSource.Manual)
if (!settings.RespectManual || !state.Sources[slot, true].IsManual())
Editor.ChangeStain(state, slot, mergedDesign.Design.DesignData.Stain(slot),
Source(slot.ToState(true)), out _, settings.Key);
}
@ -241,14 +252,14 @@ public class StateEditor(
foreach (var weaponSlot in EquipSlotExtensions.WeaponSlots)
{
if (mergedDesign.Design.DoApplyStain(weaponSlot))
if (!settings.RespectManual || state.Sources[weaponSlot, true] is not StateSource.Manual)
if (!settings.RespectManual || !state.Sources[weaponSlot, true].IsManual())
Editor.ChangeStain(state, weaponSlot, mergedDesign.Design.DesignData.Stain(weaponSlot),
Source(weaponSlot.ToState(true)), out _, settings.Key);
if (!mergedDesign.Design.DoApplyEquip(weaponSlot))
continue;
if (settings.RespectManual && state.Sources[weaponSlot, false] is StateSource.Manual)
if (settings.RespectManual && !state.Sources[weaponSlot, false].IsManual())
continue;
var currentType = state.ModelData.Item(weaponSlot).Type;
@ -268,12 +279,26 @@ public class StateEditor(
foreach (var meta in MetaExtensions.AllRelevant)
{
if (!settings.RespectManual || state.Sources[meta] is not StateSource.Manual)
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 is StateSource.Manual or StateSource.Ipc
var actors = settings.Source.RequiresChange()
? Applier.ApplyAll(state, requiresRedraw, false)
: ActorData.Invalid;
@ -296,7 +321,7 @@ 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, false, false)
? merger.Merge(d.AllLinks, ((ActorState)data).ModelData, false, Config.AlwaysApplyAssociatedMods)
: new MergedDesign(design);
ApplyDesign(data, merged, settings with
@ -311,7 +336,7 @@ public class StateEditor(
/// <summary> Apply offhand item and potentially gauntlets if configured. </summary>
private void ApplyMainhandPeriphery(ActorState state, EquipItem? newMainhand, ApplySettings settings)
{
if (!Config.ChangeEntireItem || settings.Source is not StateSource.Manual)
if (!Config.ChangeEntireItem || !settings.Source.IsManual())
return;
var mh = newMainhand ?? state.ModelData.Item(EquipSlot.MainHand);

View file

@ -171,7 +171,7 @@ public class StateListener : IDisposable
var set = _customizations.Manager.GetSet(model.Clan, model.Gender);
foreach (var index in CustomizationExtensions.AllBasic)
{
if (state.Sources[index] is not StateSource.Fixed)
if (!state.Sources[index].IsFixed())
{
var newValue = customize[index];
var oldValue = model[index];
@ -214,7 +214,7 @@ public class StateListener : IDisposable
&& _manager.TryGetValue(identifier, out var state))
{
HandleEquipSlot(actor, state, slot, ref armor);
locked = state.Sources[slot, false] is StateSource.Ipc;
locked = state.Sources[slot, false] is StateSource.IpcFixed;
}
_funModule.ApplyFunToSlot(actor, ref armor, slot);
@ -241,7 +241,7 @@ public class StateListener : IDisposable
continue;
var changed = changedItem.Weapon(stain);
if (current.Value == changed.Value && state.Sources[slot, false] is not StateSource.Fixed and not StateSource.Ipc)
if (current.Value == changed.Value && !state.Sources[slot, false].IsFixed())
{
_manager.ChangeItem(state, slot, currentItem, ApplySettings.Game);
_manager.ChangeStain(state, slot, current.Stain, ApplySettings.Game);
@ -252,7 +252,7 @@ public class StateListener : IDisposable
_applier.ChangeWeapon(objects, slot, currentItem, stain);
break;
default:
_applier.ChangeArmor(objects, slot, current.ToArmor(), state.Sources[slot, false] is not StateSource.Ipc,
_applier.ChangeArmor(objects, slot, current.ToArmor(), !state.Sources[slot, false].IsFixed(),
state.ModelData.IsHatVisible());
break;
}
@ -278,20 +278,19 @@ public class StateListener : IDisposable
|| !_manager.TryGetValue(identifier, out var state))
return;
ref var actorWeapon = ref weapon;
var baseType = state.BaseData.Item(slot).Type;
var apply = false;
switch (UpdateBaseData(actor, state, slot, actorWeapon))
var baseType = state.BaseData.Item(slot).Type;
var apply = false;
switch (UpdateBaseData(actor, state, slot, weapon))
{
// Do nothing. But this usually can not happen because the hooked function also writes to game objects later.
case UpdateState.Transformed: break;
case UpdateState.Change:
if (state.Sources[slot, false] is not StateSource.Fixed and not StateSource.Ipc)
if (!state.Sources[slot, false].IsFixed())
_manager.ChangeItem(state, slot, state.BaseData.Item(slot), ApplySettings.Game);
else
apply = true;
if (state.Sources[slot, true] is not StateSource.Fixed and not StateSource.Ipc)
if (!state.Sources[slot, true].IsFixed())
_manager.ChangeStain(state, slot, state.BaseData.Stain(slot), ApplySettings.Game);
else
apply = true;
@ -306,9 +305,9 @@ public class StateListener : IDisposable
// Only allow overwriting identical weapons
var newWeapon = state.ModelData.Weapon(slot);
if (baseType is FullEquipType.Unknown || baseType == state.ModelData.Item(slot).Type || _gPose.InGPose && actor.IsGPoseOrCutscene)
actorWeapon = newWeapon;
else if (actorWeapon.Skeleton.Id != 0)
actorWeapon = actorWeapon.With(newWeapon.Stain);
weapon = newWeapon;
else if (weapon.Skeleton.Id != 0)
weapon = weapon.With(newWeapon.Stain);
}
// Fist Weapon Offhand hack.
@ -385,12 +384,12 @@ public class StateListener : IDisposable
// Update model state if not on fixed design.
case UpdateState.Change:
var apply = false;
if (state.Sources[slot, false] is not StateSource.Fixed and not StateSource.Ipc)
if (!state.Sources[slot, false].IsFixed())
_manager.ChangeItem(state, slot, state.BaseData.Item(slot), ApplySettings.Game);
else
apply = true;
if (state.Sources[slot, true] is not StateSource.Fixed and not StateSource.Ipc)
if (!state.Sources[slot, true].IsFixed())
_manager.ChangeStain(state, slot, state.BaseData.Stain(slot), ApplySettings.Game);
else
apply = true;
@ -419,7 +418,7 @@ public class StateListener : IDisposable
switch (UpdateBaseCrest(actor, state, slot, value))
{
case UpdateState.Change:
if (state.Sources[slot] is not StateSource.Fixed and not StateSource.Ipc)
if (!state.Sources[slot].IsFixed())
_manager.ChangeCrest(state, slot, state.BaseData.Crest(slot), ApplySettings.Game);
else
value = state.ModelData.Crest(slot);
@ -565,7 +564,7 @@ public class StateListener : IDisposable
{
// if base state changed, either overwrite the actual value if we have fixed values,
// or overwrite the stored model state with the new one.
if (state.Sources[MetaIndex.VisorState] is StateSource.Fixed or StateSource.Ipc)
if (!state.Sources[MetaIndex.VisorState].IsFixed())
value = state.ModelData.IsVisorToggled();
else
_manager.ChangeMetaState(state, MetaIndex.VisorState, value, ApplySettings.Game);
@ -598,7 +597,7 @@ public class StateListener : IDisposable
{
// if base state changed, either overwrite the actual value if we have fixed values,
// or overwrite the stored model state with the new one.
if (state.Sources[MetaIndex.HatState] is StateSource.Fixed or StateSource.Ipc)
if (!state.Sources[MetaIndex.HatState].IsFixed())
value = state.ModelData.IsHatVisible();
else
_manager.ChangeMetaState(state, MetaIndex.HatState, value, ApplySettings.Game);
@ -631,7 +630,7 @@ public class StateListener : IDisposable
{
// if base state changed, either overwrite the actual value if we have fixed values,
// or overwrite the stored model state with the new one.
if (state.Sources[MetaIndex.WeaponState] is StateSource.Fixed or StateSource.Ipc)
if (!state.Sources[MetaIndex.WeaponState].IsFixed())
value = state.ModelData.IsWeaponVisible();
else
_manager.ChangeMetaState(state, MetaIndex.WeaponState, value, ApplySettings.Game);
@ -700,8 +699,8 @@ public class StateListener : IDisposable
return;
var data = new ActorData(gameObject, _creatingIdentifier.ToName());
_applier.ChangeMetaState(data, MetaIndex.HatState, _creatingState.ModelData.IsHatVisible());
_applier.ChangeMetaState(data, MetaIndex.Wetness, _creatingState.ModelData.IsWet());
_applier.ChangeMetaState(data, MetaIndex.HatState, _creatingState.ModelData.IsHatVisible());
_applier.ChangeMetaState(data, MetaIndex.Wetness, _creatingState.ModelData.IsWet());
_applier.ChangeMetaState(data, MetaIndex.WeaponState, _creatingState.ModelData.IsWeaponVisible());
ApplyParameters(_creatingState, drawObject);
@ -745,12 +744,18 @@ public class StateListener : IDisposable
else if (_config.UseAdvancedParameters)
model.ApplySingleParameterData(flag, state.ModelData.Parameters);
break;
case StateSource.IpcManual:
if (state.BaseData.Parameters.Set(flag, newValue))
_manager.ChangeCustomizeParameter(state, flag, newValue, ApplySettings.Game);
else
model.ApplySingleParameterData(flag, state.ModelData.Parameters);
break;
case StateSource.Fixed:
state.BaseData.Parameters.Set(flag, newValue);
if (_config.UseAdvancedParameters)
model.ApplySingleParameterData(flag, state.ModelData.Parameters);
break;
case StateSource.Ipc:
case StateSource.IpcFixed:
state.BaseData.Parameters.Set(flag, newValue);
model.ApplySingleParameterData(flag, state.ModelData.Parameters);
break;

View file

@ -4,6 +4,7 @@ using Glamourer.Designs.Links;
using Glamourer.Events;
using Glamourer.GameData;
using Glamourer.Interop;
using Glamourer.Interop.Penumbra;
using Glamourer.Interop.Structs;
using Glamourer.Services;
using Penumbra.GameData.Actors;
@ -23,8 +24,9 @@ public sealed class StateManager(
IClientState _clientState,
Configuration config,
JobChangeState jobChange,
DesignMerger merger)
: StateEditor(editor, applier, @event, jobChange, config, items, merger), IReadOnlyDictionary<ActorIdentifier, ActorState>
DesignMerger merger,
ModSettingApplier modApplier)
: StateEditor(editor, applier, @event, jobChange, config, items, merger, modApplier), IReadOnlyDictionary<ActorIdentifier, ActorState>
{
private readonly Dictionary<ActorIdentifier, ActorState> _states = [];
@ -240,8 +242,10 @@ public sealed class StateManager(
foreach (var flag in CustomizeParameterExtensions.AllFlags)
state.Sources[flag] = StateSource.Game;
state.Materials.Clear();
var actors = ActorData.Invalid;
if (source is StateSource.Manual or StateSource.Ipc)
if (source is not StateSource.Game)
actors = Applier.ApplyAll(state, redraw, true);
Glamourer.Log.Verbose(
@ -260,7 +264,7 @@ public sealed class StateManager(
state.Sources[flag] = StateSource.Game;
var actors = ActorData.Invalid;
if (source is StateSource.Manual or StateSource.Ipc)
if (source is not StateSource.Game)
actors = Applier.ChangeParameters(state, CustomizeParameterExtensions.All, true);
Glamourer.Log.Verbose(
$"Reset advanced customization state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]");
@ -314,28 +318,10 @@ public sealed class StateManager(
}
}
if (state.Sources[MetaIndex.HatState] is StateSource.Fixed)
foreach (var meta in MetaExtensions.AllRelevant.Where(f => state.Sources[f] is StateSource.Fixed))
{
state.Sources[MetaIndex.HatState] = StateSource.Game;
state.ModelData.SetHatVisible(state.BaseData.IsHatVisible());
}
if (state.Sources[MetaIndex.VisorState] is StateSource.Fixed)
{
state.Sources[MetaIndex.VisorState] = StateSource.Game;
state.ModelData.SetVisor(state.BaseData.IsVisorToggled());
}
if (state.Sources[MetaIndex.WeaponState] is StateSource.Fixed)
{
state.Sources[MetaIndex.WeaponState] = StateSource.Game;
state.ModelData.SetWeaponVisible(state.BaseData.IsWeaponVisible());
}
if (state.Sources[MetaIndex.Wetness] is StateSource.Fixed)
{
state.Sources[MetaIndex.Wetness] = StateSource.Game;
state.ModelData.SetIsWet(state.BaseData.IsWet());
state.Sources[meta] = StateSource.Game;
state.ModelData.SetMeta(meta, state.BaseData.GetMeta(meta));
}
}

View file

@ -7,12 +7,48 @@ public enum StateSource : byte
Game,
Manual,
Fixed,
Ipc,
IpcFixed,
IpcManual,
// Only used for CustomizeParameters.
Pending,
}
public static class StateSourceExtensions
{
public static StateSource Base(this StateSource source)
=> source switch
{
StateSource.Manual or StateSource.IpcManual or StateSource.Pending => StateSource.Manual,
StateSource.Fixed or StateSource.IpcFixed => StateSource.Fixed,
_ => StateSource.Game,
};
public static bool IsGame(this StateSource source)
=> source.Base() is StateSource.Game;
public static bool IsManual(this StateSource source)
=> source.Base() is StateSource.Manual;
public static bool IsFixed(this StateSource source)
=> source.Base() is StateSource.Fixed;
public static StateSource SetPending(this StateSource source)
=> source is StateSource.Manual ? StateSource.Pending : source;
public static bool RequiresChange(this StateSource source)
=> source switch
{
StateSource.Manual => true,
StateSource.IpcFixed => true,
StateSource.IpcManual => true,
_ => false,
};
public static bool IsIpc(this StateSource source)
=> source is StateSource.IpcManual or StateSource.IpcFixed;
}
public unsafe struct StateSources
{
public const int Size = (StateIndex.Size + 1) / 2;
@ -59,14 +95,16 @@ public unsafe struct StateSources
case (byte)StateSource.Game | ((byte)StateSource.Fixed << 4):
case (byte)StateSource.Manual | ((byte)StateSource.Fixed << 4):
case (byte)StateSource.Ipc | ((byte)StateSource.Fixed << 4):
case (byte)StateSource.IpcFixed | ((byte)StateSource.Fixed << 4):
case (byte)StateSource.Pending | ((byte)StateSource.Fixed << 4):
case (byte)StateSource.IpcManual | ((byte)StateSource.Fixed << 4):
_data[i] = (byte)((value & 0x0F) | ((byte)StateSource.Manual << 4));
break;
case (byte)StateSource.Fixed:
case ((byte)StateSource.Manual << 4) | (byte)StateSource.Fixed:
case ((byte)StateSource.Ipc << 4) | (byte)StateSource.Fixed:
case ((byte)StateSource.IpcFixed << 4) | (byte)StateSource.Fixed:
case ((byte)StateSource.Pending << 4) | (byte)StateSource.Fixed:
case ((byte)StateSource.IpcManual << 4) | (byte)StateSource.Fixed:
_data[i] = (byte)((value & 0xF0) | (byte)StateSource.Manual);
break;
}

@ -1 +1 @@
Subproject commit 04eb0b5ed3930e9cb87ad00dffa9c8be90b58bb3
Subproject commit 2d8a03eebd80e19c6936a28ab2e3a8c164cc17f3

@ -1 +1 @@
Subproject commit 260ac69cd6f17050eaf9b7e0b5ce9a8843edfee4
Subproject commit fb18c80551203a1cf6cd01ec2b0850fbc8e44240