mirror of
https://github.com/Ottermandias/Glamourer.git
synced 2025-12-12 18:27:24 +01:00
Merge branch 'colortable'
This commit is contained in:
commit
73266811a0
66 changed files with 2458 additions and 286 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
84
Glamourer/Designs/ApplicationRules.cs
Normal file
84
Glamourer/Designs/ApplicationRules.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
166
Glamourer/Gui/Materials/MaterialDrawer.cs
Normal file
166
Glamourer/Gui/Materials/MaterialDrawer.cs
Normal 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",
|
||||
];
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -37,4 +37,4 @@ public class DatFilePanel(ImportService _importService) : IGameDataDrawer
|
|||
ImGui.TextUnformatted(_datFile.Value.Description);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
135
Glamourer/Gui/Tabs/DebugTab/GlamourPlatePanel.cs
Normal file
135
Glamourer/Gui/Tabs/DebugTab/GlamourPlatePanel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
116
Glamourer/Interop/Material/DirectXTextureHelper.cs
Normal file
116
Glamourer/Interop/Material/DirectXTextureHelper.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
199
Glamourer/Interop/Material/MaterialManager.cs
Normal file
199
Glamourer/Interop/Material/MaterialManager.cs
Normal 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]);
|
||||
}
|
||||
}
|
||||
99
Glamourer/Interop/Material/MaterialService.cs
Normal file
99
Glamourer/Interop/Material/MaterialService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
162
Glamourer/Interop/Material/MaterialValueIndex.cs
Normal file
162
Glamourer/Interop/Material/MaterialValueIndex.cs
Normal 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}.");
|
||||
}
|
||||
}
|
||||
431
Glamourer/Interop/Material/MaterialValueManager.cs
Normal file
431
Glamourer/Interop/Material/MaterialValueManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
103
Glamourer/Interop/Material/PrepareColorSet.cs
Normal file
103
Glamourer/Interop/Material/PrepareColorSet.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
49
Glamourer/Interop/Material/SafeTextureHandle.cs
Normal file
49
Glamourer/Interop/Material/SafeTextureHandle.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
67
Glamourer/Interop/Penumbra/ModSettingApplier.cs
Normal file
67
Glamourer/Interop/Penumbra/ModSettingApplier.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 ],
|
||||
_ => [],
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
2
OtterGui
2
OtterGui
|
|
@ -1 +1 @@
|
|||
Subproject commit 04eb0b5ed3930e9cb87ad00dffa9c8be90b58bb3
|
||||
Subproject commit 2d8a03eebd80e19c6936a28ab2e3a8c164cc17f3
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 260ac69cd6f17050eaf9b7e0b5ce9a8843edfee4
|
||||
Subproject commit fb18c80551203a1cf6cd01ec2b0850fbc8e44240
|
||||
Loading…
Add table
Add a link
Reference in a new issue