mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 10:17:22 +01:00
Add shape meta manipulations and rework attribute hook.
This commit is contained in:
parent
0adec35848
commit
6ad0b4299a
23 changed files with 900 additions and 298 deletions
2
OtterGui
2
OtterGui
|
|
@ -1 +1 @@
|
|||
Subproject commit 86b492422565abde2e8ad17c0295896a21c3439c
|
||||
Subproject commit f130c928928cb0d48d3c807b7df5874c2460fe98
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 0ca501050de72ee1cc7382dfae894f984ce241b6
|
||||
Subproject commit 8e57c2e12570bb1795efb9e5c6e38617aa8dd5e3
|
||||
|
|
@ -50,6 +50,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "structs", "structs", "{B03F
|
|||
schemas\structs\meta_gmp.json = schemas\structs\meta_gmp.json
|
||||
schemas\structs\meta_imc.json = schemas\structs\meta_imc.json
|
||||
schemas\structs\meta_rsp.json = schemas\structs\meta_rsp.json
|
||||
schemas\structs\meta_shp.json = schemas\structs\meta_shp.json
|
||||
schemas\structs\option.json = schemas\structs\option.json
|
||||
EndProjectSection
|
||||
EndProject
|
||||
|
|
|
|||
|
|
@ -245,6 +245,8 @@ public sealed class CollectionCache : IDisposable
|
|||
AddManipulation(mod, identifier, entry);
|
||||
foreach (var (identifier, entry) in files.Manipulations.Atch)
|
||||
AddManipulation(mod, identifier, entry);
|
||||
foreach (var (identifier, entry) in files.Manipulations.Shp)
|
||||
AddManipulation(mod, identifier, entry);
|
||||
foreach (var identifier in files.Manipulations.GlobalEqp)
|
||||
AddManipulation(mod, identifier, null!);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,11 +16,12 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
public readonly RspCache Rsp = new(manager, collection);
|
||||
public readonly ImcCache Imc = new(manager, collection);
|
||||
public readonly AtchCache Atch = new(manager, collection);
|
||||
public readonly ShpCache Shp = new(manager, collection);
|
||||
public readonly GlobalEqpCache GlobalEqp = new();
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
public int Count
|
||||
=> Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + GlobalEqp.Count;
|
||||
=> Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + Shp.Count + GlobalEqp.Count;
|
||||
|
||||
public IEnumerable<(IMetaIdentifier, IMod)> IdentifierSources
|
||||
=> Eqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))
|
||||
|
|
@ -30,6 +31,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
.Concat(Rsp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
|
||||
.Concat(Imc.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
|
||||
.Concat(Atch.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
|
||||
.Concat(Shp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
|
||||
.Concat(GlobalEqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value)));
|
||||
|
||||
public void Reset()
|
||||
|
|
@ -41,6 +43,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
Rsp.Reset();
|
||||
Imc.Reset();
|
||||
Atch.Reset();
|
||||
Shp.Reset();
|
||||
GlobalEqp.Clear();
|
||||
}
|
||||
|
||||
|
|
@ -57,6 +60,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
Rsp.Dispose();
|
||||
Imc.Dispose();
|
||||
Atch.Dispose();
|
||||
Shp.Dispose();
|
||||
}
|
||||
|
||||
public bool TryGetMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod)
|
||||
|
|
@ -71,6 +75,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
ImcIdentifier i => Imc.TryGetValue(i, out var p) && Convert(p, out mod),
|
||||
RspIdentifier i => Rsp.TryGetValue(i, out var p) && Convert(p, out mod),
|
||||
AtchIdentifier i => Atch.TryGetValue(i, out var p) && Convert(p, out mod),
|
||||
ShpIdentifier i => Shp.TryGetValue(i, out var p) && Convert(p, out mod),
|
||||
GlobalEqpManipulation i => GlobalEqp.TryGetValue(i, out mod),
|
||||
_ => false,
|
||||
};
|
||||
|
|
@ -92,6 +97,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
ImcIdentifier i => Imc.RevertMod(i, out mod),
|
||||
RspIdentifier i => Rsp.RevertMod(i, out mod),
|
||||
AtchIdentifier i => Atch.RevertMod(i, out mod),
|
||||
ShpIdentifier i => Shp.RevertMod(i, out mod),
|
||||
GlobalEqpManipulation i => GlobalEqp.RevertMod(i, out mod),
|
||||
_ => (mod = null) != null,
|
||||
};
|
||||
|
|
@ -108,6 +114,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
ImcIdentifier i when entry is ImcEntry e => Imc.ApplyMod(mod, i, e),
|
||||
RspIdentifier i when entry is RspEntry e => Rsp.ApplyMod(mod, i, e),
|
||||
AtchIdentifier i when entry is AtchEntry e => Atch.ApplyMod(mod, i, e),
|
||||
ShpIdentifier i when entry is ShpEntry e => Shp.ApplyMod(mod, i, e),
|
||||
GlobalEqpManipulation i => GlobalEqp.ApplyMod(mod, i),
|
||||
_ => false,
|
||||
};
|
||||
|
|
|
|||
109
Penumbra/Collections/Cache/ShpCache.cs
Normal file
109
Penumbra/Collections/Cache/ShpCache.cs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Meta;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
|
||||
namespace Penumbra.Collections.Cache;
|
||||
|
||||
public sealed class ShpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<ShpIdentifier, ShpEntry>(manager, collection)
|
||||
{
|
||||
public bool ShouldBeEnabled(in ShapeString shape, HumanSlot slot, PrimaryId id)
|
||||
=> _shpData.TryGetValue(shape, out var value) && value.Contains(slot, id);
|
||||
|
||||
internal IReadOnlyDictionary<ShapeString, ShpHashSet> State
|
||||
=> _shpData;
|
||||
|
||||
internal sealed class ShpHashSet : HashSet<(HumanSlot Slot, PrimaryId Id)>
|
||||
{
|
||||
private readonly BitArray _allIds = new(ShapeManager.ModelSlotSize);
|
||||
|
||||
public bool All
|
||||
{
|
||||
get => _allIds[^1];
|
||||
set => _allIds[^1] = value;
|
||||
}
|
||||
|
||||
public bool this[HumanSlot slot]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (slot is HumanSlot.Unknown)
|
||||
return All;
|
||||
|
||||
return _allIds[(int)slot];
|
||||
}
|
||||
set
|
||||
{
|
||||
if (slot is HumanSlot.Unknown)
|
||||
_allIds[^1] = value;
|
||||
else
|
||||
_allIds[(int)slot] = value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Contains(HumanSlot slot, PrimaryId id)
|
||||
=> All || this[slot] || Contains((slot, id));
|
||||
|
||||
public bool TrySet(HumanSlot slot, PrimaryId? id, ShpEntry value)
|
||||
{
|
||||
if (slot is HumanSlot.Unknown)
|
||||
{
|
||||
var old = All;
|
||||
All = value.Value;
|
||||
return old != value.Value;
|
||||
}
|
||||
|
||||
if (!id.HasValue)
|
||||
{
|
||||
var old = this[slot];
|
||||
this[slot] = value.Value;
|
||||
return old != value.Value;
|
||||
}
|
||||
|
||||
if (value.Value)
|
||||
return Add((slot, id.Value));
|
||||
|
||||
return Remove((slot, id.Value));
|
||||
}
|
||||
|
||||
public new void Clear()
|
||||
{
|
||||
base.Clear();
|
||||
_allIds.SetAll(false);
|
||||
}
|
||||
|
||||
public bool IsEmpty
|
||||
=> !_allIds.HasAnySet() && Count is 0;
|
||||
}
|
||||
|
||||
private readonly Dictionary<ShapeString, ShpHashSet> _shpData = [];
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Clear();
|
||||
_shpData.Clear();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool _)
|
||||
=> Clear();
|
||||
|
||||
protected override void ApplyModInternal(ShpIdentifier identifier, ShpEntry entry)
|
||||
{
|
||||
if (!_shpData.TryGetValue(identifier.Shape, out var value))
|
||||
{
|
||||
value = [];
|
||||
_shpData.Add(identifier.Shape, value);
|
||||
}
|
||||
|
||||
value.TrySet(identifier.Slot, identifier.Id, entry);
|
||||
}
|
||||
|
||||
protected override void RevertModInternal(ShpIdentifier identifier)
|
||||
{
|
||||
if (!_shpData.TryGetValue(identifier.Shape, out var value))
|
||||
return;
|
||||
|
||||
if (value.TrySet(identifier.Slot, identifier.Id, ShpEntry.False) && value.IsEmpty)
|
||||
_shpData.Remove(identifier.Shape);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
using OtterGui.Classes;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Interop;
|
||||
|
||||
namespace Penumbra.Communication;
|
||||
|
||||
/// <summary>
|
||||
/// Triggered whenever a model recomputes its attribute masks.
|
||||
/// <list type="number">
|
||||
/// <item>Parameter is the game object that recomputed its attributes. </item>
|
||||
/// <item>Parameter is the draw object on which the recomputation was called. </item>
|
||||
/// <item>Parameter is the collection associated with the game object. </item>
|
||||
/// <item>Parameter is the slot that was recomputed. If this is Unknown, it is a general new update call. </item>
|
||||
/// </list> </summary>
|
||||
public sealed class ModelAttributeComputed()
|
||||
: EventWrapper<Actor, Model, ModCollection, HumanSlot, ModelAttributeComputed.Priority>(nameof(ModelAttributeComputed))
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="Meta.ShapeManager.OnAttributeComputed"/>
|
||||
ShapeManager = 0,
|
||||
}
|
||||
}
|
||||
85
Penumbra/Interop/Hooks/PostProcessing/AttributeHook.cs
Normal file
85
Penumbra/Interop/Hooks/PostProcessing/AttributeHook.cs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
using Dalamud.Hooking;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.GameData.Interop;
|
||||
using Penumbra.Interop.PathResolving;
|
||||
using Penumbra.Meta;
|
||||
|
||||
namespace Penumbra.Interop.Hooks.PostProcessing;
|
||||
|
||||
/// <summary>
|
||||
/// Triggered whenever a model recomputes its attribute masks.
|
||||
/// <list type="number">
|
||||
/// <item>Parameter is the game object that recomputed its attributes. </item>
|
||||
/// <item>Parameter is the draw object on which the recomputation was called. </item>
|
||||
/// <item>Parameter is the collection associated with the game object. </item>
|
||||
/// <item>Parameter is the slot that was recomputed. If this is Unknown, it is a general new update call. </item>
|
||||
/// </list> </summary>
|
||||
public sealed unsafe class AttributeHook : EventWrapper<Actor, Model, ModCollection, AttributeHook.Priority>, IHookService
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="ShapeManager.OnAttributeComputed"/>
|
||||
ShapeManager = 0,
|
||||
}
|
||||
|
||||
private readonly CollectionResolver _resolver;
|
||||
private readonly Configuration _config;
|
||||
|
||||
public AttributeHook(HookManager hooks, Configuration config, CollectionResolver resolver)
|
||||
: base("Update Model Attributes")
|
||||
{
|
||||
_config = config;
|
||||
_resolver = resolver;
|
||||
_task = hooks.CreateHook<Delegate>(Name, Sigs.UpdateAttributes, Detour, config.EnableCustomShapes);
|
||||
}
|
||||
|
||||
private readonly Task<Hook<Delegate>> _task;
|
||||
|
||||
public nint Address
|
||||
=> _task.Result.Address;
|
||||
|
||||
public void Enable()
|
||||
=> SetState(true);
|
||||
|
||||
public void Disable()
|
||||
=> SetState(false);
|
||||
|
||||
public void SetState(bool enabled)
|
||||
{
|
||||
if (_config.EnableCustomShapes == enabled)
|
||||
return;
|
||||
|
||||
_config.EnableCustomShapes = enabled;
|
||||
_config.Save();
|
||||
if (enabled)
|
||||
_task.Result.Enable();
|
||||
else
|
||||
_task.Result.Disable();
|
||||
}
|
||||
|
||||
|
||||
public Task Awaiter
|
||||
=> _task;
|
||||
|
||||
public bool Finished
|
||||
=> _task.IsCompletedSuccessfully;
|
||||
|
||||
private delegate void Delegate(Human* human);
|
||||
|
||||
private void Detour(Human* human)
|
||||
{
|
||||
_task.Result.Original(human);
|
||||
var resolveData = _resolver.IdentifyCollection((DrawObject*)human, true);
|
||||
var identifiedActor = resolveData.AssociatedGameObject;
|
||||
var identifiedCollection = resolveData.ModCollection;
|
||||
Penumbra.Log.Excessive($"[{Name}] Invoked on 0x{(ulong)human:X} (0x{identifiedActor:X}).");
|
||||
Invoke(identifiedActor, human, identifiedCollection);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
=> _task.Result.Dispose();
|
||||
}
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
using Dalamud.Hooking;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Communication;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Interop;
|
||||
using Penumbra.Interop.PathResolving;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Interop.Hooks.PostProcessing;
|
||||
|
||||
public sealed unsafe class AttributeHooks : IRequiredService, IDisposable
|
||||
{
|
||||
private delegate void SetupAttributes(Human* human, byte* data);
|
||||
private delegate void AttributeUpdate(Human* human);
|
||||
|
||||
private readonly Configuration _config;
|
||||
private readonly ModelAttributeComputed _event;
|
||||
private readonly CollectionResolver _resolver;
|
||||
|
||||
private readonly AttributeHook[] _hooks;
|
||||
private readonly Task<Hook<AttributeUpdate>> _updateHook;
|
||||
private ModCollection _identifiedCollection = ModCollection.Empty;
|
||||
private Actor _identifiedActor = Actor.Null;
|
||||
private bool _inUpdate;
|
||||
|
||||
public AttributeHooks(Configuration config, CommunicatorService communication, CollectionResolver resolver, HookManager hooks)
|
||||
{
|
||||
_config = config;
|
||||
_event = communication.ModelAttributeComputed;
|
||||
_resolver = resolver;
|
||||
_hooks =
|
||||
[
|
||||
new AttributeHook(this, hooks, Sigs.SetupTopModelAttributes, _config.EnableCustomShapes, HumanSlot.Body),
|
||||
new AttributeHook(this, hooks, Sigs.SetupHandModelAttributes, _config.EnableCustomShapes, HumanSlot.Hands),
|
||||
new AttributeHook(this, hooks, Sigs.SetupLegModelAttributes, _config.EnableCustomShapes, HumanSlot.Legs),
|
||||
new AttributeHook(this, hooks, Sigs.SetupFootModelAttributes, _config.EnableCustomShapes, HumanSlot.Feet),
|
||||
];
|
||||
_updateHook = hooks.CreateHook<AttributeUpdate>("UpdateAttributes", Sigs.UpdateAttributes, UpdateAttributesDetour,
|
||||
_config.EnableCustomShapes);
|
||||
}
|
||||
|
||||
private class AttributeHook
|
||||
{
|
||||
private readonly AttributeHooks _parent;
|
||||
public readonly string Name;
|
||||
public readonly Task<Hook<SetupAttributes>> Hook;
|
||||
public readonly uint ModelIndex;
|
||||
public readonly HumanSlot Slot;
|
||||
|
||||
public AttributeHook(AttributeHooks parent, HookManager hooks, string signature, bool enabled, HumanSlot slot)
|
||||
{
|
||||
_parent = parent;
|
||||
Name = $"Setup{slot}Attributes";
|
||||
Slot = slot;
|
||||
ModelIndex = slot.ToIndex();
|
||||
Hook = hooks.CreateHook<SetupAttributes>(Name, signature, Detour, enabled);
|
||||
}
|
||||
|
||||
private void Detour(Human* human, byte* data)
|
||||
{
|
||||
Penumbra.Log.Excessive($"[{Name}] Invoked on 0x{(ulong)human:X} (0x{_parent._identifiedActor.Address:X}).");
|
||||
Hook.Result.Original(human, data);
|
||||
if (_parent is { _inUpdate: true, _identifiedActor.Valid: true })
|
||||
_parent._event.Invoke(_parent._identifiedActor, human, _parent._identifiedCollection, Slot);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAttributesDetour(Human* human)
|
||||
{
|
||||
var resolveData = _resolver.IdentifyCollection((DrawObject*)human, true);
|
||||
_identifiedActor = resolveData.AssociatedGameObject;
|
||||
_identifiedCollection = resolveData.ModCollection;
|
||||
_inUpdate = true;
|
||||
Penumbra.Log.Excessive($"[UpdateAttributes] Invoked on 0x{(ulong)human:X} (0x{_identifiedActor.Address:X}).");
|
||||
_event.Invoke(_identifiedActor, human, _identifiedCollection, HumanSlot.Unknown);
|
||||
_updateHook.Result.Original(human);
|
||||
_inUpdate = false;
|
||||
}
|
||||
|
||||
public void SetState(bool enabled)
|
||||
{
|
||||
if (_config.EnableCustomShapes == enabled)
|
||||
return;
|
||||
|
||||
_config.EnableCustomShapes = enabled;
|
||||
_config.Save();
|
||||
if (enabled)
|
||||
{
|
||||
foreach (var hook in _hooks)
|
||||
hook.Hook.Result.Enable();
|
||||
_updateHook.Result.Enable();
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var hook in _hooks)
|
||||
hook.Hook.Result.Disable();
|
||||
_updateHook.Result.Disable();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var hook in _hooks)
|
||||
hook.Hook.Result.Dispose();
|
||||
_updateHook.Result.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ public enum MetaManipulationType : byte
|
|||
Rsp = 6,
|
||||
GlobalEqp = 7,
|
||||
Atch = 8,
|
||||
Shp = 9,
|
||||
}
|
||||
|
||||
public interface IMetaIdentifier
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ public class MetaDictionary
|
|||
private readonly Dictionary<RspIdentifier, RspEntry> _rsp = [];
|
||||
private readonly Dictionary<GmpIdentifier, GmpEntry> _gmp = [];
|
||||
private readonly Dictionary<AtchIdentifier, AtchEntry> _atch = [];
|
||||
private readonly Dictionary<ShpIdentifier, ShpEntry> _shp = [];
|
||||
private readonly HashSet<GlobalEqpManipulation> _globalEqp = [];
|
||||
|
||||
public IReadOnlyDictionary<ImcIdentifier, ImcEntry> Imc
|
||||
|
|
@ -41,6 +42,9 @@ public class MetaDictionary
|
|||
public IReadOnlyDictionary<AtchIdentifier, AtchEntry> Atch
|
||||
=> _atch;
|
||||
|
||||
public IReadOnlyDictionary<ShpIdentifier, ShpEntry> Shp
|
||||
=> _shp;
|
||||
|
||||
public IReadOnlySet<GlobalEqpManipulation> GlobalEqp
|
||||
=> _globalEqp;
|
||||
|
||||
|
|
@ -56,6 +60,7 @@ public class MetaDictionary
|
|||
MetaManipulationType.Gmp => _gmp.Count,
|
||||
MetaManipulationType.Rsp => _rsp.Count,
|
||||
MetaManipulationType.Atch => _atch.Count,
|
||||
MetaManipulationType.Shp => _shp.Count,
|
||||
MetaManipulationType.GlobalEqp => _globalEqp.Count,
|
||||
_ => 0,
|
||||
};
|
||||
|
|
@ -70,6 +75,7 @@ public class MetaDictionary
|
|||
GmpIdentifier i => _gmp.ContainsKey(i),
|
||||
ImcIdentifier i => _imc.ContainsKey(i),
|
||||
AtchIdentifier i => _atch.ContainsKey(i),
|
||||
ShpIdentifier i => _shp.ContainsKey(i),
|
||||
RspIdentifier i => _rsp.ContainsKey(i),
|
||||
_ => false,
|
||||
};
|
||||
|
|
@ -84,6 +90,7 @@ public class MetaDictionary
|
|||
_rsp.Clear();
|
||||
_gmp.Clear();
|
||||
_atch.Clear();
|
||||
_shp.Clear();
|
||||
_globalEqp.Clear();
|
||||
}
|
||||
|
||||
|
|
@ -108,6 +115,7 @@ public class MetaDictionary
|
|||
&& _rsp.SetEquals(other._rsp)
|
||||
&& _gmp.SetEquals(other._gmp)
|
||||
&& _atch.SetEquals(other._atch)
|
||||
&& _shp.SetEquals(other._shp)
|
||||
&& _globalEqp.SetEquals(other._globalEqp);
|
||||
|
||||
public IEnumerable<IMetaIdentifier> Identifiers
|
||||
|
|
@ -118,6 +126,7 @@ public class MetaDictionary
|
|||
.Concat(_gmp.Keys.Cast<IMetaIdentifier>())
|
||||
.Concat(_rsp.Keys.Cast<IMetaIdentifier>())
|
||||
.Concat(_atch.Keys.Cast<IMetaIdentifier>())
|
||||
.Concat(_shp.Keys.Cast<IMetaIdentifier>())
|
||||
.Concat(_globalEqp.Cast<IMetaIdentifier>());
|
||||
|
||||
#region TryAdd
|
||||
|
|
@ -191,6 +200,15 @@ public class MetaDictionary
|
|||
return true;
|
||||
}
|
||||
|
||||
public bool TryAdd(ShpIdentifier identifier, in ShpEntry entry)
|
||||
{
|
||||
if (!_shp.TryAdd(identifier, entry))
|
||||
return false;
|
||||
|
||||
++Count;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryAdd(GlobalEqpManipulation identifier)
|
||||
{
|
||||
if (!_globalEqp.Add(identifier))
|
||||
|
|
@ -273,6 +291,15 @@ public class MetaDictionary
|
|||
return true;
|
||||
}
|
||||
|
||||
public bool Update(ShpIdentifier identifier, in ShpEntry entry)
|
||||
{
|
||||
if (!_shp.ContainsKey(identifier))
|
||||
return false;
|
||||
|
||||
_shp[identifier] = entry;
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TryGetValue
|
||||
|
|
@ -298,6 +325,9 @@ public class MetaDictionary
|
|||
public bool TryGetValue(AtchIdentifier identifier, out AtchEntry value)
|
||||
=> _atch.TryGetValue(identifier, out value);
|
||||
|
||||
public bool TryGetValue(ShpIdentifier identifier, out ShpEntry value)
|
||||
=> _shp.TryGetValue(identifier, out value);
|
||||
|
||||
#endregion
|
||||
|
||||
public bool Remove(IMetaIdentifier identifier)
|
||||
|
|
@ -312,6 +342,7 @@ public class MetaDictionary
|
|||
ImcIdentifier i => _imc.Remove(i),
|
||||
RspIdentifier i => _rsp.Remove(i),
|
||||
AtchIdentifier i => _atch.Remove(i),
|
||||
ShpIdentifier i => _shp.Remove(i),
|
||||
_ => false,
|
||||
};
|
||||
if (ret)
|
||||
|
|
@ -344,6 +375,9 @@ public class MetaDictionary
|
|||
foreach (var (identifier, entry) in manips._atch)
|
||||
TryAdd(identifier, entry);
|
||||
|
||||
foreach (var (identifier, entry) in manips._shp)
|
||||
TryAdd(identifier, entry);
|
||||
|
||||
foreach (var identifier in manips._globalEqp)
|
||||
TryAdd(identifier);
|
||||
}
|
||||
|
|
@ -393,13 +427,19 @@ public class MetaDictionary
|
|||
return false;
|
||||
}
|
||||
|
||||
foreach (var (identifier, _) in manips._shp.Where(kvp => !TryAdd(kvp.Key, kvp.Value)))
|
||||
{
|
||||
failedIdentifier = identifier;
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var identifier in manips._globalEqp.Where(identifier => !TryAdd(identifier)))
|
||||
{
|
||||
failedIdentifier = identifier;
|
||||
return false;
|
||||
}
|
||||
|
||||
failedIdentifier = default;
|
||||
failedIdentifier = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -412,8 +452,9 @@ public class MetaDictionary
|
|||
_rsp.SetTo(other._rsp);
|
||||
_gmp.SetTo(other._gmp);
|
||||
_atch.SetTo(other._atch);
|
||||
_shp.SetTo(other._shp);
|
||||
_globalEqp.SetTo(other._globalEqp);
|
||||
Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _atch.Count + _globalEqp.Count;
|
||||
Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _atch.Count + _shp.Count + _globalEqp.Count;
|
||||
}
|
||||
|
||||
public void UpdateTo(MetaDictionary other)
|
||||
|
|
@ -425,8 +466,9 @@ public class MetaDictionary
|
|||
_rsp.UpdateTo(other._rsp);
|
||||
_gmp.UpdateTo(other._gmp);
|
||||
_atch.UpdateTo(other._atch);
|
||||
_shp.UpdateTo(other._shp);
|
||||
_globalEqp.UnionWith(other._globalEqp);
|
||||
Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _atch.Count + _globalEqp.Count;
|
||||
Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _atch.Count + _shp.Count + _globalEqp.Count;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
@ -514,6 +556,16 @@ public class MetaDictionary
|
|||
}),
|
||||
};
|
||||
|
||||
public static JObject Serialize(ShpIdentifier identifier, ShpEntry entry)
|
||||
=> new()
|
||||
{
|
||||
["Type"] = MetaManipulationType.Shp.ToString(),
|
||||
["Manipulation"] = identifier.AddToJson(new JObject
|
||||
{
|
||||
["Entry"] = entry.Value,
|
||||
}),
|
||||
};
|
||||
|
||||
public static JObject Serialize(GlobalEqpManipulation identifier)
|
||||
=> new()
|
||||
{
|
||||
|
|
@ -543,6 +595,8 @@ public class MetaDictionary
|
|||
return Serialize(Unsafe.As<TIdentifier, ImcIdentifier>(ref identifier), Unsafe.As<TEntry, ImcEntry>(ref entry));
|
||||
if (typeof(TIdentifier) == typeof(AtchIdentifier) && typeof(TEntry) == typeof(AtchEntry))
|
||||
return Serialize(Unsafe.As<TIdentifier, AtchIdentifier>(ref identifier), Unsafe.As<TEntry, AtchEntry>(ref entry));
|
||||
if (typeof(TIdentifier) == typeof(ShpIdentifier) && typeof(TEntry) == typeof(ShpEntry))
|
||||
return Serialize(Unsafe.As<TIdentifier, ShpIdentifier>(ref identifier), Unsafe.As<TEntry, ShpEntry>(ref entry));
|
||||
if (typeof(TIdentifier) == typeof(GlobalEqpManipulation))
|
||||
return Serialize(Unsafe.As<TIdentifier, GlobalEqpManipulation>(ref identifier));
|
||||
|
||||
|
|
@ -588,6 +642,7 @@ public class MetaDictionary
|
|||
SerializeTo(array, value._rsp);
|
||||
SerializeTo(array, value._gmp);
|
||||
SerializeTo(array, value._atch);
|
||||
SerializeTo(array, value._shp);
|
||||
SerializeTo(array, value._globalEqp);
|
||||
array.WriteTo(writer);
|
||||
}
|
||||
|
|
@ -685,6 +740,16 @@ public class MetaDictionary
|
|||
Penumbra.Log.Warning("Invalid ATCH Manipulation encountered.");
|
||||
break;
|
||||
}
|
||||
case MetaManipulationType.Shp:
|
||||
{
|
||||
var identifier = ShpIdentifier.FromJson(manip);
|
||||
var entry = new ShpEntry(manip["Entry"]?.Value<bool>() ?? true);
|
||||
if (identifier.HasValue)
|
||||
dict.TryAdd(identifier.Value, entry);
|
||||
else
|
||||
Penumbra.Log.Warning("Invalid SHP Manipulation encountered.");
|
||||
break;
|
||||
}
|
||||
case MetaManipulationType.GlobalEqp:
|
||||
{
|
||||
var identifier = GlobalEqpManipulation.FromJson(manip);
|
||||
|
|
@ -716,6 +781,7 @@ public class MetaDictionary
|
|||
_gmp = cache.Gmp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
|
||||
_rsp = cache.Rsp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
|
||||
_atch = cache.Atch.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
|
||||
_shp = cache.Shp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
|
||||
_globalEqp = cache.GlobalEqp.Select(kvp => kvp.Key).ToHashSet();
|
||||
Count = cache.Count;
|
||||
}
|
||||
|
|
|
|||
157
Penumbra/Meta/Manipulations/ShpIdentifier.cs
Normal file
157
Penumbra/Meta/Manipulations/ShpIdentifier.cs
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
using Lumina.Models.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Interop.Structs;
|
||||
|
||||
namespace Penumbra.Meta.Manipulations;
|
||||
|
||||
public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, ShapeString Shape)
|
||||
: IComparable<ShpIdentifier>, IMetaIdentifier
|
||||
{
|
||||
public int CompareTo(ShpIdentifier other)
|
||||
{
|
||||
var slotComparison = Slot.CompareTo(other.Slot);
|
||||
if (slotComparison is not 0)
|
||||
return slotComparison;
|
||||
|
||||
if (Id.HasValue)
|
||||
{
|
||||
if (other.Id.HasValue)
|
||||
{
|
||||
var idComparison = Id.Value.Id.CompareTo(other.Id.Value.Id);
|
||||
if (idComparison is not 0)
|
||||
return idComparison;
|
||||
}
|
||||
else
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
else if (other.Id.HasValue)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
return Shape.CompareTo(other.Shape);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
=> $"Shp - {Shape}{(Slot is HumanSlot.Unknown ? " - All Slots & IDs" : $" - {Slot.ToName()}{(Id.HasValue ? $" - {Id.Value.Id}" : " - All IDs")}")}";
|
||||
|
||||
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
|
||||
{
|
||||
// Nothing for now since it depends entirely on the shape key.
|
||||
}
|
||||
|
||||
public MetaIndex FileIndex()
|
||||
=> (MetaIndex)(-1);
|
||||
|
||||
public bool Validate()
|
||||
{
|
||||
if (!Enum.IsDefined(Slot) || Slot is HumanSlot.UnkBonus)
|
||||
return false;
|
||||
|
||||
if (Slot is HumanSlot.Unknown && Id is not null)
|
||||
return false;
|
||||
|
||||
return ValidateCustomShapeString(Shape);
|
||||
}
|
||||
|
||||
public static bool ValidateCustomShapeString(ReadOnlySpan<byte> shape)
|
||||
{
|
||||
// "shp_xx_y"
|
||||
if (shape.Length < 8)
|
||||
return false;
|
||||
|
||||
if (shape[0] is not (byte)'s'
|
||||
|| shape[1] is not (byte)'h'
|
||||
|| shape[2] is not (byte)'p'
|
||||
|| shape[3] is not (byte)'_'
|
||||
|| shape[6] is not (byte)'_')
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static unsafe bool ValidateCustomShapeString(byte* shape)
|
||||
{
|
||||
// "shp_xx_y"
|
||||
if (shape is null)
|
||||
return false;
|
||||
|
||||
if (*shape++ is not (byte)'s'
|
||||
|| *shape++ is not (byte)'h'
|
||||
|| *shape++ is not (byte)'p'
|
||||
|| *shape++ is not (byte)'_'
|
||||
|| *shape++ is 0
|
||||
|| *shape++ is 0
|
||||
|| *shape++ is not (byte)'_'
|
||||
|| *shape is 0)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool ValidateCustomShapeString(in ShapeString shape)
|
||||
{
|
||||
// "shp_xx_y"
|
||||
if (shape.Length < 8)
|
||||
return false;
|
||||
|
||||
var span = shape.AsSpan;
|
||||
if (span[0] is not (byte)'s'
|
||||
|| span[1] is not (byte)'h'
|
||||
|| span[2] is not (byte)'p'
|
||||
|| span[3] is not (byte)'_'
|
||||
|| span[6] is not (byte)'_')
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public JObject AddToJson(JObject jObj)
|
||||
{
|
||||
if (Slot is not HumanSlot.Unknown)
|
||||
jObj["Slot"] = Slot.ToString();
|
||||
if (Id.HasValue)
|
||||
jObj["Id"] = Id.Value.Id.ToString();
|
||||
jObj["Shape"] = Shape.ToString();
|
||||
return jObj;
|
||||
}
|
||||
|
||||
public static ShpIdentifier? FromJson(JObject jObj)
|
||||
{
|
||||
var slot = jObj["Slot"]?.ToObject<HumanSlot>() ?? HumanSlot.Unknown;
|
||||
var id = jObj["Id"]?.ToObject<ushort>();
|
||||
var shape = jObj["Shape"]?.ToObject<string>();
|
||||
if (shape is null || !ShapeString.TryRead(shape, out var shapeString))
|
||||
return null;
|
||||
|
||||
var identifier = new ShpIdentifier(slot, id, shapeString);
|
||||
return identifier.Validate() ? identifier : null;
|
||||
}
|
||||
|
||||
public MetaManipulationType Type
|
||||
=> MetaManipulationType.Shp;
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(Converter))]
|
||||
public readonly record struct ShpEntry(bool Value)
|
||||
{
|
||||
public static readonly ShpEntry True = new(true);
|
||||
public static readonly ShpEntry False = new(false);
|
||||
|
||||
private class Converter : JsonConverter<ShpEntry>
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, ShpEntry value, JsonSerializer serializer)
|
||||
=> serializer.Serialize(writer, value.Value);
|
||||
|
||||
public override ShpEntry ReadJson(JsonReader reader, Type objectType, ShpEntry existingValue, bool hasExistingValue,
|
||||
JsonSerializer serializer)
|
||||
=> new(serializer.Deserialize<bool>(reader));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +1,30 @@
|
|||
using System.Reflection.Metadata.Ecma335;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Communication;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Interop;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.Interop.Hooks.PostProcessing;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
|
||||
namespace Penumbra.Meta;
|
||||
|
||||
public class ShapeManager : IRequiredService, IDisposable
|
||||
{
|
||||
public const int NumSlots = 4;
|
||||
private readonly CommunicatorService _communicator;
|
||||
public const int NumSlots = 14;
|
||||
public const int ModelSlotSize = 18;
|
||||
private readonly AttributeHook _attributeHook;
|
||||
|
||||
private static ReadOnlySpan<byte> UsedModels
|
||||
=> [1, 2, 3, 4];
|
||||
public static ReadOnlySpan<HumanSlot> UsedModels
|
||||
=>
|
||||
[
|
||||
HumanSlot.Head, HumanSlot.Body, HumanSlot.Hands, HumanSlot.Legs, HumanSlot.Feet, HumanSlot.Ears, HumanSlot.Neck, HumanSlot.Wrists,
|
||||
HumanSlot.RFinger, HumanSlot.LFinger, HumanSlot.Glasses, HumanSlot.Hair, HumanSlot.Face, HumanSlot.Ear,
|
||||
];
|
||||
|
||||
public ShapeManager(CommunicatorService communicator)
|
||||
public ShapeManager(AttributeHook attributeHook)
|
||||
{
|
||||
_communicator = communicator;
|
||||
_communicator.ModelAttributeComputed.Subscribe(OnAttributeComputed, ModelAttributeComputed.Priority.ShapeManager);
|
||||
_attributeHook = attributeHook;
|
||||
_attributeHook.Subscribe(OnAttributeComputed, AttributeHook.Priority.ShapeManager);
|
||||
}
|
||||
|
||||
private readonly Dictionary<ShapeString, short>[] _temporaryIndices =
|
||||
|
|
@ -27,38 +33,30 @@ public class ShapeManager : IRequiredService, IDisposable
|
|||
private readonly uint[] _temporaryMasks = new uint[NumSlots];
|
||||
private readonly uint[] _temporaryValues = new uint[NumSlots];
|
||||
|
||||
private unsafe void OnAttributeComputed(Actor actor, Model model, ModCollection collection, HumanSlot slot)
|
||||
{
|
||||
int index;
|
||||
switch (slot)
|
||||
{
|
||||
case HumanSlot.Unknown:
|
||||
ResetCache(model);
|
||||
return;
|
||||
case HumanSlot.Body: index = 0; break;
|
||||
case HumanSlot.Hands: index = 1; break;
|
||||
case HumanSlot.Legs: index = 2; break;
|
||||
case HumanSlot.Feet: index = 3; break;
|
||||
default: return;
|
||||
}
|
||||
public void Dispose()
|
||||
=> _attributeHook.Unsubscribe(OnAttributeComputed);
|
||||
|
||||
if (_temporaryMasks[index] is 0)
|
||||
private unsafe void OnAttributeComputed(Actor actor, Model model, ModCollection collection)
|
||||
{
|
||||
ComputeCache(model, collection);
|
||||
for (var i = 0; i < NumSlots; ++i)
|
||||
{
|
||||
if (_temporaryMasks[i] is 0)
|
||||
continue;
|
||||
|
||||
var modelIndex = UsedModels[i];
|
||||
var currentMask = model.AsHuman->Models[modelIndex.ToIndex()]->EnabledShapeKeyIndexMask;
|
||||
var newMask = (currentMask & ~_temporaryMasks[i]) | _temporaryValues[i];
|
||||
Penumbra.Log.Excessive($"Changed Model Mask from {currentMask:X} to {newMask:X}.");
|
||||
model.AsHuman->Models[modelIndex.ToIndex()]->EnabledShapeKeyIndexMask = newMask;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void ComputeCache(Model human, ModCollection collection)
|
||||
{
|
||||
if (!collection.HasCache)
|
||||
return;
|
||||
|
||||
var modelIndex = UsedModels[index];
|
||||
var currentMask = model.AsHuman->Models[modelIndex]->EnabledShapeKeyIndexMask;
|
||||
var newMask = (currentMask & ~_temporaryMasks[index]) | _temporaryValues[index];
|
||||
Penumbra.Log.Excessive($"Changed Model Mask from {currentMask:X} to {newMask:X}.");
|
||||
model.AsHuman->Models[modelIndex]->EnabledShapeKeyIndexMask = newMask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_communicator.ModelAttributeComputed.Unsubscribe(OnAttributeComputed);
|
||||
}
|
||||
|
||||
private unsafe void ResetCache(Model human)
|
||||
{
|
||||
for (var i = 0; i < NumSlots; ++i)
|
||||
{
|
||||
_temporaryMasks[i] = 0;
|
||||
|
|
@ -66,17 +64,20 @@ public class ShapeManager : IRequiredService, IDisposable
|
|||
_temporaryIndices[i].Clear();
|
||||
|
||||
var modelIndex = UsedModels[i];
|
||||
var model = human.AsHuman->Models[modelIndex];
|
||||
var model = human.AsHuman->Models[modelIndex.ToIndex()];
|
||||
if (model is null || model->ModelResourceHandle is null)
|
||||
continue;
|
||||
|
||||
ref var shapes = ref model->ModelResourceHandle->Shapes;
|
||||
foreach (var (shape, index) in shapes.Where(kvp => CheckShapes(kvp.Key.AsSpan(), modelIndex)))
|
||||
foreach (var (shape, index) in shapes.Where(kvp => ShpIdentifier.ValidateCustomShapeString(kvp.Key.Value)))
|
||||
{
|
||||
if (ShapeString.TryRead(shape.Value, out var shapeString))
|
||||
{
|
||||
_temporaryIndices[i].TryAdd(shapeString, index);
|
||||
_temporaryMasks[i] |= (ushort)(1 << index);
|
||||
if (collection.MetaCache!.Shp.State.Count > 0
|
||||
&& collection.MetaCache!.Shp.ShouldBeEnabled(shapeString, modelIndex, human.GetArmorChanged(modelIndex).Set))
|
||||
_temporaryValues[i] |= (ushort)(1 << index);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -85,42 +86,32 @@ public class ShapeManager : IRequiredService, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
UpdateMasks();
|
||||
UpdateDefaultMasks();
|
||||
}
|
||||
|
||||
private static bool CheckShapes(ReadOnlySpan<byte> shape, byte index)
|
||||
=> index switch
|
||||
{
|
||||
1 => shape.StartsWith("shp_wa_"u8) || shape.StartsWith("shp_wr_"u8),
|
||||
2 => shape.StartsWith("shp_wr_"u8),
|
||||
3 => shape.StartsWith("shp_wa_"u8) || shape.StartsWith("shp_an"u8),
|
||||
4 => shape.StartsWith("shp_an"u8),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
private void UpdateMasks()
|
||||
private void UpdateDefaultMasks()
|
||||
{
|
||||
foreach (var (shape, topIndex) in _temporaryIndices[0])
|
||||
foreach (var (shape, topIndex) in _temporaryIndices[1])
|
||||
{
|
||||
if (_temporaryIndices[1].TryGetValue(shape, out var handIndex))
|
||||
if (shape[4] is (byte)'w' && shape[5] is (byte)'r' && _temporaryIndices[2].TryGetValue(shape, out var handIndex))
|
||||
{
|
||||
_temporaryValues[0] |= 1u << topIndex;
|
||||
_temporaryValues[1] |= 1u << handIndex;
|
||||
_temporaryValues[1] |= 1u << topIndex;
|
||||
_temporaryValues[2] |= 1u << handIndex;
|
||||
}
|
||||
|
||||
if (_temporaryIndices[2].TryGetValue(shape, out var legIndex))
|
||||
if (shape[4] is (byte)'w' && shape[5] is (byte)'a' && _temporaryIndices[3].TryGetValue(shape, out var legIndex))
|
||||
{
|
||||
_temporaryValues[0] |= 1u << topIndex;
|
||||
_temporaryValues[2] |= 1u << legIndex;
|
||||
_temporaryValues[1] |= 1u << topIndex;
|
||||
_temporaryValues[3] |= 1u << legIndex;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (shape, bottomIndex) in _temporaryIndices[2])
|
||||
foreach (var (shape, bottomIndex) in _temporaryIndices[3])
|
||||
{
|
||||
if (_temporaryIndices[3].TryGetValue(shape, out var footIndex))
|
||||
if (shape[4] is (byte)'a' && shape[5] is (byte)'n' && _temporaryIndices[4].TryGetValue(shape, out var footIndex))
|
||||
{
|
||||
_temporaryValues[2] |= 1u << bottomIndex;
|
||||
_temporaryValues[3] |= 1u << footIndex;
|
||||
_temporaryValues[3] |= 1u << bottomIndex;
|
||||
_temporaryValues[4] |= 1u << footIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
using Lumina.Misc;
|
||||
using Newtonsoft.Json;
|
||||
using Penumbra.GameData.Files.PhybStructs;
|
||||
using Penumbra.String.Functions;
|
||||
|
||||
namespace Penumbra.Meta;
|
||||
|
||||
[JsonConverter(typeof(Converter))]
|
||||
public struct ShapeString : IEquatable<ShapeString>
|
||||
public struct ShapeString : IEquatable<ShapeString>, IComparable<ShapeString>
|
||||
{
|
||||
public const int MaxLength = 30;
|
||||
|
||||
|
|
@ -22,6 +23,20 @@ public struct ShapeString : IEquatable<ShapeString>
|
|||
public override string ToString()
|
||||
=> Encoding.UTF8.GetString(_buffer[..Length]);
|
||||
|
||||
public byte this[int index]
|
||||
=> _buffer[index];
|
||||
|
||||
public unsafe ReadOnlySpan<byte> AsSpan
|
||||
{
|
||||
get
|
||||
{
|
||||
fixed (void* ptr = &this)
|
||||
{
|
||||
return new ReadOnlySpan<byte>(ptr, Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool Equals(ShapeString other)
|
||||
=> Length == other.Length && _buffer[..Length].SequenceEqual(other._buffer[..Length]);
|
||||
|
||||
|
|
@ -43,6 +58,14 @@ public struct ShapeString : IEquatable<ShapeString>
|
|||
return TryRead(span, out ret);
|
||||
}
|
||||
|
||||
public unsafe int CompareTo(ShapeString other)
|
||||
{
|
||||
fixed (void* lhs = &this)
|
||||
{
|
||||
return ByteStringFunctions.Compare((byte*)lhs, Length, (byte*)&other, other.Length);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryRead(ReadOnlySpan<byte> utf8, out ShapeString ret)
|
||||
{
|
||||
if (utf8.Length is 0 or > MaxLength)
|
||||
|
|
@ -69,6 +92,14 @@ public struct ShapeString : IEquatable<ShapeString>
|
|||
return true;
|
||||
}
|
||||
|
||||
public void ForceLength(byte length)
|
||||
{
|
||||
if (length > MaxLength)
|
||||
length = MaxLength;
|
||||
_buffer[length] = 0;
|
||||
_buffer[31] = length;
|
||||
}
|
||||
|
||||
private sealed class Converter : JsonConverter<ShapeString>
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, ShapeString value, JsonSerializer serializer)
|
||||
|
|
|
|||
|
|
@ -81,9 +81,6 @@ public class CommunicatorService : IDisposable, IService
|
|||
/// <inheritdoc cref="Communication.ResolvedFileChanged"/>
|
||||
public readonly ResolvedFileChanged ResolvedFileChanged = new();
|
||||
|
||||
/// <inheritdoc cref="Communication.ModelAttributeComputed"/>
|
||||
public readonly ModelAttributeComputed ModelAttributeComputed = new();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
CollectionChange.Dispose();
|
||||
|
|
@ -108,6 +105,5 @@ public class CommunicatorService : IDisposable, IService
|
|||
ChangedItemClick.Dispose();
|
||||
SelectTab.Dispose();
|
||||
ResolvedFileChanged.Dispose();
|
||||
ModelAttributeComputed.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ public class MetaDrawers(
|
|||
GmpMetaDrawer gmp,
|
||||
ImcMetaDrawer imc,
|
||||
RspMetaDrawer rsp,
|
||||
AtchMetaDrawer atch) : IService
|
||||
AtchMetaDrawer atch,
|
||||
ShpMetaDrawer shp) : IService
|
||||
{
|
||||
public readonly EqdpMetaDrawer Eqdp = eqdp;
|
||||
public readonly EqpMetaDrawer Eqp = eqp;
|
||||
|
|
@ -21,6 +22,7 @@ public class MetaDrawers(
|
|||
public readonly ImcMetaDrawer Imc = imc;
|
||||
public readonly GlobalEqpMetaDrawer GlobalEqp = globalEqp;
|
||||
public readonly AtchMetaDrawer Atch = atch;
|
||||
public readonly ShpMetaDrawer Shp = shp;
|
||||
|
||||
public IMetaDrawer? Get(MetaManipulationType type)
|
||||
=> type switch
|
||||
|
|
@ -32,6 +34,7 @@ public class MetaDrawers(
|
|||
MetaManipulationType.Gmp => Gmp,
|
||||
MetaManipulationType.Rsp => Rsp,
|
||||
MetaManipulationType.Atch => Atch,
|
||||
MetaManipulationType.Shp => Shp,
|
||||
MetaManipulationType.GlobalEqp => GlobalEqp,
|
||||
_ => null,
|
||||
};
|
||||
|
|
|
|||
247
Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs
Normal file
247
Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
using Dalamud.Interface;
|
||||
using ImGuiNET;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Text;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.Meta;
|
||||
using Penumbra.Meta.Files;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods.Editor;
|
||||
using Penumbra.UI.Classes;
|
||||
|
||||
namespace Penumbra.UI.AdvancedWindow.Meta;
|
||||
|
||||
public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles)
|
||||
: MetaDrawer<ShpIdentifier, ShpEntry>(editor, metaFiles), IService
|
||||
{
|
||||
public override ReadOnlySpan<byte> Label
|
||||
=> "Shape Keys (SHP)###SHP"u8;
|
||||
|
||||
private ShapeString _buffer = ShapeString.TryRead("shp_"u8, out var s) ? s : ShapeString.Empty;
|
||||
private bool _identifierValid;
|
||||
|
||||
public override int NumColumns
|
||||
=> 6;
|
||||
|
||||
public override float ColumnHeight
|
||||
=> ImUtf8.FrameHeightSpacing;
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
Identifier = new ShpIdentifier(HumanSlot.Unknown, null, ShapeString.Empty);
|
||||
}
|
||||
|
||||
protected override void DrawNew()
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
CopyToClipboardButton("Copy all current SHP manipulations to clipboard."u8,
|
||||
new Lazy<JToken?>(() => MetaDictionary.SerializeTo([], Editor.Shp)));
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
var canAdd = !Editor.Contains(Identifier) && _identifierValid;
|
||||
var tt = canAdd
|
||||
? "Stage this edit."u8
|
||||
: _identifierValid
|
||||
? "This entry does not contain a valid shape key."u8
|
||||
: "This entry is already edited."u8;
|
||||
if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd))
|
||||
Editor.Changes |= Editor.TryAdd(Identifier, ShpEntry.True);
|
||||
|
||||
DrawIdentifierInput(ref Identifier);
|
||||
DrawEntry(ref Entry, true);
|
||||
}
|
||||
|
||||
protected override void DrawEntry(ShpIdentifier identifier, ShpEntry entry)
|
||||
{
|
||||
DrawMetaButtons(identifier, entry);
|
||||
DrawIdentifier(identifier);
|
||||
|
||||
if (DrawEntry(ref entry, false))
|
||||
Editor.Changes |= Editor.Update(identifier, entry);
|
||||
}
|
||||
|
||||
protected override IEnumerable<(ShpIdentifier, ShpEntry)> Enumerate()
|
||||
=> Editor.Shp
|
||||
.OrderBy(kvp => kvp.Key.Shape)
|
||||
.ThenBy(kvp => kvp.Key.Slot)
|
||||
.ThenBy(kvp => kvp.Key.Id)
|
||||
.Select(kvp => (kvp.Key, kvp.Value));
|
||||
|
||||
protected override int Count
|
||||
=> Editor.Shp.Count;
|
||||
|
||||
private bool DrawIdentifierInput(ref ShpIdentifier identifier)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
var changes = DrawHumanSlot(ref identifier);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
changes |= DrawPrimaryId(ref identifier);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
changes |= DrawShapeKeyInput(ref identifier, ref _buffer, ref _identifierValid);
|
||||
return changes;
|
||||
}
|
||||
|
||||
private static void DrawIdentifier(ShpIdentifier identifier)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
|
||||
ImUtf8.TextFramed(SlotName(identifier.Slot), FrameColor);
|
||||
ImUtf8.HoverTooltip("Model Slot"u8);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if (identifier.Id.HasValue)
|
||||
ImUtf8.TextFramed($"{identifier.Id.Value.Id}", FrameColor);
|
||||
else
|
||||
ImUtf8.TextFramed("All IDs"u8, FrameColor);
|
||||
ImUtf8.HoverTooltip("Primary ID"u8);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImUtf8.TextFramed(identifier.Shape.AsSpan, FrameColor);
|
||||
}
|
||||
|
||||
private static bool DrawEntry(ref ShpEntry entry, bool disabled)
|
||||
{
|
||||
using var dis = ImRaii.Disabled(disabled);
|
||||
ImGui.TableNextColumn();
|
||||
var value = entry.Value;
|
||||
var changes = ImUtf8.Checkbox("##shpEntry"u8, ref value);
|
||||
if (changes)
|
||||
entry = new ShpEntry(value);
|
||||
ImUtf8.HoverTooltip("Whether to enable or disable this shape key for the selected items.");
|
||||
return changes;
|
||||
}
|
||||
|
||||
public static bool DrawPrimaryId(ref ShpIdentifier identifier, float unscaledWidth = 100)
|
||||
{
|
||||
var allSlots = identifier.Slot is HumanSlot.Unknown;
|
||||
var all = !identifier.Id.HasValue;
|
||||
var ret = false;
|
||||
using (ImRaii.Disabled(allSlots))
|
||||
{
|
||||
if (ImUtf8.Checkbox("##shpAll"u8, ref all))
|
||||
{
|
||||
identifier = identifier with { Id = all ? null : 0 };
|
||||
ret = true;
|
||||
}
|
||||
}
|
||||
|
||||
ImUtf8.HoverTooltip(allSlots ? "When using all slots, you also need to use all IDs."u8 : "Enable this shape key for all model IDs."u8);
|
||||
|
||||
ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
|
||||
if (all)
|
||||
{
|
||||
using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0.05f, 0.5f));
|
||||
ImUtf8.TextFramed("All IDs"u8, ImGui.GetColorU32(ImGuiCol.FrameBg, all || allSlots ? ImGui.GetStyle().DisabledAlpha : 1f),
|
||||
new Vector2(unscaledWidth, 0), ImGui.GetColorU32(ImGuiCol.TextDisabled));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (IdInput("##shpPrimaryId"u8, unscaledWidth, identifier.Id.GetValueOrDefault(0).Id, out var setId, 0,
|
||||
ExpandedEqpGmpBase.Count - 1,
|
||||
false))
|
||||
{
|
||||
identifier = identifier with { Id = setId };
|
||||
ret = true;
|
||||
}
|
||||
}
|
||||
|
||||
ImUtf8.HoverTooltip("Primary ID - You can usually find this as the 'e####' part of an item path or similar for customizations."u8);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static bool DrawHumanSlot(ref ShpIdentifier identifier, float unscaledWidth = 150)
|
||||
{
|
||||
var ret = false;
|
||||
ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale);
|
||||
using (var combo = ImUtf8.Combo("##shpSlot"u8, SlotName(identifier.Slot)))
|
||||
{
|
||||
if (combo)
|
||||
foreach (var slot in AvailableSlots)
|
||||
{
|
||||
if (!ImUtf8.Selectable(SlotName(slot), slot == identifier.Slot) || slot == identifier.Slot)
|
||||
continue;
|
||||
|
||||
ret = true;
|
||||
if (slot is HumanSlot.Unknown)
|
||||
identifier = identifier with
|
||||
{
|
||||
Id = null,
|
||||
Slot = slot,
|
||||
};
|
||||
else
|
||||
identifier = identifier with { Slot = slot };
|
||||
}
|
||||
}
|
||||
|
||||
ImUtf8.HoverTooltip("Model Slot"u8);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static unsafe bool DrawShapeKeyInput(ref ShpIdentifier identifier, ref ShapeString buffer, ref bool valid, float unscaledWidth = 150)
|
||||
{
|
||||
var ret = false;
|
||||
var ptr = Unsafe.AsPointer(ref buffer);
|
||||
var span = new Span<byte>(ptr, ShapeString.MaxLength + 1);
|
||||
using (new ImRaii.ColorStyle().Push(ImGuiCol.Border, Colors.RegexWarningBorder, !valid).Push(ImGuiStyleVar.FrameBorderSize, 1f, !valid))
|
||||
{
|
||||
ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale);
|
||||
if (ImUtf8.InputText("##shpShape"u8, span, out int newLength, "Shape Key..."u8))
|
||||
{
|
||||
buffer.ForceLength((byte)newLength);
|
||||
valid = ShpIdentifier.ValidateCustomShapeString(buffer);
|
||||
if (valid)
|
||||
identifier = identifier with { Shape = buffer };
|
||||
ret = true;
|
||||
}
|
||||
}
|
||||
|
||||
ImUtf8.HoverTooltip("Supported shape keys need to have the format `shp_xx_*` and a maximum length of 30 characters."u8);
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static ReadOnlySpan<HumanSlot> AvailableSlots
|
||||
=>
|
||||
[
|
||||
HumanSlot.Unknown,
|
||||
HumanSlot.Head,
|
||||
HumanSlot.Body,
|
||||
HumanSlot.Hands,
|
||||
HumanSlot.Legs,
|
||||
HumanSlot.Feet,
|
||||
HumanSlot.Ears,
|
||||
HumanSlot.Neck,
|
||||
HumanSlot.Wrists,
|
||||
HumanSlot.RFinger,
|
||||
HumanSlot.LFinger,
|
||||
HumanSlot.Glasses,
|
||||
HumanSlot.Hair,
|
||||
HumanSlot.Face,
|
||||
HumanSlot.Ear,
|
||||
];
|
||||
|
||||
private static ReadOnlySpan<byte> SlotName(HumanSlot slot)
|
||||
=> slot switch
|
||||
{
|
||||
HumanSlot.Unknown => "All Slots"u8,
|
||||
HumanSlot.Head => "Equipment: Head"u8,
|
||||
HumanSlot.Body => "Equipment: Body"u8,
|
||||
HumanSlot.Hands => "Equipment: Hands"u8,
|
||||
HumanSlot.Legs => "Equipment: Legs"u8,
|
||||
HumanSlot.Feet => "Equipment: Feet"u8,
|
||||
HumanSlot.Ears => "Equipment: Ears"u8,
|
||||
HumanSlot.Neck => "Equipment: Neck"u8,
|
||||
HumanSlot.Wrists => "Equipment: Wrists"u8,
|
||||
HumanSlot.RFinger => "Equipment: Right Finger"u8,
|
||||
HumanSlot.LFinger => "Equipment: Left Finger"u8,
|
||||
HumanSlot.Glasses => "Equipment: Glasses"u8,
|
||||
HumanSlot.Hair => "Customization: Hair"u8,
|
||||
HumanSlot.Face => "Customization: Face"u8,
|
||||
HumanSlot.Ear => "Customization: Ears"u8,
|
||||
_ => "Unknown"u8,
|
||||
};
|
||||
}
|
||||
|
|
@ -61,6 +61,7 @@ public partial class ModEditWindow
|
|||
DrawEditHeader(MetaManipulationType.Gmp);
|
||||
DrawEditHeader(MetaManipulationType.Rsp);
|
||||
DrawEditHeader(MetaManipulationType.Atch);
|
||||
DrawEditHeader(MetaManipulationType.Shp);
|
||||
DrawEditHeader(MetaManipulationType.GlobalEqp);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,12 @@ using OtterGui.Services;
|
|||
using OtterGui.Text;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Interop;
|
||||
using Penumbra.Interop.PathResolving;
|
||||
using Penumbra.Meta;
|
||||
|
||||
namespace Penumbra.UI.Tabs.Debug;
|
||||
|
||||
public class ShapeInspector(ObjectManager objects) : IUiService
|
||||
public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) : IUiService
|
||||
{
|
||||
private int _objectIndex = 0;
|
||||
|
||||
|
|
@ -29,42 +31,92 @@ public class ShapeInspector(ObjectManager objects) : IUiService
|
|||
return;
|
||||
}
|
||||
|
||||
using var table = ImUtf8.Table("##table"u8, 4, ImGuiTableFlags.RowBg);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
ImUtf8.TableSetupColumn("idx"u8, ImGuiTableColumnFlags.WidthFixed, 25 * ImUtf8.GlobalScale);
|
||||
ImUtf8.TableSetupColumn("ptr"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14);
|
||||
ImUtf8.TableSetupColumn("mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8);
|
||||
ImUtf8.TableSetupColumn("shapes"u8, ImGuiTableColumnFlags.WidthStretch);
|
||||
|
||||
var disabledColor = ImGui.GetColorU32(ImGuiCol.TextDisabled);
|
||||
foreach (var slot in Enum.GetValues<HumanSlot>())
|
||||
var data = resolver.IdentifyCollection(actor.AsObject, true);
|
||||
using (var treeNode1 = ImUtf8.TreeNode($"Collection Shape Cache ({data.ModCollection})"))
|
||||
{
|
||||
ImUtf8.DrawTableColumn($"{(uint)slot:D2}");
|
||||
ImGui.TableNextColumn();
|
||||
var model = human.AsHuman->Models[(int)slot];
|
||||
Penumbra.Dynamis.DrawPointer((nint)model);
|
||||
if (model is not null)
|
||||
if (treeNode1.Success && data.ModCollection.HasCache)
|
||||
{
|
||||
var mask = model->EnabledShapeKeyIndexMask;
|
||||
ImUtf8.DrawTableColumn($"{mask:X8}");
|
||||
ImGui.TableNextColumn();
|
||||
foreach (var (shape, idx) in model->ModelResourceHandle->Shapes)
|
||||
using var table = ImUtf8.Table("##cacheTable"u8, 2, ImGuiTableFlags.RowBg);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
ImUtf8.TableSetupColumn("shape"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale);
|
||||
ImUtf8.TableSetupColumn("enabled"u8, ImGuiTableColumnFlags.WidthStretch);
|
||||
|
||||
foreach (var (shape, set) in data.ModCollection.MetaCache!.Shp.State)
|
||||
{
|
||||
var disabled = (mask & (1u << idx)) is 0;
|
||||
using var color = ImRaii.PushColor(ImGuiCol.Text, disabledColor, disabled);
|
||||
ImUtf8.Text(shape.AsSpan());
|
||||
ImGui.SameLine(0, 0);
|
||||
ImUtf8.Text(", "u8);
|
||||
if ((idx % 8) < 7)
|
||||
ImGui.SameLine(0, 0);
|
||||
ImUtf8.DrawTableColumn(shape.AsSpan);
|
||||
if (set.All)
|
||||
{
|
||||
ImUtf8.DrawTableColumn("All"u8);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
foreach (var slot in ShapeManager.UsedModels)
|
||||
{
|
||||
if (!set[slot])
|
||||
continue;
|
||||
|
||||
ImUtf8.Text($"All {slot.ToName()}, ");
|
||||
ImGui.SameLine(0, 0);
|
||||
}
|
||||
|
||||
foreach (var item in set.Where(i => !set[i.Slot]))
|
||||
{
|
||||
ImUtf8.Text($"{item.Slot.ToName()} {item.Id.Id:D4}, ");
|
||||
ImGui.SameLine(0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
}
|
||||
|
||||
using (var treeNode2 = ImUtf8.TreeNode("Character Model Shapes"u8))
|
||||
{
|
||||
if (treeNode2)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableNextColumn();
|
||||
using var table = ImUtf8.Table("##table"u8, 5, ImGuiTableFlags.RowBg);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
ImUtf8.TableSetupColumn("idx"u8, ImGuiTableColumnFlags.WidthFixed, 25 * ImUtf8.GlobalScale);
|
||||
ImUtf8.TableSetupColumn("name"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale);
|
||||
ImUtf8.TableSetupColumn("ptr"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14);
|
||||
ImUtf8.TableSetupColumn("mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8);
|
||||
ImUtf8.TableSetupColumn("shapes"u8, ImGuiTableColumnFlags.WidthStretch);
|
||||
|
||||
var disabledColor = ImGui.GetColorU32(ImGuiCol.TextDisabled);
|
||||
for (var i = 0; i < human.AsHuman->SlotCount; ++i)
|
||||
{
|
||||
ImUtf8.DrawTableColumn($"{(uint)i:D2}");
|
||||
ImUtf8.DrawTableColumn(((HumanSlot)i).ToName());
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
var model = human.AsHuman->Models[i];
|
||||
Penumbra.Dynamis.DrawPointer((nint)model);
|
||||
if (model is not null)
|
||||
{
|
||||
var mask = model->EnabledShapeKeyIndexMask;
|
||||
ImUtf8.DrawTableColumn($"{mask:X8}");
|
||||
ImGui.TableNextColumn();
|
||||
foreach (var (shape, idx) in model->ModelResourceHandle->Shapes)
|
||||
{
|
||||
var disabled = (mask & (1u << idx)) is 0;
|
||||
using var color = ImRaii.PushColor(ImGuiCol.Text, disabledColor, disabled);
|
||||
ImUtf8.Text(shape.AsSpan());
|
||||
ImGui.SameLine(0, 0);
|
||||
ImUtf8.Text(", "u8);
|
||||
if (idx % 8 < 7)
|
||||
ImGui.SameLine(0, 0);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableNextColumn();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ public class SettingsTab : ITab, IUiService
|
|||
private readonly MigrationSectionDrawer _migrationDrawer;
|
||||
private readonly CollectionAutoSelector _autoSelector;
|
||||
private readonly CleanupService _cleanupService;
|
||||
private readonly AttributeHooks _attributeHooks;
|
||||
private readonly AttributeHook _attributeHook;
|
||||
|
||||
private int _minimumX = int.MaxValue;
|
||||
private int _minimumY = int.MaxValue;
|
||||
|
|
@ -64,7 +64,7 @@ public class SettingsTab : ITab, IUiService
|
|||
DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig,
|
||||
IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService,
|
||||
MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService,
|
||||
AttributeHooks attributeHooks)
|
||||
AttributeHook attributeHook)
|
||||
{
|
||||
_pluginInterface = pluginInterface;
|
||||
_config = config;
|
||||
|
|
@ -89,7 +89,7 @@ public class SettingsTab : ITab, IUiService
|
|||
_migrationDrawer = migrationDrawer;
|
||||
_autoSelector = autoSelector;
|
||||
_cleanupService = cleanupService;
|
||||
_attributeHooks = attributeHooks;
|
||||
_attributeHook = attributeHook;
|
||||
}
|
||||
|
||||
public void DrawHeader()
|
||||
|
|
@ -525,55 +525,6 @@ public class SettingsTab : ITab, IUiService
|
|||
ImGuiUtil.LabeledHelpMarker("Sort Mode", "Choose the sort mode for the mod selector in the mods tab.");
|
||||
}
|
||||
|
||||
private float _absoluteSelectorSize = float.NaN;
|
||||
|
||||
/// <summary> Draw a selector for the absolute size of the mod selector in pixels. </summary>
|
||||
private void DrawAbsoluteSizeSelector()
|
||||
{
|
||||
if (float.IsNaN(_absoluteSelectorSize))
|
||||
_absoluteSelectorSize = _config.ModSelectorAbsoluteSize;
|
||||
|
||||
if (ImGuiUtil.DragFloat("##absoluteSize", ref _absoluteSelectorSize, UiHelpers.InputTextWidth.X, 1,
|
||||
Configuration.Constants.MinAbsoluteSize, Configuration.Constants.MaxAbsoluteSize, "%.0f")
|
||||
&& _absoluteSelectorSize != _config.ModSelectorAbsoluteSize)
|
||||
{
|
||||
_config.ModSelectorAbsoluteSize = _absoluteSelectorSize;
|
||||
_config.Save();
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGuiUtil.LabeledHelpMarker("Mod Selector Absolute Size",
|
||||
"The minimal absolute size of the mod selector in the mod tab in pixels.");
|
||||
}
|
||||
|
||||
private int _relativeSelectorSize = int.MaxValue;
|
||||
|
||||
/// <summary> Draw a selector for the relative size of the mod selector as a percentage and a toggle to enable relative sizing. </summary>
|
||||
private void DrawRelativeSizeSelector()
|
||||
{
|
||||
var scaleModSelector = _config.ScaleModSelector;
|
||||
if (ImGui.Checkbox("Scale Mod Selector With Window Size", ref scaleModSelector))
|
||||
{
|
||||
_config.ScaleModSelector = scaleModSelector;
|
||||
_config.Save();
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if (_relativeSelectorSize == int.MaxValue)
|
||||
_relativeSelectorSize = _config.ModSelectorScaledSize;
|
||||
if (ImGuiUtil.DragInt("##relativeSize", ref _relativeSelectorSize, UiHelpers.InputTextWidth.X - ImGui.GetCursorPosX(), 0.1f,
|
||||
Configuration.Constants.MinScaledSize, Configuration.Constants.MaxScaledSize, "%i%%")
|
||||
&& _relativeSelectorSize != _config.ModSelectorScaledSize)
|
||||
{
|
||||
_config.ModSelectorScaledSize = _relativeSelectorSize;
|
||||
_config.Save();
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGuiUtil.LabeledHelpMarker("Mod Selector Relative Size",
|
||||
"Instead of keeping the mod-selector in the Installed Mods tab a fixed width, this will let it scale with the total size of the Penumbra window.");
|
||||
}
|
||||
|
||||
private void DrawRenameSettings()
|
||||
{
|
||||
ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X);
|
||||
|
|
@ -607,8 +558,6 @@ public class SettingsTab : ITab, IUiService
|
|||
private void DrawModSelectorSettings()
|
||||
{
|
||||
DrawFolderSortType();
|
||||
DrawAbsoluteSizeSelector();
|
||||
DrawRelativeSizeSelector();
|
||||
DrawRenameSettings();
|
||||
Checkbox("Open Folders by Default", "Whether to start with all folders collapsed or expanded in the mod selector.",
|
||||
_config.OpenFoldersByDefault, v =>
|
||||
|
|
@ -626,7 +575,8 @@ public class SettingsTab : ITab, IUiService
|
|||
_config.Save();
|
||||
});
|
||||
Widget.DoubleModifierSelector("Incognito Modifier",
|
||||
"A modifier you need to hold while clicking the Incognito or Temporary Settings Mode button for it to take effect.", UiHelpers.InputTextWidth.X,
|
||||
"A modifier you need to hold while clicking the Incognito or Temporary Settings Mode button for it to take effect.",
|
||||
UiHelpers.InputTextWidth.X,
|
||||
_config.IncognitoModifier,
|
||||
v =>
|
||||
{
|
||||
|
|
@ -811,8 +761,8 @@ public class SettingsTab : ITab, IUiService
|
|||
"Normally, metadata changes that equal their default values, which are sometimes exported by TexTools, are discarded. "
|
||||
+ "Toggle this to keep them, for example if an option in a mod is supposed to disable a metadata change from a prior option.",
|
||||
_config.KeepDefaultMetaChanges, v => _config.KeepDefaultMetaChanges = v);
|
||||
Checkbox("Enable Advanced Shape Support", "Penumbra will allow for custom shape keys for modded models to be considered and combined.",
|
||||
_config.EnableAttributeHooks, _attributeHooks.SetState);
|
||||
Checkbox("Enable Custom Shape Support", "Penumbra will allow for custom shape keys for modded models to be considered and combined.",
|
||||
_config.EnableCustomShapes, _attributeHook.SetState);
|
||||
DrawWaitForPluginsReflection();
|
||||
DrawEnableHttpApiBox();
|
||||
DrawEnableDebugModeBox();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"type": "object",
|
||||
"properties": {
|
||||
"Type": {
|
||||
"enum": [ "Unknown", "Imc", "Eqdp", "Eqp", "Est", "Gmp", "Rsp", "GlobalEqp", "Atch" ]
|
||||
"enum": [ "Unknown", "Imc", "Eqdp", "Eqp", "Est", "Gmp", "Rsp", "GlobalEqp", "Atch", "Shp" ]
|
||||
},
|
||||
"Manipulation": {
|
||||
"type": "object"
|
||||
|
|
@ -90,6 +90,16 @@
|
|||
"$ref": "meta_atch.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"Type": {
|
||||
"const": "Shp"
|
||||
},
|
||||
"Manipulation": {
|
||||
"$ref": "meta_shp.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"$anchor": "EquipSlot",
|
||||
"enum": [ "Unknown", "MainHand", "OffHand", "Head", "Body", "Hands", "Belt", "Legs", "Feet", "Ears", "Neck", "Wrists", "RFinger", "BothHand", "LFinger", "HeadBody", "BodyHandsLegsFeet", "SoulCrystal", "LegsFeet", "FullBody", "BodyHands", "BodyLegsFeet", "ChestHands", "Nothing", "All" ]
|
||||
},
|
||||
"HumanSlot": {
|
||||
"$anchor": "HumanSlot",
|
||||
"enum": [ "Head", "Body", "Hands", "Legs", "Feet", "Ears", "Neck", "Wrists", "RFinger", "LFinger", "Hair", "Face", "Ear", "Glasses", "Unknown" ]
|
||||
},
|
||||
"Gender": {
|
||||
"$anchor": "Gender",
|
||||
"enum": [ "Unknown", "Male", "Female", "MaleNpc", "FemaleNpc" ]
|
||||
|
|
|
|||
23
schemas/structs/meta_shp.json
Normal file
23
schemas/structs/meta_shp.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Entry": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"Slot": {
|
||||
"$ref": "meta_enums.json#HumanSlot"
|
||||
},
|
||||
"Id": {
|
||||
"$ref": "meta_enums.json#U16"
|
||||
},
|
||||
"Shape": {
|
||||
"type": "string",
|
||||
"minLength": 8,
|
||||
"maxLength": 30
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"Shape"
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue