FC crest visibility draft

This commit is contained in:
Exter-N 2023-11-23 20:44:50 +01:00
parent 0a7d800706
commit 63f7818481
28 changed files with 980 additions and 223 deletions

View file

@ -4,7 +4,7 @@ using Penumbra.GameData.Enums;
namespace Glamourer.Structs;
[Flags]
public enum EquipFlag : uint
public enum EquipFlag : ulong
{
Head = 0x00000001,
Body = 0x00000002,
@ -30,12 +30,34 @@ public enum EquipFlag : uint
LFingerStain = 0x00200000,
MainhandStain = 0x00400000,
OffhandStain = 0x00800000,
HeadCrest = 0x01000000,
BodyCrest = 0x02000000,
HandsCrest = 0x04000000,
LegsCrest = 0x08000000,
FeetCrest = 0x10000000,
EarsCrest = 0x20000000,
NeckCrest = 0x40000000,
WristCrest = 0x80000000,
RFingerCrest = 0x100000000,
LFingerCrest = 0x200000000,
MainhandCrest = 0x400000000,
OffhandCrest = 0x800000000,
}
public static class EquipFlagExtensions
{
public const EquipFlag All = (EquipFlag)(((uint)EquipFlag.OffhandStain << 1) - 1);
public const int NumEquipFlags = 24;
public const EquipFlag All = (EquipFlag)(((ulong)EquipFlag.OffhandCrest << 1) - 1);
public const EquipFlag AllRelevant = All
& ~EquipFlag.HandsCrest
& ~EquipFlag.LegsCrest
& ~EquipFlag.FeetCrest
& ~EquipFlag.EarsCrest
& ~EquipFlag.NeckCrest
& ~EquipFlag.WristCrest
& ~EquipFlag.RFingerCrest
& ~EquipFlag.LFingerCrest
& ~EquipFlag.MainhandCrest;
public const int NumEquipFlags = 36;
public static EquipFlag ToFlag(this EquipSlot slot)
=> slot switch
@ -73,21 +95,39 @@ public static class EquipFlagExtensions
_ => 0,
};
public static EquipFlag ToCrestFlag(this EquipSlot slot)
=> slot switch
{
EquipSlot.MainHand => EquipFlag.MainhandCrest,
EquipSlot.OffHand => EquipFlag.OffhandCrest,
EquipSlot.Head => EquipFlag.HeadCrest,
EquipSlot.Body => EquipFlag.BodyCrest,
EquipSlot.Hands => EquipFlag.HandsCrest,
EquipSlot.Legs => EquipFlag.LegsCrest,
EquipSlot.Feet => EquipFlag.FeetCrest,
EquipSlot.Ears => EquipFlag.EarsCrest,
EquipSlot.Neck => EquipFlag.NeckCrest,
EquipSlot.Wrists => EquipFlag.WristCrest,
EquipSlot.RFinger => EquipFlag.RFingerCrest,
EquipSlot.LFinger => EquipFlag.LFingerCrest,
_ => 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,
EquipSlot.MainHand => EquipFlag.Mainhand | EquipFlag.MainhandStain | EquipFlag.MainhandCrest,
EquipSlot.OffHand => EquipFlag.Offhand | EquipFlag.OffhandStain | EquipFlag.OffhandCrest,
EquipSlot.Head => EquipFlag.Head | EquipFlag.HeadStain | EquipFlag.HeadCrest,
EquipSlot.Body => EquipFlag.Body | EquipFlag.BodyStain | EquipFlag.BodyCrest,
EquipSlot.Hands => EquipFlag.Hands | EquipFlag.HandsStain | EquipFlag.HandsCrest,
EquipSlot.Legs => EquipFlag.Legs | EquipFlag.LegsStain | EquipFlag.LegsCrest,
EquipSlot.Feet => EquipFlag.Feet | EquipFlag.FeetStain | EquipFlag.FeetCrest,
EquipSlot.Ears => EquipFlag.Ears | EquipFlag.EarsStain | EquipFlag.EarsCrest,
EquipSlot.Neck => EquipFlag.Neck | EquipFlag.NeckStain | EquipFlag.NeckCrest,
EquipSlot.Wrists => EquipFlag.Wrist | EquipFlag.WristStain | EquipFlag.WristCrest,
EquipSlot.RFinger => EquipFlag.RFinger | EquipFlag.RFingerStain | EquipFlag.RFingerCrest,
EquipSlot.LFinger => EquipFlag.LFinger | EquipFlag.LFingerStain | EquipFlag.LFingerCrest,
_ => 0,
};
}

View file

@ -20,8 +20,9 @@ public class AutoDesign
Weapons = 0x04,
Stains = 0x08,
Accessories = 0x10,
Crests = 0x20,
All = Armor | Accessories | Customizations | Weapons | Stains,
All = Armor | Accessories | Customizations | Weapons | Stains | Crests,
}
public Design? Design;
@ -69,7 +70,8 @@ public class AutoDesign
var equipFlags = (ApplicationType.HasFlag(Type.Weapons) ? WeaponFlags : 0)
| (ApplicationType.HasFlag(Type.Armor) ? ArmorFlags : 0)
| (ApplicationType.HasFlag(Type.Accessories) ? AccessoryFlags : 0)
| (ApplicationType.HasFlag(Type.Stains) ? StainFlags : 0);
| (ApplicationType.HasFlag(Type.Stains) ? StainFlags : 0)
| (ApplicationType.HasFlag(Type.Crests) ? CrestFlags : 0);
var customizeFlags = ApplicationType.HasFlag(Type.Customizations) ? CustomizeFlagExtensions.All : 0;
if (Revert)
@ -99,4 +101,21 @@ public class AutoDesign
| EquipFlag.WristStain
| EquipFlag.RFingerStain
| EquipFlag.LFingerStain;
public const EquipFlag CrestFlags = EquipFlag.MainhandCrest
| EquipFlag.OffhandCrest
| EquipFlag.HeadCrest
| EquipFlag.BodyCrest
| EquipFlag.HandsCrest
| EquipFlag.LegsCrest
| EquipFlag.FeetCrest
| EquipFlag.EarsCrest
| EquipFlag.NeckCrest
| EquipFlag.WristCrest
| EquipFlag.RFingerCrest
| EquipFlag.LFingerCrest;
public const EquipFlag RelevantCrestFlags = EquipFlag.OffhandCrest
| EquipFlag.HeadCrest
| EquipFlag.BodyCrest;
}

View file

@ -333,7 +333,7 @@ public class AutoDesignApplier : IDisposable
var item = design.Item(slot);
if (!_config.UnlockedItemMode || _itemUnlocks.IsUnlocked(item.Id, out _))
{
if (!respectManual || state[slot, false] is not StateChanged.Source.Manual)
if (!respectManual || state[slot, ActorState.EquipField.Item] is not StateChanged.Source.Manual)
_state.ChangeItem(state, slot, item, source);
totalEquipFlags |= flag;
}
@ -342,17 +342,25 @@ public class AutoDesignApplier : IDisposable
var stainFlag = slot.ToStainFlag();
if (equipFlags.HasFlag(stainFlag))
{
if (!respectManual || state[slot, true] is not StateChanged.Source.Manual)
if (!respectManual || state[slot, ActorState.EquipField.Stain] is not StateChanged.Source.Manual)
_state.ChangeStain(state, slot, design.Stain(slot), source);
totalEquipFlags |= stainFlag;
}
var crestFlag = slot.ToCrestFlag();
if (equipFlags.HasFlag(crestFlag))
{
if (!respectManual || state[slot, ActorState.EquipField.Crest] is not StateChanged.Source.Manual)
_state.ChangeCrest(state, slot, design.Crest(slot), source);
totalEquipFlags |= crestFlag;
}
}
if (equipFlags.HasFlag(EquipFlag.Mainhand))
{
var item = design.Item(EquipSlot.MainHand);
var checkUnlock = !_config.UnlockedItemMode || _itemUnlocks.IsUnlocked(item.Id, out _);
var checkState = !respectManual || state[EquipSlot.MainHand, false] is not StateChanged.Source.Manual;
var checkState = !respectManual || state[EquipSlot.MainHand, ActorState.EquipField.Item] is not StateChanged.Source.Manual;
if (checkUnlock && checkState)
{
if (fromJobChange)
@ -372,7 +380,7 @@ public class AutoDesignApplier : IDisposable
{
var item = design.Item(EquipSlot.OffHand);
var checkUnlock = !_config.UnlockedItemMode || _itemUnlocks.IsUnlocked(item.Id, out _);
var checkState = !respectManual || state[EquipSlot.OffHand, false] is not StateChanged.Source.Manual;
var checkState = !respectManual || state[EquipSlot.OffHand, ActorState.EquipField.Item] is not StateChanged.Source.Manual;
if (checkUnlock && checkState)
{
if (fromJobChange)
@ -390,17 +398,31 @@ public class AutoDesignApplier : IDisposable
if (equipFlags.HasFlag(EquipFlag.MainhandStain))
{
if (!respectManual || state[EquipSlot.MainHand, true] is not StateChanged.Source.Manual)
if (!respectManual || state[EquipSlot.MainHand, ActorState.EquipField.Stain] is not StateChanged.Source.Manual)
_state.ChangeStain(state, EquipSlot.MainHand, design.Stain(EquipSlot.MainHand), source);
totalEquipFlags |= EquipFlag.MainhandStain;
}
if (equipFlags.HasFlag(EquipFlag.MainhandCrest))
{
if (!respectManual || state[EquipSlot.MainHand, ActorState.EquipField.Crest] is not StateChanged.Source.Manual)
_state.ChangeCrest(state, EquipSlot.MainHand, design.Crest(EquipSlot.MainHand), source);
totalEquipFlags |= EquipFlag.MainhandCrest;
}
if (equipFlags.HasFlag(EquipFlag.OffhandStain))
{
if (!respectManual || state[EquipSlot.OffHand, true] is not StateChanged.Source.Manual)
if (!respectManual || state[EquipSlot.OffHand, ActorState.EquipField.Stain] is not StateChanged.Source.Manual)
_state.ChangeStain(state, EquipSlot.OffHand, design.Stain(EquipSlot.OffHand), source);
totalEquipFlags |= EquipFlag.OffhandStain;
}
if (equipFlags.HasFlag(EquipFlag.OffhandCrest))
{
if (!respectManual || state[EquipSlot.OffHand, ActorState.EquipField.Crest] is not StateChanged.Source.Manual)
_state.ChangeCrest(state, EquipSlot.OffHand, design.Crest(EquipSlot.OffHand), source);
totalEquipFlags |= EquipFlag.OffhandCrest;
}
}
private void ReduceCustomize(ActorState state, in DesignData design, CustomizeFlag customizeFlags, ref CustomizeFlag totalCustomizeFlags,

View file

@ -82,7 +82,7 @@ public class DesignBase
internal CustomizeFlag ApplyCustomizeRaw
=> _applyCustomize;
internal EquipFlag ApplyEquip = EquipFlagExtensions.All;
internal EquipFlag ApplyEquip = EquipFlagExtensions.AllRelevant;
private DesignFlags _designFlags = DesignFlags.ApplyHatVisible | DesignFlags.ApplyVisorState | DesignFlags.ApplyWeaponVisible;
public bool SetCustomize(CustomizationService customizationService, Customize customize)
@ -166,6 +166,9 @@ public class DesignBase
public bool DoApplyStain(EquipSlot slot)
=> ApplyEquip.HasFlag(slot.ToStainFlag());
public bool DoApplyCrest(EquipSlot slot)
=> ApplyEquip.HasFlag(slot.ToCrestFlag());
public bool DoApplyCustomize(CustomizeIndex idx)
=> ApplyCustomize.HasFlag(idx.ToFlag());
@ -189,6 +192,16 @@ public class DesignBase
return true;
}
internal bool SetApplyCrest(EquipSlot slot, bool value)
{
var newValue = value ? ApplyEquip | slot.ToCrestFlag() : ApplyEquip & ~slot.ToCrestFlag();
if (newValue == ApplyEquip)
return false;
ApplyEquip = newValue;
return true;
}
internal bool SetApplyCustomize(CustomizeIndex idx, bool value)
{
var newValue = value ? _applyCustomize | idx.ToFlag() : _applyCustomize & ~idx.ToFlag();
@ -246,13 +259,15 @@ public class DesignBase
protected JObject SerializeEquipment()
{
static JObject Serialize(CustomItemId id, StainId stain, bool apply, bool applyStain)
static JObject Serialize(CustomItemId id, StainId stain, bool crest, bool apply, bool applyStain, bool applyCrest)
=> new()
{
["ItemId"] = id.Id,
["Stain"] = stain.Id,
["Crest"] = crest,
["Apply"] = apply,
["ApplyStain"] = applyStain,
["ApplyCrest"] = applyCrest,
};
var ret = new JObject();
@ -262,7 +277,8 @@ public class DesignBase
{
var item = _designData.Item(slot);
var stain = _designData.Stain(slot);
ret[slot.ToString()] = Serialize(item.Id, stain, DoApplyEquip(slot), DoApplyStain(slot));
var crest = _designData.Crest(slot);
ret[slot.ToString()] = Serialize(item.Id, stain, crest, DoApplyEquip(slot), DoApplyStain(slot), DoApplyCrest(slot));
}
ret["Hat"] = new QuadBool(_designData.IsHatVisible(), DoApplyHatVisible()).ToJObject("Show", "Apply");
@ -345,13 +361,15 @@ public class DesignBase
return;
}
static (CustomItemId, StainId, bool, bool) ParseItem(EquipSlot slot, JToken? item)
static (CustomItemId, StainId, bool, bool, bool, bool) ParseItem(EquipSlot slot, JToken? item)
{
var id = item?["ItemId"]?.ToObject<ulong>() ?? ItemManager.NothingId(slot).Id;
var stain = (StainId)(item?["Stain"]?.ToObject<byte>() ?? 0);
var crest = item?["Crest"]?.ToObject<bool>() ?? false;
var apply = item?["Apply"]?.ToObject<bool>() ?? false;
var applyStain = item?["ApplyStain"]?.ToObject<bool>() ?? false;
return (id, stain, apply, applyStain);
var applyCrest = item?["ApplyCrest"]?.ToObject<bool>() ?? false;
return (id, stain, crest, apply, applyStain, applyCrest);
}
void PrintWarning(string msg)
@ -362,21 +380,23 @@ public class DesignBase
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var (id, stain, apply, applyStain) = ParseItem(slot, equip[slot.ToString()]);
var (id, stain, crest, apply, applyStain, applyCrest) = ParseItem(slot, equip[slot.ToString()]);
PrintWarning(items.ValidateItem(slot, id, out var item, allowUnknown));
PrintWarning(items.ValidateStain(stain, out stain, allowUnknown));
design._designData.SetItem(slot, item);
design._designData.SetStain(slot, stain);
design._designData.SetCrest(slot, crest);
design.SetApplyEquip(slot, apply);
design.SetApplyStain(slot, applyStain);
design.SetApplyCrest(slot, applyCrest);
}
{
var (id, stain, apply, applyStain) = ParseItem(EquipSlot.MainHand, equip[EquipSlot.MainHand.ToString()]);
var (id, stain, crest, apply, applyStain, applyCrest) = ParseItem(EquipSlot.MainHand, equip[EquipSlot.MainHand.ToString()]);
if (id == ItemManager.NothingId(EquipSlot.MainHand))
id = items.DefaultSword.ItemId;
var (idOff, stainOff, applyOff, applyStainOff) = ParseItem(EquipSlot.OffHand, equip[EquipSlot.OffHand.ToString()]);
var (idOff, stainOff, crestOff, applyOff, applyStainOff, applyCrestOff) = ParseItem(EquipSlot.OffHand, equip[EquipSlot.OffHand.ToString()]);
if (id == ItemManager.NothingId(EquipSlot.OffHand))
id = ItemManager.NothingId(FullEquipType.Shield);
@ -387,10 +407,14 @@ public class DesignBase
design._designData.SetItem(EquipSlot.OffHand, off);
design._designData.SetStain(EquipSlot.MainHand, stain);
design._designData.SetStain(EquipSlot.OffHand, stainOff);
design._designData.SetCrest(EquipSlot.MainHand, crest);
design._designData.SetCrest(EquipSlot.OffHand, crestOff);
design.SetApplyEquip(EquipSlot.MainHand, apply);
design.SetApplyEquip(EquipSlot.OffHand, applyOff);
design.SetApplyStain(EquipSlot.MainHand, applyStain);
design.SetApplyStain(EquipSlot.OffHand, applyStainOff);
design.SetApplyCrest(EquipSlot.MainHand, applyCrest);
design.SetApplyCrest(EquipSlot.OffHand, applyCrestOff);
}
var metaValue = QuadBool.FromJObject(equip["Hat"], "Show", "Apply", QuadBool.NullFalse);
design.SetApplyHatVisible(metaValue.Enabled);

View file

@ -34,6 +34,7 @@ public unsafe struct DesignData
private FullEquipType _typeMainhand;
private FullEquipType _typeOffhand;
private byte _states;
public ushort CrestVisibility;
public bool IsHuman = true;
public DesignData()
@ -59,6 +60,15 @@ public unsafe struct DesignData
return index > 11 ? (StainId)0 : _equipmentBytes[4 * index + 3];
}
private static ushort CrestMask(EquipSlot slot)
{
var index = slot.ToIndex();
return index <= 11 ? (ushort)(1u << (int)index) : (ushort)0;
}
public readonly bool Crest(EquipSlot slot)
=> (CrestVisibility & CrestMask(slot)) != 0;
public FullEquipType MainhandType
=> _typeMainhand;
@ -173,6 +183,16 @@ public unsafe struct DesignData
_ => false,
};
public bool SetCrest(EquipSlot slot, bool visible)
{
var crestVisibility = CrestVisibility;
if (visible)
crestVisibility |= CrestMask(slot);
else
crestVisibility &= (ushort)~CrestMask(slot);
return SetIfDifferent(ref CrestVisibility, crestVisibility);
}
public readonly bool IsWet()
=> (_states & 0x01) == 0x01;
@ -228,12 +248,15 @@ public unsafe struct DesignData
{
SetItem(slot, ItemManager.NothingItem(slot));
SetStain(slot, 0);
SetCrest(slot, false);
}
SetItem(EquipSlot.MainHand, items.DefaultSword);
SetStain(EquipSlot.MainHand, 0);
SetCrest(EquipSlot.MainHand, false);
SetItem(EquipSlot.OffHand, ItemManager.NothingItem(FullEquipType.Shield));
SetStain(EquipSlot.OffHand, 0);
SetCrest(EquipSlot.OffHand, false);
}

