mirror of
https://github.com/Ottermandias/Glamourer.git
synced 2025-12-12 18:27:24 +01:00
.
This commit is contained in:
parent
5e6f797af4
commit
803fd1b247
16 changed files with 521 additions and 132 deletions
|
|
@ -72,4 +72,22 @@ public static class EquipFlagExtensions
|
||||||
EquipSlot.LFinger => EquipFlag.LFingerStain,
|
EquipSlot.LFinger => EquipFlag.LFingerStain,
|
||||||
_ => 0,
|
_ => 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static EquipFlag ToBothFlags(this EquipSlot slot)
|
||||||
|
=> slot switch
|
||||||
|
{
|
||||||
|
EquipSlot.MainHand => EquipFlag.Mainhand | EquipFlag.MainhandStain,
|
||||||
|
EquipSlot.OffHand => EquipFlag.Offhand | EquipFlag.OffhandStain,
|
||||||
|
EquipSlot.Head => EquipFlag.Head | EquipFlag.HeadStain,
|
||||||
|
EquipSlot.Body => EquipFlag.Body | EquipFlag.BodyStain,
|
||||||
|
EquipSlot.Hands => EquipFlag.Hands | EquipFlag.HandsStain,
|
||||||
|
EquipSlot.Legs => EquipFlag.Legs | EquipFlag.LegsStain,
|
||||||
|
EquipSlot.Feet => EquipFlag.Feet | EquipFlag.FeetStain,
|
||||||
|
EquipSlot.Ears => EquipFlag.Ears | EquipFlag.EarsStain,
|
||||||
|
EquipSlot.Neck => EquipFlag.Neck | EquipFlag.NeckStain,
|
||||||
|
EquipSlot.Wrists => EquipFlag.Wrist | EquipFlag.WristStain,
|
||||||
|
EquipSlot.RFinger => EquipFlag.RFinger | EquipFlag.RFingerStain,
|
||||||
|
EquipSlot.LFinger => EquipFlag.LFinger | EquipFlag.LFingerStain,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ public class Configuration : IPluginConfiguration, ISavable
|
||||||
public bool Enabled { get; set; } = true;
|
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 bool OpenFoldersByDefault { get; set; } = false;
|
||||||
|
public bool AutoRedrawEquipOnChanges { 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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ public class Design : ISavable
|
||||||
|
|
||||||
/// <summary> Unconditionally apply a design to a designdata. </summary>
|
/// <summary> Unconditionally apply a design to a designdata. </summary>
|
||||||
/// <returns>Whether a redraw is required for the changes to take effect.</returns>
|
/// <returns>Whether a redraw is required for the changes to take effect.</returns>
|
||||||
public bool ApplyDesign(ref DesignData data)
|
public (bool, CustomizeFlag, EquipFlag) ApplyDesign(ref DesignData data)
|
||||||
{
|
{
|
||||||
var modelChanged = data.ModelId != DesignData.ModelId;
|
var modelChanged = data.ModelId != DesignData.ModelId;
|
||||||
data.ModelId = DesignData.ModelId;
|
data.ModelId = DesignData.ModelId;
|
||||||
|
|
@ -52,13 +52,16 @@ public class Design : ISavable
|
||||||
customizeFlags |= index.ToFlag();
|
customizeFlags |= index.ToFlag();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EquipFlag equipFlags = 0;
|
||||||
foreach (var slot in EquipSlotExtensions.EqdpSlots.Append(EquipSlot.MainHand).Append(EquipSlot.OffHand))
|
foreach (var slot in EquipSlotExtensions.EqdpSlots.Append(EquipSlot.MainHand).Append(EquipSlot.OffHand))
|
||||||
{
|
{
|
||||||
if (DoApplyEquip(slot))
|
if (DoApplyEquip(slot))
|
||||||
data.SetItem(slot, DesignData.Item(slot));
|
if (data.SetItem(slot, DesignData.Item(slot)))
|
||||||
|
equipFlags |= slot.ToFlag();
|
||||||
|
|
||||||
if (DoApplyStain(slot))
|
if (DoApplyStain(slot))
|
||||||
data.SetStain(slot, DesignData.Stain(slot));
|
if (data.SetStain(slot, DesignData.Stain(slot)))
|
||||||
|
equipFlags |= slot.ToStainFlag();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DoApplyHatVisible())
|
if (DoApplyHatVisible())
|
||||||
|
|
@ -72,7 +75,7 @@ public class Design : ISavable
|
||||||
|
|
||||||
if (DoApplyWetness())
|
if (DoApplyWetness())
|
||||||
data.SetIsWet(DesignData.IsWet());
|
data.SetIsWet(DesignData.IsWet());
|
||||||
return modelChanged || customizeFlags.RequiresRedraw();
|
return (modelChanged, customizeFlags, equipFlags);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,24 @@ public unsafe struct DesignData
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public readonly CharacterArmor Armor(EquipSlot slot)
|
||||||
|
{
|
||||||
|
fixed (byte* ptr = _equipmentBytes)
|
||||||
|
{
|
||||||
|
var armorPtr = (CharacterArmor*)ptr;
|
||||||
|
return armorPtr[slot.ToIndex()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly CharacterWeapon Weapon(EquipSlot slot)
|
||||||
|
{
|
||||||
|
fixed (byte* ptr = _equipmentBytes)
|
||||||
|
{
|
||||||
|
var armorPtr = (CharacterArmor*)ptr;
|
||||||
|
return armorPtr[slot is EquipSlot.MainHand ? 10 : 11].ToWeapon(_secondaryMainhand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public bool SetItem(EquipSlot slot, EquipItem item)
|
public bool SetItem(EquipSlot slot, EquipItem item)
|
||||||
{
|
{
|
||||||
var index = slot.ToIndex();
|
var index = slot.ToIndex();
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@ using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using Dalamud.Game.ClientState.Objects;
|
using Dalamud.Game.ClientState.Objects;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
using Glamourer.Customization;
|
using Glamourer.Customization;
|
||||||
using Glamourer.Designs;
|
using Glamourer.Designs;
|
||||||
|
using Glamourer.Events;
|
||||||
using Glamourer.Interop;
|
using Glamourer.Interop;
|
||||||
using Glamourer.Interop.Penumbra;
|
using Glamourer.Interop.Penumbra;
|
||||||
using Glamourer.Interop.Structs;
|
using Glamourer.Interop.Structs;
|
||||||
|
|
@ -854,11 +856,81 @@ public unsafe class DebugTab : ITab
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void DrawState(ActorData data, ActorState state)
|
||||||
|
{
|
||||||
|
using var table = ImRaii.Table("##state", 7, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit);
|
||||||
|
if (!table)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ImGuiUtil.DrawTableColumn("Name");
|
||||||
|
ImGuiUtil.DrawTableColumn(state.Identifier.ToString());
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
if (ImGui.Button("Reset"))
|
||||||
|
_state.ResetState(state);
|
||||||
|
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
|
||||||
|
static void PrintRow<T>(string label, T actor, T model, StateChanged.Source source) where T : notnull
|
||||||
|
{
|
||||||
|
ImGuiUtil.DrawTableColumn(label);
|
||||||
|
ImGuiUtil.DrawTableColumn(actor.ToString()!);
|
||||||
|
ImGuiUtil.DrawTableColumn(model.ToString()!);
|
||||||
|
ImGuiUtil.DrawTableColumn(source.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
static string ItemString(in DesignData data, EquipSlot slot)
|
||||||
|
{
|
||||||
|
var item = data.Item(slot);
|
||||||
|
return $"{item.Name} ({item.ModelId.Value}{(item.WeaponType != 0 ? $"-{item.WeaponType.Value}" : string.Empty)}-{item.Variant})";
|
||||||
|
}
|
||||||
|
|
||||||
|
PrintRow("Model ID", state.BaseData.ModelId, state.ModelData.ModelId, state[ActorState.MetaFlag.ModelId]);
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
PrintRow("Wetness", state.BaseData.IsWet(), state.ModelData.IsWet(), state[ActorState.MetaFlag.Wetness]);
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
|
||||||
|
if (state.BaseData.ModelId == 0 && state.ModelData.ModelId == 0)
|
||||||
|
{
|
||||||
|
PrintRow("Hat Visible", state.BaseData.IsHatVisible(), state.ModelData.IsHatVisible(), state[ActorState.MetaFlag.HatState]);
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
PrintRow("Visor Toggled", state.BaseData.IsVisorToggled(), state.ModelData.IsVisorToggled(),
|
||||||
|
state[ActorState.MetaFlag.VisorState]);
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
PrintRow("Weapon Visible", state.BaseData.IsWeaponVisible(), state.ModelData.IsWeaponVisible(),
|
||||||
|
state[ActorState.MetaFlag.WeaponState]);
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand))
|
||||||
|
{
|
||||||
|
PrintRow(slot.ToName(), ItemString(state.BaseData, slot), ItemString(state.ModelData, slot), state[slot, false]);
|
||||||
|
ImGuiUtil.DrawTableColumn(state.BaseData.Stain(slot).Value.ToString());
|
||||||
|
ImGuiUtil.DrawTableColumn(state.ModelData.Stain(slot).Value.ToString());
|
||||||
|
ImGuiUtil.DrawTableColumn(state[slot, true].ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var type in Enum.GetValues<CustomizeIndex>())
|
||||||
|
{
|
||||||
|
PrintRow(type.ToDefaultName(), state.BaseData.Customize[type].Value, state.ModelData.Customize[type].Value, state[type]);
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGuiUtil.DrawTableColumn(string.Join(" ", state.BaseData.GetCustomizeBytes().Select(b => b.ToString("X2"))));
|
||||||
|
ImGuiUtil.DrawTableColumn(string.Join(" ", state.ModelData.GetCustomizeBytes().Select(b => b.ToString("X2"))));
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
ImGuiUtil.DrawTableColumn(string.Join(" ", state.BaseData.GetEquipmentBytes().Select(b => b.ToString("X2"))));
|
||||||
|
ImGuiUtil.DrawTableColumn(string.Join(" ", state.ModelData.GetEquipmentBytes().Select(b => b.ToString("X2"))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void DrawDesignData(in DesignData data)
|
public static void DrawDesignData(in DesignData data)
|
||||||
{
|
{
|
||||||
if (data.ModelId == 0)
|
if (data.ModelId == 0)
|
||||||
{
|
{
|
||||||
using var table = ImRaii.Table("##equip", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit);
|
using var table = ImRaii.Table("##equip", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit);
|
||||||
|
if (!table)
|
||||||
|
return;
|
||||||
|
|
||||||
foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand))
|
foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand))
|
||||||
{
|
{
|
||||||
var item = data.Item(slot);
|
var item = data.Item(slot);
|
||||||
|
|
@ -1011,7 +1083,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.ModelData);
|
DrawState(actors, state);
|
||||||
else
|
else
|
||||||
ImGui.TextUnformatted("Invalid actor.");
|
ImGui.TextUnformatted("Invalid actor.");
|
||||||
}
|
}
|
||||||
|
|
@ -1026,8 +1098,10 @@ public unsafe class DebugTab : ITab
|
||||||
foreach (var (identifier, state) in _state.Where(kvp => !_objectManager.ContainsKey(kvp.Key)))
|
foreach (var (identifier, state) in _state.Where(kvp => !_objectManager.ContainsKey(kvp.Key)))
|
||||||
{
|
{
|
||||||
using var t = ImRaii.TreeNode(identifier.ToString());
|
using var t = ImRaii.TreeNode(identifier.ToString());
|
||||||
if (t)
|
if (!t)
|
||||||
DrawDesignData(state.ModelData);
|
return;
|
||||||
|
|
||||||
|
DrawState(ActorData.Invalid, state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,44 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using Glamourer.Customization;
|
||||||
using Glamourer.Designs;
|
using Glamourer.Designs;
|
||||||
using Glamourer.Gui.Customization;
|
using Glamourer.Gui.Customization;
|
||||||
|
using Glamourer.Interop;
|
||||||
|
using Glamourer.Interop.Penumbra;
|
||||||
|
using Glamourer.State;
|
||||||
|
using Glamourer.Structs;
|
||||||
|
using ImGuiNET;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
|
using Penumbra.Api.Enums;
|
||||||
|
using Penumbra.Api.Helpers;
|
||||||
|
using Penumbra.GameData.Enums;
|
||||||
|
|
||||||
namespace Glamourer.Gui.Tabs.DesignTab;
|
namespace Glamourer.Gui.Tabs.DesignTab;
|
||||||
|
|
||||||
public class DesignPanel
|
public class DesignPanel
|
||||||
{
|
{
|
||||||
|
private readonly ObjectManager _objects;
|
||||||
private readonly DesignFileSystemSelector _selector;
|
private readonly DesignFileSystemSelector _selector;
|
||||||
private readonly DesignManager _manager;
|
private readonly DesignManager _manager;
|
||||||
private readonly CustomizationDrawer _customizationDrawer;
|
private readonly CustomizationDrawer _customizationDrawer;
|
||||||
|
private readonly StateManager _state;
|
||||||
|
private readonly PenumbraService _penumbra;
|
||||||
|
private readonly UpdateSlotService _updateSlot;
|
||||||
|
private readonly WeaponService _weaponService;
|
||||||
|
private readonly ChangeCustomizeService _changeCustomizeService;
|
||||||
|
|
||||||
public DesignPanel(DesignFileSystemSelector selector, CustomizationDrawer customizationDrawer, DesignManager manager)
|
public DesignPanel(DesignFileSystemSelector selector, CustomizationDrawer customizationDrawer, DesignManager manager, ObjectManager objects,
|
||||||
|
StateManager state, PenumbraService penumbra, ChangeCustomizeService changeCustomizeService, WeaponService weaponService,
|
||||||
|
UpdateSlotService updateSlot)
|
||||||
{
|
{
|
||||||
_selector = selector;
|
_selector = selector;
|
||||||
_customizationDrawer = customizationDrawer;
|
_customizationDrawer = customizationDrawer;
|
||||||
_manager = manager;
|
_manager = manager;
|
||||||
|
_objects = objects;
|
||||||
|
_state = state;
|
||||||
|
_penumbra = penumbra;
|
||||||
|
_changeCustomizeService = changeCustomizeService;
|
||||||
|
_weaponService = weaponService;
|
||||||
|
_updateSlot = updateSlot;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Draw()
|
public void Draw()
|
||||||
|
|
@ -28,6 +51,14 @@ public class DesignPanel
|
||||||
if (!child)
|
if (!child)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (ImGui.Button("TEST"))
|
||||||
|
{
|
||||||
|
var (id, data) = _objects.PlayerData;
|
||||||
|
|
||||||
|
if (data.Valid && _state.GetOrCreate(id, data.Objects[0], out var state))
|
||||||
|
_state.ApplyDesign(design, state);
|
||||||
|
}
|
||||||
|
|
||||||
_customizationDrawer.Draw(design.DesignData.Customize, design.WriteProtected());
|
_customizationDrawer.Draw(design.DesignData.Customize, design.WriteProtected());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Glamourer.Gui.Tabs.DesignTab;
|
using Glamourer.Gui.Tabs.DesignTab;
|
||||||
|
using Glamourer.Interop.Penumbra;
|
||||||
using Glamourer.State;
|
using Glamourer.State;
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
|
|
@ -15,12 +16,14 @@ public class SettingsTab : ITab
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
private readonly DesignFileSystemSelector _selector;
|
private readonly DesignFileSystemSelector _selector;
|
||||||
private readonly StateListener _stateListener;
|
private readonly StateListener _stateListener;
|
||||||
|
private readonly PenumbraAutoRedraw _autoRedraw;
|
||||||
|
|
||||||
public SettingsTab(Configuration config, DesignFileSystemSelector selector, StateListener stateListener)
|
public SettingsTab(Configuration config, DesignFileSystemSelector selector, StateListener stateListener, PenumbraAutoRedraw autoRedraw)
|
||||||
{
|
{
|
||||||
_config = config;
|
_config = config;
|
||||||
_selector = selector;
|
_selector = selector;
|
||||||
_stateListener = stateListener;
|
_stateListener = stateListener;
|
||||||
|
_autoRedraw = autoRedraw;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ReadOnlySpan<byte> Label
|
public ReadOnlySpan<byte> Label
|
||||||
|
|
@ -31,10 +34,14 @@ 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("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);
|
||||||
|
Checkbox("Auto-Reload Gear",
|
||||||
|
"Automatically reload equipment pieces on your own character when changing any mod options in Penumbra in their associated collection.",
|
||||||
|
_config.AutoRedrawEquipOnChanges, _autoRedraw.SetState);
|
||||||
if (Widget.DoubleModifierSelector("Design Deletion Modifier",
|
if (Widget.DoubleModifierSelector("Design Deletion Modifier",
|
||||||
"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))
|
||||||
|
|
|
||||||
67
Glamourer/Interop/Penumbra/PenumbraAutoRedraw.cs
Normal file
67
Glamourer/Interop/Penumbra/PenumbraAutoRedraw.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
using System;
|
||||||
|
using Glamourer.State;
|
||||||
|
using Penumbra.Api.Enums;
|
||||||
|
|
||||||
|
namespace Glamourer.Interop.Penumbra;
|
||||||
|
|
||||||
|
public class PenumbraAutoRedraw : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Configuration _config;
|
||||||
|
private readonly PenumbraService _penumbra;
|
||||||
|
private readonly StateManager _state;
|
||||||
|
private readonly ObjectManager _objects;
|
||||||
|
private bool _enabled;
|
||||||
|
|
||||||
|
public PenumbraAutoRedraw(PenumbraService penumbra, Configuration config, StateManager state, ObjectManager objects)
|
||||||
|
{
|
||||||
|
_penumbra = penumbra;
|
||||||
|
_config = config;
|
||||||
|
_state = state;
|
||||||
|
_objects = objects;
|
||||||
|
if (_config.AutoRedrawEquipOnChanges)
|
||||||
|
Enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetState(bool value)
|
||||||
|
{
|
||||||
|
if (value == _config.AutoRedrawEquipOnChanges)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_config.AutoRedrawEquipOnChanges = value;
|
||||||
|
_config.Save();
|
||||||
|
if (value)
|
||||||
|
Enable();
|
||||||
|
else
|
||||||
|
Disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Enable()
|
||||||
|
{
|
||||||
|
if (_enabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_penumbra.ModSettingChanged += OnModSettingChange;
|
||||||
|
_enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Disable()
|
||||||
|
{
|
||||||
|
if (!_enabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_penumbra.ModSettingChanged -= OnModSettingChange;
|
||||||
|
_enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnModSettingChange(ModSettingChange type, string name, string mod, bool inherited)
|
||||||
|
{
|
||||||
|
var playerName = _penumbra.GetCurrentPlayerCollection();
|
||||||
|
if (playerName == name)
|
||||||
|
_state.ReapplyState(_objects.Player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,14 +13,16 @@ public unsafe class PenumbraService : IDisposable
|
||||||
public const int RequiredPenumbraBreakingVersion = 4;
|
public const int RequiredPenumbraBreakingVersion = 4;
|
||||||
public const int RequiredPenumbraFeatureVersion = 15;
|
public const int RequiredPenumbraFeatureVersion = 15;
|
||||||
|
|
||||||
private readonly DalamudPluginInterface _pluginInterface;
|
private readonly DalamudPluginInterface _pluginInterface;
|
||||||
private readonly EventSubscriber<ChangedItemType, uint> _tooltipSubscriber;
|
private readonly EventSubscriber<ChangedItemType, uint> _tooltipSubscriber;
|
||||||
private readonly EventSubscriber<MouseButton, ChangedItemType, uint> _clickSubscriber;
|
private readonly EventSubscriber<MouseButton, ChangedItemType, uint> _clickSubscriber;
|
||||||
private readonly EventSubscriber<nint, string, nint, nint, nint> _creatingCharacterBase;
|
private readonly EventSubscriber<nint, string, nint, nint, nint> _creatingCharacterBase;
|
||||||
private readonly EventSubscriber<nint, string, nint> _createdCharacterBase;
|
private readonly EventSubscriber<nint, string, nint> _createdCharacterBase;
|
||||||
private ActionSubscriber<int, RedrawType> _redrawSubscriber;
|
private readonly EventSubscriber<ModSettingChange, string, string, bool> _modSettingChanged;
|
||||||
private FuncSubscriber<nint, (nint, string)> _drawObjectInfo;
|
private ActionSubscriber<int, RedrawType> _redrawSubscriber;
|
||||||
private FuncSubscriber<int, int> _cutsceneParent;
|
private FuncSubscriber<nint, (nint, string)> _drawObjectInfo;
|
||||||
|
private FuncSubscriber<int, int> _cutsceneParent;
|
||||||
|
private FuncSubscriber<int, (bool, bool, string)> _objectCollection;
|
||||||
|
|
||||||
private readonly EventSubscriber _initializedEvent;
|
private readonly EventSubscriber _initializedEvent;
|
||||||
private readonly EventSubscriber _disposedEvent;
|
private readonly EventSubscriber _disposedEvent;
|
||||||
|
|
@ -35,6 +37,7 @@ public unsafe class PenumbraService : IDisposable
|
||||||
_clickSubscriber = Ipc.ChangedItemClick.Subscriber(pi);
|
_clickSubscriber = Ipc.ChangedItemClick.Subscriber(pi);
|
||||||
_createdCharacterBase = Ipc.CreatedCharacterBase.Subscriber(pi);
|
_createdCharacterBase = Ipc.CreatedCharacterBase.Subscriber(pi);
|
||||||
_creatingCharacterBase = Ipc.CreatingCharacterBase.Subscriber(pi);
|
_creatingCharacterBase = Ipc.CreatingCharacterBase.Subscriber(pi);
|
||||||
|
_modSettingChanged = Ipc.ModSettingChanged.Subscriber(pi);
|
||||||
Reattach();
|
Reattach();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,6 +66,22 @@ public unsafe class PenumbraService : IDisposable
|
||||||
remove => _createdCharacterBase.Event -= value;
|
remove => _createdCharacterBase.Event -= value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public event Action<ModSettingChange, string, string, bool> ModSettingChanged
|
||||||
|
{
|
||||||
|
add => _modSettingChanged.Event += value;
|
||||||
|
remove => _modSettingChanged.Event -= value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Obtain the name of the collection currently assigned to the player. </summary>
|
||||||
|
public string GetCurrentPlayerCollection()
|
||||||
|
{
|
||||||
|
if (!Available)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var (valid, _, name) = _objectCollection.Invoke(0);
|
||||||
|
return valid ? name : string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary> Obtain the game object corresponding to a draw object. </summary>
|
/// <summary> Obtain the game object corresponding to a draw object. </summary>
|
||||||
public Actor GameObjectFromDrawObject(Model drawObject)
|
public Actor GameObjectFromDrawObject(Model drawObject)
|
||||||
=> Available ? _drawObjectInfo.Invoke(drawObject.Address).Item1 : Actor.Null;
|
=> Available ? _drawObjectInfo.Invoke(drawObject.Address).Item1 : Actor.Null;
|
||||||
|
|
@ -103,9 +122,11 @@ public unsafe class PenumbraService : IDisposable
|
||||||
_clickSubscriber.Enable();
|
_clickSubscriber.Enable();
|
||||||
_creatingCharacterBase.Enable();
|
_creatingCharacterBase.Enable();
|
||||||
_createdCharacterBase.Enable();
|
_createdCharacterBase.Enable();
|
||||||
|
_modSettingChanged.Enable();
|
||||||
_drawObjectInfo = Ipc.GetDrawObjectInfo.Subscriber(_pluginInterface);
|
_drawObjectInfo = Ipc.GetDrawObjectInfo.Subscriber(_pluginInterface);
|
||||||
_cutsceneParent = Ipc.GetCutsceneParentIndex.Subscriber(_pluginInterface);
|
_cutsceneParent = Ipc.GetCutsceneParentIndex.Subscriber(_pluginInterface);
|
||||||
_redrawSubscriber = Ipc.RedrawObjectByIndex.Subscriber(_pluginInterface);
|
_redrawSubscriber = Ipc.RedrawObjectByIndex.Subscriber(_pluginInterface);
|
||||||
|
_objectCollection = Ipc.GetCollectionForObject.Subscriber(_pluginInterface);
|
||||||
Available = true;
|
Available = true;
|
||||||
Glamourer.Log.Debug("Glamourer attached to Penumbra.");
|
Glamourer.Log.Debug("Glamourer attached to Penumbra.");
|
||||||
}
|
}
|
||||||
|
|
@ -122,6 +143,7 @@ public unsafe class PenumbraService : IDisposable
|
||||||
_clickSubscriber.Disable();
|
_clickSubscriber.Disable();
|
||||||
_creatingCharacterBase.Disable();
|
_creatingCharacterBase.Disable();
|
||||||
_createdCharacterBase.Disable();
|
_createdCharacterBase.Disable();
|
||||||
|
_modSettingChanged.Disable();
|
||||||
if (Available)
|
if (Available)
|
||||||
{
|
{
|
||||||
Available = false;
|
Available = false;
|
||||||
|
|
@ -138,5 +160,6 @@ public unsafe class PenumbraService : IDisposable
|
||||||
_createdCharacterBase.Dispose();
|
_createdCharacterBase.Dispose();
|
||||||
_initializedEvent.Dispose();
|
_initializedEvent.Dispose();
|
||||||
_disposedEvent.Dispose();
|
_disposedEvent.Dispose();
|
||||||
|
_modSettingChanged.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using OtterGui.Log;
|
||||||
|
|
||||||
namespace Glamourer.Interop.Structs;
|
namespace Glamourer.Interop.Structs;
|
||||||
|
|
||||||
|
|
@ -8,7 +10,7 @@ namespace Glamourer.Interop.Structs;
|
||||||
public readonly struct ActorData
|
public readonly struct ActorData
|
||||||
{
|
{
|
||||||
public readonly List<Actor> Objects;
|
public readonly List<Actor> Objects;
|
||||||
public readonly string Label;
|
public readonly string Label;
|
||||||
|
|
||||||
public bool Valid
|
public bool Valid
|
||||||
=> Objects.Count > 0;
|
=> Objects.Count > 0;
|
||||||
|
|
@ -16,7 +18,7 @@ public readonly struct ActorData
|
||||||
public ActorData(Actor actor, string label)
|
public ActorData(Actor actor, string label)
|
||||||
{
|
{
|
||||||
Objects = new List<Actor> { actor };
|
Objects = new List<Actor> { actor };
|
||||||
Label = label;
|
Label = label;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly ActorData Invalid = new(false);
|
public static readonly ActorData Invalid = new(false);
|
||||||
|
|
@ -24,6 +26,14 @@ public readonly struct ActorData
|
||||||
private ActorData(bool _)
|
private ActorData(bool _)
|
||||||
{
|
{
|
||||||
Objects = new List<Actor>(0);
|
Objects = new List<Actor>(0);
|
||||||
Label = string.Empty;
|
Label = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LazyString ToLazyString(string invalid)
|
||||||
|
{
|
||||||
|
var objects = Objects;
|
||||||
|
return Valid
|
||||||
|
? new LazyString(() => string.Join(", ", objects.Select(o => o.ToString())))
|
||||||
|
: new LazyString(() => invalid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ 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;
|
||||||
|
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||||||
|
|
||||||
namespace Glamourer.Interop;
|
namespace Glamourer.Interop;
|
||||||
|
|
||||||
|
|
@ -31,24 +32,18 @@ public unsafe class UpdateSlotService : IDisposable
|
||||||
{
|
{
|
||||||
if (!drawObject.IsCharacterBase)
|
if (!drawObject.IsCharacterBase)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
FlagSlotForUpdateInterop(drawObject, slot, data);
|
FlagSlotForUpdateInterop(drawObject, slot, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor data)
|
public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor armor, StainId stain)
|
||||||
{
|
=> UpdateSlot(drawObject, slot, armor.With(stain));
|
||||||
if (!drawObject.IsCharacterBase)
|
|
||||||
return;
|
|
||||||
|
|
||||||
FlagSlotForUpdateInterop(drawObject, slot, data.With(drawObject.GetArmor(slot).Stain));
|
public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor armor)
|
||||||
}
|
=> UpdateArmor(drawObject, slot, armor, drawObject.GetArmor(slot).Stain);
|
||||||
|
|
||||||
public void UpdateStain(Model drawObject, EquipSlot slot, StainId stain)
|
public void UpdateStain(Model drawObject, EquipSlot slot, StainId stain)
|
||||||
{
|
=> UpdateArmor(drawObject, slot, drawObject.GetArmor(slot), stain);
|
||||||
if (!drawObject.IsHuman)
|
|
||||||
return;
|
|
||||||
|
|
||||||
FlagSlotForUpdateInterop(drawObject, slot, drawObject.GetArmor(slot).With(stain));
|
|
||||||
}
|
|
||||||
|
|
||||||
private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data)
|
private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,8 @@ public static class ServiceManager
|
||||||
.AddSingleton<UpdateSlotService>()
|
.AddSingleton<UpdateSlotService>()
|
||||||
.AddSingleton<WeaponService>()
|
.AddSingleton<WeaponService>()
|
||||||
.AddSingleton<PenumbraService>()
|
.AddSingleton<PenumbraService>()
|
||||||
.AddSingleton<ObjectManager>();
|
.AddSingleton<ObjectManager>()
|
||||||
|
.AddSingleton<PenumbraAutoRedraw>();
|
||||||
|
|
||||||
private static IServiceCollection AddDesigns(this IServiceCollection services)
|
private static IServiceCollection AddDesigns(this IServiceCollection services)
|
||||||
=> services.AddSingleton<DesignManager>()
|
=> services.AddSingleton<DesignManager>()
|
||||||
|
|
@ -77,6 +78,7 @@ public static class ServiceManager
|
||||||
|
|
||||||
private static IServiceCollection AddState(this IServiceCollection services)
|
private static IServiceCollection AddState(this IServiceCollection services)
|
||||||
=> services.AddSingleton<StateManager>()
|
=> services.AddSingleton<StateManager>()
|
||||||
|
.AddSingleton<StateEditor>()
|
||||||
.AddSingleton<StateListener>();
|
.AddSingleton<StateListener>();
|
||||||
|
|
||||||
private static IServiceCollection AddUi(this IServiceCollection services)
|
private static IServiceCollection AddUi(this IServiceCollection services)
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,20 @@ public class ActorState
|
||||||
HatState,
|
HatState,
|
||||||
VisorState,
|
VisorState,
|
||||||
WeaponState,
|
WeaponState,
|
||||||
|
ModelId,
|
||||||
}
|
}
|
||||||
|
|
||||||
public ActorIdentifier Identifier { get; internal init; }
|
public ActorIdentifier Identifier { get; internal init; }
|
||||||
public DesignData ActorData;
|
|
||||||
public DesignData ModelData;
|
|
||||||
|
|
||||||
|
/// <summary> This should always represent the unmodified state of the draw object. </summary>
|
||||||
|
public DesignData BaseData;
|
||||||
|
|
||||||
|
/// <summary> This should be the desired state of the draw object. </summary>
|
||||||
|
public DesignData ModelData;
|
||||||
|
|
||||||
|
/// <summary> This contains whether a change to the base data was made by the game, the user via manual input or through automatic application. </summary>
|
||||||
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 + 5).ToArray();
|
||||||
|
|
||||||
internal ActorState(ActorIdentifier identifier)
|
internal ActorState(ActorIdentifier identifier)
|
||||||
=> Identifier = identifier;
|
=> Identifier = identifier;
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ public class StateEditor
|
||||||
_items = items;
|
_items = items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void ChangeCustomize(ActorData data, Customize customize)
|
public void ChangeCustomize(ActorData data, Customize customize)
|
||||||
{
|
{
|
||||||
foreach (var actor in data.Objects)
|
foreach (var actor in data.Objects)
|
||||||
|
|
@ -43,19 +44,15 @@ public class StateEditor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ChangeArmor(ActorData data, EquipSlot slot, EquipItem item)
|
public void ChangeArmor(ActorState state, ActorData data, EquipSlot slot)
|
||||||
{
|
{
|
||||||
var idx = slot.ToIndex();
|
var armor = state.ModelData.Armor(slot);
|
||||||
if (idx >= 10)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var armor = item.Armor();
|
|
||||||
foreach (var actor in data.Objects.Where(a => a.IsCharacter))
|
foreach (var actor in data.Objects.Where(a => a.IsCharacter))
|
||||||
{
|
{
|
||||||
var mdl = actor.Model;
|
var mdl = actor.Model;
|
||||||
var customize = mdl.IsHuman ? mdl.GetCustomize() : actor.GetCustomize();
|
var customize = mdl.IsHuman ? mdl.GetCustomize() : actor.GetCustomize();
|
||||||
var (_, resolvedItem) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender);
|
var (_, resolvedItem) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender);
|
||||||
_updateSlot.UpdateArmor(actor.Model, slot, resolvedItem);
|
_updateSlot.UpdateSlot(actor.Model, slot, resolvedItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,42 +61,46 @@ public class StateListener : IDisposable
|
||||||
Unsubscribe();
|
Unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum UpdateState
|
||||||
|
{
|
||||||
|
NoChange,
|
||||||
|
Transformed,
|
||||||
|
Change,
|
||||||
|
}
|
||||||
|
|
||||||
private unsafe void OnCreatingCharacterBase(nint actorPtr, string _, nint modelPtr, nint customizePtr, nint equipDataPtr)
|
private unsafe void OnCreatingCharacterBase(nint actorPtr, string _, nint modelPtr, nint customizePtr, nint equipDataPtr)
|
||||||
{
|
{
|
||||||
// TODO: Fixed Designs.
|
// TODO: Fixed Designs.
|
||||||
var actor = (Actor)actorPtr;
|
var actor = (Actor)actorPtr;
|
||||||
var identifier = actor.GetIdentifier(_actors.AwaitedService);
|
var identifier = actor.GetIdentifier(_actors.AwaitedService);
|
||||||
|
|
||||||
if (*(int*)modelPtr != actor.AsCharacter->ModelCharaId)
|
var modelId = *(uint*)modelPtr;
|
||||||
return;
|
|
||||||
|
|
||||||
ref var customize = ref *(Customize*)customizePtr;
|
ref var customize = ref *(Customize*)customizePtr;
|
||||||
if (_manager.TryGetValue(identifier, out var state))
|
if (_manager.TryGetValue(identifier, out var state))
|
||||||
{
|
switch (UpdateBaseData(actor, state, modelId, customizePtr, equipDataPtr))
|
||||||
ApplyCustomize(actor, state, ref customize);
|
{
|
||||||
ApplyEquipment(actor, state, (CharacterArmor*)equipDataPtr);
|
case UpdateState.Change: break;
|
||||||
if (_config.UseRestrictedGearProtection)
|
case UpdateState.Transformed: break;
|
||||||
ProtectRestrictedGear(equipDataPtr, customize.Race, customize.Gender);
|
case UpdateState.NoChange:
|
||||||
}
|
UpdateBaseData(actor, state, customize);
|
||||||
else if (_config.UseRestrictedGearProtection && *(uint*)modelPtr == 0)
|
break;
|
||||||
{
|
}
|
||||||
|
|
||||||
|
if (_config.UseRestrictedGearProtection && modelId == 0)
|
||||||
ProtectRestrictedGear(equipDataPtr, customize.Race, customize.Gender);
|
ProtectRestrictedGear(equipDataPtr, customize.Race, customize.Gender);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnSlotUpdating(Model model, EquipSlot slot, Ref<CharacterArmor> armor, Ref<ulong> returnValue)
|
private void OnSlotUpdating(Model model, EquipSlot slot, Ref<CharacterArmor> armor, Ref<ulong> returnValue)
|
||||||
{
|
{
|
||||||
// TODO handle hat state
|
// TODO handle hat state
|
||||||
// TODO handle fixed designs
|
|
||||||
var actor = _penumbra.GameObjectFromDrawObject(model);
|
var actor = _penumbra.GameObjectFromDrawObject(model);
|
||||||
var customize = model.GetCustomize();
|
var customize = model.GetCustomize();
|
||||||
if (actor.Identifier(_actors.AwaitedService, out var identifier)
|
if (actor.Identifier(_actors.AwaitedService, out var identifier)
|
||||||
&& _manager.TryGetValue(identifier, out var state))
|
&& _manager.TryGetValue(identifier, out var state))
|
||||||
ApplyEquipmentPiece(actor, state, slot, ref armor.Value);
|
ApplyEquipmentPiece(actor, state, slot, ref armor.Value);
|
||||||
|
|
||||||
var (replaced, replacedArmor) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender);
|
if (_config.UseRestrictedGearProtection)
|
||||||
if (replaced)
|
(_, armor.Value) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender);
|
||||||
armor.Assign(replacedArmor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnWeaponLoading(Actor actor, EquipSlot slot, Ref<CharacterWeapon> weapon)
|
private void OnWeaponLoading(Actor actor, EquipSlot slot, Ref<CharacterWeapon> weapon)
|
||||||
|
|
@ -111,7 +115,7 @@ public class StateListener : IDisposable
|
||||||
|| actorWeapon.Type.Value != stateItem.WeaponType
|
|| actorWeapon.Type.Value != stateItem.WeaponType
|
||||||
|| actorWeapon.Variant != stateItem.Variant)
|
|| actorWeapon.Variant != stateItem.Variant)
|
||||||
{
|
{
|
||||||
var oldActorItem = state.ActorData.Item(slot);
|
var oldActorItem = state.BaseData.Item(slot);
|
||||||
if (oldActorItem.ModelId.Value == actorWeapon.Set.Value
|
if (oldActorItem.ModelId.Value == actorWeapon.Set.Value
|
||||||
&& oldActorItem.WeaponType.Value == actorWeapon.Type.Value
|
&& oldActorItem.WeaponType.Value == actorWeapon.Type.Value
|
||||||
&& oldActorItem.Variant == actorWeapon.Variant)
|
&& oldActorItem.Variant == actorWeapon.Variant)
|
||||||
|
|
@ -123,8 +127,8 @@ public class StateListener : IDisposable
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var identified = _items.Identify(slot, actorWeapon.Set, actorWeapon.Type, (byte)actorWeapon.Variant,
|
var identified = _items.Identify(slot, actorWeapon.Set, actorWeapon.Type, (byte)actorWeapon.Variant,
|
||||||
slot == EquipSlot.OffHand ? state.ActorData.Item(EquipSlot.MainHand).Type : FullEquipType.Unknown);
|
slot == EquipSlot.OffHand ? state.BaseData.Item(EquipSlot.MainHand).Type : FullEquipType.Unknown);
|
||||||
state.ActorData.SetItem(slot, identified);
|
state.BaseData.SetItem(slot, identified);
|
||||||
if (state[slot, false] is not StateChanged.Source.Fixed)
|
if (state[slot, false] is not StateChanged.Source.Fixed)
|
||||||
{
|
{
|
||||||
state.ModelData.SetItem(slot, identified);
|
state.ModelData.SetItem(slot, identified);
|
||||||
|
|
@ -142,7 +146,7 @@ public class StateListener : IDisposable
|
||||||
var stateStain = state.ModelData.Stain(slot);
|
var stateStain = state.ModelData.Stain(slot);
|
||||||
if (actorWeapon.Stain.Value != stateStain.Value)
|
if (actorWeapon.Stain.Value != stateStain.Value)
|
||||||
{
|
{
|
||||||
var oldActorStain = state.ActorData.Stain(slot);
|
var oldActorStain = state.BaseData.Stain(slot);
|
||||||
if (state[slot, true] is not StateChanged.Source.Fixed)
|
if (state[slot, true] is not StateChanged.Source.Fixed)
|
||||||
{
|
{
|
||||||
state.ModelData.SetStain(slot, actorWeapon.Stain);
|
state.ModelData.SetStain(slot, actorWeapon.Stain);
|
||||||
|
|
@ -159,7 +163,7 @@ public class StateListener : IDisposable
|
||||||
private void ApplyCustomize(Actor actor, ActorState state, ref Customize customize)
|
private void ApplyCustomize(Actor actor, ActorState state, ref Customize customize)
|
||||||
{
|
{
|
||||||
var actorCustomize = actor.GetCustomize();
|
var actorCustomize = actor.GetCustomize();
|
||||||
ref var oldActorCustomize = ref state.ActorData.Customize;
|
ref var oldActorCustomize = ref state.BaseData.Customize;
|
||||||
ref var stateCustomize = ref state.ModelData.Customize;
|
ref var stateCustomize = ref state.ModelData.Customize;
|
||||||
foreach (var idx in Enum.GetValues<CustomizeIndex>())
|
foreach (var idx in Enum.GetValues<CustomizeIndex>())
|
||||||
{
|
{
|
||||||
|
|
@ -201,57 +205,34 @@ public class StateListener : IDisposable
|
||||||
|
|
||||||
private void ApplyEquipmentPiece(Actor actor, ActorState state, EquipSlot slot, ref CharacterArmor armor)
|
private void ApplyEquipmentPiece(Actor actor, ActorState state, EquipSlot slot, ref CharacterArmor armor)
|
||||||
{
|
{
|
||||||
var actorArmor = actor.GetArmor(slot);
|
var changeState = UpdateBaseData(actor, state, slot, armor);
|
||||||
if (armor.Value != actorArmor.Value)
|
if (changeState is UpdateState.Transformed)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var stateArmor = state.ModelData.Item(slot);
|
if (changeState is UpdateState.NoChange)
|
||||||
if (armor.Set.Value != stateArmor.ModelId.Value || armor.Variant != stateArmor.Variant)
|
|
||||||
{
|
{
|
||||||
var oldActorArmor = state.ActorData.Item(slot);
|
armor = state.ModelData.Armor(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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
var stateStain = state.ModelData.Stain(slot);
|
|
||||||
if (armor.Stain.Value != stateStain.Value)
|
|
||||||
{
|
{
|
||||||
var oldActorStain = state.ActorData.Stain(slot);
|
var modelArmor = state.ModelData.Armor(slot);
|
||||||
if (oldActorStain.Value == actorArmor.Stain.Value)
|
if (armor.Value == modelArmor.Value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (state[slot, false] is StateChanged.Source.Fixed)
|
||||||
{
|
{
|
||||||
armor.Stain = stateStain;
|
armor.Set = modelArmor.Set;
|
||||||
|
armor.Variant = modelArmor.Variant;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
state.ActorData.SetStain(slot, actorArmor.Stain);
|
_manager.ChangeEquip(state, slot, state.BaseData.Item(slot), StateChanged.Source.Game);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state[slot, true] is StateChanged.Source.Fixed)
|
||||||
|
armor.Stain = modelArmor.Stain;
|
||||||
|
else
|
||||||
|
_manager.ChangeStain(state, slot, state.BaseData.Stain(slot), StateChanged.Source.Game);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -280,4 +261,81 @@ public class StateListener : IDisposable
|
||||||
_slotUpdating.Unsubscribe(OnSlotUpdating);
|
_slotUpdating.Unsubscribe(OnSlotUpdating);
|
||||||
_weaponLoading.Unsubscribe(OnWeaponLoading);
|
_weaponLoading.Unsubscribe(OnWeaponLoading);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private UpdateState UpdateBaseData(Actor actor, ActorState state, EquipSlot slot, CharacterArmor armor)
|
||||||
|
{
|
||||||
|
var actorArmor = actor.GetArmor(slot);
|
||||||
|
// The actor armor does not correspond to the model armor, thus the actor is transformed.
|
||||||
|
if (actorArmor.Value != armor.Value)
|
||||||
|
return UpdateState.Transformed;
|
||||||
|
|
||||||
|
// TODO: Hat State.
|
||||||
|
|
||||||
|
var baseData = state.BaseData.Armor(slot);
|
||||||
|
var change = UpdateState.NoChange;
|
||||||
|
if (baseData.Stain != armor.Stain)
|
||||||
|
{
|
||||||
|
state.BaseData.SetStain(slot, armor.Stain);
|
||||||
|
change = UpdateState.Change;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseData.Set.Value != armor.Set.Value || baseData.Variant != armor.Variant)
|
||||||
|
{
|
||||||
|
var item = _items.Identify(slot, armor.Set, armor.Variant);
|
||||||
|
state.BaseData.SetItem(slot, item);
|
||||||
|
change = UpdateState.Change;
|
||||||
|
}
|
||||||
|
|
||||||
|
return change;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UpdateState UpdateBaseData(Actor actor, ActorState state, EquipSlot slot, CharacterWeapon weapon)
|
||||||
|
{
|
||||||
|
var baseData = state.BaseData.Weapon(slot);
|
||||||
|
var change = UpdateState.NoChange;
|
||||||
|
|
||||||
|
if (baseData.Stain != weapon.Stain)
|
||||||
|
{
|
||||||
|
state.BaseData.SetStain(slot, weapon.Stain);
|
||||||
|
change = UpdateState.Change;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseData.Set.Value != weapon.Set.Value || baseData.Type.Value != weapon.Type.Value || baseData.Variant != weapon.Variant)
|
||||||
|
{
|
||||||
|
var item = _items.Identify(slot, weapon.Set, weapon.Type, (byte)weapon.Variant,
|
||||||
|
slot is EquipSlot.OffHand ? state.BaseData.Item(EquipSlot.MainHand).Type : FullEquipType.Unknown);
|
||||||
|
state.BaseData.SetItem(slot, item);
|
||||||
|
change = UpdateState.Change;
|
||||||
|
}
|
||||||
|
|
||||||
|
return change;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe UpdateState UpdateBaseData(Actor actor, ActorState state, uint modelId, nint customizeData, nint equipData)
|
||||||
|
{
|
||||||
|
if (modelId != (uint)actor.AsCharacter->CharacterData.ModelCharaId)
|
||||||
|
return UpdateState.Transformed;
|
||||||
|
|
||||||
|
if (modelId == state.BaseData.ModelId)
|
||||||
|
return UpdateState.NoChange;
|
||||||
|
|
||||||
|
if (modelId == 0)
|
||||||
|
state.BaseData.LoadNonHuman(modelId, *(Customize*)customizeData, (byte*)equipData);
|
||||||
|
else
|
||||||
|
state.BaseData = _manager.FromActor(actor);
|
||||||
|
|
||||||
|
return UpdateState.Change;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UpdateState UpdateBaseData(Actor actor, ActorState state, Customize customize)
|
||||||
|
{
|
||||||
|
if (!actor.GetCustomize().Equals(customize))
|
||||||
|
return UpdateState.Transformed;
|
||||||
|
|
||||||
|
if (state.BaseData.Customize.Equals(customize))
|
||||||
|
return UpdateState.NoChange;
|
||||||
|
|
||||||
|
state.BaseData.Customize.Load(customize);
|
||||||
|
return UpdateState.Change;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using Glamourer.Customization;
|
using Glamourer.Customization;
|
||||||
using Glamourer.Designs;
|
using Glamourer.Designs;
|
||||||
using Glamourer.Events;
|
using Glamourer.Events;
|
||||||
|
|
@ -11,6 +12,7 @@ using Glamourer.Interop.Penumbra;
|
||||||
using Glamourer.Interop.Structs;
|
using Glamourer.Interop.Structs;
|
||||||
using Glamourer.Services;
|
using Glamourer.Services;
|
||||||
using Glamourer.Structs;
|
using Glamourer.Structs;
|
||||||
|
using OtterGui.Log;
|
||||||
using Penumbra.GameData.Actors;
|
using Penumbra.GameData.Actors;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.GameData.Structs;
|
||||||
|
|
@ -24,13 +26,15 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
|
||||||
private readonly CustomizationService _customizations;
|
private readonly CustomizationService _customizations;
|
||||||
private readonly VisorService _visor;
|
private readonly VisorService _visor;
|
||||||
private readonly StateChanged _event;
|
private readonly StateChanged _event;
|
||||||
|
private readonly ObjectManager _objects;
|
||||||
|
private readonly StateEditor _editor;
|
||||||
|
|
||||||
private readonly PenumbraService _penumbra;
|
private readonly PenumbraService _penumbra;
|
||||||
|
|
||||||
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,
|
||||||
PenumbraService penumbra)
|
PenumbraService penumbra, ObjectManager objects, StateEditor editor)
|
||||||
{
|
{
|
||||||
_actors = actors;
|
_actors = actors;
|
||||||
_items = items;
|
_items = items;
|
||||||
|
|
@ -38,7 +42,8 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
|
||||||
_visor = visor;
|
_visor = visor;
|
||||||
_event = @event;
|
_event = @event;
|
||||||
_penumbra = penumbra;
|
_penumbra = penumbra;
|
||||||
|
_objects = objects;
|
||||||
|
_editor = editor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool GetOrCreate(Actor actor, [NotNullWhen(true)] out ActorState? state)
|
public bool GetOrCreate(Actor actor, [NotNullWhen(true)] out ActorState? state)
|
||||||
|
|
@ -55,7 +60,7 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
|
||||||
state = new ActorState(identifier)
|
state = new ActorState(identifier)
|
||||||
{
|
{
|
||||||
ModelData = designData,
|
ModelData = designData,
|
||||||
ActorData = designData
|
BaseData = designData,
|
||||||
};
|
};
|
||||||
_states.Add(identifier, state);
|
_states.Add(identifier, state);
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -115,7 +120,7 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
|
||||||
UpdateEquip(state, slot, model.GetArmor(slot));
|
UpdateEquip(state, slot, model.GetArmor(slot));
|
||||||
|
|
||||||
state.ModelData.Customize = model.GetCustomize();
|
state.ModelData.Customize = model.GetCustomize();
|
||||||
var (_, _, main, off) = model.GetWeapons(actor);
|
var (_, _, main, off) = model.GetWeapons(actor);
|
||||||
UpdateWeapon(state, EquipSlot.MainHand, main);
|
UpdateWeapon(state, EquipSlot.MainHand, main);
|
||||||
UpdateWeapon(state, EquipSlot.OffHand, off);
|
UpdateWeapon(state, EquipSlot.OffHand, off);
|
||||||
state.ModelData.SetVisor(_visor.GetVisorState(model));
|
state.ModelData.SetVisor(_visor.GetVisorState(model));
|
||||||
|
|
@ -165,9 +170,9 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actor.AsCharacter->ModelCharaId != 0)
|
if (actor.AsCharacter->CharacterData.ModelCharaId != 0)
|
||||||
{
|
{
|
||||||
ret.LoadNonHuman((uint)actor.AsCharacter->ModelCharaId, *(Customize*)&actor.AsCharacter->DrawData.CustomizeData,
|
ret.LoadNonHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId, *(Customize*)&actor.AsCharacter->DrawData.CustomizeData,
|
||||||
(byte*)&actor.AsCharacter->DrawData.Head);
|
(byte*)&actor.AsCharacter->DrawData.Head);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
@ -242,6 +247,94 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
|
||||||
$"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}.");
|
||||||
_event.Invoke(StateChanged.Type.Customize, source, state, data, (oldValue, value, idx));
|
_event.Invoke(StateChanged.Type.Customize, source, state, data, (oldValue, value, idx));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ApplyDesign(Design design, ActorState state)
|
||||||
|
{
|
||||||
|
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
||||||
|
{
|
||||||
|
switch (design.DoApplyEquip(slot), design.DoApplyStain(slot))
|
||||||
|
{
|
||||||
|
case (false, false): continue;
|
||||||
|
case (true, false):
|
||||||
|
ChangeEquip(state, slot, design.DesignData.Item(slot), StateChanged.Source.Manual);
|
||||||
|
break;
|
||||||
|
case (false, true):
|
||||||
|
ChangeStain(state, slot, design.DesignData.Stain(slot), StateChanged.Source.Manual);
|
||||||
|
break;
|
||||||
|
case (true, true):
|
||||||
|
ChangeEquip(state, slot, design.DesignData.Item(slot), design.DesignData.Stain(slot), StateChanged.Source.Manual);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetState(ActorState state)
|
||||||
|
{
|
||||||
|
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
|
||||||
|
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
||||||
|
{
|
||||||
|
ChangeEquip(state, slot, state.BaseData.Item(slot), state.BaseData.Stain(slot), StateChanged.Source.Game);
|
||||||
|
_editor.ChangeArmor(state, objects, slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReapplyState(Actor actor)
|
||||||
|
{
|
||||||
|
if (!GetOrCreate(actor, out var state))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_objects.Update();
|
||||||
|
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
|
||||||
|
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
||||||
|
_editor.ChangeArmor(state, objects, slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChangeEquip(ActorState state, EquipSlot slot, EquipItem item, StateChanged.Source source)
|
||||||
|
{
|
||||||
|
var old = state.ModelData.Item(slot);
|
||||||
|
state.ModelData.SetItem(slot, item);
|
||||||
|
state[slot, false] = source;
|
||||||
|
_objects.Update();
|
||||||
|
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
|
||||||
|
if (source is StateChanged.Source.Manual)
|
||||||
|
_editor.ChangeArmor(state, objects, slot);
|
||||||
|
Glamourer.Log.Verbose(
|
||||||
|
$"Set {slot.ToName()} equipment piece in state {state.Identifier} from {old.Name} ({old.Id}) to {item.Name} ({item.Id}). [Affecting {objects.ToLazyString("nothing")}.]");
|
||||||
|
_event.Invoke(StateChanged.Type.Equip, source, state, objects, (old, item, slot));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChangeEquip(ActorState state, EquipSlot slot, EquipItem item, StainId stain, StateChanged.Source source)
|
||||||
|
{
|
||||||
|
var old = state.ModelData.Item(slot);
|
||||||
|
var oldStain = state.ModelData.Stain(slot);
|
||||||
|
state.ModelData.SetItem(slot, item);
|
||||||
|
state.ModelData.SetStain(slot, stain);
|
||||||
|
state[slot, false] = source;
|
||||||
|
state[slot, true] = source;
|
||||||
|
_objects.Update();
|
||||||
|
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
|
||||||
|
if (source is StateChanged.Source.Manual)
|
||||||
|
_editor.ChangeArmor(state, objects, slot);
|
||||||
|
Glamourer.Log.Verbose(
|
||||||
|
$"Set {slot.ToName()} equipment piece in state {state.Identifier} from {old.Name} ({old.Id}) to {item.Name} ({item.Id}) and its stain from {oldStain.Value} to {stain.Value}. [Affecting {objects.ToLazyString("nothing")}.]");
|
||||||
|
_event.Invoke(StateChanged.Type.Equip, source, state, objects, (old, item, slot));
|
||||||
|
_event.Invoke(StateChanged.Type.Stain, source, state, objects, (oldStain, stain, slot));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChangeStain(ActorState state, EquipSlot slot, StainId stain, StateChanged.Source source)
|
||||||
|
{
|
||||||
|
var old = state.ModelData.Stain(slot);
|
||||||
|
state.ModelData.SetStain(slot, stain);
|
||||||
|
state[slot, true] = source;
|
||||||
|
_objects.Update();
|
||||||
|
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
|
||||||
|
if (source is StateChanged.Source.Manual)
|
||||||
|
_editor.ChangeArmor(state, objects, slot);
|
||||||
|
Glamourer.Log.Verbose(
|
||||||
|
$"Set {slot.ToName()} stain in state {state.Identifier} from {old.Value} to {stain.Value}. [Affecting {objects.ToLazyString("nothing")}.]");
|
||||||
|
_event.Invoke(StateChanged.Type.Stain, source, state, objects, (old, stain, slot));
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
///// <summary> Change whether to apply a specific customize value. </summary>
|
///// <summary> Change whether to apply a specific customize value. </summary>
|
||||||
//public void ChangeApplyCustomize(Design design, CustomizeIndex idx, bool value)
|
//public void ChangeApplyCustomize(Design design, CustomizeIndex idx, bool value)
|
||||||
|
|
@ -255,21 +348,7 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
|
||||||
// _event.Invoke(DesignChanged.Type.ApplyCustomize, design, idx);
|
// _event.Invoke(DesignChanged.Type.ApplyCustomize, design, idx);
|
||||||
//}
|
//}
|
||||||
//
|
//
|
||||||
///// <summary> Change a non-weapon equipment piece. </summary>
|
|
||||||
//public void ChangeEquip(Design design, EquipSlot slot, EquipItem item)
|
|
||||||
//{
|
|
||||||
// if (_items.ValidateItem(slot, item.Id, out item).Length > 0)
|
|
||||||
// return;
|
|
||||||
//
|
|
||||||
// var old = design.DesignData.Item(slot);
|
|
||||||
// if (!design.DesignData.SetItem(slot, item))
|
|
||||||
// return;
|
|
||||||
//
|
|
||||||
// Glamourer.Log.Debug(
|
|
||||||
// $"Set {slot.ToName()} equipment piece in design {design.Identifier} from {old.Name} ({old.Id}) to {item.Name} ({item.Id}).");
|
|
||||||
// _saveService.QueueSave(design);
|
|
||||||
// _event.Invoke(DesignChanged.Type.Equip, design, (old, item, slot));
|
|
||||||
//}
|
|
||||||
//
|
//
|
||||||
///// <summary> Change a weapon. </summary>
|
///// <summary> Change a weapon. </summary>
|
||||||
//public void ChangeWeapon(Design design, EquipSlot slot, EquipItem item)
|
//public void ChangeWeapon(Design design, EquipSlot slot, EquipItem item)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue