Update shp conditions.

This commit is contained in:
Ottermandias 2025-05-18 15:52:47 +02:00
parent fbc4c2d054
commit e326e3d809
8 changed files with 137 additions and 179 deletions

View file

@ -8,27 +8,24 @@ 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);
=> EnabledCount > 0 && _shpData.TryGetValue(shape, out var value) && value.Contains(slot, id);
internal IReadOnlyDictionary<ShapeString, ShpHashSet> State
=> _shpData;
internal IEnumerable<(ShapeString, IReadOnlyDictionary<ShapeString, ShpHashSet>)> ConditionState
=> _conditionalSet.Select(kvp => (kvp.Key, (IReadOnlyDictionary<ShapeString, ShpHashSet>)kvp.Value));
public bool CheckConditionState(ShapeString condition, [NotNullWhen(true)] out IReadOnlyDictionary<ShapeString, ShpHashSet>? dict)
{
if (_conditionalSet.TryGetValue(condition, out var d))
internal IReadOnlyDictionary<ShapeString, ShpHashSet> State(ShapeConnectorCondition connector)
=> connector switch
{
dict = d;
return true;
}
ShapeConnectorCondition.None => _shpData,
ShapeConnectorCondition.Wrists => _wristConnectors,
ShapeConnectorCondition.Waist => _waistConnectors,
ShapeConnectorCondition.Ankles => _ankleConnectors,
_ => [],
};
dict = null;
return false;
}
public int EnabledCount { get; private set; }
public bool ShouldBeEnabled(ShapeConnectorCondition connector, in ShapeString shape, HumanSlot slot, PrimaryId id)
=> State(connector).TryGetValue(shape, out var value) && value.Contains(slot, id);
public sealed class ShpHashSet : HashSet<(HumanSlot Slot, PrimaryId Id)>
{
private readonly BitArray _allIds = new(ShapeManager.ModelSlotSize);
@ -92,14 +89,18 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection)
=> !_allIds.HasAnySet() && Count is 0;
}
private readonly Dictionary<ShapeString, ShpHashSet> _shpData = [];
private readonly Dictionary<ShapeString, Dictionary<ShapeString, ShpHashSet>> _conditionalSet = [];
private readonly Dictionary<ShapeString, ShpHashSet> _shpData = [];
private readonly Dictionary<ShapeString, ShpHashSet> _wristConnectors = [];
private readonly Dictionary<ShapeString, ShpHashSet> _waistConnectors = [];
private readonly Dictionary<ShapeString, ShpHashSet> _ankleConnectors = [];
public void Reset()
{
Clear();
_shpData.Clear();
_conditionalSet.Clear();
_wristConnectors.Clear();
_waistConnectors.Clear();
_ankleConnectors.Clear();
}
protected override void Dispose(bool _)
@ -107,24 +108,16 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection)
protected override void ApplyModInternal(ShpIdentifier identifier, ShpEntry entry)
{
if (identifier.ShapeCondition.Length > 0)
switch (identifier.ConnectorCondition)
{
if (!_conditionalSet.TryGetValue(identifier.ShapeCondition, out var shapes))
{
if (!entry.Value)
return;
shapes = new Dictionary<ShapeString, ShpHashSet>();
_conditionalSet.Add(identifier.ShapeCondition, shapes);
}
Func(shapes);
}
else
{
Func(_shpData);
case ShapeConnectorCondition.None: Func(_shpData); break;
case ShapeConnectorCondition.Wrists: Func(_wristConnectors); break;
case ShapeConnectorCondition.Waist: Func(_waistConnectors); break;
case ShapeConnectorCondition.Ankles: Func(_ankleConnectors); break;
}
return;
void Func(Dictionary<ShapeString, ShpHashSet> dict)
{
if (!dict.TryGetValue(identifier.Shape, out var value))
@ -136,22 +129,19 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection)
dict.Add(identifier.Shape, value);
}
value.TrySet(identifier.Slot, identifier.Id, entry);
if (value.TrySet(identifier.Slot, identifier.Id, entry))
++EnabledCount;
}
}
protected override void RevertModInternal(ShpIdentifier identifier)
{
if (identifier.ShapeCondition.Length > 0)
switch (identifier.ConnectorCondition)
{
if (!_conditionalSet.TryGetValue(identifier.ShapeCondition, out var shapes))
return;
Func(shapes);
}
else
{
Func(_shpData);
case ShapeConnectorCondition.None: Func(_shpData); break;
case ShapeConnectorCondition.Wrists: Func(_wristConnectors); break;
case ShapeConnectorCondition.Waist: Func(_waistConnectors); break;
case ShapeConnectorCondition.Ankles: Func(_ankleConnectors); break;
}
return;
@ -162,7 +152,10 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection)
return;
if (value.TrySet(identifier.Slot, identifier.Id, ShpEntry.False) && value.IsEmpty)
{
--EnabledCount;
_shpData.Remove(identifier.Shape);
}
}
}
}