View file

@ -446,6 +446,31 @@ public class DesignManager
_event.Invoke(DesignChanged.Type.ApplyStain, design, slot);
}
/// <summary> Change the crest visibility for any equipment piece. </summary>
public void ChangeCrest(Design design, EquipSlot slot, bool crest)
{
var oldCrest = design.DesignData.Crest(slot);
if (!design.GetDesignDataRef().SetCrest(slot, crest))
return;
design.LastEdit = DateTimeOffset.UtcNow;
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Set crest visibility of {slot} equipment piece to {crest}.");
_event.Invoke(DesignChanged.Type.Crest, design, (oldCrest, crest, slot));
}
/// <summary> Change whether to apply a specific crest visibility. </summary>
public void ChangeApplyCrest(Design design, EquipSlot slot, bool value)
{
if (!design.SetApplyCrest(slot, value))
return;
design.LastEdit = DateTimeOffset.UtcNow;
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Set applying of crest visibility of {slot} equipment piece to {value}.");
_event.Invoke(DesignChanged.Type.ApplyCrest, design, slot);
}
/// <summary> Change the bool value of one of the meta flags. </summary>
public void ChangeMeta(Design design, ActorState.MetaIndex metaIndex, bool value)
{
@ -514,6 +539,9 @@ public class DesignManager
if (other.DoApplyStain(slot))
ChangeStain(design, slot, other.DesignData.Stain(slot));
if (other.DoApplyCrest(slot))
ChangeCrest(design, slot, other.DesignData.Crest(slot));
}
}
@ -528,6 +556,12 @@ public class DesignManager
if (other.DoApplyStain(EquipSlot.OffHand))
ChangeStain(design, EquipSlot.OffHand, other.DesignData.Stain(EquipSlot.OffHand));
if (other.DoApplyCrest(EquipSlot.MainHand))
ChangeCrest(design, EquipSlot.MainHand, other.DesignData.Crest(EquipSlot.MainHand));
if (other.DoApplyCrest(EquipSlot.OffHand))
ChangeCrest(design, EquipSlot.OffHand, other.DesignData.Crest(EquipSlot.OffHand));
}
public void UndoDesignChange(Design design)

View file

@ -0,0 +1,34 @@
using System;
using Glamourer.Interop.Structs;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
namespace Glamourer.Events;
/// <summary>
/// Triggered when the crest visibility is updated on a model.
/// <list type="number">
/// <item>Parameter is the model with an update. </item>
/// <item>Parameter is the equipment slot changed. </item>
/// <item>Parameter is the whether the crest will be shown. </item>
/// </list>
/// </summary>
public sealed class CrestVisibilityUpdating : EventWrapper<Action<Model, EquipSlot, Ref<bool>>, CrestVisibilityUpdating.Priority>
{
public enum Priority
{
/// <seealso cref="State.StateListener.OnCrestVisibilityUpdating"/>
StateListener = 0,
}
public CrestVisibilityUpdating()
: base(nameof(CrestVisibilityUpdating))
{ }
public void Invoke(Model model, EquipSlot slot, ref bool visible)
{
var @return = new Ref<bool>(visible);
Invoke(this, model, slot, @return);
visible = @return;
}
}

View file

@ -62,6 +62,9 @@ public sealed class DesignChanged : EventWrapper<Action<DesignChanged.Type, Desi
/// <summary> An existing design had a stain changed. Data is the old stain id, the new stain id and the slot [(StainId, StainId, EquipSlot)]. </summary>
Stain,
/// <summary> An existing design had a crest visibility changed. Data is the old crest visibility, the new crest visibility and the slot [(bool, bool, EquipSlot)]. </summary>
Crest,
/// <summary> An existing design changed whether a specific customization is applied. Data is the type of customization [CustomizeIndex]. </summary>
ApplyCustomize,
@ -71,6 +74,9 @@ public sealed class DesignChanged : EventWrapper<Action<DesignChanged.Type, Desi
/// <summary> An existing design changed whether a specific stain is applied. Data is the slot of the equipment [EquipSlot]. </summary>
ApplyStain,
/// <summary> An existing design changed whether a specific crest visibility is applied. Data is the slot of the equipment [EquipSlot]. </summary>
ApplyCrest,
/// <summary> An existing design changed its write protection status. Data is the new value [bool]. </summary>
WriteProtection,

View file

@ -37,6 +37,9 @@ public sealed class StateChanged : EventWrapper<Action<StateChanged.Type, StateC
/// <summary> A characters saved state had a stain changed. Data is the old stain id, the new stain id and the slot [(StainId, StainId, EquipSlot)]. </summary>
Stain,
/// <summary> A characters saved state had a crest visibility changed. Data is the old crest visibility, the new crest visibility and the slot [(bool, bool, EquipSlot)]. </summary>
Crest,
/// <summary> A characters saved state had a design applied. This means everything may have changed. Data is the applied design. [DesignBase] </summary>
Design,

View file

