This commit is contained in:
Ottermandias 2023-06-20 18:54:33 +02:00
parent d1d369a56b
commit 65ce391051
19 changed files with 757 additions and 158 deletions

View file

@ -4,20 +4,29 @@ using System.IO;
using System.Linq; using System.Linq;
using Dalamud.Configuration; using Dalamud.Configuration;
using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Notifications;
using Glamourer.Designs;
using Glamourer.Gui; using Glamourer.Gui;
using Glamourer.Services; using Glamourer.Services;
using Newtonsoft.Json; using Newtonsoft.Json;
using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Filesystem;
using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
namespace Glamourer; namespace Glamourer;
public class Configuration : IPluginConfiguration, ISavable public class Configuration : IPluginConfiguration, ISavable
{ {
public bool Enabled { get; set; } = true;
public bool UseRestrictedGearProtection { get; set; } = true; public bool UseRestrictedGearProtection { get; set; } = true;
public bool OpenFoldersByDefault { get; set; } = false;
public MainWindow.TabType SelectedTab { get; set; } = MainWindow.TabType.Settings; public MainWindow.TabType SelectedTab { get; set; } = MainWindow.TabType.Settings;
public DoubleModifier DeleteDesignModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); public DoubleModifier DeleteDesignModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift);
[JsonConverter(typeof(SortModeConverter))]
[JsonProperty(Order = int.MaxValue)]
public ISortMode<Design> SortMode { get; set; } = ISortMode<Design>.FoldersFirst;
#if DEBUG #if DEBUG
public bool DebugMode { get; set; } = true; public bool DebugMode { get; set; } = true;
@ -86,5 +95,42 @@ public class Configuration : IPluginConfiguration, ISavable
public static class Constants public static class Constants
{ {
public const int CurrentVersion = 2; public const int CurrentVersion = 2;
public static readonly ISortMode<Design>[] ValidSortModes =
{
ISortMode<Design>.FoldersFirst,
ISortMode<Design>.Lexicographical,
new DesignFileSystem.CreationDate(),
new DesignFileSystem.InverseCreationDate(),
new DesignFileSystem.UpdateDate(),
new DesignFileSystem.InverseUpdateDate(),
ISortMode<Design>.InverseFoldersFirst,
ISortMode<Design>.InverseLexicographical,
ISortMode<Design>.FoldersLast,
ISortMode<Design>.InverseFoldersLast,
ISortMode<Design>.InternalOrder,
ISortMode<Design>.InverseInternalOrder,
};
}
/// <summary> Convert SortMode Types to their name. </summary>
private class SortModeConverter : JsonConverter<ISortMode<Design>>
{
public override void WriteJson(JsonWriter writer, ISortMode<Design>? value, JsonSerializer serializer)
{
value ??= ISortMode<Design>.FoldersFirst;
serializer.Serialize(writer, value.GetType().Name);
}
public override ISortMode<Design> ReadJson(JsonReader reader, Type objectType, ISortMode<Design>? existingValue,
bool hasExistingValue,
JsonSerializer serializer)
{
var name = serializer.Deserialize<string>(reader);
if (name == null || !Constants.ValidSortModes.FindFirst(s => s.GetType().Name == name, out var mode))
return existingValue ?? ISortMode<Design>.FoldersFirst;
return mode;
}
} }
} }

View file

@ -15,16 +15,16 @@ namespace Glamourer.Events;
/// <item>Parameter is the return value the function should return, if it is ulong.MaxValue, the original will be called and returned. </item> /// <item>Parameter is the return value the function should return, if it is ulong.MaxValue, the original will be called and returned. </item>
/// </list> /// </list>
/// </summary> /// </summary>
public sealed class UpdatedSlot : EventWrapper<Action<Model, EquipSlot, Ref<CharacterArmor>, Ref<ulong>>, UpdatedSlot.Priority> public sealed class SlotUpdating : EventWrapper<Action<Model, EquipSlot, Ref<CharacterArmor>, Ref<ulong>>, SlotUpdating.Priority>
{ {
public enum Priority public enum Priority
{ {
/// <seealso cref="State.StateManager.OnSlotUpdated"/> /// <seealso cref="State.StateListener.OnSlotUpdating"/>
StateManager = 0, StateListener = 0,
} }
public UpdatedSlot() public SlotUpdating()
: base(nameof(UpdatedSlot)) : base(nameof(SlotUpdating))
{ } { }
public void Invoke(Model model, EquipSlot slot, ref CharacterArmor armor, ref ulong returnValue) public void Invoke(Model model, EquipSlot slot, ref CharacterArmor armor, ref ulong returnValue)

View file

@ -0,0 +1,35 @@
using System;
using Glamourer.Interop.Structs;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Events;
/// <summary>
/// Triggered when a model flags an equipment slot for an update.
/// <list type="number">
/// <item>Parameter is the actor that has its weapons changed. </item>
/// <item>Parameter is the equipment slot changed (Mainhand or Offhand). </item>
/// <item>Parameter is the model values to change the weapon to. </item>
/// </list>
/// </summary>
public sealed class WeaponLoading : EventWrapper<Action<Actor, EquipSlot, Ref<CharacterWeapon>>, WeaponLoading.Priority>
{
public enum Priority
{
/// <seealso cref="State.StateListener.OnWeaponLoading"/>
StateListener = 0,
}
public WeaponLoading()
: base(nameof(WeaponLoading))
{ }
public void Invoke(Actor actor, EquipSlot slot, ref CharacterWeapon weapon)
{
var value = new Ref<CharacterWeapon>(weapon);
Invoke(this, actor, slot, value);
weapon = value;
}
}

View file

@ -9,24 +9,25 @@ public enum ColorId
EquipmentDesign, EquipmentDesign,
ActorAvailable, ActorAvailable,
ActorUnavailable, ActorUnavailable,
FolderExpanded,
FolderCollapsed,
FolderLine,
} }
public static class Colors public static class Colors
{ {
public const uint DiscordColor = 0xFFDA8972;
public const uint ReniColorButton = 0xFFCC648D;
public const uint ReniColorHovered = 0xFFB070B0;
public const uint ReniColorActive = 0xFF9070E0;
public static (uint DefaultColor, string Name, string Description) Data(this ColorId color) public static (uint DefaultColor, string Name, string Description) Data(this ColorId color)
=> color switch => color switch
{ {
// @formatter:off // @formatter:off
ColorId.CustomizationDesign => (0xFFC000C0, "Customization Design", "A design that only changes customizations on a character." ), ColorId.CustomizationDesign => (0xFFC000C0, "Customization Design", "A design that only changes customizations on a character." ),
ColorId.StateDesign => (0xFF00C0C0, "State Design", "A design that only changes meta state on a character." ), ColorId.StateDesign => (0xFF00C0C0, "State Design", "A design that only changes meta state on a character." ),
ColorId.EquipmentDesign => (0xFF00C000, "Equipment Design", "A design that only changes equipment on a character." ), ColorId.EquipmentDesign => (0xFF00C000, "Equipment Design", "A design that only changes equipment on a character." ),
ColorId.ActorAvailable => (0xFF18C018, "Actor Available", "The header in the Actor tab panel if the currently selected actor exists in the game world at least once." ), ColorId.ActorAvailable => (0xFF18C018, "Actor Available", "The header in the Actor tab panel if the currently selected actor exists in the game world at least once." ),
ColorId.ActorUnavailable => (0xFF1818C0, "Actor Unavailable", "The Header in the Actor tab panel if the currently selected actor does not exist in the game world." ), ColorId.ActorUnavailable => (0xFF1818C0, "Actor Unavailable", "The Header in the Actor tab panel if the currently selected actor does not exist in the game world." ),
ColorId.FolderExpanded => (0xFFFFF0C0, "Expanded Design Folder", "A design folder that is currently expanded." ),
ColorId.FolderCollapsed => (0xFFFFF0C0, "Collapsed Design Folder", "A design folder that is currently collapsed." ),
ColorId.FolderLine => (0xFFFFF0C0, "Expanded Design Folder Line", "The line signifying which descendants belong to an expanded design folder." ),
_ => (0x00000000, string.Empty, string.Empty ), _ => (0x00000000, string.Empty, string.Empty ),
// @formatter:on // @formatter:on
}; };

View file

@ -6,14 +6,16 @@ namespace Glamourer.Gui;
public class GlamourerWindowSystem : IDisposable public class GlamourerWindowSystem : IDisposable
{ {
private readonly WindowSystem _windowSystem = new("Glamourer"); private readonly WindowSystem _windowSystem = new("Glamourer");
private readonly UiBuilder _uiBuilder; private readonly UiBuilder _uiBuilder;
private readonly MainWindow _ui; private readonly MainWindow _ui;
private readonly PenumbraChangedItemTooltip _penumbraTooltip;
public GlamourerWindowSystem(UiBuilder uiBuilder, MainWindow ui) public GlamourerWindowSystem(UiBuilder uiBuilder, MainWindow ui, PenumbraChangedItemTooltip penumbraTooltip)
{ {
_uiBuilder = uiBuilder; _uiBuilder = uiBuilder;
_ui = ui; _ui = ui;
_penumbraTooltip = penumbraTooltip;
_windowSystem.AddWindow(ui); _windowSystem.AddWindow(ui);
_uiBuilder.Draw += _windowSystem.Draw; _uiBuilder.Draw += _windowSystem.Draw;
_uiBuilder.OpenConfigUi += _ui.Toggle; _uiBuilder.OpenConfigUi += _ui.Toggle;

View file

@ -0,0 +1,182 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Glamourer.Interop;
using Glamourer.Interop.Penumbra;
using Glamourer.Services;
using Glamourer.State;
using Glamourer.Structs;
using ImGuiNET;
using Penumbra.Api.Enums;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui;
public class PenumbraChangedItemTooltip : IDisposable
{
private readonly PenumbraService _penumbra;
private readonly StateManager _stateManager;
private readonly ItemManager _items;
private readonly ObjectManager _objects;
private readonly EquipItem[] _lastItems = new EquipItem[EquipFlagExtensions.NumEquipFlags / 2];
public IEnumerable<KeyValuePair<EquipSlot, EquipItem>> LastItems
=> EquipSlotExtensions.EqdpSlots.Append(EquipSlot.MainHand).Append(EquipSlot.OffHand).Zip(_lastItems)
.Select(p => new KeyValuePair<EquipSlot, EquipItem>(p.First, p.Second));
public DateTime LastTooltip { get; private set; } = DateTime.MinValue;
public DateTime LastClick { get; private set; } = DateTime.MinValue;
public PenumbraChangedItemTooltip(PenumbraService penumbra, StateManager stateManager, ItemManager items, ObjectManager objects)
{
_penumbra = penumbra;
_stateManager = stateManager;
_items = items;
_objects = objects;
_penumbra.Tooltip += OnPenumbraTooltip;
_penumbra.Click += OnPenumbraClick;
}
public void Dispose()
{
_penumbra.Tooltip -= OnPenumbraTooltip;
_penumbra.Click -= OnPenumbraClick;
}
private void OnPenumbraTooltip(ChangedItemType type, uint id)
{
LastTooltip = DateTime.UtcNow;
if (!_objects.Player.Valid)
return;
switch (type)
{
case ChangedItemType.Item:
if (!_items.ItemService.AwaitedService.TryGetValue(id, out var item))
return;
var slot = item.Type.ToSlot();
var last = _lastItems[slot.ToIndex()];
switch (slot)
{
case EquipSlot.MainHand when !CanApplyWeapon(EquipSlot.MainHand, item):
case EquipSlot.OffHand when !CanApplyWeapon(EquipSlot.OffHand, item):
break;
case EquipSlot.RFinger:
ImGui.TextUnformatted("[Glamourer] Right-Click to apply to current actor (Right Finger).");
ImGui.TextUnformatted("[Glamourer] Shift + Right-Click to apply to current actor (Left Finger).");
if (last.Valid)
ImGui.TextUnformatted(
$"[Glamourer] Control + Right-Click to re-apply {last.Name} to current actor (Right Finger).");
var last2 = _lastItems[EquipSlot.LFinger.ToIndex()];
if (last2.Valid)
ImGui.TextUnformatted(
$"[Glamourer] Shift + Control + Right-Click to re-apply {last.Name} to current actor (Left Finger).");
break;
default:
ImGui.TextUnformatted("[Glamourer] Right-Click to apply to current actor.");
if (last.Valid)
ImGui.TextUnformatted($"[Glamourer] Control + Right-Click to re-apply {last.Name} to current actor.");
break;
}
return;
}
}
private bool CanApplyWeapon(EquipSlot slot, EquipItem item)
{
var main = _objects.Player.GetMainhand();
var mainItem = _items.Identify(slot, main.Set, main.Type, (byte)main.Variant);
if (slot == EquipSlot.MainHand)
return item.Type == mainItem.Type;
return item.Type == mainItem.Type.Offhand();
}
private void OnPenumbraClick(MouseButton button, ChangedItemType type, uint id)
{
LastClick = DateTime.UtcNow;
switch (type)
{
case ChangedItemType.Item:
if (button is not MouseButton.Right)
return;
var (identifier, data) = _objects.PlayerData;
if (!data.Valid)
return;
if (!_stateManager.GetOrCreate(identifier, data.Objects[0], out var state))
return;
if (!_items.ItemService.AwaitedService.TryGetValue(id, out var item))
return;
var slot = item.Type.ToSlot();
var last = _lastItems[slot.ToIndex()];
switch (slot)
{
case EquipSlot.MainHand when !CanApplyWeapon(EquipSlot.MainHand, item):
case EquipSlot.OffHand when !CanApplyWeapon(EquipSlot.OffHand, item):
break;
case EquipSlot.RFinger:
switch (ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift)
{
case (false, false):
Glamourer.Log.Information($"Applying {item.Name} to Right Finger.");
SetLastItem(EquipSlot.RFinger, item, state);
break;
case (false, true):
Glamourer.Log.Information($"Applying {item.Name} to Left Finger.");
SetLastItem(EquipSlot.LFinger, item, state);
break;
case (true, false) when last.Valid:
Glamourer.Log.Information($"Re-Applying {last.Name} to Right Finger.");
SetLastItem(EquipSlot.RFinger, default, state);
break;
case (true, true) when _lastItems[EquipSlot.LFinger.ToIndex()].Valid:
Glamourer.Log.Information($"Re-Applying {last.Name} to Left Finger.");
SetLastItem(EquipSlot.LFinger, default, state);
break;
}
return;
default:
if (ImGui.GetIO().KeyCtrl && last.Valid)
{
Glamourer.Log.Information($"Re-Applying {last.Name} to {slot.ToName()}.");
SetLastItem(slot, default, state);
}
else
{
Glamourer.Log.Information($"Applying {item.Name} to {slot.ToName()}.");
SetLastItem(slot, item, state);
}
return;
}
return;
}
}
private void SetLastItem(EquipSlot slot, EquipItem item, ActorState state)
{
ref var last = ref _lastItems[slot.ToIndex()];
if (!item.Valid)
{
last = default;
}
else
{
var oldItem = state.ModelData.Item(slot);
if (oldItem.Id != item.Id)
_lastItems[slot.ToIndex()] = oldItem;
}
}
}

View file

@ -75,7 +75,7 @@ public class ActorPanel
if (!child || _state == null) if (!child || _state == null)
return; return;
if (_customizationDrawer.Draw(_state.Data.Customize, false)) if (_customizationDrawer.Draw(_state.ModelData.Customize, false))
{ {
} }
// if (_currentData.Valid) // if (_currentData.Valid)

View file

@ -41,6 +41,8 @@ public unsafe class DebugTab : ITab
private readonly DesignManager _designManager; private readonly DesignManager _designManager;
private readonly DesignFileSystem _designFileSystem; private readonly DesignFileSystem _designFileSystem;
private readonly PenumbraChangedItemTooltip _penumbraTooltip;
private readonly StateManager _state; private readonly StateManager _state;
private int _gameObjectIndex; private int _gameObjectIndex;
@ -51,7 +53,8 @@ public unsafe class DebugTab : ITab
public DebugTab(ChangeCustomizeService changeCustomizeService, VisorService visorService, ObjectTable objects, public DebugTab(ChangeCustomizeService changeCustomizeService, VisorService visorService, ObjectTable objects,
UpdateSlotService updateSlotService, WeaponService weaponService, PenumbraService penumbra, UpdateSlotService updateSlotService, WeaponService weaponService, PenumbraService penumbra,
ActorService actors, ItemManager items, CustomizationService customization, ObjectManager objectManager, ActorService actors, ItemManager items, CustomizationService customization, ObjectManager objectManager,
DesignFileSystem designFileSystem, DesignManager designManager, StateManager state, Configuration config) DesignFileSystem designFileSystem, DesignManager designManager, StateManager state, Configuration config,
PenumbraChangedItemTooltip penumbraTooltip)
{ {
_changeCustomizeService = changeCustomizeService; _changeCustomizeService = changeCustomizeService;
_visorService = visorService; _visorService = visorService;
@ -67,6 +70,7 @@ public unsafe class DebugTab : ITab
_designManager = designManager; _designManager = designManager;
_state = state; _state = state;
_config = config; _config = config;
_penumbraTooltip = penumbraTooltip;
} }
public ReadOnlySpan<byte> Label public ReadOnlySpan<byte> Label
@ -434,6 +438,23 @@ public unsafe class DebugTab : ITab
if (ImGui.SmallButton("Redraw")) if (ImGui.SmallButton("Redraw"))
_penumbra.RedrawObject(_objects.GetObjectAddress(_gameObjectIndex), RedrawType.Redraw); _penumbra.RedrawObject(_objects.GetObjectAddress(_gameObjectIndex), RedrawType.Redraw);
} }
ImGuiUtil.DrawTableColumn("Last Tooltip Date");
ImGuiUtil.DrawTableColumn(_penumbraTooltip.LastTooltip > DateTime.MinValue ? _penumbraTooltip.LastTooltip.ToLongTimeString() : "Never");
ImGui.TableNextColumn();
ImGuiUtil.DrawTableColumn("Last Click Date");
ImGuiUtil.DrawTableColumn(_penumbraTooltip.LastClick > DateTime.MinValue ? _penumbraTooltip.LastClick.ToLongTimeString() : "Never");
ImGui.TableNextColumn();
ImGui.Separator();
ImGui.Separator();
foreach (var (slot, item) in _penumbraTooltip.LastItems)
{
ImGuiUtil.DrawTableColumn($"{slot.ToName()} Revert-Item");
ImGuiUtil.DrawTableColumn(item.Valid ? item.Name : "None");
ImGui.TableNextColumn();
}
} }
#endregion #endregion
@ -990,7 +1011,7 @@ public unsafe class DebugTab : ITab
continue; continue;
if (_state.GetOrCreate(identifier, actors.Objects[0], out var state)) if (_state.GetOrCreate(identifier, actors.Objects[0], out var state))
DrawDesignData(state.Data); DrawDesignData(state.ModelData);
else else
ImGui.TextUnformatted("Invalid actor."); ImGui.TextUnformatted("Invalid actor.");
} }
@ -1006,7 +1027,7 @@ public unsafe class DebugTab : ITab
{ {
using var t = ImRaii.TreeNode(identifier.ToString()); using var t = ImRaii.TreeNode(identifier.ToString());
if (t) if (t)
DrawDesignData(state.Data); DrawDesignData(state.ModelData);
} }
} }

View file

@ -35,6 +35,21 @@ public sealed class DesignFileSystemSelector : FileSystemSelector<Design, Design
_event.Unsubscribe(OnDesignChange); _event.Unsubscribe(OnDesignChange);
} }
public override ISortMode<Design> SortMode
=> _config.SortMode;
protected override uint ExpandedFolderColor
=> ColorId.FolderExpanded.Value();
protected override uint CollapsedFolderColor
=> ColorId.FolderCollapsed.Value();
protected override uint FolderLineColor
=> ColorId.FolderLine.Value();
protected override bool FoldersDefaultOpen
=> _config.OpenFoldersByDefault;
private void OnDesignChange(DesignChanged.Type type, Design design, object? oldData) private void OnDesignChange(DesignChanged.Type type, Design design, object? oldData)
{ {
switch (type) switch (type)

View file

@ -1,10 +1,10 @@
using System; using System;
using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Dalamud.Interface; using Dalamud.Interface;
using Glamourer.Gui.Tabs.DesignTab;
using Glamourer.State;
using ImGuiNET; using ImGuiNET;
using OtterGui; using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii; using OtterGui.Raii;
using OtterGui.Widgets; using OtterGui.Widgets;
@ -12,10 +12,16 @@ namespace Glamourer.Gui.Tabs;
public class SettingsTab : ITab public class SettingsTab : ITab
{ {
private readonly Configuration _config; private readonly Configuration _config;
private readonly DesignFileSystemSelector _selector;
private readonly StateListener _stateListener;
public SettingsTab(Configuration config) public SettingsTab(Configuration config, DesignFileSystemSelector selector, StateListener stateListener)
=> _config = config; {
_config = config;
_selector = selector;
_stateListener = stateListener;
}
public ReadOnlySpan<byte> Label public ReadOnlySpan<byte> Label
=> "Settings"u8; => "Settings"u8;
@ -25,7 +31,7 @@ public class SettingsTab : ITab
using var child = ImRaii.Child("MainWindowChild"); using var child = ImRaii.Child("MainWindowChild");
if (!child) if (!child)
return; return;
Checkbox("Enabled", "Enable main functionality of keeping and applying state.", _stateListener.Enabled, _stateListener.Enable);
Checkbox("Restricted Gear Protection", Checkbox("Restricted Gear Protection",
"Use gender- and race-appropriate models when detecting certain items not available for a characters current gender and race.", "Use gender- and race-appropriate models when detecting certain items not available for a characters current gender and race.",
_config.UseRestrictedGearProtection, v => _config.UseRestrictedGearProtection = v); _config.UseRestrictedGearProtection, v => _config.UseRestrictedGearProtection = v);
@ -33,6 +39,10 @@ public class SettingsTab : ITab
"A modifier you need to hold while clicking the Delete Design button for it to take effect.", 100 * ImGuiHelpers.GlobalScale, "A modifier you need to hold while clicking the Delete Design button for it to take effect.", 100 * ImGuiHelpers.GlobalScale,
_config.DeleteDesignModifier, v => _config.DeleteDesignModifier = v)) _config.DeleteDesignModifier, v => _config.DeleteDesignModifier = v))
_config.Save(); _config.Save();
DrawFolderSortType();
Checkbox("Auto-Open Design Folders",
"Have design folders open or closed as their default state after launching.", _config.OpenFoldersByDefault,
v => _config.OpenFoldersByDefault = v);
Checkbox("Debug Mode", "Show the debug tab. Only useful for debugging or advanced use.", _config.DebugMode, v => _config.DebugMode = v); Checkbox("Debug Mode", "Show the debug tab. Only useful for debugging or advanced use.", _config.DebugMode, v => _config.DebugMode = v);
DrawColorSettings(); DrawColorSettings();
@ -70,4 +80,28 @@ public class SettingsTab : ITab
ImGui.SameLine(); ImGui.SameLine();
ImGuiUtil.LabeledHelpMarker(label, tooltip); ImGuiUtil.LabeledHelpMarker(label, tooltip);
} }
/// <summary> Different supported sort modes as a combo. </summary>
private void DrawFolderSortType()
{
var sortMode = _config.SortMode;
ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale);
using (var combo = ImRaii.Combo("##sortMode", sortMode.Name))
{
if (combo)
foreach (var val in Configuration.Constants.ValidSortModes)
{
if (ImGui.Selectable(val.Name, val.GetType() == sortMode.GetType()) && val.GetType() != sortMode.GetType())
{
_config.SortMode = val;
_selector.SetFilterDirty();
_config.Save();
}
ImGuiUtil.HoverTooltip(val.Description);
}
}
ImGuiUtil.LabeledHelpMarker("Sort Mode", "Choose the sort mode for the mod selector in the designs tab.");
}
} }