View file

@ -1,4 +1,5 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
@ -7,7 +8,16 @@ using Penumbra.Interop.Structs;
namespace Penumbra.Meta.Manipulations;
public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, ShapeString Shape, ShapeString ShapeCondition)
[JsonConverter(typeof(StringEnumConverter))]
public enum ShapeConnectorCondition : byte
{
None = 0,
Wrists = 1,
Waist = 2,
Ankles = 3,
}
public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, ShapeString Shape, ShapeConnectorCondition ConnectorCondition)
: IComparable<ShpIdentifier>, IMetaIdentifier
{
public int CompareTo(ShpIdentifier other)
@ -34,11 +44,11 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape
return 1;
}
var shapeComparison = Shape.CompareTo(other.Shape);
if (shapeComparison is not 0)
return shapeComparison;
var conditionComparison = ConnectorCondition.CompareTo(other.ConnectorCondition);
if (conditionComparison is not 0)
return conditionComparison;
return ShapeCondition.CompareTo(other.ShapeCondition);
return Shape.CompareTo(other.Shape);
}
@ -62,9 +72,13 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape
sb.Append("All IDs");
}
if (ShapeCondition.Length > 0)
sb.Append(" - ")
.Append(ShapeCondition);
switch (ConnectorCondition)
{
case ShapeConnectorCondition.Wrists: sb.Append(" - Wrist Connector"); break;
case ShapeConnectorCondition.Waist: sb.Append(" - Waist Connector"); break;
case ShapeConnectorCondition.Ankles: sb.Append(" - Ankle Connector"); break;
}
return sb.ToString();
}
@ -87,20 +101,16 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape
if (!ValidateCustomShapeString(Shape))
return false;
if (ShapeCondition.Length is 0)
return true;
if (!ValidateCustomShapeString(ShapeCondition))
if (!Enum.IsDefined(ConnectorCondition))
return false;
return Slot switch
return ConnectorCondition switch
{
HumanSlot.Hands when ShapeCondition.IsWrist() => true,
HumanSlot.Body when ShapeCondition.IsWrist() || ShapeCondition.IsWaist() => true,
HumanSlot.Legs when ShapeCondition.IsWaist() || ShapeCondition.IsAnkle() => true,
HumanSlot.Feet when ShapeCondition.IsAnkle() => true,
HumanSlot.Unknown when ShapeCondition.IsWrist() || ShapeCondition.IsWaist() || ShapeCondition.IsAnkle() => true,
_ => false,
ShapeConnectorCondition.None => true,
ShapeConnectorCondition.Wrists => Slot is HumanSlot.Body or HumanSlot.Hands or HumanSlot.Unknown,
ShapeConnectorCondition.Waist => Slot is HumanSlot.Body or HumanSlot.Legs or HumanSlot.Unknown,
ShapeConnectorCondition.Ankles => Slot is HumanSlot.Legs or HumanSlot.Feet or HumanSlot.Unknown,
_ => false,
};
}
@ -145,8 +155,8 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape
if (Id.HasValue)
jObj["Id"] = Id.Value.Id.ToString();
jObj["Shape"] = Shape.ToString();
if (ShapeCondition.Length > 0)
jObj["ShapeCondition"] = ShapeCondition.ToString();
if (ConnectorCondition is not ShapeConnectorCondition.None)
jObj["ConnectorCondition"] = ConnectorCondition.ToString();
return jObj;
}
@ -156,11 +166,10 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape
if (shape is null || !ShapeString.TryRead(shape, out var shapeString))
return null;
var slot = jObj["Slot"]?.ToObject<HumanSlot>() ?? HumanSlot.Unknown;
var id = jObj["Id"]?.ToObject<ushort>();
var shapeCondition = jObj["ShapeCondition"]?.ToObject<string>();
var shapeConditionString = shapeCondition is null || !ShapeString.TryRead(shapeCondition, out var s) ? ShapeString.Empty : s;
var identifier = new ShpIdentifier(slot, id, shapeString, shapeConditionString);
var slot = jObj["Slot"]?.ToObject<HumanSlot>() ?? HumanSlot.Unknown;
var id = jObj["Id"]?.ToObject<ushort>();
var connectorCondition = jObj["ConnectorCondition"]?.ToObject<ShapeConnectorCondition>() ?? ShapeConnectorCondition.None;
var identifier = new ShpIdentifier(slot, id, shapeString, connectorCondition);
return identifier.Validate() ? identifier : null;
}