@ -13,6 +13,7 @@ using Glamourer.Structs;
using Glamourer.Unlocks;
using ImGuiNET;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
@ -86,13 +87,13 @@ public class EquipmentDrawer
}
public DataChange DrawEquip(EquipSlot slot, in DesignData designData, out EquipItem rArmor, out StainId rStain, EquipFlag? cApply,
out bool rApply, out bool rApplyStain, bool locked)
=> DrawEquip(slot, designData.Item(slot), out rArmor, designData.Stain(slot), out rStain, cApply, out rApply, out rApplyStain, locked,
public DataChange DrawEquip(EquipSlot slot, in DesignData designData, out EquipItem rArmor, out StainId rStain, out bool rCrest, EquipFlag? cApply,
out bool rApply, out bool rApplyStain, out bool rApplyCrest, bool locked)
=> DrawEquip(slot, designData.Item(slot), out rArmor, designData.Stain(slot), out rStain, designData.Crest(slot), out rCrest, cApply, out rApply, out rApplyStain, out rApplyCrest, locked,
designData.Customize.Gender, designData.Customize.Race);
public DataChange DrawEquip(EquipSlot slot, EquipItem cArmor, out EquipItem rArmor, StainId cStain, out StainId rStain, EquipFlag? cApply,
out bool rApply, out bool rApplyStain, bool locked, Gender gender = Gender.Unknown, Race race = Race.Unknown)
public DataChange DrawEquip(EquipSlot slot, EquipItem cArmor, out EquipItem rArmor, StainId cStain, out StainId rStain, bool cCrest, out bool rCrest, EquipFlag? cApply,
out bool rApply, out bool rApplyStain, out bool rApplyCrest, bool locked, Gender gender = Gender.Unknown, Race race = Race.Unknown)
{
if (_config.HideApplyCheckmarks)
cApply = null;
@ -102,24 +103,26 @@ public class EquipmentDrawer
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing);
if (_config.SmallEquip)
return DrawEquipSmall(slot, cArmor, out rArmor, cStain, out rStain, cApply, out rApply, out rApplyStain, locked, gender, race);
return DrawEquipSmall(slot, cArmor, out rArmor, cStain, out rStain, cCrest, out rCrest, cApply, out rApply, out rApplyStain, out rApplyCrest, locked, gender, race);
if (!locked && _codes.EnabledArtisan)
return DrawEquipArtisan(slot, cArmor, out rArmor, cStain, out rStain, cApply, out rApply, out rApplyStain);
return DrawEquipArtisan(slot, cArmor, out rArmor, cStain, out rStain, cCrest, out rCrest, cApply, out rApply, out rApplyStain, out rApplyCrest);
return DrawEquipNormal(slot, cArmor, out rArmor, cStain, out rStain, cApply, out rApply, out rApplyStain, locked, gender, race);
return DrawEquipNormal(slot, cArmor, out rArmor, cStain, out rStain, cCrest, out rCrest, cApply, out rApply, out rApplyStain, out rApplyCrest, locked, gender, race);
}
public DataChange DrawWeapons(in DesignData designData, out EquipItem rMainhand, out EquipItem rOffhand, out StainId rMainhandStain,
out StainId rOffhandStain, EquipFlag? cApply, bool allWeapons, out bool rApplyMainhand, out bool rApplyMainhandStain,
out bool rApplyOffhand, out bool rApplyOffhandStain, bool locked)
out StainId rOffhandStain, out bool rMainhandCrest, out bool rOffhandCrest, EquipFlag? cApply, bool allWeapons, out bool rApplyMainhand, out bool rApplyMainhandStain,
out bool rApplyMainhandCrest, out bool rApplyOffhand, out bool rApplyOffhandStain, out bool rApplyOffhandCrest, bool locked)
=> DrawWeapons(designData.Item(EquipSlot.MainHand), out rMainhand, designData.Item(EquipSlot.OffHand), out rOffhand,
designData.Stain(EquipSlot.MainHand), out rMainhandStain, designData.Stain(EquipSlot.OffHand), out rOffhandStain, cApply,
allWeapons, out rApplyMainhand, out rApplyMainhandStain, out rApplyOffhand, out rApplyOffhandStain, locked);
designData.Stain(EquipSlot.MainHand), out rMainhandStain, designData.Stain(EquipSlot.OffHand), out rOffhandStain,
designData.Crest(EquipSlot.MainHand), out rMainhandCrest, designData.Crest(EquipSlot.OffHand), out rOffhandCrest, cApply,
allWeapons, out rApplyMainhand, out rApplyMainhandStain, out rApplyMainhandCrest, out rApplyOffhand, out rApplyOffhandStain, out rApplyOffhandCrest, locked);
private DataChange DrawWeapons(EquipItem cMainhand, out EquipItem rMainhand, EquipItem cOffhand, out EquipItem rOffhand,
StainId cMainhandStain, out StainId rMainhandStain, StainId cOffhandStain, out StainId rOffhandStain, EquipFlag? cApply,
bool allWeapons, out bool rApplyMainhand, out bool rApplyMainhandStain, out bool rApplyOffhand, out bool rApplyOffhandStain,
StainId cMainhandStain, out StainId rMainhandStain, StainId cOffhandStain, out StainId rOffhandStain,
bool cMainhandCrest, out bool rMainhandCrest, bool cOffhandCrest, out bool rOffhandCrest, EquipFlag? cApply,
bool allWeapons, out bool rApplyMainhand, out bool rApplyMainhandStain, out bool rApplyMainhandCrest, out bool rApplyOffhand, out bool rApplyOffhandStain, out bool rApplyOffhandCrest,
bool locked)
{
if (cMainhand.ModelId.Id == 0)
@ -128,10 +131,14 @@ public class EquipmentDrawer
rMainhand = cMainhand;
rMainhandStain = cMainhandStain;
rOffhandStain = cOffhandStain;
rMainhandCrest = cMainhandCrest;
rOffhandCrest = cOffhandCrest;
rApplyMainhand = false;
rApplyMainhandStain = false;
rApplyMainhandCrest = false;
rApplyOffhand = false;
rApplyOffhandStain = false;
rApplyOffhandCrest = false;
return DataChange.None;
}
@ -144,15 +151,18 @@ public class EquipmentDrawer
if (_config.SmallEquip)
return DrawWeaponsSmall(cMainhand, out rMainhand, cOffhand, out rOffhand, cMainhandStain, out rMainhandStain, cOffhandStain,
out rOffhandStain, cApply, out rApplyMainhand, out rApplyMainhandStain, out rApplyOffhand, out rApplyOffhandStain, locked,
out rOffhandStain, cMainhandCrest, out rMainhandCrest, cOffhandCrest, out rOffhandCrest,
cApply, out rApplyMainhand, out rApplyMainhandStain, out rApplyMainhandCrest, out rApplyOffhand, out rApplyOffhandStain, out rApplyOffhandCrest, locked,
allWeapons);
if (!locked && _codes.EnabledArtisan)
return DrawWeaponsArtisan(cMainhand, out rMainhand, cOffhand, out rOffhand, cMainhandStain, out rMainhandStain, cOffhandStain,
out rOffhandStain, cApply, out rApplyMainhand, out rApplyMainhandStain, out rApplyOffhand, out rApplyOffhandStain);
out rOffhandStain, cMainhandCrest, out rMainhandCrest, cOffhandCrest, out rOffhandCrest,
cApply, out rApplyMainhand, out rApplyMainhandStain, out rApplyMainhandCrest, out rApplyOffhand, out rApplyOffhandStain, out rApplyOffhandCrest);
return DrawWeaponsNormal(cMainhand, out rMainhand, cOffhand, out rOffhand, cMainhandStain, out rMainhandStain, cOffhandStain,
out rOffhandStain, cApply, out rApplyMainhand, out rApplyMainhandStain, out rApplyOffhand, out rApplyOffhandStain, locked,
out rOffhandStain, cMainhandCrest, out rMainhandCrest, cOffhandCrest, out rOffhandCrest,
cApply, out rApplyMainhand, out rApplyMainhandStain, out rApplyMainhandCrest, out rApplyOffhand, out rApplyOffhandStain, out rApplyOffhandCrest, locked,
allWeapons);
}
@ -331,6 +341,49 @@ public class EquipmentDrawer
return change;
}
internal static bool CanHaveCrest(EquipSlot slot)
=> slot is EquipSlot.OffHand or EquipSlot.Head or EquipSlot.Body;
private DataChange DrawCrest(EquipSlot slot, bool current, EquipFlag? cApply, out bool ret, out bool rApply, bool locked, bool small, bool change2)
{
var changes = DataChange.None;
if (cApply.HasValue)
{
var apply = cApply.Value.HasFlag(slot.ToCrestFlag());
var flags = (sbyte)(apply ? current ? 1 : -1 : 0);
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing);
using (var disabled = ImRaii.Disabled(locked))
{
if (new TristateCheckbox(ColorId.TriStateCross.Value(), ColorId.TriStateCheck.Value(), ColorId.TriStateNeutral.Value()).Draw($"##crest{slot}", flags, out flags))
{
(ret, rApply) = flags switch
{
-1 => (false, true),
0 => (current, false),
_ => (true, true),
};
}
else
{
ret = current;
rApply = apply;
}
}
ImGuiUtil.HoverTooltip($"Display your Free Company crest on this equipment.\n\nThis attribute will be {(apply ? current ? "enabled." : "disabled." : "kept as is.")}");
if (ret != current)
changes |= change2 ? DataChange.Crest2 : DataChange.Crest;
if (rApply != apply)
changes |= change2 ? DataChange.ApplyCrest2 : DataChange.ApplyCrest;
}
else
{
rApply = false;
if (UiHelpers.DrawCheckbox($"##crest{slot}", "Display your Free Company crest on this equipment.", current, out ret, locked))
changes |= change2 ? DataChange.Crest2 : DataChange.Crest;
}
return changes;
}
/// <summary> Draw an input for armor that can set arbitrary values instead of choosing items. </summary>
private bool DrawArmorArtisan(EquipSlot slot, EquipItem current, out EquipItem armor)
{
@ -383,8 +436,8 @@ public class EquipmentDrawer
return false;
}
private DataChange DrawEquipArtisan(EquipSlot slot, EquipItem cArmor, out EquipItem rArmor, StainId cStain, out StainId rStain,
EquipFlag? cApply, out bool rApply, out bool rApplyStain)
private DataChange DrawEquipArtisan(EquipSlot slot, EquipItem cArmor, out EquipItem rArmor, StainId cStain, out StainId rStain, bool cCrest, out bool rCrest,
EquipFlag? cApply, out bool rApply, out bool rApplyStain, out bool rApplyCrest)
{
var changes = DataChange.None;
if (DrawStainArtisan(slot, cStain, out rStain))
@ -392,6 +445,16 @@ public class EquipmentDrawer
ImGui.SameLine();
if (DrawArmorArtisan(slot, cArmor, out rArmor))
changes |= DataChange.Item;
if (CanHaveCrest(slot))
{
ImGui.SameLine();
changes |= DrawCrest(slot, cCrest, cApply, out rCrest, out rApplyCrest, false, true, false);
}
else
{
rCrest = cCrest;
rApplyCrest = false;
}
if (cApply.HasValue)
{
ImGui.SameLine();
@ -410,8 +473,8 @@ public class EquipmentDrawer
return changes;
}
private DataChange DrawEquipSmall(EquipSlot slot, EquipItem cArmor, out EquipItem rArmor, StainId cStain, out StainId rStain,
EquipFlag? cApply, out bool rApply, out bool rApplyStain, bool locked, Gender gender, Race race)
private DataChange DrawEquipSmall(EquipSlot slot, EquipItem cArmor, out EquipItem rArmor, StainId cStain, out StainId rStain, bool cCrest, out bool rCrest,
EquipFlag? cApply, out bool rApply, out bool rApplyStain, out bool rApplyCrest, bool locked, Gender gender, Race race)
{
var changes = DataChange.None;
if (DrawStain(slot, cStain, out rStain, locked, true))
@ -419,6 +482,16 @@ public class EquipmentDrawer
ImGui.SameLine();
if (DrawItem(slot, cArmor, out rArmor, out var label, locked, true, false, false))
changes |= DataChange.Item;
if (CanHaveCrest(slot))
{
ImGui.SameLine();
changes |= DrawCrest(slot, cCrest, cApply, out rCrest, out rApplyCrest, locked, true, false);
}
else
{
rCrest = cCrest;
rApplyCrest = false;
}
if (cApply.HasValue)
{
ImGui.SameLine();
@ -443,8 +516,8 @@ public class EquipmentDrawer
return changes;
}
private DataChange DrawEquipNormal(EquipSlot slot, EquipItem cArmor, out EquipItem rArmor, StainId cStain, out StainId rStain,
EquipFlag? cApply, out bool rApply, out bool rApplyStain, bool locked, Gender gender, Race race)
private DataChange DrawEquipNormal(EquipSlot slot, EquipItem cArmor, out EquipItem rArmor, StainId cStain, out StainId rStain, bool cCrest, out bool rCrest,
EquipFlag? cApply, out bool rApply, out bool rApplyStain, out bool rApplyCrest, bool locked, Gender gender, Race race)
{
var changes = DataChange.None;
cArmor.DrawIcon(_textures, _iconSize, slot);
@ -479,6 +552,16 @@ public class EquipmentDrawer
{
rApplyStain = true;
}
if (CanHaveCrest(slot))
{
ImGui.SameLine();
changes |= DrawCrest(slot, cCrest, cApply, out rCrest, out rApplyCrest, locked, false, false);
}
else
{
rCrest = cCrest;
rApplyCrest = true;
}
if (VerifyRestrictedGear(slot, rArmor, gender, race))
{
@ -506,8 +589,9 @@ public class EquipmentDrawer
}
private DataChange DrawWeaponsSmall(EquipItem cMainhand, out EquipItem rMainhand, EquipItem cOffhand, out EquipItem rOffhand,
StainId cMainhandStain, out StainId rMainhandStain, StainId cOffhandStain, out StainId rOffhandStain, EquipFlag? cApply,
out bool rApplyMainhand, out bool rApplyMainhandStain, out bool rApplyOffhand, out bool rApplyOffhandStain, bool locked,
StainId cMainhandStain, out StainId rMainhandStain, StainId cOffhandStain, out StainId rOffhandStain,
bool cMainhandCrest, out bool rMainhandCrest, bool cOffhandCrest, out bool rOffhandCrest, EquipFlag? cApply,
out bool rApplyMainhand, out bool rApplyMainhandStain, out bool rApplyMainhandCrest, out bool rApplyOffhand, out bool rApplyOffhandStain, out bool rApplyOffhandCrest, bool locked,
bool allWeapons)
{
var changes = DataChange.None;
@ -526,6 +610,16 @@ public class EquipmentDrawer
}
}
if (CanHaveCrest(EquipSlot.MainHand))
{
ImGui.SameLine();
changes |= DrawCrest(EquipSlot.MainHand, cMainhandCrest, cApply, out rMainhandCrest, out rApplyMainhandCrest, locked, false, false);
}
else
{
rMainhandCrest = cMainhandCrest;
rApplyMainhandCrest = true;
}
if (cApply.HasValue)
{
ImGui.SameLine();
@ -548,8 +642,10 @@ public class EquipmentDrawer
if (rOffhand.Type is FullEquipType.Unknown)
{
rOffhandStain = cOffhandStain;
rOffhandCrest = cOffhandCrest;
rApplyOffhand = false;
rApplyOffhandStain = false;
rApplyOffhandCrest = false;
return changes;
}
@ -559,6 +655,17 @@ public class EquipmentDrawer
ImGui.SameLine();
if (DrawOffhand(rMainhand, rOffhand, out rOffhand, out var offhandLabel, locked, true, false, false))
changes |= DataChange.Item2;
if (CanHaveCrest(EquipSlot.OffHand))
{
ImGui.SameLine();
changes |= DrawCrest(EquipSlot.OffHand, cOffhandCrest, cApply, out rOffhandCrest, out rApplyOffhandCrest, locked, false, true);
}
else
{
rOffhandCrest = cOffhandCrest;
rApplyOffhandCrest = true;
}
if (cApply.HasValue)
{
ImGui.SameLine();
@ -580,8 +687,9 @@ public class EquipmentDrawer
}
private DataChange DrawWeaponsNormal(EquipItem cMainhand, out EquipItem rMainhand, EquipItem cOffhand, out EquipItem rOffhand,
StainId cMainhandStain, out StainId rMainhandStain, StainId cOffhandStain, out StainId rOffhandStain, EquipFlag? cApply,
out bool rApplyMainhand, out bool rApplyMainhandStain, out bool rApplyOffhand, out bool rApplyOffhandStain, bool locked,
StainId cMainhandStain, out StainId rMainhandStain, StainId cOffhandStain, out StainId rOffhandStain,
bool cMainhandCrest, out bool rMainhandCrest, bool cOffhandCrest, out bool rOffhandCrest, EquipFlag? cApply,
out bool rApplyMainhand, out bool rApplyMainhandStain, out bool rApplyMainhandCrest, out bool rApplyOffhand, out bool rApplyOffhandStain, out bool rApplyOffhandCrest, bool locked,
bool allWeapons)
{
var changes = DataChange.None;
@ -630,13 +738,26 @@ public class EquipmentDrawer
{
rApplyMainhandStain = true;
}
if (CanHaveCrest(EquipSlot.MainHand))
{
ImGui.SameLine();
changes |= DrawCrest(EquipSlot.MainHand, cMainhandCrest, cApply, out rMainhandCrest, out rApplyMainhandCrest, locked, false, false);
}
else
{
rMainhandCrest = cMainhandCrest;
rApplyMainhandCrest = true;
}
}
if (rOffhand.Type is FullEquipType.Unknown)
{
rOffhandStain = cOffhandStain;
rOffhandCrest = cOffhandCrest;
rApplyOffhand = false;
rApplyOffhandStain = false;
rApplyOffhandCrest = false;
return changes;
}
@ -673,19 +794,31 @@ public class EquipmentDrawer
{
rApplyOffhandStain = true;
}
if (CanHaveCrest(EquipSlot.OffHand))
{
ImGui.SameLine();
changes |= DrawCrest(EquipSlot.OffHand, cOffhandCrest, cApply, out rOffhandCrest, out rApplyOffhandCrest, locked, false, true);
}
else
{
rOffhandCrest = cOffhandCrest;
rApplyOffhandCrest = true;
}
}
return changes;
}
private DataChange DrawWeaponsArtisan(EquipItem cMainhand, out EquipItem rMainhand, EquipItem cOffhand, out EquipItem rOffhand,
StainId cMainhandStain, out StainId rMainhandStain, StainId cOffhandStain, out StainId rOffhandStain, EquipFlag? cApply,
out bool rApplyMainhand, out bool rApplyMainhandStain, out bool rApplyOffhand, out bool rApplyOffhandStain)
StainId cMainhandStain, out StainId rMainhandStain, StainId cOffhandStain, out StainId rOffhandStain,
bool cMainhandCrest, out bool rMainhandCrest, bool cOffhandCrest, out bool rOffhandCrest, EquipFlag? cApply,
out bool rApplyMainhand, out bool rApplyMainhandStain, out bool rApplyMainhandCrest, out bool rApplyOffhand, out bool rApplyOffhandStain, out bool rApplyOffhandCrest)
{
rApplyMainhand = (cApply ?? 0).HasFlag(EquipFlag.Mainhand);
rApplyMainhandStain = (cApply ?? 0).HasFlag(EquipFlag.MainhandStain);
rApplyOffhand = (cApply ?? 0).HasFlag(EquipFlag.Offhand);
rApplyOffhandStain = (cApply ?? 0).HasFlag(EquipFlag.MainhandStain);
rApplyOffhandStain = (cApply ?? 0).HasFlag(EquipFlag.OffhandStain);
bool DrawWeapon(EquipItem current, out EquipItem ret)
{
@ -741,15 +874,35 @@ public class EquipmentDrawer
ImGui.SameLine();
if (DrawWeapon(cMainhand, out rMainhand))
ret |= DataChange.Item;
if (CanHaveCrest(EquipSlot.MainHand))
{
ImGui.SameLine();
ret |= DrawCrest(EquipSlot.MainHand, cMainhandCrest, cApply, out rMainhandCrest, out rApplyMainhandCrest, false, true, false);
}
else
{
rMainhandCrest = cMainhandCrest;
rApplyMainhandCrest = true;
}
}
using (var id = ImRaii.PushId(1))
{
if (DrawStainArtisan(EquipSlot.OffHand, cOffhandStain, out rOffhandStain))
ret |= DataChange.Stain;
ret |= DataChange.Stain2;
ImGui.SameLine();
if (DrawWeapon(cOffhand, out rOffhand))
ret |= DataChange.Item;
ret |= DataChange.Item2;
if (CanHaveCrest(EquipSlot.OffHand))
{
ImGui.SameLine();
ret |= DrawCrest(EquipSlot.OffHand, cOffhandCrest, cApply, out rOffhandCrest, out rApplyOffhandCrest, false, true, true);
}
else
{
rOffhandCrest = cOffhandCrest;
rApplyOffhandCrest = true;
}
}
return ret;

View file

@ -176,7 +176,7 @@ public class ActorPanel
var usedAllStain = _equipmentDrawer.DrawAllStain(out var newAllStain, _state!.IsLocked);
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var changes = _equipmentDrawer.DrawEquip(slot, _state!.ModelData, out var newArmor, out var newStain, null, out _, out _,
var changes = _equipmentDrawer.DrawEquip(slot, _state!.ModelData, out var newArmor, out var newStain, out var newCrest, null, out _, out _, out _,
_state.IsLocked);
if (usedAllStain)
{
@ -192,14 +192,20 @@ public class ActorPanel
case DataChange.Stain:
_stateManager.ChangeStain(_state, slot, newStain, StateChanged.Source.Manual);
break;
case DataChange.Crest:
_stateManager.ChangeCrest(_state, slot, newCrest, StateChanged.Source.Manual);
break;
case DataChange.Item | DataChange.Stain:
_stateManager.ChangeEquip(_state, slot, newArmor, newStain, StateChanged.Source.Manual);
case DataChange.Item | DataChange.Crest:
case DataChange.Stain | DataChange.Crest:
case DataChange.Item | DataChange.Stain | DataChange.Crest:
_stateManager.ChangeEquip(_state, slot, changes.HasFlag(DataChange.Item) ? newArmor : null, changes.HasFlag(DataChange.Stain) ? newStain : null, changes.HasFlag(DataChange.Crest) ? newCrest : null, StateChanged.Source.Manual);
break;
}
}
var weaponChanges = _equipmentDrawer.DrawWeapons(_state!.ModelData, out var newMainhand, out var newOffhand, out var newMainhandStain,
out var newOffhandStain, null, GameMain.IsInGPose(), out _, out _, out _, out _, _state.IsLocked);
out var newOffhandStain, out var newMainhandCrest, out var newOffhandCrest, null, GameMain.IsInGPose(), out _, out _, out _, out _, out _, out _, _state.IsLocked);
if (usedAllStain)
{
weaponChanges |= DataChange.Stain | DataChange.Stain2;
@ -208,20 +214,34 @@ public class ActorPanel
}
if (weaponChanges.HasFlag(DataChange.Item))
if (weaponChanges.HasFlag(DataChange.Stain))
_stateManager.ChangeEquip(_state, EquipSlot.MainHand, newMainhand, newMainhandStain, StateChanged.Source.Manual);
{
if (weaponChanges.HasFlag(DataChange.Stain) || weaponChanges.HasFlag(DataChange.Crest))
_stateManager.ChangeEquip(_state, EquipSlot.MainHand, newMainhand, weaponChanges.HasFlag(DataChange.Stain) ? newMainhandStain : null, weaponChanges.HasFlag(DataChange.Crest) ? newMainhandCrest : null, StateChanged.Source.Manual);
else
_stateManager.ChangeItem(_state, EquipSlot.MainHand, newMainhand, StateChanged.Source.Manual);
else if (weaponChanges.HasFlag(DataChange.Stain))
_stateManager.ChangeStain(_state, EquipSlot.MainHand, newMainhandStain, StateChanged.Source.Manual);
}
else
{
if (weaponChanges.HasFlag(DataChange.Stain))
_stateManager.ChangeStain(_state, EquipSlot.MainHand, newMainhandStain, StateChanged.Source.Manual);
if (weaponChanges.HasFlag(DataChange.Crest))
_stateManager.ChangeCrest(_state, EquipSlot.MainHand, newMainhandCrest, StateChanged.Source.Manual);
}
if (weaponChanges.HasFlag(DataChange.Item2))
if (weaponChanges.HasFlag(DataChange.Stain2))
_stateManager.ChangeEquip(_state, EquipSlot.OffHand, newOffhand, newOffhandStain, StateChanged.Source.Manual);
{
if (weaponChanges.HasFlag(DataChange.Stain2) || weaponChanges.HasFlag(DataChange.Crest2))
_stateManager.ChangeEquip(_state, EquipSlot.OffHand, newOffhand, weaponChanges.HasFlag(DataChange.Stain2) ? newOffhandStain : null, weaponChanges.HasFlag(DataChange.Crest2) ? newOffhandCrest : null, StateChanged.Source.Manual);
else
_stateManager.ChangeItem(_state, EquipSlot.OffHand, newOffhand, StateChanged.Source.Manual);
else if (weaponChanges.HasFlag(DataChange.Stain2))
_stateManager.ChangeStain(_state, EquipSlot.OffHand, newOffhandStain, StateChanged.Source.Manual);
}
else
{
if (weaponChanges.HasFlag(DataChange.Stain2))
_stateManager.ChangeStain(_state, EquipSlot.OffHand, newOffhandStain, StateChanged.Source.Manual);
if (weaponChanges.HasFlag(DataChange.Crest2))
_stateManager.ChangeCrest(_state, EquipSlot.OffHand, newOffhandCrest, StateChanged.Source.Manual);
}
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
if (EquipmentDrawer.DrawHatState(_state!.ModelData.IsHatVisible(), out var newHatState, _state!.IsLocked))

View file

@ -141,8 +141,8 @@ public class SetPanel
{
var (numCheckboxes, numSpacing) = (_config.ShowAllAutomatedApplicationRules, _config.ShowUnlockedItemWarnings) switch
{
(true, true) => (9, 14),
(true, false) => (7, 10),
(true, true) => (10, 15),
(true, false) => (8, 11),
(false, true) => (4, 4),
(false, false) => (2, 0),
};
@ -173,7 +173,7 @@ public class SetPanel
ImGui.TableSetupColumn("Design", ImGuiTableColumnFlags.WidthFixed, 220 * ImGuiHelpers.GlobalScale);
if (_config.ShowAllAutomatedApplicationRules)
ImGui.TableSetupColumn("Application", ImGuiTableColumnFlags.WidthFixed,
6 * ImGui.GetFrameHeight() + 10 * ImGuiHelpers.GlobalScale);
7 * ImGui.GetFrameHeight() + 11 * ImGuiHelpers.GlobalScale);
else
ImGui.TableSetupColumn("Use", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Use").X);
}
@ -182,7 +182,7 @@ public class SetPanel
ImGui.TableSetupColumn("Design / Job Restrictions", ImGuiTableColumnFlags.WidthFixed, 250 * ImGuiHelpers.GlobalScale);
if (_config.ShowAllAutomatedApplicationRules)
ImGui.TableSetupColumn("Application", ImGuiTableColumnFlags.WidthFixed,
3 * ImGui.GetFrameHeight() + 4 * ImGuiHelpers.GlobalScale);
4 * ImGui.GetFrameHeight() + 5 * ImGuiHelpers.GlobalScale);
else
ImGui.TableSetupColumn("Use", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Use").X);
}
@ -383,14 +383,16 @@ public class SetPanel
Box(0);
ImGui.SameLine();
Box(1);
ImGui.SameLine();
Box(2);
if (singleLine)
ImGui.SameLine();
Box(2);
ImGui.SameLine();
Box(3);
ImGui.SameLine();
Box(4);
ImGui.SameLine();
Box(5);
}
_manager.ChangeApplicationType(set, autoDesignIndex, newType);
@ -424,6 +426,7 @@ public class SetPanel
(AutoDesign.Type.Armor, "Apply all armor piece changes that are enabled in this design and that are valid in a fixed design."),
(AutoDesign.Type.Accessories, "Apply all accessory changes that are enabled in this design and that are valid in a fixed design."),
(AutoDesign.Type.Stains, "Apply all dye changes that are enabled in this design."),
(AutoDesign.Type.Crests, "Apply all crest visibility changes that are enabled in this design."),
(AutoDesign.Type.Weapons, "Apply all weapon changes that are enabled in this design and that are valid with the current weapon worn."),
};

View file

@ -335,14 +335,15 @@ public unsafe class DebugTab : ITab
return;
if (ImGui.SmallButton("Hide"))
_updateSlotService.UpdateSlot(model, EquipSlot.Head, CharacterArmor.Empty);
_updateSlotService.UpdateSlot(model, EquipSlot.Head, CharacterArmor.Empty, null);
ImGui.SameLine();
if (ImGui.SmallButton("Show"))
_updateSlotService.UpdateSlot(model, EquipSlot.Head, actor.GetArmor(EquipSlot.Head));
_updateSlotService.UpdateSlot(model, EquipSlot.Head, actor.GetArmor(EquipSlot.Head), actor.GetCrest(EquipSlot.Head));
ImGui.SameLine();
if (ImGui.SmallButton("Toggle"))
_updateSlotService.UpdateSlot(model, EquipSlot.Head,
model.AsHuman->Head.Value == 0 ? actor.GetArmor(EquipSlot.Head) : CharacterArmor.Empty);
model.AsHuman->Head.Value == 0 ? actor.GetArmor(EquipSlot.Head) : CharacterArmor.Empty,
model.AsHuman->Head.Value == 0 ? actor.GetCrest(EquipSlot.Head) : null);
}
private void DrawWeaponState(Actor actor, Model model)
@ -423,8 +424,11 @@ public unsafe class DebugTab : ITab
if (ImGui.SmallButton("Change Stain"))
_updateSlotService.UpdateStain(model, slot, 5);
ImGui.SameLine();
if (ImGui.SmallButton("Toggle Crest"))
_updateSlotService.UpdateCrest(model, slot, !model.IsFreeCompanyCrestVisibleOnSlot((byte)slot.ToIndex()));
ImGui.SameLine();
if (ImGui.SmallButton("Reset"))
_updateSlotService.UpdateSlot(model, slot, actor.GetArmor(slot));
_updateSlotService.UpdateSlot(model, slot, actor.GetArmor(slot), actor.GetCrest(slot));
}
}
@ -1146,7 +1150,7 @@ public unsafe class DebugTab : ITab
public void DrawState(ActorData data, ActorState state)
{
using var table = ImRaii.Table("##state", 7, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit);
using var table = ImRaii.Table("##state", 9, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
@ -1189,10 +1193,12 @@ public unsafe class DebugTab : ITab
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]);
PrintRow(slot.ToName(), ItemString(state.BaseData, slot), ItemString(state.ModelData, slot), state[slot, ActorState.EquipField.Item]);
ImGuiUtil.DrawTableColumn(state.BaseData.Stain(slot).Id.ToString());
ImGuiUtil.DrawTableColumn(state.ModelData.Stain(slot).Id.ToString());
ImGuiUtil.DrawTableColumn(state[slot, true].ToString());
ImGuiUtil.DrawTableColumn(state[slot, ActorState.EquipField.Stain].ToString());
ImGuiUtil.DrawTableColumn(state.BaseData.Crest(slot).ToString());
ImGuiUtil.DrawTableColumn(state[slot, ActorState.EquipField.Crest].ToString());
}
foreach (var type in Enum.GetValues<CustomizeIndex>())
@ -1307,12 +1313,16 @@ public unsafe class DebugTab : ITab
var apply = design.DoApplyEquip(slot);
var stain = design.DesignData.Stain(slot);
var applyStain = design.DoApplyStain(slot);
var crest = design.DesignData.Crest(slot);
var applyCrest = design.DoApplyCrest(slot);
ImGuiUtil.DrawTableColumn(slot.ToName());
ImGuiUtil.DrawTableColumn(item.Name);
ImGuiUtil.DrawTableColumn(item.ItemId.ToString());
ImGuiUtil.DrawTableColumn(apply ? "Apply" : "Keep");
ImGuiUtil.DrawTableColumn(stain.ToString());
ImGuiUtil.DrawTableColumn(applyStain ? "Apply" : "Keep");
ImGuiUtil.DrawTableColumn(crest.ToString());
ImGuiUtil.DrawTableColumn(applyCrest ? "Apply" : "Keep");
}
ImGuiUtil.DrawTableColumn("Hat Visible");

View file

@ -118,6 +118,7 @@ public sealed class DesignFileSystemSelector : FileSystemSelector<Design, Design
case DesignChanged.Type.ApplyCustomize:
case DesignChanged.Type.ApplyEquip:
case DesignChanged.Type.ApplyStain:
case DesignChanged.Type.ApplyCrest:
case DesignChanged.Type.Customize:
case DesignChanged.Type.Equip:
case DesignChanged.Type.ChangedColor:

View file

@ -28,6 +28,8 @@ public class DesignPanel(DesignFileSystemSelector _selector, CustomizationDrawer
ObjectManager _objects, StateManager _state, EquipmentDrawer _equipmentDrawer, ModAssociationsTab _modAssociations,
DesignDetailTab _designDetails, DesignConverter _converter, ImportService _importService, MultiDesignPanel _multiDesignPanel)
{
private static readonly EquipSlot[] CrestSlots = EquipSlotExtensions.FullSlots.Where(EquipmentDrawer.CanHaveCrest).ToArray();
private readonly FileDialogManager _fileDialog = new();
private HeaderDrawer.Button LockButton()
@ -95,23 +97,28 @@ public class DesignPanel(DesignFileSystemSelector _selector, CustomizationDrawer
var usedAllStain = _equipmentDrawer.DrawAllStain(out var newAllStain, _selector.Selected!.WriteProtected());
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var changes = _equipmentDrawer.DrawEquip(slot, _selector.Selected!.DesignData, out var newArmor, out var newStain,
_selector.Selected.ApplyEquip, out var newApply, out var newApplyStain, _selector.Selected!.WriteProtected());
var changes = _equipmentDrawer.DrawEquip(slot, _selector.Selected!.DesignData, out var newArmor, out var newStain, out var newCrest,
_selector.Selected.ApplyEquip, out var newApply, out var newApplyStain, out var newApplyCrest, _selector.Selected!.WriteProtected());
if (changes.HasFlag(DataChange.Item))
_manager.ChangeEquip(_selector.Selected, slot, newArmor);
if (changes.HasFlag(DataChange.Stain))
_manager.ChangeStain(_selector.Selected, slot, newStain);
else if (usedAllStain)
_manager.ChangeStain(_selector.Selected, slot, newAllStain);
if (changes.HasFlag(DataChange.Crest))
_manager.ChangeCrest(_selector.Selected, slot, newCrest);
if (changes.HasFlag(DataChange.ApplyItem))
_manager.ChangeApplyEquip(_selector.Selected, slot, newApply);
if (changes.HasFlag(DataChange.ApplyStain))
_manager.ChangeApplyStain(_selector.Selected, slot, newApplyStain);
if (changes.HasFlag(DataChange.ApplyCrest))
_manager.ChangeApplyCrest(_selector.Selected, slot, newApplyCrest);
}
var weaponChanges = _equipmentDrawer.DrawWeapons(_selector.Selected!.DesignData, out var newMainhand, out var newOffhand,
out var newMainhandStain, out var newOffhandStain, _selector.Selected.ApplyEquip, true, out var applyMain, out var applyMainStain,
out var applyOff, out var applyOffStain, _selector.Selected!.WriteProtected());
out var newMainhandStain, out var newOffhandStain, out var newMainhandCrest, out var newOffhandCrest,
_selector.Selected.ApplyEquip, true, out var applyMain, out var applyMainStain, out var applyMainCrest,
out var applyOff, out var applyOffStain, out var applyOffCrest, _selector.Selected!.WriteProtected());
if (weaponChanges.HasFlag(DataChange.Item))
_manager.ChangeWeapon(_selector.Selected, EquipSlot.MainHand, newMainhand);
@ -119,20 +126,28 @@ public class DesignPanel(DesignFileSystemSelector _selector, CustomizationDrawer
_manager.ChangeStain(_selector.Selected, EquipSlot.MainHand, newMainhandStain);
else if (usedAllStain)
_manager.ChangeStain(_selector.Selected, EquipSlot.MainHand, newAllStain);
if (weaponChanges.HasFlag(DataChange.Crest))
_manager.ChangeCrest(_selector.Selected, EquipSlot.MainHand, newMainhandCrest);
if (weaponChanges.HasFlag(DataChange.ApplyItem))
_manager.ChangeApplyEquip(_selector.Selected, EquipSlot.MainHand, applyMain);
if (weaponChanges.HasFlag(DataChange.ApplyStain))
_manager.ChangeApplyStain(_selector.Selected, EquipSlot.MainHand, applyMainStain);
if (weaponChanges.HasFlag(DataChange.ApplyCrest))
_manager.ChangeApplyCrest(_selector.Selected, EquipSlot.MainHand, applyMainCrest);
if (weaponChanges.HasFlag(DataChange.Item2))
_manager.ChangeWeapon(_selector.Selected, EquipSlot.OffHand, newOffhand);
if (weaponChanges.HasFlag(DataChange.Stain2))
_manager.ChangeStain(_selector.Selected, EquipSlot.OffHand, newOffhandStain);
else if (usedAllStain)
_manager.ChangeStain(_selector.Selected, EquipSlot.OffHand, newAllStain);
if (weaponChanges.HasFlag(DataChange.Crest2))
_manager.ChangeCrest(_selector.Selected, EquipSlot.OffHand, newOffhandCrest);
if (weaponChanges.HasFlag(DataChange.ApplyItem2))
_manager.ChangeApplyEquip(_selector.Selected, EquipSlot.OffHand, applyOff);
if (weaponChanges.HasFlag(DataChange.ApplyStain2))
_manager.ChangeApplyStain(_selector.Selected, EquipSlot.OffHand, applyOffStain);
if (weaponChanges.HasFlag(DataChange.ApplyCrest2))
_manager.ChangeApplyCrest(_selector.Selected, EquipSlot.OffHand, applyOffCrest);
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
DrawEquipmentMetaToggles();
@ -223,43 +238,59 @@ public class DesignPanel(DesignFileSystemSelector _selector, CustomizationDrawer
ImGui.SameLine(ImGui.GetContentRegionAvail().X / 2);
using (var _ = ImRaii.Group())
{
void ApplyEquip(string label, EquipFlag allFlags, bool stain, IEnumerable<EquipSlot> slots)
void ApplyEquip(string label, EquipFlag allFlags, ActorState.EquipField equipField, IEnumerable<EquipSlot> slots)
{
var flags = (uint)(allFlags & _selector.Selected!.ApplyEquip);
// The flags we may edit with a single "big change" checkbox will always fit in a uint, but the whole bitfield doesn't
var shift = (int)equipField;
var flags = (uint)((ulong)(allFlags & _selector.Selected!.ApplyEquip) >> shift);
var bigChange = ImGui.CheckboxFlags($"Apply All {label}", ref flags, (uint)allFlags);
if (stain)
foreach (var slot in slots)
{
var apply = bigChange ? ((EquipFlag)flags).HasFlag(slot.ToStainFlag()) : _selector.Selected!.DoApplyStain(slot);
if (ImGui.Checkbox($"Apply {slot.ToName()} Dye", ref apply) || bigChange)
_manager.ChangeApplyStain(_selector.Selected!, slot, apply);
}
else
foreach (var slot in slots)
{
var apply = bigChange ? ((EquipFlag)flags).HasFlag(slot.ToFlag()) : _selector.Selected!.DoApplyEquip(slot);
if (ImGui.Checkbox($"Apply {slot.ToName()}", ref apply) || bigChange)
_manager.ChangeApplyEquip(_selector.Selected!, slot, apply);
}
var bigChange = ImGui.CheckboxFlags($"Apply All {label}", ref flags, (uint)((ulong)allFlags >> shift));
var adjustedFlags = (EquipFlag)((ulong)flags << shift);
switch (equipField)
{
case ActorState.EquipField.Stain:
foreach (var slot in slots)
{
var apply = bigChange ? adjustedFlags.HasFlag(slot.ToStainFlag()) : _selector.Selected!.DoApplyStain(slot);
if (ImGui.Checkbox($"Apply {slot.ToName()} Dye", ref apply) || bigChange)
_manager.ChangeApplyStain(_selector.Selected!, slot, apply);
}
break;
case ActorState.EquipField.Crest:
foreach (var slot in slots)
{
var apply = bigChange ? adjustedFlags.HasFlag(slot.ToCrestFlag()) : _selector.Selected!.DoApplyCrest(slot);
if (ImGui.Checkbox($"Apply {slot.ToName()} Crest Visibility", ref apply) || bigChange)
_manager.ChangeApplyCrest(_selector.Selected!, slot, apply);
}
break;
default:
foreach (var slot in slots)
{
var apply = bigChange ? adjustedFlags.HasFlag(slot.ToFlag()) : _selector.Selected!.DoApplyEquip(slot);
if (ImGui.Checkbox($"Apply {slot.ToName()}", ref apply) || bigChange)
_manager.ChangeApplyEquip(_selector.Selected!, slot, apply);
}
break;
}
}
ApplyEquip("Weapons", AutoDesign.WeaponFlags, false, new[]
{
EquipSlot.MainHand,
EquipSlot.OffHand,
});
ApplyEquip("Weapons", AutoDesign.WeaponFlags, ActorState.EquipField.Item, EquipSlotExtensions.WeaponSlots);
ImGui.NewLine();
ApplyEquip("Armor", AutoDesign.ArmorFlags, false, EquipSlotExtensions.EquipmentSlots);
ApplyEquip("Armor", AutoDesign.ArmorFlags, ActorState.EquipField.Item, EquipSlotExtensions.EquipmentSlots);
ImGui.NewLine();
ApplyEquip("Accessories", AutoDesign.AccessoryFlags, false, EquipSlotExtensions.AccessorySlots);
ApplyEquip("Accessories", AutoDesign.AccessoryFlags, ActorState.EquipField.Item, EquipSlotExtensions.AccessorySlots);
ImGui.NewLine();
ApplyEquip("Dyes", AutoDesign.StainFlags, true,
ApplyEquip("Dyes", AutoDesign.StainFlags, ActorState.EquipField.Stain,
EquipSlotExtensions.FullSlots);
ImGui.NewLine();
ApplyEquip("Crest Visibilities", AutoDesign.RelevantCrestFlags, ActorState.EquipField.Crest, CrestSlots);
ImGui.NewLine();
const uint all = 0x0Fu;
var flags = (_selector.Selected!.DoApplyHatVisible() ? 0x01u : 0x00)

View file

@ -17,7 +17,7 @@ using Penumbra.GameData.Structs;
namespace Glamourer.Gui;
[Flags]
public enum DataChange : byte
public enum DataChange : ushort
{
None = 0x00,
Item = 0x01,
@ -28,6 +28,10 @@ public enum DataChange : byte
Stain2 = 0x20,
ApplyItem2 = 0x40,
ApplyStain2 = 0x80,
Crest = 0x100,
ApplyCrest = 0x200,
Crest2 = 0x400,
ApplyCrest2 = 0x800,
}
public static class UiHelpers

View file

@ -119,14 +119,14 @@ public class ContextMenuService : IDisposable
return;
var slot = item.Type.ToSlot();
_state.ChangeEquip(state, slot, item, 0, StateChanged.Source.Manual);
_state.ChangeEquip(state, slot, item, 0, false, StateChanged.Source.Manual);
if (item.Type.ValidOffhand().IsOffhandType())
{
if (item.ModelId.Id is > 1600 and < 1651
&& _items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.Hands, out var gauntlets))
_state.ChangeEquip(state, EquipSlot.Hands, gauntlets, 0, StateChanged.Source.Manual);
_state.ChangeEquip(state, EquipSlot.Hands, gauntlets, 0, false, StateChanged.Source.Manual);
if (_items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand))
_state.ChangeEquip(state, EquipSlot.OffHand, offhand, 0, StateChanged.Source.Manual);
_state.ChangeEquip(state, EquipSlot.OffHand, offhand, 0, false, StateChanged.Source.Manual);
}
};
}
@ -143,14 +143,14 @@ public class ContextMenuService : IDisposable
return;
var slot = item.Type.ToSlot();
_state.ChangeEquip(state, slot, item, 0, StateChanged.Source.Manual);
_state.ChangeEquip(state, slot, item, 0, false, StateChanged.Source.Manual);
if (item.Type.ValidOffhand().IsOffhandType())
{
if (item.ModelId.Id is > 1600 and < 1651
&& _items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.Hands, out var gauntlets))
_state.ChangeEquip(state, EquipSlot.Hands, gauntlets, 0, StateChanged.Source.Manual);
_state.ChangeEquip(state, EquipSlot.Hands, gauntlets, 0, false, StateChanged.Source.Manual);
if (_items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand))
_state.ChangeEquip(state, EquipSlot.OffHand, offhand, 0, StateChanged.Source.Manual);
_state.ChangeEquip(state, EquipSlot.OffHand, offhand, 0, false, StateChanged.Source.Manual);
}
};
}

View file

@ -106,6 +106,21 @@ public readonly unsafe struct Actor : IEquatable<Actor>
public CharacterArmor GetArmor(EquipSlot slot)
=> ((CharacterArmor*)&AsCharacter->DrawData.Head)[slot.ToIndex()];
public bool GetCrest(EquipSlot slot)
=> (GetFreeCompanyCrestBitfield() & CrestMask(slot)) != 0;
private static byte CrestMask(EquipSlot slot)
=> slot switch
{
EquipSlot.OffHand => 0x01,
EquipSlot.Head => 0x02,
EquipSlot.Body => 0x04,
EquipSlot.Hands => 0x08,
EquipSlot.Legs => 0x10,
EquipSlot.Feet => 0x20,
_ => 0x00,
};
public CharacterWeapon GetMainhand()
=> new(AsCharacter->DrawData.Weapon(DrawDataContainer.WeaponSlot.MainHand).ModelId.Value);
@ -115,6 +130,10 @@ public readonly unsafe struct Actor : IEquatable<Actor>
public Customize GetCustomize()
=> *(Customize*)&AsCharacter->DrawData.CustomizeData;
// TODO remove this when available in ClientStructs
public byte GetFreeCompanyCrestBitfield()
=> ((byte*)Address)[0x1BBB];
public override string ToString()
=> $"0x{Address:X}";
}

View file

@ -91,6 +91,9 @@ public readonly unsafe struct Model : IEquatable<Model>
public CharacterArmor GetArmor(EquipSlot slot)
=> ((CharacterArmor*)&AsHuman->Head)[slot.ToIndex()];
public bool GetCrest(EquipSlot slot)
=> IsFreeCompanyCrestVisibleOnSlot((byte)slot.ToIndex());
public Customize GetCustomize()
=> *(Customize*)&AsHuman->Customize;
@ -195,6 +198,27 @@ public readonly unsafe struct Model : IEquatable<Model>
return discriminator1 == 0 && discriminator2 != 0 ? (second, first) : (first, second);
}
// TODO remove these when available in ClientStructs
public bool IsFreeCompanyCrestVisibleOnSlot(byte slot)
{
if (!IsCharacterBase)
return false;
var characterBase = AsCharacterBase;
var getter = (delegate* unmanaged<CharacterBase*, byte, byte>)((nint*)characterBase->VTable)[95];
return getter(characterBase, slot) != 0;
}
public void SetFreeCompanyCrestVisibleOnSlot(byte slot, bool visible)
{
if (!IsCharacterBase)
return;
var characterBase = AsCharacterBase;
var setter = (delegate* unmanaged<CharacterBase*, byte, byte, void>)((nint*)characterBase->VTable)[96];
setter(characterBase, slot, visible ? (byte)1 : (byte)0);
}
public override string ToString()
=> $"0x{Address:X}";
}

View file