View file

@ -113,6 +113,17 @@ public class ObjectManager : IReadOnlyDictionary<ActorIdentifier, ActorData>
public Actor Player public Actor Player
=> _objects.GetObjectAddress(0); => _objects.GetObjectAddress(0);
public (ActorIdentifier Identifier, ActorData Data) PlayerData
{
get
{
Update();
return Player.Identifier(_actors.AwaitedService, out var ident) && _identifiers.TryGetValue(ident, out var data)
? (ident, data)
: (ActorIdentifier.Invalid, ActorData.Invalid);
}
}
public IEnumerator<KeyValuePair<ActorIdentifier, ActorData>> GetEnumerator() public IEnumerator<KeyValuePair<ActorIdentifier, ActorData>> GetEnumerator()
=> Identifiers.GetEnumerator(); => Identifiers.GetEnumerator();

View file

@ -10,11 +10,11 @@ namespace Glamourer.Interop;
public unsafe class UpdateSlotService : IDisposable public unsafe class UpdateSlotService : IDisposable
{ {
public readonly UpdatedSlot Event; public readonly SlotUpdating Event;
public UpdateSlotService(UpdatedSlot updatedSlot) public UpdateSlotService(SlotUpdating slotUpdating)
{ {
Event = updatedSlot; Event = slotUpdating;
SignatureHelper.Initialise(this); SignatureHelper.Initialise(this);
_flagSlotForUpdateHook.Enable(); _flagSlotForUpdateHook.Enable();
} }

View file

@ -23,13 +23,7 @@ public class VisorService : IDisposable
/// <summary> Obtain the current state of the Visor for the given draw object (true: toggled). </summary> /// <summary> Obtain the current state of the Visor for the given draw object (true: toggled). </summary>
public unsafe bool GetVisorState(Model characterBase) public unsafe bool GetVisorState(Model characterBase)
{ => characterBase.IsCharacterBase && characterBase.AsCharacterBase->VisorToggled;
if (!characterBase.IsCharacterBase)
return false;
// TODO: use client structs.
return (characterBase.AsCharacterBase->UnkFlags_01 & Offsets.DrawObjectVisorStateFlag) != 0;
}
/// <summary> Manually set the state of the Visor for the given draw object. </summary> /// <summary> Manually set the state of the Visor for the given draw object. </summary>
/// <param name="human"> The draw object. </param> /// <param name="human"> The draw object. </param>
@ -45,7 +39,6 @@ public class VisorService : IDisposable
if (oldState == on) if (oldState == on)
return false; return false;
SetupVisorHook(human, (ushort)human.AsHuman->HeadSetID, on); SetupVisorHook(human, (ushort)human.AsHuman->HeadSetID, on);
return true; return true;
} }

View file

@ -2,6 +2,7 @@
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.Utility.Signatures; using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Character;
using Glamourer.Events;
using Glamourer.Interop.Structs; using Glamourer.Interop.Structs;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
@ -10,8 +11,11 @@ namespace Glamourer.Interop;
public unsafe class WeaponService : IDisposable public unsafe class WeaponService : IDisposable
{ {
public WeaponService() private readonly WeaponLoading _event;
public WeaponService(WeaponLoading @event)
{ {
_event = @event;
SignatureHelper.Initialise(this); SignatureHelper.Initialise(this);
_loadWeaponHook = Hook<LoadWeaponDelegate>.FromAddress((nint)DrawDataContainer.MemberFunctionPointers.LoadWeapon, LoadWeaponDetour); _loadWeaponHook = Hook<LoadWeaponDelegate>.FromAddress((nint)DrawDataContainer.MemberFunctionPointers.LoadWeapon, LoadWeaponDetour);
_loadWeaponHook.Enable(); _loadWeaponHook.Enable();
@ -22,20 +26,38 @@ public unsafe class WeaponService : IDisposable
_loadWeaponHook.Dispose(); _loadWeaponHook.Dispose();
} }
// Weapons for a specific character are reloaded with this function.
// slot is 0 for main hand, 1 for offhand, 2 for combat effects.
// weapon argument is the new weapon data.
// redrawOnEquality controls whether the game does anything if the new weapon is identical to the old one.
// skipGameObject seems to control whether the new weapons are written to the game object or just influence the draw object. (1 = skip, 0 = change)
// unk4 seemed to be the same as unk1.
private delegate void LoadWeaponDelegate(DrawDataContainer* drawData, uint slot, ulong weapon, byte redrawOnEquality, byte unk2, private delegate void LoadWeaponDelegate(DrawDataContainer* drawData, uint slot, ulong weapon, byte redrawOnEquality, byte unk2,
byte skipGameObject, byte unk4); byte skipGameObject, byte unk4);
private readonly Hook<LoadWeaponDelegate> _loadWeaponHook; private readonly Hook<LoadWeaponDelegate> _loadWeaponHook;
private void LoadWeaponDetour(DrawDataContainer* drawData, uint slot, ulong weapon, byte redrawOnEquality, byte unk2, byte skipGameObject,
private void LoadWeaponDetour(DrawDataContainer* drawData, uint slot, ulong weaponValue, byte redrawOnEquality, byte unk2,
byte skipGameObject,
byte unk4) byte unk4)
{ {
var actor = (Actor) (nint)(drawData + 1); var actor = (Actor)((nint*)drawData)[1];
var weapon = new CharacterWeapon(weaponValue);
var equipSlot = slot switch
{
0 => EquipSlot.MainHand,
1 => EquipSlot.OffHand,
_ => EquipSlot.Unknown,
};
// First call the regular function. // First call the regular function.
_loadWeaponHook.Original(drawData, slot, weapon, redrawOnEquality, unk2, skipGameObject, unk4); if (equipSlot is not EquipSlot.Unknown)
_event.Invoke(actor, equipSlot, ref weapon);
_loadWeaponHook.Original(drawData, slot, weapon.Value, redrawOnEquality, unk2, skipGameObject, unk4);
Glamourer.Log.Excessive( Glamourer.Log.Excessive(
$"Weapon reloaded for 0x{actor.Address:X} with attributes {slot} {weapon:X14}, {redrawOnEquality}, {unk2}, {skipGameObject}, {unk4}"); $"Weapon reloaded for 0x{actor.Address:X} ({actor.Utf8Name}) with attributes {slot} {weapon.Value:X14}, {redrawOnEquality}, {unk2}, {skipGameObject}, {unk4}");
} }
// Load a specific weapon for a character by its data and slot. // Load a specific weapon for a character by its data and slot.

View file

@ -45,11 +45,9 @@ public class ItemManager : IDisposable
RestrictedGear.Dispose(); RestrictedGear.Dispose();
} }
public (bool, CharacterArmor) ResolveRestrictedGear(CharacterArmor armor, EquipSlot slot, Race race, Gender gender) public (bool, CharacterArmor) ResolveRestrictedGear(CharacterArmor armor, EquipSlot slot, Race race, Gender gender)
=> _config.UseRestrictedGearProtection ? RestrictedGear.ResolveRestricted(armor, slot, race, gender) : (false, armor); => _config.UseRestrictedGearProtection ? RestrictedGear.ResolveRestricted(armor, slot, race, gender) : (false, armor);
public static uint NothingId(EquipSlot slot) public static uint NothingId(EquipSlot slot)
=> uint.MaxValue - 128 - (uint)slot.ToSlot(); => uint.MaxValue - 128 - (uint)slot.ToSlot();

View file

@ -51,9 +51,10 @@ public static class ServiceManager
private static IServiceCollection AddEvents(this IServiceCollection services) private static IServiceCollection AddEvents(this IServiceCollection services)
=> services.AddSingleton<VisorStateChanged>() => services.AddSingleton<VisorStateChanged>()
.AddSingleton<UpdatedSlot>() .AddSingleton<SlotUpdating>()
.AddSingleton<DesignChanged>() .AddSingleton<DesignChanged>()
.AddSingleton<StateChanged>(); .AddSingleton<StateChanged>()
.AddSingleton<WeaponLoading>();
private static IServiceCollection AddData(this IServiceCollection services) private static IServiceCollection AddData(this IServiceCollection services)
=> services.AddSingleton<IdentifierService>() => services.AddSingleton<IdentifierService>()
@ -75,7 +76,8 @@ public static class ServiceManager
.AddSingleton<DesignFileSystem>(); .AddSingleton<DesignFileSystem>();
private static IServiceCollection AddState(this IServiceCollection services) private static IServiceCollection AddState(this IServiceCollection services)
=> services.AddSingleton<StateManager>(); => services.AddSingleton<StateManager>()
.AddSingleton<StateListener>();
private static IServiceCollection AddUi(this IServiceCollection services) private static IServiceCollection AddUi(this IServiceCollection services)
=> services.AddSingleton<DebugTab>() => services.AddSingleton<DebugTab>()
@ -88,7 +90,8 @@ public static class ServiceManager
.AddSingleton<CustomizationDrawer>() .AddSingleton<CustomizationDrawer>()
.AddSingleton<DesignFileSystemSelector>() .AddSingleton<DesignFileSystemSelector>()
.AddSingleton<DesignPanel>() .AddSingleton<DesignPanel>()
.AddSingleton<DesignTab>(); .AddSingleton<DesignTab>()
.AddSingleton<PenumbraChangedItemTooltip>();
private static IServiceCollection AddApi(this IServiceCollection services) private static IServiceCollection AddApi(this IServiceCollection services)
=> services.AddSingleton<CommandService>(); => services.AddSingleton<CommandService>();

View file

@ -20,7 +20,8 @@ public class ActorState
} }
public ActorIdentifier Identifier { get; internal init; } public ActorIdentifier Identifier { get; internal init; }
public DesignData Data; public DesignData ActorData;
public DesignData ModelData;
private readonly StateChanged.Source[] _sources = Enumerable private readonly StateChanged.Source[] _sources = Enumerable
.Repeat(StateChanged.Source.Game, EquipFlagExtensions.NumEquipFlags + CustomizationExtensions.NumIndices + 4).ToArray(); .Repeat(StateChanged.Source.Game, EquipFlagExtensions.NumEquipFlags + CustomizationExtensions.NumIndices + 4).ToArray();

View file

@ -0,0 +1,283 @@
using System;
using Glamourer.Customization;
using Glamourer.Events;
using Glamourer.Interop.Penumbra;
using Glamourer.Interop.Structs;
using Glamourer.Services;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.State;
public class StateListener : IDisposable
{
private readonly Configuration _config;
private readonly ActorService _actors;
private readonly StateManager _manager;
private readonly ItemManager _items;
private readonly PenumbraService _penumbra;
private readonly SlotUpdating _slotUpdating;
private readonly WeaponLoading _weaponLoading;
public bool Enabled
{
get => _config.Enabled;
set => Enable(value);
}
public StateListener(StateManager manager, ItemManager items, PenumbraService penumbra, ActorService actors, Configuration config,
SlotUpdating slotUpdating, WeaponLoading weaponLoading)
{
_manager = manager;
_items = items;
_penumbra = penumbra;
_actors = actors;
_config = config;
_slotUpdating = slotUpdating;
_weaponLoading = weaponLoading;
if (Enabled)
Subscribe();
}
public void Enable(bool value)
{
if (value == Enabled)
return;
_config.Enabled = value;
_config.Save();
if (value)
Subscribe();
else
Unsubscribe();
}
void IDisposable.Dispose()
{
if (Enabled)
Unsubscribe();
}
private unsafe void OnCreatingCharacterBase(nint actorPtr, string _, nint modelPtr, nint customizePtr, nint equipDataPtr)
{
// TODO: Fixed Designs.
var actor = (Actor)actorPtr;
var identifier = actor.GetIdentifier(_actors.AwaitedService);
if (*(int*)modelPtr != actor.AsCharacter->ModelCharaId)
return;
ref var customize = ref *(Customize*)customizePtr;
if (_manager.TryGetValue(identifier, out var state))
{
ApplyCustomize(actor, state, ref customize);
ApplyEquipment(actor, state, (CharacterArmor*)equipDataPtr);
if (_config.UseRestrictedGearProtection)
ProtectRestrictedGear(equipDataPtr, customize.Race, customize.Gender);
}
else if (_config.UseRestrictedGearProtection && *(uint*)modelPtr == 0)
{
ProtectRestrictedGear(equipDataPtr, customize.Race, customize.Gender);
}
}
private void OnSlotUpdating(Model model, EquipSlot slot, Ref<CharacterArmor> armor, Ref<ulong> returnValue)
{
// TODO handle hat state
// TODO handle fixed designs
var actor = _penumbra.GameObjectFromDrawObject(model);
var customize = model.GetCustomize();
if (actor.Identifier(_actors.AwaitedService, out var identifier)
&& _manager.TryGetValue(identifier, out var state))
ApplyEquipmentPiece(actor, state, slot, ref armor.Value);
var (replaced, replacedArmor) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender);
if (replaced)
armor.Assign(replacedArmor);
}
private void OnWeaponLoading(Actor actor, EquipSlot slot, Ref<CharacterWeapon> weapon)
{
if (!actor.Identifier(_actors.AwaitedService, out var identifier)
|| !_manager.TryGetValue(identifier, out var state))
return;
ref var actorWeapon = ref weapon.Value;
var stateItem = state.ModelData.Item(slot);
if (actorWeapon.Set.Value != stateItem.ModelId.Value
|| actorWeapon.Type.Value != stateItem.WeaponType
|| actorWeapon.Variant != stateItem.Variant)
{
var oldActorItem = state.ActorData.Item(slot);
if (oldActorItem.ModelId.Value == actorWeapon.Set.Value
&& oldActorItem.WeaponType.Value == actorWeapon.Type.Value
&& oldActorItem.Variant == actorWeapon.Variant)
{
actorWeapon.Set = stateItem.ModelId;
actorWeapon.Type = stateItem.WeaponType;
actorWeapon.Variant = stateItem.Variant;
}
else
{
var identified = _items.Identify(slot, actorWeapon.Set, actorWeapon.Type, (byte)actorWeapon.Variant,
slot == EquipSlot.OffHand ? state.ActorData.Item(EquipSlot.MainHand).Type : FullEquipType.Unknown);
state.ActorData.SetItem(slot, identified);
if (state[slot, false] is not StateChanged.Source.Fixed)
{
state.ModelData.SetItem(slot, identified);
state[slot, false] = StateChanged.Source.Game;
}
else
{
actorWeapon.Set = stateItem.ModelId;
actorWeapon.Type = stateItem.Variant;
actorWeapon.Variant = stateItem.Variant;
}
}
}
var stateStain = state.ModelData.Stain(slot);
if (actorWeapon.Stain.Value != stateStain.Value)
{
var oldActorStain = state.ActorData.Stain(slot);
if (state[slot, true] is not StateChanged.Source.Fixed)
{
state.ModelData.SetStain(slot, actorWeapon.Stain);
state[slot, true] = StateChanged.Source.Game;
}
else
{
actorWeapon.Stain = stateStain;
}
}
}
private void ApplyCustomize(Actor actor, ActorState state, ref Customize customize)
{
var actorCustomize = actor.GetCustomize();
ref var oldActorCustomize = ref state.ActorData.Customize;
ref var stateCustomize = ref state.ModelData.Customize;
foreach (var idx in Enum.GetValues<CustomizeIndex>())
{
var value = customize[idx];
var actorValue = actorCustomize[idx];
if (value.Value != actorValue.Value)
continue;
var stateValue = stateCustomize[idx];
if (value.Value == stateValue.Value)
continue;
if (oldActorCustomize[idx].Value == actorValue.Value)
{
customize[idx] = stateValue;
}
else
{
oldActorCustomize[idx] = actorValue;
if (state[idx] is StateChanged.Source.Fixed)
{
state.ModelData.Customize[idx] = value;
state[idx] = StateChanged.Source.Game;
}
else
{
customize[idx] = stateValue;
}
}
}
}
private unsafe void ApplyEquipment(Actor actor, ActorState state, CharacterArmor* equipData)
{
// TODO: Handle hat state
foreach (var slot in EquipSlotExtensions.EqdpSlots)
ApplyEquipmentPiece(actor, state, slot, ref *equipData++);
}
private void ApplyEquipmentPiece(Actor actor, ActorState state, EquipSlot slot, ref CharacterArmor armor)
{
var actorArmor = actor.GetArmor(slot);
if (armor.Value != actorArmor.Value)
return;
var stateArmor = state.ModelData.Item(slot);
if (armor.Set.Value != stateArmor.ModelId.Value || armor.Variant != stateArmor.Variant)
{
var oldActorArmor = state.ActorData.Item(slot);
if (oldActorArmor.ModelId.Value == actorArmor.Set.Value && oldActorArmor.Variant == actorArmor.Variant)
{
armor.Set = stateArmor.ModelId;
armor.Variant = stateArmor.Variant;
}
else
{
var identified = _items.Identify(slot, actorArmor.Set, actorArmor.Variant);
state.ActorData.SetItem(slot, identified);
if (state[slot, false] is not StateChanged.Source.Fixed)
{
state.ModelData.SetItem(slot, identified);
state[slot, false] = StateChanged.Source.Game;
}
else
{
armor.Set = stateArmor.ModelId;
armor.Variant = stateArmor.Variant;
}
}
}
var stateStain = state.ModelData.Stain(slot);
if (armor.Stain.Value != stateStain.Value)
{
var oldActorStain = state.ActorData.Stain(slot);
if (oldActorStain.Value == actorArmor.Stain.Value)
{
armor.Stain = stateStain;
}
else
{
state.ActorData.SetStain(slot, actorArmor.Stain);
if (state[slot, true] is not StateChanged.Source.Fixed)
{
state.ModelData.SetStain(slot, actorArmor.Stain);
state[slot, true] = StateChanged.Source.Game;
}
else
{
armor.Stain = stateStain;
}
}
}
}
private unsafe void ProtectRestrictedGear(nint equipDataPtr, Race race, Gender gender)
{
var idx = 0;
var ptr = (CharacterArmor*)equipDataPtr;
for (var end = ptr + 10; ptr < end; ++ptr)
{
var (_, newArmor) =
_items.RestrictedGear.ResolveRestricted(*ptr, EquipSlotExtensions.EqdpSlots[idx++], race, gender);
*ptr = newArmor;
}
}
private void Subscribe()
{
_penumbra.CreatingCharacterBase += OnCreatingCharacterBase;
_slotUpdating.Subscribe(OnSlotUpdating, SlotUpdating.Priority.StateListener);
_weaponLoading.Subscribe(OnWeaponLoading, WeaponLoading.Priority.StateListener);
}
private void Unsubscribe()
{
_penumbra.CreatingCharacterBase -= OnCreatingCharacterBase;
_slotUpdating.Unsubscribe(OnSlotUpdating);
_weaponLoading.Unsubscribe(OnWeaponLoading);
}
}

View file

@ -17,7 +17,7 @@ using Penumbra.GameData.Structs;
namespace Glamourer.State; namespace Glamourer.State;
public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>, IDisposable public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
{ {
private readonly ActorService _actors; private readonly ActorService _actors;
private readonly ItemManager _items; private readonly ItemManager _items;
@ -26,57 +26,20 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>, ID
private readonly StateChanged _event; private readonly StateChanged _event;
private readonly PenumbraService _penumbra; private readonly PenumbraService _penumbra;
private readonly UpdatedSlot _updatedSlot;
private readonly Dictionary<ActorIdentifier, ActorState> _states = new(); private readonly Dictionary<ActorIdentifier, ActorState> _states = new();
public StateManager(ActorService actors, ItemManager items, CustomizationService customizations, VisorService visor, StateChanged @event, public StateManager(ActorService actors, ItemManager items, CustomizationService customizations, VisorService visor, StateChanged @event,
UpdatedSlot updatedSlot, PenumbraService penumbra) PenumbraService penumbra)
{ {
_actors = actors; _actors = actors;
_items = items; _items = items;
_customizations = customizations; _customizations = customizations;
_visor = visor; _visor = visor;
_event = @event; _event = @event;
_updatedSlot = updatedSlot;
_penumbra = penumbra; _penumbra = penumbra;
_updatedSlot.Subscribe(OnSlotUpdated, UpdatedSlot.Priority.StateManager);
}
public void Dispose()
{
_updatedSlot.Unsubscribe(OnSlotUpdated);
}
private unsafe void OnSlotUpdated(Model model, EquipSlot slot, Ref<CharacterArmor> armor, Ref<ulong> returnValue)
{
var actor = _penumbra.GameObjectFromDrawObject(model);
var customize = model.GetCustomize();
if (!actor.AsCharacter->DrawData.IsHatHidden && actor.Identifier(_actors.AwaitedService, out var identifier) && _states.TryGetValue(identifier, out var state))
{
ref var armorState = ref state[slot, false];
ref var stainState = ref state[slot, true];
if (armorState != StateChanged.Source.Fixed)
{
armorState = StateChanged.Source.Game;
var current = state.Data.Item(slot);
if (current.ModelId.Value != armor.Value.Set.Value || current.Variant != armor.Value.Variant)
{
var item = _items.Identify(slot, armor.Value.Set, armor.Value.Variant);
state.Data.SetItem(slot, item);
}
}
if (stainState != StateChanged.Source.Fixed)
{
stainState = StateChanged.Source.Game;
state.Data.SetStain(slot, armor.Value.Stain);
}
}
var (replaced, replacedArmor) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender);
if (replaced)
armor.Assign(replacedArmor);
} }
public bool GetOrCreate(Actor actor, [NotNullWhen(true)] out ActorState? state) public bool GetOrCreate(Actor actor, [NotNullWhen(true)] out ActorState? state)
@ -90,7 +53,12 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>, ID
try try
{ {
var designData = FromActor(actor); var designData = FromActor(actor);
_states.Add(identifier, new ActorState(identifier) { Data = designData }); state = new ActorState(identifier)
{
ModelData = designData,
ActorData = designData
};
_states.Add(identifier, state);
return true; return true;
} }
catch (Exception ex) catch (Exception ex)
@ -100,84 +68,68 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>, ID
} }
} }
public unsafe void Update(ref DesignData data, Actor actor) public void UpdateEquip(ActorState state, EquipSlot slot, CharacterArmor armor)
{
var current = state.ModelData.Item(slot);
if (armor.Set.Value != current.ModelId.Value || armor.Variant != current.Variant)
{
var item = _items.Identify(slot, armor.Set, armor.Variant);
state.ModelData.SetItem(slot, item);
}
state.ModelData.SetStain(slot, armor.Stain);
}
public void UpdateWeapon(ActorState state, EquipSlot slot, CharacterWeapon weapon)
{
var current = state.ModelData.Item(slot);
if (weapon.Set.Value != current.ModelId.Value || weapon.Variant != current.Variant || weapon.Type.Value != current.WeaponType.Value)
{
var item = _items.Identify(slot, weapon.Set, weapon.Type, (byte)weapon.Variant,
slot == EquipSlot.OffHand ? state.ModelData.Item(EquipSlot.MainHand).Type : FullEquipType.Unknown);
state.ModelData.SetItem(slot, item);
}
state.ModelData.SetStain(slot, weapon.Stain);
}
public unsafe void Update(ActorState state, Actor actor)
{ {
if (!actor.IsCharacter) if (!actor.IsCharacter)
return; return;
if (actor.AsCharacter->ModelCharaId != data.ModelId) if (actor.AsCharacter->ModelCharaId != state.ModelData.ModelId)
return; return;
var model = actor.Model; var model = actor.Model;
static bool EqualArmor(CharacterArmor armor, EquipItem item) state.ModelData.SetHatVisible(!actor.AsCharacter->DrawData.IsHatHidden);
=> armor.Set.Value == item.ModelId.Value && armor.Variant == item.Variant; state.ModelData.SetIsWet(actor.AsCharacter->IsGPoseWet);
state.ModelData.SetWeaponVisible(!actor.AsCharacter->DrawData.IsWeaponHidden);
static bool EqualWeapon(CharacterWeapon weapon, EquipItem item)
=> weapon.Set.Value == item.ModelId.Value && weapon.Type.Value == item.WeaponType.Value && weapon.Variant == item.Variant;
data.SetHatVisible(!actor.AsCharacter->DrawData.IsHatHidden);
data.SetIsWet(actor.AsCharacter->IsGPoseWet);
data.SetWeaponVisible(!actor.AsCharacter->DrawData.IsWeaponHidden);
CharacterWeapon main;
CharacterWeapon off;
if (model.IsHuman) if (model.IsHuman)
{ {
var head = data.IsHatVisible() ? model.GetArmor(EquipSlot.Head) : actor.GetArmor(EquipSlot.Head); var head = state.ModelData.IsHatVisible() ? model.GetArmor(EquipSlot.Head) : actor.GetArmor(EquipSlot.Head);
data.SetStain(EquipSlot.Head, head.Stain); UpdateEquip(state, EquipSlot.Head, head);
if (!EqualArmor(head, data.Item(EquipSlot.Head)))
{
var headItem = _items.Identify(EquipSlot.Head, head.Set, head.Variant);
data.SetItem(EquipSlot.Head, headItem);
}
foreach (var slot in EquipSlotExtensions.EqdpSlots.Skip(1)) foreach (var slot in EquipSlotExtensions.EqdpSlots.Skip(1))
{ UpdateEquip(state, slot, model.GetArmor(slot));
var armor = model.GetArmor(slot);
data.SetStain(slot, armor.Stain);
if (EqualArmor(armor, data.Item(slot)))
continue;
var item = _items.Identify(slot, armor.Set, armor.Variant); state.ModelData.Customize = model.GetCustomize();
data.SetItem(slot, item); var (_, _, main, off) = model.GetWeapons(actor);
} UpdateWeapon(state, EquipSlot.MainHand, main);
UpdateWeapon(state, EquipSlot.OffHand, off);
data.Customize = model.GetCustomize(); state.ModelData.SetVisor(_visor.GetVisorState(model));
(_, _, main, off) = model.GetWeapons(actor);
data.SetVisor(_visor.GetVisorState(model));
} }
else else
{ {
foreach (var slot in EquipSlotExtensions.EqdpSlots) foreach (var slot in EquipSlotExtensions.EqdpSlots)
{ UpdateEquip(state, slot, actor.GetArmor(slot));
var armor = actor.GetArmor(slot);
data.SetStain(slot, armor.Stain);
if (EqualArmor(armor, data.Item(slot)))
continue;
var item = _items.Identify(slot, armor.Set, armor.Variant); state.ModelData.Customize = actor.GetCustomize();
data.SetItem(slot, item); UpdateWeapon(state, EquipSlot.MainHand, actor.GetMainhand());
} UpdateWeapon(state, EquipSlot.OffHand, actor.GetOffhand());
state.ModelData.SetVisor(actor.AsCharacter->DrawData.IsVisorToggled);
data.Customize = actor.GetCustomize();
main = actor.GetMainhand();
off = actor.GetOffhand();
data.SetVisor(actor.AsCharacter->DrawData.IsVisorToggled);
}
data.SetStain(EquipSlot.MainHand, main.Stain);
data.SetStain(EquipSlot.OffHand, off.Stain);
if (!EqualWeapon(main, data.Item(EquipSlot.MainHand)))
{
var mainItem = _items.Identify(EquipSlot.MainHand, main.Set, main.Type, (byte)main.Variant);
data.SetItem(EquipSlot.MainHand, mainItem);
}
if (!EqualWeapon(off, data.Item(EquipSlot.OffHand)))
{
var offItem = _items.Identify(EquipSlot.OffHand, off.Set, off.Type, (byte)off.Variant, data.Item(EquipSlot.MainHand).Type);
data.SetItem(EquipSlot.OffHand, offItem);
} }
} }
@ -281,11 +233,11 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>, ID
if (s is StateChanged.Source.Fixed && source is StateChanged.Source.Game) if (s is StateChanged.Source.Fixed && source is StateChanged.Source.Game)
return; return;
var oldValue = state.Data.Customize[idx]; var oldValue = state.ModelData.Customize[idx];
if (oldValue == value && !force) if (oldValue == value && !force)
return; return;
state.Data.Customize[idx] = value; state.ModelData.Customize[idx] = value;
Glamourer.Log.Excessive( Glamourer.Log.Excessive(
$"Changed customize {idx.ToDefaultName()} for {state.Identifier} ({string.Join(", ", data.Objects.Select(o => $"0x{o.Address}"))}) from {oldValue.Value} to {value.Value}."); $"Changed customize {idx.ToDefaultName()} for {state.Identifier} ({string.Join(", ", data.Objects.Select(o => $"0x{o.Address}"))}) from {oldValue.Value} to {value.Value}.");