View file

@ -1,4 +1,3 @@
using System.Reflection.Metadata.Ecma335;
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.Collections.Cache;
@ -80,8 +79,7 @@ public class ShapeManager : IRequiredService, IDisposable
{
_temporaryIndices[i].TryAdd(shapeString, index);
_temporaryMasks[i] |= (ushort)(1 << index);
if (cache.State.Count > 0
&& cache.ShouldBeEnabled(shapeString, modelIndex, _ids[(int)modelIndex]))
if (cache.ShouldBeEnabled(shapeString, modelIndex, _ids[(int)modelIndex]))
_temporaryValues[i] |= (ushort)(1 << index);
}
else
@ -102,14 +100,14 @@ public class ShapeManager : IRequiredService, IDisposable
{
_temporaryValues[1] |= 1u << topIndex;
_temporaryValues[2] |= 1u << handIndex;
CheckCondition(shape, HumanSlot.Body, HumanSlot.Hands, 1, 2);
CheckCondition(cache.State(ShapeConnectorCondition.Wrists), HumanSlot.Body, HumanSlot.Hands, 1, 2);
}
if (shape.IsWaist() && _temporaryIndices[3].TryGetValue(shape, out var legIndex))
{
_temporaryValues[1] |= 1u << topIndex;
_temporaryValues[3] |= 1u << legIndex;
CheckCondition(shape, HumanSlot.Body, HumanSlot.Legs, 1, 3);
CheckCondition(cache.State(ShapeConnectorCondition.Waist), HumanSlot.Body, HumanSlot.Legs, 1, 3);
}
}
@ -119,25 +117,23 @@ public class ShapeManager : IRequiredService, IDisposable
{
_temporaryValues[3] |= 1u << bottomIndex;
_temporaryValues[4] |= 1u << footIndex;
CheckCondition(shape, HumanSlot.Legs, HumanSlot.Feet, 3, 4);
CheckCondition(cache.State(ShapeConnectorCondition.Ankles), HumanSlot.Legs, HumanSlot.Feet, 3, 4);
}
}
return;
void CheckCondition(in ShapeString shape, HumanSlot slot1, HumanSlot slot2, int idx1, int idx2)
void CheckCondition(IReadOnlyDictionary<ShapeString, ShpCache.ShpHashSet> dict, HumanSlot slot1, HumanSlot slot2, int idx1, int idx2)
{
if (!cache.CheckConditionState(shape, out var dict))
if (dict.Count is 0)
return;
foreach (var (subShape, set) in dict)
foreach (var (shape, set) in dict)
{
if (set.Contains(slot1, _ids[idx1]))
if (_temporaryIndices[idx1].TryGetValue(subShape, out var subIndex))
_temporaryValues[idx1] |= 1u << subIndex;
if (set.Contains(slot2, _ids[idx2]))
if (_temporaryIndices[idx2].TryGetValue(subShape, out var subIndex))
_temporaryValues[idx2] |= 1u << subIndex;
if (set.Contains(slot1, _ids[idx1]) && _temporaryIndices[idx1].TryGetValue(shape, out var index1))
_temporaryValues[idx1] |= 1u << index1;
if (set.Contains(slot2, _ids[idx2]) && _temporaryIndices[idx2].TryGetValue(shape, out var index2))
_temporaryValues[idx2] |= 1u << index2;
}
}
}