@ -1,4 +1,6 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading;
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures;
@ -6,47 +8,78 @@ using Glamourer.Events;
using Glamourer.Interop.Structs;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using PenumbraSigs = Penumbra.GameData.Sigs;
namespace Glamourer.Interop;
public unsafe class UpdateSlotService : IDisposable
{
public readonly SlotUpdating SlotUpdatingEvent;
public readonly SlotUpdating SlotUpdatingEvent;
public readonly CrestVisibilityUpdating CrestVisibilityUpdatingEvent;
private readonly ThreadLocal<uint> _crestVisibilityUpdate = new(() => 0, false);
public UpdateSlotService(SlotUpdating slotUpdating, IGameInteropProvider interop)
public UpdateSlotService(SlotUpdating slotUpdating, CrestVisibilityUpdating crestVisibilityUpdating, IGameInteropProvider interop)
{
SlotUpdatingEvent = slotUpdating;
SlotUpdatingEvent = slotUpdating;
CrestVisibilityUpdatingEvent = crestVisibilityUpdating;
interop.InitializeFromAttributes(this);
_humanSetFreeCompanyCrestVisibleOnSlot = interop.HookFromAddress<SetCrestDelegateIntern>(_humanVTable[96], HumanSetFreeCompanyCrestVisibleOnSlotDetour);
_weaponSetFreeCompanyCrestVisibleOnSlot = interop.HookFromAddress<SetCrestDelegateIntern>(_weaponVTable[96], WeaponSetFreeCompanyCrestVisibleOnSlotDetour);
_flagSlotForUpdateHook.Enable();
_humanSetFreeCompanyCrestVisibleOnSlot.Enable();
_weaponSetFreeCompanyCrestVisibleOnSlot.Enable();
}
public void Dispose()
{
_flagSlotForUpdateHook.Dispose();
_humanSetFreeCompanyCrestVisibleOnSlot.Dispose();
_weaponSetFreeCompanyCrestVisibleOnSlot.Dispose();
}
public void UpdateSlot(Model drawObject, EquipSlot slot, CharacterArmor data)
public void UpdateSlot(Model drawObject, EquipSlot slot, CharacterArmor data, bool? crest)
{
if (!drawObject.IsCharacterBase)
return;
FlagSlotForUpdateInterop(drawObject, slot, data);
if (crest.HasValue)
{
using var _ = EnterCrestVisibilityUpdate();
drawObject.SetFreeCompanyCrestVisibleOnSlot((byte)slot.ToIndex(), crest.Value);
}
}
public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor armor, StainId stain)
=> UpdateSlot(drawObject, slot, armor.With(stain));
public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor armor, StainId stain, bool? crest)
=> UpdateSlot(drawObject, slot, armor.With(stain), crest);
public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor armor)
=> UpdateArmor(drawObject, slot, armor, drawObject.GetArmor(slot).Stain);
=> UpdateArmor(drawObject, slot, armor, drawObject.GetArmor(slot).Stain, null);
public void UpdateStain(Model drawObject, EquipSlot slot, StainId stain)
=> UpdateArmor(drawObject, slot, drawObject.GetArmor(slot), stain);
=> UpdateArmor(drawObject, slot, drawObject.GetArmor(slot), stain, null);
public void UpdateCrest(Model drawObject, EquipSlot slot, bool crest)
{
using var _ = EnterCrestVisibilityUpdate();
drawObject.SetFreeCompanyCrestVisibleOnSlot((byte)slot.ToIndex(), crest);
}
private delegate ulong FlagSlotForUpdateDelegateIntern(nint drawObject, uint slot, CharacterArmor* data);
private delegate void SetCrestDelegateIntern(nint drawObject, byte slot, byte visible);
[Signature(Sigs.FlagSlotForUpdate, DetourName = nameof(FlagSlotForUpdateDetour))]
private readonly Hook<FlagSlotForUpdateDelegateIntern> _flagSlotForUpdateHook = null!;
[Signature(PenumbraSigs.HumanVTable, ScanType = ScanType.StaticAddress)]
private readonly nint* _humanVTable = null!;
[Signature(PenumbraSigs.WeaponVTable, ScanType = ScanType.StaticAddress)]
private readonly nint* _weaponVTable = null!;
private readonly Hook<SetCrestDelegateIntern> _humanSetFreeCompanyCrestVisibleOnSlot;
private readonly Hook<SetCrestDelegateIntern> _weaponSetFreeCompanyCrestVisibleOnSlot;
private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data)
{
var slot = slotIdx.ToEquipSlot();
@ -56,6 +89,52 @@ public unsafe class UpdateSlotService : IDisposable
return returnValue == ulong.MaxValue ? _flagSlotForUpdateHook.Original(drawObject, slotIdx, data) : returnValue;
}
private void HumanSetFreeCompanyCrestVisibleOnSlotDetour(nint drawObject, byte slotIdx, byte visible)
{
var slot = ((uint)slotIdx).ToEquipSlot();
var rVisible = visible != 0;
var inUpdate = _crestVisibilityUpdate.IsValueCreated && _crestVisibilityUpdate.Value > 0;
if (!inUpdate)
CrestVisibilityUpdatingEvent.Invoke(drawObject, slot, ref rVisible);
Glamourer.Log.Excessive($"[Human.SetFreeCompanyCrestVisibleOnSlot] Called with 0x{drawObject:X} for slot {slot} with {rVisible} (original: {visible != 0}, in update: {inUpdate}).");
_humanSetFreeCompanyCrestVisibleOnSlot.Original(drawObject, slotIdx, rVisible ? (byte)1 : (byte)0);
}
private void WeaponSetFreeCompanyCrestVisibleOnSlotDetour(nint drawObject, byte slotIdx, byte visible)
{
var rVisible = visible != 0;
var inUpdate = _crestVisibilityUpdate.IsValueCreated && _crestVisibilityUpdate.Value > 0;
if (!inUpdate)
CrestVisibilityUpdatingEvent.Invoke(drawObject, EquipSlot.BothHand, ref rVisible);
Glamourer.Log.Excessive($"[Weapon.SetFreeCompanyCrestVisibleOnSlot] Called with 0x{drawObject:X} with {rVisible} (original: {visible != 0}, in update: {inUpdate}).");
_weaponSetFreeCompanyCrestVisibleOnSlot.Original(drawObject, slotIdx, rVisible ? (byte)1 : (byte)0);
}
private ulong FlagSlotForUpdateInterop(Model drawObject, EquipSlot slot, CharacterArmor armor)
=> _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &armor);
/// <summary>
/// Temporarily disables the crest update hooks on the current thread.
/// </summary>
/// <returns> A struct that will undo this operation when disposed. Best used with: <code>using (var _ = updateSlotService.EnterCrestVisibilityUpdate()) { ... }</code> </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public CrestVisibilityUpdateRaii EnterCrestVisibilityUpdate()
=> new(this);
public readonly ref struct CrestVisibilityUpdateRaii
{
private readonly ThreadLocal<uint> _crestVisibilityUpdate;
public CrestVisibilityUpdateRaii(UpdateSlotService parent)
{
_crestVisibilityUpdate = parent._crestVisibilityUpdate;
++_crestVisibilityUpdate.Value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public readonly void Dispose()
{
--_crestVisibilityUpdate.Value;
}
}
}

View file

@ -13,6 +13,7 @@ namespace Glamourer.Interop;
public unsafe class WeaponService : IDisposable
{
private readonly WeaponLoading _event;
private readonly UpdateSlotService _updateSlotService;
private readonly ThreadLocal<bool> _inUpdate = new(() => false);
@ -20,9 +21,10 @@ public unsafe class WeaponService : IDisposable
_original;
public WeaponService(WeaponLoading @event, IGameInteropProvider interop)
public WeaponService(WeaponLoading @event, UpdateSlotService updateSlotService, IGameInteropProvider interop)
{
_event = @event;
_updateSlotService = updateSlotService;
_loadWeaponHook =
interop.HookFromAddress<LoadWeaponDelegate>((nint)DrawDataContainer.MemberFunctionPointers.LoadWeapon, LoadWeaponDetour);
_original =
@ -83,24 +85,39 @@ public unsafe class WeaponService : IDisposable
}
// Load a specific weapon for a character by its data and slot.
public void LoadWeapon(Actor character, EquipSlot slot, CharacterWeapon weapon)
public void LoadWeapon(Actor character, EquipSlot slot, CharacterWeapon weapon, bool? crest)
{
switch (slot)
{
case EquipSlot.MainHand:
_inUpdate.Value = true;
_loadWeaponHook.Original(&character.AsCharacter->DrawData, 0, weapon.Value, 1, 0, 1, 0);
if (crest.HasValue)
{
using var _ = _updateSlotService.EnterCrestVisibilityUpdate();
character.Model.GetMainhand().Address.SetFreeCompanyCrestVisibleOnSlot(0, crest.Value);
}
_inUpdate.Value = false;
return;
case EquipSlot.OffHand:
_inUpdate.Value = true;
_loadWeaponHook.Original(&character.AsCharacter->DrawData, 1, weapon.Value, 1, 0, 1, 0);
if (crest.HasValue)
{
using var _ = _updateSlotService.EnterCrestVisibilityUpdate();
character.Model.GetOffhand().Address.SetFreeCompanyCrestVisibleOnSlot(0, crest.Value);
}
_inUpdate.Value = false;
return;
case EquipSlot.BothHand:
_inUpdate.Value = true;
_loadWeaponHook.Original(&character.AsCharacter->DrawData, 0, weapon.Value, 1, 0, 1, 0);
_loadWeaponHook.Original(&character.AsCharacter->DrawData, 1, CharacterWeapon.Empty.Value, 1, 0, 1, 0);
if (crest.HasValue)
{
using var _ = _updateSlotService.EnterCrestVisibilityUpdate();
character.Model.GetMainhand().Address.SetFreeCompanyCrestVisibleOnSlot(0, crest.Value);
}
_inUpdate.Value = false;
return;
}
@ -112,6 +129,21 @@ public unsafe class WeaponService : IDisposable
var (_, _, mh, oh) = mdl.GetWeapons(character);
var value = slot == EquipSlot.OffHand ? oh : mh;
var weapon = value.With(value.Set.Id == 0 ? 0 : stain);
LoadWeapon(character, slot, weapon);
LoadWeapon(character, slot, weapon, null);
}
public void UpdateCrest(Actor character, EquipSlot slot, bool visible)
{
using var _ = _updateSlotService.EnterCrestVisibilityUpdate();
switch (slot)
{
case EquipSlot.MainHand:
case EquipSlot.BothHand:
character.Model.GetMainhand().Address.SetFreeCompanyCrestVisibleOnSlot(0, visible);
break;
case EquipSlot.OffHand:
character.Model.GetOffhand().Address.SetFreeCompanyCrestVisibleOnSlot(0, visible);
break;
}
}
}

View file

@ -165,8 +165,9 @@ public class CommandService : IDisposable
.AddInitialPurple("Customizations, ")
.AddInitialPurple("Equipment, ")
.AddInitialPurple("Accessories, ")
.AddInitialPurple("Dyes and ")
.AddInitialPurple("Weapons, where ").AddPurple("CEADW")
.AddInitialPurple("Dyes, ")
.AddInitialPurple("Free company crest visibilities and ")
.AddInitialPurple("Weapons, where ").AddPurple("CEADFW")
.AddText(" means everything should be toggled on, and no value means nothing should be toggled on.")
.BuiltString);
return false;
@ -270,6 +271,9 @@ public class CommandService : IDisposable
case 'd':
applicationFlags |= AutoDesign.Type.Stains;
break;
case 'f':
applicationFlags |= AutoDesign.Type.Crests;
break;
case 'w':
applicationFlags |= AutoDesign.Type.Weapons;
break;

View file

@ -64,6 +64,7 @@ public static class ServiceManager
private static IServiceCollection AddEvents(this IServiceCollection services)
=> services.AddSingleton<VisorStateChanged>()
.AddSingleton<SlotUpdating>()
.AddSingleton<CrestVisibilityUpdating>()
.AddSingleton<DesignChanged>()
.AddSingleton<AutomationChanged>()
.AddSingleton<StateChanged>()

View file

@ -22,6 +22,13 @@ public class ActorState
ModelId,
}
public enum EquipField
{
Item = 0,
Stain = 12,
Crest = 24,
}
public readonly ActorIdentifier Identifier;
public bool AllowsRedraw(ICondition condition)
@ -85,8 +92,8 @@ public class ActorState
internal ActorState(ActorIdentifier identifier)
=> Identifier = identifier.CreatePermanent();
public ref StateChanged.Source this[EquipSlot slot, bool stain]
=> ref _sources[slot.ToIndex() + (stain ? EquipFlagExtensions.NumEquipFlags / 2 : 0)];
public ref StateChanged.Source this[EquipSlot slot, EquipField field]
=> ref _sources[slot.ToIndex() + (int)field];
public ref StateChanged.Source this[CustomizeIndex type]
=> ref _sources[EquipFlagExtensions.NumEquipFlags + (int)type];

View file