View file

@ -19,10 +19,8 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
public override ReadOnlySpan<byte> Label
=> "Shape Keys (SHP)###SHP"u8;
private ShapeString _buffer = ShapeString.TryRead("shpx_"u8, out var s) ? s : ShapeString.Empty;
private ShapeString _conditionBuffer = ShapeString.Empty;
private ShapeString _buffer = ShapeString.TryRead("shpx_"u8, out var s) ? s : ShapeString.Empty;
private bool _identifierValid;
private bool _conditionValid = true;
public override int NumColumns
=> 7;
@ -32,7 +30,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
protected override void Initialize()
{
Identifier = new ShpIdentifier(HumanSlot.Unknown, null, ShapeString.Empty, ShapeString.Empty);
Identifier = new ShpIdentifier(HumanSlot.Unknown, null, ShapeString.Empty, ShapeConnectorCondition.None);
}
protected override void DrawNew()
@ -42,7 +40,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
new Lazy<JToken?>(() => MetaDictionary.SerializeTo([], Editor.Shp)));
ImGui.TableNextColumn();
var canAdd = !Editor.Contains(Identifier) && _identifierValid && _conditionValid;
var canAdd = !Editor.Contains(Identifier) && _identifierValid;
var tt = canAdd
? "Stage this edit."u8
: _identifierValid
@ -69,7 +67,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
.OrderBy(kvp => kvp.Key.Shape)
.ThenBy(kvp => kvp.Key.Slot)
.ThenBy(kvp => kvp.Key.Id)
.ThenBy(kvp => kvp.Key.ShapeCondition)
.ThenBy(kvp => kvp.Key.ConnectorCondition)
.Select(kvp => (kvp.Key, kvp.Value));
protected override int Count
@ -87,7 +85,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
changes |= DrawShapeKeyInput(ref identifier, ref _buffer, ref _identifierValid);
ImGui.TableNextColumn();
changes |= DrawShapeConditionInput(ref identifier, ref _conditionBuffer, ref _conditionValid);
changes |= DrawConnectorConditionInput(ref identifier);
return changes;
}
@ -109,9 +107,9 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
ImUtf8.TextFramed(identifier.Shape.AsSpan, FrameColor);
ImGui.TableNextColumn();
if (identifier.ShapeCondition.Length > 0)
if (identifier.ConnectorCondition is not ShapeConnectorCondition.None)
{
ImUtf8.TextFramed(identifier.ShapeCondition.AsSpan, FrameColor);
ImUtf8.TextFramed($"{identifier.ConnectorCondition}", FrameColor);
ImUtf8.HoverTooltip("Connector condition for this shape to be activated.");
}
}
@ -190,27 +188,18 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
}
else
{
if (_conditionBuffer.Length > 0
&& (_conditionBuffer.IsAnkle() && slot is not HumanSlot.Feet and not HumanSlot.Legs
|| _conditionBuffer.IsWrist() && slot is not HumanSlot.Hands and not HumanSlot.Body
|| _conditionBuffer.IsWaist() && slot is not HumanSlot.Body and not HumanSlot.Legs))
identifier = identifier with
{
identifier = identifier with
Slot = slot,
ConnectorCondition = Identifier.ConnectorCondition switch
{
Slot = slot,
ShapeCondition = ShapeString.Empty,
};
_conditionValid = false;
}
else
{
identifier = identifier with
{
Slot = slot,
ShapeCondition = _conditionBuffer,
};
_conditionValid = true;
}
ShapeConnectorCondition.Wrists when slot is HumanSlot.Body or HumanSlot.Hands => ShapeConnectorCondition.Wrists,
ShapeConnectorCondition.Waist when slot is HumanSlot.Body or HumanSlot.Legs => ShapeConnectorCondition.Waist,
ShapeConnectorCondition.Ankles when slot is HumanSlot.Legs or HumanSlot.Feet => ShapeConnectorCondition.Ankles,
_ => ShapeConnectorCondition.None,
},
};
ret = true;
}
}
}
@ -241,30 +230,40 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
return ret;
}
public static unsafe bool DrawShapeConditionInput(ref ShpIdentifier identifier, ref ShapeString buffer, ref bool valid,
float unscaledWidth = 150)
public static unsafe bool DrawConnectorConditionInput(ref ShpIdentifier identifier, 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))
var ret = false;
ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale);
var (showWrists, showWaist, showAnkles, disable) = identifier.Slot switch
{
ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale);
if (ImUtf8.InputText("##shpCondition"u8, span, out int newLength, "Shape Condition..."u8))
HumanSlot.Unknown => (true, true, true, false),
HumanSlot.Body => (true, true, false, false),
HumanSlot.Legs => (false, true, true, false),
HumanSlot.Hands => (true, false, false, false),
HumanSlot.Feet => (false, false, true, false),
_ => (false, false, false, true),
};
using var disabled = ImRaii.Disabled(disable);
using (var combo = ImUtf8.Combo("##shpCondition"u8, $"{identifier.ConnectorCondition}"))
{
if (combo)
{
buffer.ForceLength((byte)newLength);
valid = ShpIdentifier.ValidateCustomShapeString(buffer)
&& (buffer.IsAnkle() && identifier.Slot is HumanSlot.Unknown or HumanSlot.Feet or HumanSlot.Legs
|| buffer.IsWaist() && identifier.Slot is HumanSlot.Unknown or HumanSlot.Body or HumanSlot.Legs
|| buffer.IsWrist() && identifier.Slot is HumanSlot.Unknown or HumanSlot.Body or HumanSlot.Hands);
if (valid)
identifier = identifier with { ShapeCondition = buffer };
ret = true;
if (ImUtf8.Selectable("None"u8, identifier.ConnectorCondition is ShapeConnectorCondition.None))
identifier = identifier with { ConnectorCondition = ShapeConnectorCondition.None };
if (showWrists && ImUtf8.Selectable("Wrists"u8, identifier.ConnectorCondition is ShapeConnectorCondition.Wrists))
identifier = identifier with { ConnectorCondition = ShapeConnectorCondition.Wrists };
if (showWaist && ImUtf8.Selectable("Waist"u8, identifier.ConnectorCondition is ShapeConnectorCondition.Waist))
identifier = identifier with { ConnectorCondition = ShapeConnectorCondition.Waist };
if (showAnkles && ImUtf8.Selectable("Ankles"u8, identifier.ConnectorCondition is ShapeConnectorCondition.Ankles))
identifier = identifier with { ConnectorCondition = ShapeConnectorCondition.Ankles };
}
}
ImUtf8.HoverTooltip(
"Supported conditional shape keys need to have the format `shpx_an_*` (Legs or Feet), `shpx_wr_*` (Body or Hands), or `shpx_wa_*` (Body or Legs) and a maximum length of 30 characters."u8);
"Only activate this shape key if any custom connector shape keys (shpx_[wr|wa|an]_*) are also enabled through matching attributes."u8);
return ret;
}

View file

@ -36,41 +36,6 @@ using MdlMaterialEditor = Penumbra.Mods.Editor.MdlMaterialEditor;
namespace Penumbra.UI.AdvancedWindow;
public sealed class OptionSelectCombo(ModEditor editor, ModEditWindow window)
: FilterComboCache<(string FullName, (int Group, int Data) Index)>(
() => window.Mod!.AllDataContainers.Select(c => (c.GetFullName(), c.GetDataIndices())).ToList(), MouseWheelType.Control, Penumbra.Log)
{
private ImRaii.ColorStyle _border;
protected override void DrawCombo(string label, string preview, string tooltip, int currentSelected, float previewWidth, float itemHeight,
ImGuiComboFlags flags)
{
_border = ImRaii.PushFrameBorder(ImUtf8.GlobalScale, ColorId.FolderLine.Value());
base.DrawCombo(label, preview, tooltip, currentSelected, previewWidth, itemHeight, flags);
_border.Dispose();
}
protected override void DrawFilter(int currentSelected, float width)
{
_border.Dispose();
base.DrawFilter(currentSelected, width);
}
public bool Draw(float width)
{
var flags = window.Mod!.AllDataContainers.Count() switch
{
0 => ImGuiComboFlags.NoArrowButton,
> 8 => ImGuiComboFlags.HeightLargest,
_ => ImGuiComboFlags.None,
};
return Draw("##optionSelector", editor.Option!.GetFullName(), string.Empty, width, ImGui.GetTextLineHeight(), flags);
}
protected override bool DrawSelectable(int globalIdx, bool selected)
=> ImUtf8.Selectable(Items[globalIdx].FullName, selected);
}
public partial class ModEditWindow : Window, IDisposable, IUiService
{
private const string WindowBaseLabel = "###SubModEdit";

View file

@ -8,6 +8,7 @@ using Penumbra.GameData.Enums;
using Penumbra.GameData.Interop;
using Penumbra.Interop.PathResolving;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
namespace Penumbra.UI.Tabs.Debug;
@ -52,17 +53,11 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver)
ImUtf8.TableSetupColumn("Enabled"u8, ImGuiTableColumnFlags.WidthStretch);
ImGui.TableHeadersRow();
foreach (var (shape, set) in data.ModCollection.MetaCache!.Shp.State)
foreach (var condition in Enum.GetValues<ShapeConnectorCondition>())
{
ImGui.TableNextColumn();
DrawShape(shape, set);
}
foreach (var (condition, dict) in data.ModCollection.MetaCache!.Shp.ConditionState)
{
foreach (var (shape, set) in dict)
foreach (var (shape, set) in data.ModCollection.MetaCache!.Shp.State(condition))
{
ImUtf8.DrawTableColumn(condition.AsSpan);
ImUtf8.DrawTableColumn(condition.ToString());
DrawShape(shape, set);
}
}

View file

@ -29,6 +29,10 @@
"$anchor": "SubRace",
"enum": [ "Unknown", "Midlander", "Highlander", "Wildwood", "Duskwight", "Plainsfolk", "Dunesfolk", "SeekerOfTheSun", "KeeperOfTheMoon", "Seawolf", "Hellsguard", "Raen", "Xaela", "Helion", "Lost", "Rava", "Veena" ]
},
"ShapeConnectorCondition": {
"$anchor": "ShapeConnectorCondition",
"enum": [ "None", "Wrists", "Waist", "Ankles" ]
},
"U8": {
"$anchor": "U8",
"oneOf": [

View file

@ -17,11 +17,8 @@
"maxLength": 30,
"pattern": "^shpx_"
},
"ShapeCondition": {
"type": "string",
"minLength": 8,
"maxLength": 30,
"pattern": "^shpx_(wa|an|wr)_"
"ConnectorCondition": {
"$ref": "meta_enums.json#ShapeConnectorCondition"
}
},
"required": [