@ -99,11 +99,11 @@ public class StateApplier
}
/// <summary>
/// Change a single piece of armor and/or stain depending on slot.
/// Change a single piece of armor and/or stain and/or crest visibility depending on slot.
/// This uses the current customization of the model to potentially prevent restricted gear types from appearing.
/// This never requires redrawing.
/// </summary>
public void ChangeArmor(ActorData data, EquipSlot slot, CharacterArmor armor, bool checkRestrictions, bool isHatVisible = true)
public void ChangeArmor(ActorData data, EquipSlot slot, CharacterArmor armor, bool crest, bool checkRestrictions, bool isHatVisible = true)
{
if (slot is EquipSlot.Head && !isHatVisible)
return;
@ -118,11 +118,11 @@ public class StateApplier
{
var customize = mdl.GetCustomize();
var (_, resolvedItem) = _items.ResolveRestrictedGear(armor, slot, customize.Race, customize.Gender);
_updateSlot.UpdateSlot(actor.Model, slot, resolvedItem);
_updateSlot.UpdateSlot(actor.Model, slot, resolvedItem, crest);
}
else
{
_updateSlot.UpdateSlot(actor.Model, slot, armor);
_updateSlot.UpdateSlot(actor.Model, slot, armor, crest);
}
}
}
@ -133,7 +133,7 @@ public class StateApplier
// If the source is not IPC we do not want to apply restrictions.
var data = GetData(state);
if (apply)
ChangeArmor(data, slot, state.ModelData.Armor(slot), state[slot, false] is not StateChanged.Source.Ipc,
ChangeArmor(data, slot, state.ModelData.Armor(slot), state.ModelData.Crest(slot), state[slot, ActorState.EquipField.Item] is not StateChanged.Source.Ipc,
state.ModelData.IsHatVisible());
return data;
@ -175,13 +175,47 @@ public class StateApplier
}
/// <summary>
/// Change the crest visibility of a single piece of armor or weapon.
/// </summary>
public void ChangeCrest(ActorData data, EquipSlot slot, bool visible)
{
var idx = slot.ToIndex();
switch (idx)
{
case < 10:
foreach (var actor in data.Objects.Where(a => a.Model.IsHuman))
_updateSlot.UpdateCrest(actor.Model, slot, visible);
break;
case 10:
foreach (var actor in data.Objects.Where(a => a.Model.IsHuman))
_weapon.UpdateCrest(actor, EquipSlot.MainHand, visible);
break;
case 11:
foreach (var actor in data.Objects.Where(a => a.Model.IsHuman))
_weapon.UpdateCrest(actor, EquipSlot.OffHand, visible);
break;
}
}
/// <inheritdoc cref="ChangeCrest(ActorData,EquipSlot,bool)"/>
public ActorData ChangeCrest(ActorState state, EquipSlot slot, bool apply)
{
var data = GetData(state);
if (apply)
ChangeCrest(data, slot, state.ModelData.Crest(slot));
return data;
}
/// <summary> Apply a weapon to the appropriate slot. </summary>
public void ChangeWeapon(ActorData data, EquipSlot slot, EquipItem item, StainId stain)
public void ChangeWeapon(ActorData data, EquipSlot slot, EquipItem item, StainId stain, bool crest)
{
if (slot is EquipSlot.MainHand)
ChangeMainhand(data, item, stain);
ChangeMainhand(data, item, stain, crest);
else
ChangeOffhand(data, item, stain);
ChangeOffhand(data, item, stain, crest);
}
/// <inheritdoc cref="ChangeWeapon(ActorData,EquipSlot,EquipItem,StainId)"/>
@ -192,7 +226,7 @@ public class StateApplier
data = data.OnlyGPose();
if (apply)
ChangeWeapon(data, slot, state.ModelData.Item(slot), state.ModelData.Stain(slot));
ChangeWeapon(data, slot, state.ModelData.Item(slot), state.ModelData.Stain(slot), state.ModelData.Crest(slot));
return data;
}
@ -200,19 +234,19 @@ public class StateApplier
/// <summary>
/// Apply a weapon to the mainhand. If the weapon type has no associated offhand type, apply both.
/// </summary>
public void ChangeMainhand(ActorData data, EquipItem weapon, StainId stain)
public void ChangeMainhand(ActorData data, EquipItem weapon, StainId stain, bool crest)
{
var slot = weapon.Type.ValidOffhand() == FullEquipType.Unknown ? EquipSlot.BothHand : EquipSlot.MainHand;
foreach (var actor in data.Objects.Where(a => a.Model.IsHuman))
_weapon.LoadWeapon(actor, slot, weapon.Weapon().With(stain));
_weapon.LoadWeapon(actor, slot, weapon.Weapon().With(stain), crest);
}
/// <summary> Apply a weapon to the offhand. </summary>
public void ChangeOffhand(ActorData data, EquipItem weapon, StainId stain)
public void ChangeOffhand(ActorData data, EquipItem weapon, StainId stain, bool crest)
{
stain = weapon.ModelId.Id == 0 ? 0 : stain;
foreach (var actor in data.Objects.Where(a => a.Model.IsHuman))
_weapon.LoadWeapon(actor, EquipSlot.OffHand, weapon.Weapon().With(stain));
_weapon.LoadWeapon(actor, EquipSlot.OffHand, weapon.Weapon().With(stain), crest);
}
/// <summary> Change the visor state of actors only on the draw object. </summary>

View file

@ -67,8 +67,9 @@ public class StateEditor
state[ActorState.MetaIndex.VisorState] = source;
foreach (var slot in EquipSlotExtensions.FullSlots)
{
state[slot, true] = source;
state[slot, false] = source;
state[slot, ActorState.EquipField.Stain] = source;
state[slot, ActorState.EquipField.Item] = source;
state[slot, ActorState.EquipField.Crest] = source;
}
state[CustomizeIndex.Clan] = source;
@ -126,7 +127,7 @@ public class StateEditor
return true;
}
/// <summary> Change a single piece of equipment without stain. </summary>
/// <summary> Change a single piece of equipment without stain or crest visibility. </summary>
public bool ChangeItem(ActorState state, EquipSlot slot, EquipItem item, StateChanged.Source source, out EquipItem oldItem, uint key = 0)
{
oldItem = state.ModelData.Item(slot);
@ -144,44 +145,58 @@ public class StateEditor
_gPose.AddActionOnLeave(() =>
{
if (old.Type == state.BaseData.Item(slot).Type)
ChangeItem(state, slot, old, state[slot, false], out _, key);
ChangeItem(state, slot, old, state[slot, ActorState.EquipField.Item], out _, key);
});
}
state.ModelData.SetItem(slot, item);
state[slot, false] = source;
state[slot, ActorState.EquipField.Item] = source;
return true;
}
/// <summary> Change a single piece of equipment including stain. </summary>
public bool ChangeEquip(ActorState state, EquipSlot slot, EquipItem item, StainId stain, StateChanged.Source source, out EquipItem oldItem,
out StainId oldStain, uint key = 0)
/// <summary> Change a single piece of equipment including stain and crest visibility. </summary>
public bool ChangeEquip(ActorState state, EquipSlot slot, EquipItem? item, StainId? stain, bool? crest, StateChanged.Source source, out EquipItem oldItem,
out StainId oldStain, out bool oldCrest, uint key = 0)
{
oldItem = state.ModelData.Item(slot);
oldStain = state.ModelData.Stain(slot);
oldCrest = state.ModelData.Crest(slot);
if (!state.CanUnlock(key))
return false;
// Can not change weapon type from expected type in state.
if (slot is EquipSlot.MainHand && item.Type != state.BaseData.MainhandType
|| slot is EquipSlot.OffHand && item.Type != state.BaseData.OffhandType)
if (item.HasValue && (slot is EquipSlot.MainHand && item.Value.Type != state.BaseData.MainhandType
|| slot is EquipSlot.OffHand && item.Value.Type != state.BaseData.OffhandType))
{
if (!_gPose.InGPose)
return false;
var old = oldItem;
var oldS = oldStain;
var oldC = oldCrest;
_gPose.AddActionOnLeave(() =>
{
if (old.Type == state.BaseData.Item(slot).Type)
ChangeEquip(state, slot, old, oldS, state[slot, false], out _, out _, key);
ChangeEquip(state, slot, old, oldS, oldC, state[slot, ActorState.EquipField.Item], out _, out _, out _, key);
});
}
state.ModelData.SetItem(slot, item);
state.ModelData.SetStain(slot, stain);
state[slot, false] = source;
state[slot, true] = source;
if (item.HasValue)
{
state.ModelData.SetItem(slot, item.Value);
state[slot, ActorState.EquipField.Item] = source;
}
if (stain.HasValue)
{
state.ModelData.SetStain(slot, stain.Value);
state[slot, ActorState.EquipField.Stain] = source;
}
if (crest.HasValue)
{
state.ModelData.SetCrest(slot, crest.Value);
state[slot, ActorState.EquipField.Crest] = source;
}
return true;
}
@ -193,7 +208,19 @@ public class StateEditor
return false;
state.ModelData.SetStain(slot, stain);
state[slot, true] = source;
state[slot, ActorState.EquipField.Stain] = source;
return true;
}
/// <summary> Change only the crest visibility of an equipment piece. </summary>
public bool ChangeCrest(ActorState state, EquipSlot slot, bool crest, StateChanged.Source source, out bool oldCrest, uint key = 0)
{
oldCrest = state.ModelData.Crest(slot);
if (!state.CanUnlock(key))
return false;
state.ModelData.SetCrest(slot, crest);
state[slot, ActorState.EquipField.Crest] = source;
return true;
}

View file

@ -32,6 +32,7 @@ public class StateListener : IDisposable
private readonly CustomizationService _customizations;
private readonly PenumbraService _penumbra;
private readonly SlotUpdating _slotUpdating;
private readonly CrestVisibilityUpdating _crestVisibilityUpdating;
private readonly WeaponLoading _weaponLoading;
private readonly HeadGearVisibilityChanged _headGearVisibility;
private readonly VisorStateChanged _visorState;
@ -51,29 +52,30 @@ public class StateListener : IDisposable
public StateListener(StateManager manager, ItemManager items, PenumbraService penumbra, ActorService actors, Configuration config,
SlotUpdating slotUpdating, WeaponLoading weaponLoading, VisorStateChanged visorState, WeaponVisibilityChanged weaponVisibility,
HeadGearVisibilityChanged headGearVisibility, AutoDesignApplier autoDesignApplier, FunModule funModule, HumanModelList humans,
StateApplier applier, MovedEquipment movedEquipment, ObjectManager objects, GPoseService gPose,
CrestVisibilityUpdating crestVisibilityUpdating, StateApplier applier, MovedEquipment movedEquipment, ObjectManager objects, GPoseService gPose,
ChangeCustomizeService changeCustomizeService, CustomizationService customizations, ICondition condition)
{
_manager = manager;
_items = items;
_penumbra = penumbra;
_actors = actors;
_config = config;
_slotUpdating = slotUpdating;
_weaponLoading = weaponLoading;
_visorState = visorState;
_weaponVisibility = weaponVisibility;
_headGearVisibility = headGearVisibility;
_autoDesignApplier = autoDesignApplier;
_funModule = funModule;
_humans = humans;
_applier = applier;
_movedEquipment = movedEquipment;
_objects = objects;
_gPose = gPose;
_changeCustomizeService = changeCustomizeService;
_customizations = customizations;
_condition = condition;
_manager = manager;
_items = items;
_penumbra = penumbra;
_actors = actors;
_config = config;
_slotUpdating = slotUpdating;
_crestVisibilityUpdating = crestVisibilityUpdating;
_weaponLoading = weaponLoading;
_visorState = visorState;
_weaponVisibility = weaponVisibility;
_headGearVisibility = headGearVisibility;
_autoDesignApplier = autoDesignApplier;
_funModule = funModule;
_humans = humans;
_applier = applier;
_movedEquipment = movedEquipment;
_objects = objects;
_gPose = gPose;
_changeCustomizeService = changeCustomizeService;
_customizations = customizations;
_condition = condition;
Subscribe();
}
@ -212,7 +214,7 @@ public class StateListener : IDisposable
&& _manager.TryGetValue(identifier, out var state))
{
HandleEquipSlot(actor, state, slot, ref armor.Value);
locked = state[slot, false] is StateChanged.Source.Ipc;
locked = state[slot, ActorState.EquipField.Item] is StateChanged.Source.Ipc;
}
_funModule.ApplyFun(actor, ref armor.Value, slot);
@ -223,6 +225,34 @@ public class StateListener : IDisposable
(_, armor.Value) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender);
}
private void OnCrestVisibilityUpdating(Model model, EquipSlot slot, Ref<bool> visible)
{
var actor = _penumbra.GameObjectFromDrawObject(model);
if (_condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart)
return;
if (slot == EquipSlot.BothHand)
{
var rootModel = actor.Model;
var (mainHand, offHand, _, _) = rootModel.GetWeapons(actor);
if (model == mainHand)
slot = EquipSlot.MainHand;
else if (model == offHand)
slot = EquipSlot.OffHand;
else
{
Glamourer.Log.Excessive($"Cannot identify weapon slot: got model {model.Address:X}, main hand is {mainHand.Address:X}, off hand is {offHand.Address:X}");
return;
}
}
if (actor.Identifier(_actors.AwaitedService, out var identifier)
&& _manager.TryGetValue(identifier, out var state))
{
HandleCrestVisibility(actor, state, slot, ref visible.Value);
}
}
private void OnMovedEquipment((EquipSlot, uint, StainId)[] items)
{
_objects.Update();
@ -235,11 +265,12 @@ public class StateListener : IDisposable
var currentItem = state.BaseData.Item(slot);
var model = state.ModelData.Weapon(slot);
var current = currentItem.Weapon(state.BaseData.Stain(slot));
var crest = state.BaseData.Crest(slot);
if (model.Value == current.Value || !_items.ItemService.AwaitedService.TryGetValue(item, EquipSlot.MainHand, out var changedItem))
continue;
var changed = changedItem.Weapon(stain);
if (current.Value == changed.Value && state[slot, false] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc)
if (current.Value == changed.Value && state[slot, ActorState.EquipField.Item] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc)
{
_manager.ChangeItem(state, slot, currentItem, StateChanged.Source.Game);
_manager.ChangeStain(state, slot, current.Stain, StateChanged.Source.Game);
@ -247,10 +278,10 @@ public class StateListener : IDisposable
{
case EquipSlot.MainHand:
case EquipSlot.OffHand:
_applier.ChangeWeapon(objects, slot, currentItem, stain);
_applier.ChangeWeapon(objects, slot, currentItem, stain, crest);
break;
default:
_applier.ChangeArmor(objects, slot, current.ToArmor(), state[slot, false] is not StateChanged.Source.Ipc,
_applier.ChangeArmor(objects, slot, current.ToArmor(), crest, state[slot, ActorState.EquipField.Item] is not StateChanged.Source.Ipc,
state.ModelData.IsHatVisible());
break;
}
@ -284,15 +315,20 @@ public class StateListener : IDisposable
// Do nothing. But this usually can not happen because the hooked function also writes to game objects later.
case UpdateState.Transformed: break;
case UpdateState.Change:
if (state[slot, false] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc)
if (state[slot, ActorState.EquipField.Item] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc)
_manager.ChangeItem(state, slot, state.BaseData.Item(slot), StateChanged.Source.Game);
else
apply = true;
if (state[slot, true] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc)
if (state[slot, ActorState.EquipField.Stain] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc)
_manager.ChangeStain(state, slot, state.BaseData.Stain(slot), StateChanged.Source.Game);
else
apply = true;
if (state[slot, ActorState.EquipField.Crest] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc)
_manager.ChangeCrest(state, slot, state.BaseData.Crest(slot), StateChanged.Source.Game);
else
apply = true;
break;
case UpdateState.NoChange:
apply = true;
@ -346,6 +382,7 @@ public class StateListener : IDisposable
var item = _items.Identify(slot, actorArmor.Set, actorArmor.Variant);
state.BaseData.SetItem(EquipSlot.Head, item);
state.BaseData.SetStain(EquipSlot.Head, actorArmor.Stain);
state.BaseData.SetCrest(EquipSlot.Head, actor.GetCrest(slot));
return UpdateState.Change;
}
@ -374,6 +411,20 @@ public class StateListener : IDisposable
return change;
}
private UpdateState UpdateBaseCrest(Actor actor, ActorState state, EquipSlot slot, bool visible)
{
if (actor.IsTransformed)
return UpdateState.Transformed;
if (state.BaseData.Crest(slot) != visible)
{
state.BaseData.SetCrest(slot, visible);
return UpdateState.Change;
}
return UpdateState.NoChange;
}
/// <summary> Handle a full equip slot update for base data and model data. </summary>
private void HandleEquipSlot(Actor actor, ActorState state, EquipSlot slot, ref CharacterArmor armor)
{
@ -383,12 +434,12 @@ public class StateListener : IDisposable
// Update model state if not on fixed design.
case UpdateState.Change:
var apply = false;
if (state[slot, false] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc)
if (state[slot, ActorState.EquipField.Item] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc)
_manager.ChangeItem(state, slot, state.BaseData.Item(slot), StateChanged.Source.Game);
else
apply = true;
if (state[slot, true] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc)
if (state[slot, ActorState.EquipField.Stain] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc)
_manager.ChangeStain(state, slot, state.BaseData.Stain(slot), StateChanged.Source.Game);
else
apply = true;
@ -405,6 +456,24 @@ public class StateListener : IDisposable
}
}
private void HandleCrestVisibility(Actor actor, ActorState state, EquipSlot slot, ref bool visible)
{
switch (UpdateBaseCrest(actor, state, slot, visible))
{
case UpdateState.Change:
if (state[slot, ActorState.EquipField.Crest] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc)
_manager.ChangeCrest(state, slot, state.BaseData.Crest(slot), StateChanged.Source.Game);
else
visible = state.ModelData.Crest(slot);
break;
case UpdateState.NoChange:
case UpdateState.HatHack:
visible = state.ModelData.Crest(slot);
break;
case UpdateState.Transformed: break;
}
}
/// <summary> Update base data for a single changed weapon slot. </summary>
private unsafe UpdateState UpdateBaseData(Actor actor, ActorState state, EquipSlot slot, CharacterWeapon weapon)
{
@ -415,6 +484,7 @@ public class StateListener : IDisposable
return UpdateState.Transformed;
}
var crest = actor.GetCrest(slot);
var baseData = state.BaseData.Weapon(slot);
var change = UpdateState.NoChange;
@ -610,6 +680,7 @@ public class StateListener : IDisposable
_penumbra.CreatingCharacterBase += OnCreatingCharacterBase;
_penumbra.CreatedCharacterBase += OnCreatedCharacterBase;
_slotUpdating.Subscribe(OnSlotUpdating, SlotUpdating.Priority.StateListener);
_crestVisibilityUpdating.Subscribe(OnCrestVisibilityUpdating, CrestVisibilityUpdating.Priority.StateListener);
_movedEquipment.Subscribe(OnMovedEquipment, MovedEquipment.Priority.StateListener);
_weaponLoading.Subscribe(OnWeaponLoading, WeaponLoading.Priority.StateListener);
_visorState.Subscribe(OnVisorChange, VisorStateChanged.Priority.StateListener);
@ -623,6 +694,7 @@ public class StateListener : IDisposable
_penumbra.CreatingCharacterBase -= OnCreatingCharacterBase;
_penumbra.CreatedCharacterBase -= OnCreatedCharacterBase;
_slotUpdating.Unsubscribe(OnSlotUpdating);
_crestVisibilityUpdating.Unsubscribe(OnCrestVisibilityUpdating);
_movedEquipment.Unsubscribe(OnMovedEquipment);
_weaponLoading.Unsubscribe(OnWeaponLoading);
_visorState.Unsubscribe(OnVisorChange);

View file

@ -143,10 +143,12 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
ret.Customize = model.GetCustomize();
// We can not use the head slot data from the draw object if the hat is hidden.
var head = ret.IsHatVisible() || ignoreHatState ? model.GetArmor(EquipSlot.Head) : actor.GetArmor(EquipSlot.Head);
var headItem = _items.Identify(EquipSlot.Head, head.Set, head.Variant);
var head = ret.IsHatVisible() || ignoreHatState ? model.GetArmor(EquipSlot.Head) : actor.GetArmor(EquipSlot.Head);
var headCrest = ret.IsHatVisible() || ignoreHatState ? model.GetCrest(EquipSlot.Head) : actor.GetCrest(EquipSlot.Head);
var headItem = _items.Identify(EquipSlot.Head, head.Set, head.Variant);
ret.SetItem(EquipSlot.Head, headItem);
ret.SetStain(EquipSlot.Head, head.Stain);
ret.SetCrest(EquipSlot.Head, headCrest);
// The other slots can be used from the draw object.
foreach (var slot in EquipSlotExtensions.EqdpSlots.Skip(1))
@ -155,6 +157,7 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
var item = _items.Identify(slot, armor.Set, armor.Variant);
ret.SetItem(slot, item);
ret.SetStain(slot, armor.Stain);
ret.SetCrest(slot, model.GetCrest(slot));
}
// Weapons use the draw objects of the weapons, but require the game object either way.
@ -174,6 +177,7 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
var item = _items.Identify(slot, armor.Set, armor.Variant);
ret.SetItem(slot, item);
ret.SetStain(slot, armor.Stain);
ret.SetCrest(slot, actor.GetCrest(slot));
}
main = actor.GetMainhand();
@ -187,8 +191,10 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
var offItem = _items.Identify(EquipSlot.OffHand, off.Set, off.Type, off.Variant, mainItem.Type);
ret.SetItem(EquipSlot.MainHand, mainItem);
ret.SetStain(EquipSlot.MainHand, main.Stain);
ret.SetCrest(EquipSlot.MainHand, actor.GetCrest(EquipSlot.MainHand));
ret.SetItem(EquipSlot.OffHand, offItem);
ret.SetStain(EquipSlot.OffHand, off.Stain);
ret.SetCrest(EquipSlot.OffHand, actor.GetCrest(EquipSlot.OffHand));
// Wetness can technically only be set in GPose or via external tools.
// It is only available in the game object.
@ -257,7 +263,7 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
_event.Invoke(StateChanged.Type.EntireCustomize, source, state, actors, (old, applied));
}
/// <summary> Change a single piece of equipment without stain. </summary>
/// <summary> Change a single piece of equipment without stain or crest visibility. </summary>
/// <remarks> Do not use this in the same frame as ChangeStain, use <see cref="ChangeEquip(ActorState,EquipSlot,EquipItem,StainId,StateChanged.Source,uint)"/> instead. </remarks>
public void ChangeItem(ActorState state, EquipSlot slot, EquipItem item, StateChanged.Source source, uint key = 0)
{
@ -274,21 +280,25 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
_event.Invoke(type, source, state, actors, (old, item, slot));
}
/// <summary> Change a single piece of equipment including stain. </summary>
public void ChangeEquip(ActorState state, EquipSlot slot, EquipItem item, StainId stain, StateChanged.Source source, uint key = 0)
/// <summary> Change a single piece of equipment including stain and crest visibility. </summary>
public void ChangeEquip(ActorState state, EquipSlot slot, EquipItem? item, StainId? stain, bool? crest, StateChanged.Source source, uint key = 0)
{
if (!_editor.ChangeEquip(state, slot, item, stain, source, out var old, out var oldStain, key))
if (!_editor.ChangeEquip(state, slot, item, stain, crest, source, out var old, out var oldStain, out var oldCrest, key))
return;
var type = slot.ToIndex() < 10 ? StateChanged.Type.Equip : StateChanged.Type.Weapon;
var actors = type is StateChanged.Type.Equip
? _applier.ChangeArmor(state, slot, source is StateChanged.Source.Manual or StateChanged.Source.Ipc)
: _applier.ChangeWeapon(state, slot, source is StateChanged.Source.Manual or StateChanged.Source.Ipc,
item.Type != (slot is EquipSlot.MainHand ? state.BaseData.MainhandType : state.BaseData.OffhandType));
(item ?? old).Type != (slot is EquipSlot.MainHand ? state.BaseData.MainhandType : state.BaseData.OffhandType));
Glamourer.Log.Verbose(
$"Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.ItemId}) to {item.Name} ({item.ItemId}) and its stain from {oldStain.Id} to {stain.Id}. [Affecting {actors.ToLazyString("nothing")}.]");
_event.Invoke(type, source, state, actors, (old, item, slot));
_event.Invoke(StateChanged.Type.Stain, source, state, actors, (oldStain, stain, slot));
$"Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.ItemId}) to {(item ?? old).Name} ({(item ?? old).ItemId}), its stain from {oldStain.Id} to {(stain ?? oldStain).Id} and its crest visibility from {oldCrest} to {crest ?? oldCrest}. [Affecting {actors.ToLazyString("nothing")}.]");
if (item.HasValue)
_event.Invoke(type, source, state, actors, (old, item.Value, slot));
if (stain.HasValue)
_event.Invoke(StateChanged.Type.Stain, source, state, actors, (oldStain, stain.Value, slot));
if (crest.HasValue)
_event.Invoke(StateChanged.Type.Crest, source, state, actors, (oldCrest, crest.Value, slot));
}
/// <summary> Change only the stain of an equipment piece. </summary>
@ -304,6 +314,19 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
_event.Invoke(StateChanged.Type.Stain, source, state, actors, (old, stain, slot));
}
/// <summary> Change only the crest visibility of an equipment piece. </summary>
/// <remarks> Do not use this in the same frame as ChangeEquip, use <see cref="ChangeEquip(ActorState,EquipSlot,EquipItem,StainId,StateChanged.Source,uint)"/> instead. </remarks>
public void ChangeCrest(ActorState state, EquipSlot slot, bool crest, StateChanged.Source source, uint key = 0)
{
if (!_editor.ChangeCrest(state, slot, crest, source, out var old, key))
return;
var actors = _applier.ChangeCrest(state, slot, source is StateChanged.Source.Manual or StateChanged.Source.Ipc);
Glamourer.Log.Verbose(
$"Set {slot.ToName()} crest visibility in state {state.Identifier.Incognito(null)} from {old} to {crest}. [Affecting {actors.ToLazyString("nothing")}.]");
_event.Invoke(StateChanged.Type.Crest, source, state, actors, (old, crest, slot));
}
/// <summary> Change hat visibility. </summary>
public void ChangeHatState(ActorState state, bool value, StateChanged.Source source, uint key = 0)
{
@ -356,15 +379,16 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
public void ApplyDesign(DesignBase design, ActorState state, StateChanged.Source source, uint key = 0)
{
void HandleEquip(EquipSlot slot, bool applyPiece, bool applyStain)
void HandleEquip(EquipSlot slot, bool applyPiece, bool applyStain, bool applyCrest)
{
var unused = (applyPiece, applyStain) switch
var unused = (applyPiece, applyStain, applyCrest) switch
{
(false, false) => false,
(true, false) => _editor.ChangeItem(state, slot, design.DesignData.Item(slot), source, out _, key),
(false, true) => _editor.ChangeStain(state, slot, design.DesignData.Stain(slot), source, out _, key),
(true, true) => _editor.ChangeEquip(state, slot, design.DesignData.Item(slot), design.DesignData.Stain(slot), source, out _,
out _, key),
(false, false, false) => false,
(true, false, false) => _editor.ChangeItem(state, slot, design.DesignData.Item(slot), source, out _, key),
(false, true, false) => _editor.ChangeStain(state, slot, design.DesignData.Stain(slot), source, out _, key),
(false, false, true) => _editor.ChangeCrest(state, slot, design.DesignData.Crest(slot), source, out _, key),
_ => _editor.ChangeEquip(state, slot, applyPiece ? design.DesignData.Item(slot) : null, applyStain ? design.DesignData.Stain(slot) : null, applyCrest ? design.DesignData.Crest(slot) : null, source, out _,
out _, out _, key),
};
}
@ -392,7 +416,7 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
redraw |= applied.RequiresRedraw();
foreach (var slot in EquipSlotExtensions.FullSlots)
HandleEquip(slot, design.DoApplyEquip(slot), design.DoApplyStain(slot));
HandleEquip(slot, design.DoApplyEquip(slot), design.DoApplyStain(slot), design.DoApplyCrest(slot));
}
var actors = ApplyAll(state, redraw, false);
@ -415,14 +439,14 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
_applier.ChangeCustomize(actors, state.ModelData.Customize);
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
_applier.ChangeArmor(actors, slot, state.ModelData.Armor(slot), state[slot, false] is not StateChanged.Source.Ipc,
_applier.ChangeArmor(actors, slot, state.ModelData.Armor(slot), state.ModelData.Crest(slot), state[slot, ActorState.EquipField.Item] is not StateChanged.Source.Ipc,
state.ModelData.IsHatVisible());
}
var mainhandActors = state.ModelData.MainhandType != state.BaseData.MainhandType ? actors.OnlyGPose() : actors;
_applier.ChangeMainhand(mainhandActors, state.ModelData.Item(EquipSlot.MainHand), state.ModelData.Stain(EquipSlot.MainHand));
_applier.ChangeMainhand(mainhandActors, state.ModelData.Item(EquipSlot.MainHand), state.ModelData.Stain(EquipSlot.MainHand), state.ModelData.Crest(EquipSlot.MainHand));
var offhandActors = state.ModelData.OffhandType != state.BaseData.OffhandType ? actors.OnlyGPose() : actors;
_applier.ChangeOffhand(offhandActors, state.ModelData.Item(EquipSlot.OffHand), state.ModelData.Stain(EquipSlot.OffHand));
_applier.ChangeOffhand(offhandActors, state.ModelData.Item(EquipSlot.OffHand), state.ModelData.Stain(EquipSlot.OffHand), state.ModelData.Crest(EquipSlot.OffHand));
}
if (state.ModelData.IsHuman)
@ -450,8 +474,9 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
foreach (var slot in EquipSlotExtensions.FullSlots)
{
state[slot, true] = StateChanged.Source.Game;
state[slot, false] = StateChanged.Source.Game;
state[slot, ActorState.EquipField.Stain] = StateChanged.Source.Game;
state[slot, ActorState.EquipField.Item] = StateChanged.Source.Game;
state[slot, ActorState.EquipField.Crest] = StateChanged.Source.Game;
}
foreach (var type in Enum.GetValues<ActorState.MetaIndex>())
@ -478,15 +503,21 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
foreach (var slot in EquipSlotExtensions.FullSlots)
{
if (state[slot, true] is StateChanged.Source.Fixed)
if (state[slot, ActorState.EquipField.Crest] is StateChanged.Source.Fixed)
{
state[slot, true] = StateChanged.Source.Game;
state[slot, ActorState.EquipField.Crest] = StateChanged.Source.Game;
state.ModelData.SetCrest(slot, state.BaseData.Crest(slot));
}
if (state[slot, ActorState.EquipField.Stain] is StateChanged.Source.Fixed)
{
state[slot, ActorState.EquipField.Stain] = StateChanged.Source.Game;
state.ModelData.SetStain(slot, state.BaseData.Stain(slot));
}
if (state[slot, false] is StateChanged.Source.Fixed)
if (state[slot, ActorState.EquipField.Item] is StateChanged.Source.Fixed)
{
state[slot, false] = StateChanged.Source.Game;
state[slot, ActorState.EquipField.Item] = StateChanged.Source.Game;
state.ModelData.SetItem(slot, state.BaseData.Item(slot));
}
}