Remove local GameData

This commit is contained in:
Ottermandias 2023-07-25 15:52:39 +02:00
parent 8d7c779439
commit b8c9a98ba2
69 changed files with 0 additions and 30444 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,225 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Objects.Enums;
using Newtonsoft.Json.Linq;
using Penumbra.String;
namespace Penumbra.GameData.Actors;
[StructLayout(LayoutKind.Explicit)]
public readonly struct ActorIdentifier : IEquatable<ActorIdentifier>
{
public static ActorManager? Manager;
public static readonly ActorIdentifier Invalid = new(IdentifierType.Invalid, 0, 0, 0, ByteString.Empty);
public enum RetainerType : ushort
{
Both = 0,
Bell = 1,
Mannequin = 2,
}
// @formatter:off
[FieldOffset( 0 )] public readonly IdentifierType Type; // All
[FieldOffset( 1 )] public readonly ObjectKind Kind; // Npc, Owned
[FieldOffset( 2 )] public readonly ushort HomeWorld; // Player, Owned
[FieldOffset( 2 )] public readonly ushort Index; // NPC
[FieldOffset( 2 )] public readonly RetainerType Retainer; // Retainer
[FieldOffset( 2 )] public readonly ScreenActor Special; // Special
[FieldOffset( 4 )] public readonly uint DataId; // Owned, NPC
[FieldOffset( 8 )] public readonly ByteString PlayerName; // Player, Owned
// @formatter:on
public ActorIdentifier CreatePermanent()
=> new(Type, Kind, Index, DataId, PlayerName.IsEmpty || PlayerName.IsOwned ? PlayerName : PlayerName.Clone());
public bool Equals(ActorIdentifier other)
{
if (Type != other.Type)
return false;
return Type switch
{
IdentifierType.Player => HomeWorld == other.HomeWorld && PlayerName.EqualsCi(other.PlayerName),
IdentifierType.Retainer => PlayerName.EqualsCi(other.PlayerName),
IdentifierType.Owned => HomeWorld == other.HomeWorld && PlayerName.EqualsCi(other.PlayerName) && Manager.DataIdEquals(this, other),
IdentifierType.Special => Special == other.Special,
IdentifierType.Npc => Manager.DataIdEquals(this, other)
&& (Index == other.Index || Index == ushort.MaxValue || other.Index == ushort.MaxValue),
IdentifierType.UnkObject => PlayerName.EqualsCi(other.PlayerName) && Index == other.Index,
_ => false,
};
}
public override bool Equals(object? obj)
=> obj is ActorIdentifier other && Equals(other);
public static bool operator ==(ActorIdentifier lhs, ActorIdentifier rhs)
=> lhs.Equals(rhs);
public static bool operator !=(ActorIdentifier lhs, ActorIdentifier rhs)
=> !lhs.Equals(rhs);
public bool IsValid
=> Type is not (IdentifierType.UnkObject or IdentifierType.Invalid);
public string Incognito(string? name)
{
name ??= ToString();
if (Type is not (IdentifierType.Player or IdentifierType.Owned))
return name;
var parts = name.Split(' ', 3);
return string.Join(" ",
parts.Length != 3 ? parts.Select(n => $"{n[0]}.") : parts[..2].Select(n => $"{n[0]}.").Append(parts[2]));
}
public override string ToString()
=> Manager?.ToString(this)
?? Type switch
{
IdentifierType.Player => $"{PlayerName} ({HomeWorld})",
IdentifierType.Retainer => $"{PlayerName} (Retainer)",
IdentifierType.Owned => $"{PlayerName}s {Kind.ToName()} {DataId} ({HomeWorld})",
IdentifierType.Special => Special.ToName(),
IdentifierType.Npc =>
Index == ushort.MaxValue
? $"{Kind.ToName()} #{DataId}"
: $"{Kind.ToName()} #{DataId} at {Index}",
IdentifierType.UnkObject => PlayerName.IsEmpty
? $"Unknown Object at {Index}"
: $"{PlayerName} at {Index}",
_ => "Invalid",
};
public string ToName()
=> Manager?.ToName(this) ?? "Unknown Object";
public override int GetHashCode()
=> Type switch
{
IdentifierType.Player => HashCode.Combine(IdentifierType.Player, PlayerName, HomeWorld),
IdentifierType.Retainer => HashCode.Combine(IdentifierType.Player, PlayerName),
IdentifierType.Owned => HashCode.Combine(IdentifierType.Owned, Kind, PlayerName, HomeWorld, DataId),
IdentifierType.Special => HashCode.Combine(IdentifierType.Special, Special),
IdentifierType.Npc => HashCode.Combine(IdentifierType.Npc, Kind, DataId),
IdentifierType.UnkObject => HashCode.Combine(IdentifierType.UnkObject, PlayerName, Index),
_ => 0,
};
internal ActorIdentifier(IdentifierType type, ObjectKind kind, ushort index, uint data, ByteString playerName)
{
Type = type;
Kind = kind;
Special = (ScreenActor)index;
HomeWorld = Index = index;
DataId = data;
PlayerName = playerName;
}
public JObject ToJson()
{
var ret = new JObject { { nameof(Type), Type.ToString() } };
switch (Type)
{
case IdentifierType.Player:
ret.Add(nameof(PlayerName), PlayerName.ToString());
ret.Add(nameof(HomeWorld), HomeWorld);
return ret;
case IdentifierType.Retainer:
ret.Add(nameof(PlayerName), PlayerName.ToString());
return ret;
case IdentifierType.Owned:
ret.Add(nameof(PlayerName), PlayerName.ToString());
ret.Add(nameof(HomeWorld), HomeWorld);
ret.Add(nameof(Kind), Kind.ToString());
ret.Add(nameof(DataId), DataId);
return ret;
case IdentifierType.Special:
ret.Add(nameof(Special), Special.ToString());
return ret;
case IdentifierType.Npc:
ret.Add(nameof(Kind), Kind.ToString());
if (Index != ushort.MaxValue)
ret.Add(nameof(Index), Index);
ret.Add(nameof(DataId), DataId);
return ret;
case IdentifierType.UnkObject:
ret.Add(nameof(PlayerName), PlayerName.ToString());
ret.Add(nameof(Index), Index);
return ret;
}
return ret;
}
}
public static class ActorManagerExtensions
{
public static bool DataIdEquals(this ActorManager? manager, ActorIdentifier lhs, ActorIdentifier rhs)
{
if (lhs.Kind != rhs.Kind)
return false;
if (lhs.DataId == rhs.DataId)
return true;
if (manager == null)
return lhs.Kind == rhs.Kind && lhs.DataId == rhs.DataId || lhs.DataId == uint.MaxValue || rhs.DataId == uint.MaxValue;
var dict = lhs.Kind switch
{
ObjectKind.MountType => manager.Data.Mounts,
ObjectKind.Companion => manager.Data.Companions,
ObjectKind.Ornament => manager.Data.Ornaments,
ObjectKind.BattleNpc => manager.Data.BNpcs,
ObjectKind.EventNpc => manager.Data.ENpcs,
_ => new Dictionary<uint, string>(),
};
return dict.TryGetValue(lhs.DataId, out var lhsName)
&& dict.TryGetValue(rhs.DataId, out var rhsName)
&& lhsName.Equals(rhsName, StringComparison.OrdinalIgnoreCase);
}
public static string ToName(this ObjectKind kind)
=> kind switch
{
ObjectKind.None => "Unknown",
ObjectKind.BattleNpc => "Battle NPC",
ObjectKind.EventNpc => "Event NPC",
ObjectKind.MountType => "Mount",
ObjectKind.Companion => "Companion",
ObjectKind.Ornament => "Accessory",
_ => kind.ToString(),
};
public static string ToName(this IdentifierType type)
=> type switch
{
IdentifierType.Player => "Player",
IdentifierType.Retainer => "Retainer (Bell)",
IdentifierType.Owned => "Owned NPC",
IdentifierType.Special => "Special Actor",
IdentifierType.Npc => "NPC",
IdentifierType.UnkObject => "Unknown Object",
_ => "Invalid",
};
/// <summary>
/// Fixed names for special actors.
/// </summary>
public static string ToName(this ScreenActor actor)
=> actor switch
{
ScreenActor.CharacterScreen => "Character Screen Actor",
ScreenActor.ExamineScreen => "Examine Screen Actor",
ScreenActor.FittingRoom => "Fitting Room Actor",
ScreenActor.DyePreview => "Dye Preview Actor",
ScreenActor.Portrait => "Portrait Actor",
_ => "Invalid",
};
}

View file

@ -1,406 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using Dalamud;
using Dalamud.Data;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.Gui;
using Dalamud.Plugin;
using Dalamud.Utility;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Lumina.Excel.GeneratedSheets;
using Lumina.Text;
using Penumbra.GameData.Data;
using Penumbra.GameData.Structs;
using Penumbra.String;
using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character;
using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
namespace Penumbra.GameData.Actors;
public sealed partial class ActorManager : IDisposable
{
public sealed class ActorManagerData : DataSharer
{
/// <summary> Worlds available for players. </summary>
public IReadOnlyDictionary<ushort, string> Worlds { get; }
/// <summary> Valid Mount names in title case by mount id. </summary>
public IReadOnlyDictionary<uint, string> Mounts { get; }
/// <summary> Valid Companion names in title case by companion id. </summary>
public IReadOnlyDictionary<uint, string> Companions { get; }
/// <summary> Valid ornament names by id. </summary>
public IReadOnlyDictionary<uint, string> Ornaments { get; }
/// <summary> Valid BNPC names in title case by BNPC Name id. </summary>
public IReadOnlyDictionary<uint, string> BNpcs { get; }
/// <summary> Valid ENPC names in title case by ENPC id. </summary>
public IReadOnlyDictionary<uint, string> ENpcs { get; }
public ActorManagerData(DalamudPluginInterface pluginInterface, DataManager gameData, ClientLanguage language)
: base(pluginInterface, language, 2)
{
var worldTask = TryCatchDataAsync("Worlds", CreateWorldData(gameData));
var mountsTask = TryCatchDataAsync("Mounts", CreateMountData(gameData));
var companionsTask = TryCatchDataAsync("Companions", CreateCompanionData(gameData));
var ornamentsTask = TryCatchDataAsync("Ornaments", CreateOrnamentData(gameData));
var bNpcsTask = TryCatchDataAsync("BNpcs", CreateBNpcData(gameData));
var eNpcsTask = TryCatchDataAsync("ENpcs", CreateENpcData(gameData));
Worlds = worldTask.Result;
Mounts = mountsTask.Result;
Companions = companionsTask.Result;
Ornaments = ornamentsTask.Result;
BNpcs = bNpcsTask.Result;
ENpcs = eNpcsTask.Result;
}
/// <summary>
/// Return the world name including the Any World option.
/// </summary>
public string ToWorldName(ushort worldId)
=> worldId == ushort.MaxValue ? "Any World" : Worlds.TryGetValue(worldId, out var name) ? name : "Invalid";
/// <summary>
/// Return the world id corresponding to the given name.
/// </summary>
/// <returns>ushort.MaxValue if the name is empty, 0 if it is not a valid world, or the worlds id.</returns>
public ushort ToWorldId(string worldName)
=> worldName.Length != 0
? Worlds.FirstOrDefault(kvp => string.Equals(kvp.Value, worldName, StringComparison.OrdinalIgnoreCase), default).Key
: ushort.MaxValue;
/// <summary>
/// Convert a given ID for a certain ObjectKind to a name.
/// </summary>
/// <returns>Invalid or a valid name.</returns>
public string ToName(ObjectKind kind, uint dataId)
=> TryGetName(kind, dataId, out var ret) ? ret : "Invalid";
/// <summary>
/// Convert a given ID for a certain ObjectKind to a name.
/// </summary>
public bool TryGetName(ObjectKind kind, uint dataId, [NotNullWhen(true)] out string? name)
{
name = null;
return kind switch
{
ObjectKind.MountType => Mounts.TryGetValue(dataId, out name),
ObjectKind.Companion => Companions.TryGetValue(dataId, out name),
ObjectKind.Ornament => Ornaments.TryGetValue(dataId, out name),
ObjectKind.BattleNpc => BNpcs.TryGetValue(dataId, out name),
ObjectKind.EventNpc => ENpcs.TryGetValue(dataId, out name),
_ => false,
};
}
protected override void DisposeInternal()
{
DisposeTag("Worlds");
DisposeTag("Mounts");
DisposeTag("Companions");
DisposeTag("Ornaments");
DisposeTag("BNpcs");
DisposeTag("ENpcs");
}
private Action<Dictionary<ushort, string>> CreateWorldData(DataManager gameData)
=> d =>
{
foreach (var w in gameData.GetExcelSheet<World>(Language)!.Where(w => w.IsPublic && !w.Name.RawData.IsEmpty))
d.TryAdd((ushort)w.RowId, string.Intern(w.Name.ToDalamudString().TextValue));
};
private Action<Dictionary<uint, string>> CreateMountData(DataManager gameData)
=> d =>
{
foreach (var m in gameData.GetExcelSheet<Mount>(Language)!.Where(m => m.Singular.RawData.Length > 0 && m.Order >= 0))
d.TryAdd(m.RowId, ToTitleCaseExtended(m.Singular, m.Article));
};
private Action<Dictionary<uint, string>> CreateCompanionData(DataManager gameData)
=> d =>
{
foreach (var c in gameData.GetExcelSheet<Companion>(Language)!.Where(c
=> c.Singular.RawData.Length > 0 && c.Order < ushort.MaxValue))
d.TryAdd(c.RowId, ToTitleCaseExtended(c.Singular, c.Article));
};
private Action<Dictionary<uint, string>> CreateOrnamentData(DataManager gameData)
=> d =>
{
foreach (var o in gameData.GetExcelSheet<Ornament>(Language)!.Where(o => o.Singular.RawData.Length > 0))
d.TryAdd(o.RowId, ToTitleCaseExtended(o.Singular, o.Article));
};
private Action<Dictionary<uint, string>> CreateBNpcData(DataManager gameData)
=> d =>
{
foreach (var n in gameData.GetExcelSheet<BNpcName>(Language)!.Where(n => n.Singular.RawData.Length > 0))
d.TryAdd(n.RowId, ToTitleCaseExtended(n.Singular, n.Article));
};
private Action<Dictionary<uint, string>> CreateENpcData(DataManager gameData)
=> d =>
{
foreach (var n in gameData.GetExcelSheet<ENpcResident>(Language)!.Where(e => e.Singular.RawData.Length > 0))
d.TryAdd(n.RowId, ToTitleCaseExtended(n.Singular, n.Article));
};
private static string ToTitleCaseExtended(SeString s, sbyte article)
{
if (article == 1)
return string.Intern(s.ToDalamudString().ToString());
var sb = new StringBuilder(s.ToDalamudString().ToString());
var lastSpace = true;
for (var i = 0; i < sb.Length; ++i)
{
if (sb[i] == ' ')
{
lastSpace = true;
}
else if (lastSpace)
{
lastSpace = false;
sb[i] = char.ToUpperInvariant(sb[i]);
}
}
return string.Intern(sb.ToString());
}
}
public readonly ActorManagerData Data;
public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, Dalamud.Game.Framework framework,
DataManager gameData, GameGui gameGui,
Func<ushort, short> toParentIdx)
: this(pluginInterface, objects, state, framework, gameData, gameGui, gameData.Language, toParentIdx)
{ }
public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, Dalamud.Game.Framework framework,
DataManager gameData, GameGui gameGui,
ClientLanguage language, Func<ushort, short> toParentIdx)
{
_framework = framework;
_objects = objects;
_gameGui = gameGui;
_clientState = state;
_toParentIdx = toParentIdx;
Data = new ActorManagerData(pluginInterface, gameData, language);
ActorIdentifier.Manager = this;
SignatureHelper.Initialise(this);
}
public unsafe ActorIdentifier GetCurrentPlayer()
{
var address = (Character*)_objects.GetObjectAddress(0);
return address == null
? ActorIdentifier.Invalid
: CreateIndividualUnchecked(IdentifierType.Player, new ByteString(address->GameObject.Name), address->HomeWorld,
ObjectKind.None, uint.MaxValue);
}
public ActorIdentifier GetInspectPlayer()
{
var addon = _gameGui.GetAddonByName("CharacterInspect", 1);
if (addon == IntPtr.Zero)
return ActorIdentifier.Invalid;
return CreatePlayer(InspectName, InspectWorldId);
}
public unsafe bool ResolvePartyBannerPlayer(ScreenActor type, out ActorIdentifier id)
{
id = ActorIdentifier.Invalid;
var module = Framework.Instance()->GetUiModule()->GetAgentModule();
if (module == null)
return false;
var agent = (AgentBannerInterface*)module->GetAgentByInternalId(AgentId.BannerParty);
if (agent == null || !agent->AgentInterface.IsAgentActive())
agent = (AgentBannerInterface*)module->GetAgentByInternalId(AgentId.BannerMIP);
if (agent == null || !agent->AgentInterface.IsAgentActive())
return false;
var idx = (ushort)type - (ushort)ScreenActor.CharacterScreen;
var character = agent->Character(idx);
if (character == null)
return true;
var name = new ByteString(character->Name1.StringPtr);
id = CreatePlayer(name, (ushort)character->WorldId);
return true;
}
private unsafe bool SearchPlayerCustomize(Character* character, int idx, out ActorIdentifier id)
{
var other = (Character*)_objects.GetObjectAddress(idx);
if (other == null || !CustomizeData.ScreenActorEquals((CustomizeData*)character->DrawData.CustomizeData.Data, (CustomizeData*)other->DrawData.CustomizeData.Data))
{
id = ActorIdentifier.Invalid;
return false;
}
id = FromObject(&other->GameObject, out _, false, true, false);
return true;
}
private unsafe ActorIdentifier SearchPlayersCustomize(Character* gameObject, int idx1, int idx2, int idx3)
=> SearchPlayerCustomize(gameObject, idx1, out var ret)
|| SearchPlayerCustomize(gameObject, idx2, out ret)
|| SearchPlayerCustomize(gameObject, idx3, out ret)
? ret
: ActorIdentifier.Invalid;
private unsafe ActorIdentifier SearchPlayersCustomize(Character* gameObject)
{
static bool Compare(Character* a, Character* b)
{
var data1 = (CustomizeData*)a->DrawData.CustomizeData.Data;
var data2 = (CustomizeData*)b->DrawData.CustomizeData.Data;
var equals = CustomizeData.ScreenActorEquals(data1, data2);
return equals;
}
for (var i = 0; i < (int)ScreenActor.CutsceneStart; i += 2)
{
var obj = (GameObject*)_objects.GetObjectAddress(i);
if (obj != null
&& obj->ObjectKind is (byte)ObjectKind.Player
&& Compare(gameObject, (Character*)obj))
return FromObject(obj, out _, false, true, false);
}
return ActorIdentifier.Invalid;
}
public unsafe bool ResolveMahjongPlayer(ScreenActor type, out ActorIdentifier id)
{
id = ActorIdentifier.Invalid;
if (_clientState.TerritoryType != 831 && _gameGui.GetAddonByName("EmjIntro") == IntPtr.Zero)
return false;
var obj = (Character*)_objects.GetObjectAddress((int)type);
if (obj == null)
return false;
id = type switch
{
ScreenActor.CharacterScreen => GetCurrentPlayer(),
ScreenActor.ExamineScreen => SearchPlayersCustomize(obj, 2, 4, 6),
ScreenActor.FittingRoom => SearchPlayersCustomize(obj, 4, 2, 6),
ScreenActor.DyePreview => SearchPlayersCustomize(obj, 6, 2, 4),
_ => ActorIdentifier.Invalid,
};
return true;
}
public unsafe bool ResolvePvPBannerPlayer(ScreenActor type, out ActorIdentifier id)
{
id = ActorIdentifier.Invalid;
if (!_clientState.IsPvPExcludingDen)
return false;
var addon = (AtkUnitBase*)_gameGui.GetAddonByName("PvPMap");
if (addon == null || addon->IsVisible)
return false;
var obj = (Character*)_objects.GetObjectAddress((int)type);
if (obj == null)
return false;
id = type switch
{
ScreenActor.CharacterScreen => SearchPlayersCustomize(obj),
ScreenActor.ExamineScreen => SearchPlayersCustomize(obj),
ScreenActor.FittingRoom => SearchPlayersCustomize(obj),
ScreenActor.DyePreview => SearchPlayersCustomize(obj),
ScreenActor.Portrait => SearchPlayersCustomize(obj),
_ => ActorIdentifier.Invalid,
};
return true;
}
public unsafe ActorIdentifier GetCardPlayer()
{
var agent = AgentCharaCard.Instance();
if (agent == null || agent->Data == null)
return ActorIdentifier.Invalid;
var worldId = *(ushort*)((byte*)agent->Data + Offsets.AgentCharaCardDataWorldId);
return CreatePlayer(new ByteString(agent->Data->Name.StringPtr), worldId);
}
public ActorIdentifier GetGlamourPlayer()
{
var addon = _gameGui.GetAddonByName("MiragePrismMiragePlate", 1);
return addon == IntPtr.Zero ? ActorIdentifier.Invalid : GetCurrentPlayer();
}
public void Dispose()
{
Data.Dispose();
if (ActorIdentifier.Manager == this)
ActorIdentifier.Manager = null;
}
~ActorManager()
=> Dispose();
private readonly Dalamud.Game.Framework _framework;
private readonly ObjectTable _objects;
private readonly ClientState _clientState;
private readonly GameGui _gameGui;
private readonly Func<ushort, short> _toParentIdx;
[Signature(Sigs.InspectTitleId, ScanType = ScanType.StaticAddress)]
private static unsafe ushort* _inspectTitleId = null!;
[Signature(Sigs.InspectWorldId, ScanType = ScanType.StaticAddress)]
private static unsafe ushort* _inspectWorldId = null!;
private static unsafe ushort InspectTitleId
=> *_inspectTitleId;
private static unsafe ByteString InspectName
=> new((byte*)(_inspectWorldId + 1));
private static unsafe ushort InspectWorldId
=> *_inspectWorldId;
public static readonly IReadOnlySet<uint> MannequinIds = new HashSet<uint>()
{
1026228u,
1026229u,
1026986u,
1026987u,
1026988u,
1026989u,
1032291u,
1032292u,
1032293u,
1032294u,
1033046u,
1033047u,
1033658u,
1033659u,
1007137u,
// TODO: Female Hrothgar
};
}

View file

@ -1,606 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.Types;
using Newtonsoft.Json.Linq;
using Penumbra.String;
using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character;
namespace Penumbra.GameData.Actors;
public partial class ActorManager
{
/// <summary>
/// Try to create an ActorIdentifier from a already parsed JObject <paramref name="data"/>.
/// </summary>
/// <param name="data">A parsed JObject</param>
/// <returns>ActorIdentifier.Invalid if the JObject can not be converted, a valid ActorIdentifier otherwise.</returns>
public ActorIdentifier FromJson(JObject? data)
{
if (data == null)
return ActorIdentifier.Invalid;
var type = data[nameof(ActorIdentifier.Type)]?.ToObject<IdentifierType>() ?? IdentifierType.Invalid;
switch (type)
{
case IdentifierType.Player:
{
var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.ToObject<string>(), false);
var homeWorld = data[nameof(ActorIdentifier.HomeWorld)]?.ToObject<ushort>() ?? 0;
return CreatePlayer(name, homeWorld);
}
case IdentifierType.Retainer:
{
var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.ToObject<string>(), false);
return CreateRetainer(name, 0);
}
case IdentifierType.Owned:
{
var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.ToObject<string>(), false);
var homeWorld = data[nameof(ActorIdentifier.HomeWorld)]?.ToObject<ushort>() ?? 0;
var kind = data[nameof(ActorIdentifier.Kind)]?.ToObject<ObjectKind>() ?? ObjectKind.CardStand;
var dataId = data[nameof(ActorIdentifier.DataId)]?.ToObject<uint>() ?? 0;
return CreateOwned(name, homeWorld, kind, dataId);
}
case IdentifierType.Special:
{
var special = data[nameof(ActorIdentifier.Special)]?.ToObject<ScreenActor>() ?? 0;
return CreateSpecial(special);
}
case IdentifierType.Npc:
{
var index = data[nameof(ActorIdentifier.Index)]?.ToObject<ushort>() ?? ushort.MaxValue;
var kind = data[nameof(ActorIdentifier.Kind)]?.ToObject<ObjectKind>() ?? ObjectKind.CardStand;
var dataId = data[nameof(ActorIdentifier.DataId)]?.ToObject<uint>() ?? 0;
return CreateNpc(kind, dataId, index);
}
case IdentifierType.UnkObject:
{
var index = data[nameof(ActorIdentifier.Index)]?.ToObject<ushort>() ?? ushort.MaxValue;
var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.ToObject<string>(), false);
return CreateIndividualUnchecked(IdentifierType.UnkObject, name, index, ObjectKind.None, 0);
}
default: return ActorIdentifier.Invalid;
}
}
/// <summary>
/// Use stored data to convert an ActorIdentifier to a string.
/// </summary>
public string ToString(ActorIdentifier id)
{
return id.Type switch
{
IdentifierType.Player => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id
? $"{id.PlayerName} ({Data.ToWorldName(id.HomeWorld)})"
: id.PlayerName.ToString(),
IdentifierType.Retainer => id.PlayerName.ToString(),
IdentifierType.Owned => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id
? $"{id.PlayerName} ({Data.ToWorldName(id.HomeWorld)})'s {Data.ToName(id.Kind, id.DataId)}"
: $"{id.PlayerName}s {Data.ToName(id.Kind, id.DataId)}",
IdentifierType.Special => id.Special.ToName(),
IdentifierType.Npc =>
id.Index == ushort.MaxValue
? Data.ToName(id.Kind, id.DataId)
: $"{Data.ToName(id.Kind, id.DataId)} at {id.Index}",
IdentifierType.UnkObject => id.PlayerName.IsEmpty
? $"Unknown Object at {id.Index}"
: $"{id.PlayerName} at {id.Index}",
_ => "Invalid",
};
}
/// <summary>
/// Use stored data to convert an ActorIdentifier to a name only.
/// </summary>
public string ToName(ActorIdentifier id)
{
return id.Type switch
{
IdentifierType.Player => id.PlayerName.ToString(),
IdentifierType.Retainer => id.PlayerName.ToString(),
IdentifierType.Owned => $"{id.PlayerName}s {Data.ToName(id.Kind, id.DataId)}",
IdentifierType.Special => id.Special.ToName(),
IdentifierType.Npc => Data.ToName(id.Kind, id.DataId),
IdentifierType.UnkObject => id.PlayerName.IsEmpty ? id.PlayerName.ToString() : "Unknown Object",
_ => "Invalid",
};
}
private unsafe FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* HandleCutscene(
FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* main)
{
if (main == null)
return null;
if (main->ObjectIndex is >= (ushort)ScreenActor.CutsceneStart and < (ushort)ScreenActor.CutsceneEnd)
{
var parentIdx = _toParentIdx(main->ObjectIndex);
if (parentIdx >= 0)
return (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_objects.GetObjectAddress(parentIdx);
}
return main;
}
public class IdentifierParseError : Exception
{
public IdentifierParseError(string reason)
: base(reason)
{ }
}
public ActorIdentifier FromUserString(string userString)
{
if (userString.Length == 0)
throw new IdentifierParseError("The identifier string was empty.");
var split = userString.Split('|', 3, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
if (split.Length < 2)
throw new IdentifierParseError($"The identifier string {userString} does not contain a type and a value.");
IdentifierType type;
var playerName = ByteString.Empty;
ushort worldId = 0;
var kind = ObjectKind.Player;
var objectId = 0u;
(ByteString, ushort) ParsePlayer(string player)
{
var parts = player.Split('@', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
if (!VerifyPlayerName(parts[0]))
throw new IdentifierParseError($"{parts[0]} is not a valid player name.");
if (!ByteString.FromString(parts[0], out var p))
throw new IdentifierParseError($"The player string {parts[0]} contains invalid symbols.");
var world = parts.Length == 2
? Data.ToWorldId(parts[1])
: ushort.MaxValue;
if (!VerifyWorld(world))
throw new IdentifierParseError($"{parts[1]} is not a valid world name.");
return (p, world);
}
(ObjectKind, uint) ParseNpc(string npc)
{
var split = npc.Split(':', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
if (split.Length != 2)
throw new IdentifierParseError("NPCs need to be specified by '[Object Type]:[NPC Name]'.");
static bool FindDataId(string name, IReadOnlyDictionary<uint, string> data, out uint dataId)
{
var kvp = data.FirstOrDefault(kvp => kvp.Value.Equals(name, StringComparison.OrdinalIgnoreCase),
new KeyValuePair<uint, string>(uint.MaxValue, string.Empty));
dataId = kvp.Key;
return kvp.Value.Length > 0;
}
switch (split[0].ToLowerInvariant())
{
case "m":
case "mount":
return FindDataId(split[1], Data.Mounts, out var id)
? (ObjectKind.MountType, id)
: throw new IdentifierParseError($"Could not identify a Mount named {split[1]}.");
case "c":
case "companion":
case "minion":
case "mini":
return FindDataId(split[1], Data.Companions, out id)
? (ObjectKind.Companion, id)
: throw new IdentifierParseError($"Could not identify a Minion named {split[1]}.");
case "a":
case "o":
case "accessory":
case "ornament":
return FindDataId(split[1], Data.Ornaments, out id)
? (ObjectKind.Ornament, id)
: throw new IdentifierParseError($"Could not identify an Accessory named {split[1]}.");
case "e":
case "enpc":
case "eventnpc":
case "event npc":
return FindDataId(split[1], Data.ENpcs, out id)
? (ObjectKind.EventNpc, id)
: throw new IdentifierParseError($"Could not identify an Event NPC named {split[1]}.");
case "b":
case "bnpc":
case "battlenpc":
case "battle npc":
return FindDataId(split[1], Data.BNpcs, out id)
? (ObjectKind.BattleNpc, id)
: throw new IdentifierParseError($"Could not identify a Battle NPC named {split[1]}.");
default: throw new IdentifierParseError($"The argument {split[0]} is not a valid NPC Type.");
}
}
switch (split[0].ToLowerInvariant())
{
case "p":
case "player":
type = IdentifierType.Player;
(playerName, worldId) = ParsePlayer(split[1]);
break;
case "r":
case "retainer":
type = IdentifierType.Retainer;
if (!VerifyRetainerName(split[1]))
throw new IdentifierParseError($"{split[1]} is not a valid player name.");
if (!ByteString.FromString(split[1], out playerName))
throw new IdentifierParseError($"The retainer string {split[1]} contains invalid symbols.");
break;
case "n":
case "npc":
type = IdentifierType.Npc;
(kind, objectId) = ParseNpc(split[1]);
break;
case "o":
case "owned":
if (split.Length < 3)
throw new IdentifierParseError(
"Owned NPCs need a NPC and a player, separated by '|', but only one was provided.");
type = IdentifierType.Owned;
(kind, objectId) = ParseNpc(split[1]);
(playerName, worldId) = ParsePlayer(split[2]);
break;
default:
throw new IdentifierParseError(
$"{split[0]} is not a valid identifier type. Valid types are [P]layer, [R]etainer, [N]PC, or [O]wned");
}
return CreateIndividualUnchecked(type, playerName, worldId, kind, objectId);
}
/// <summary>
/// Compute an ActorIdentifier from a GameObject. If check is true, the values are checked for validity.
/// </summary>
public unsafe ActorIdentifier FromObject(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* actor,
out FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* owner, bool allowPlayerNpc, bool check, bool withoutIndex)
{
owner = null;
if (actor == null)
return ActorIdentifier.Invalid;
actor = HandleCutscene(actor);
var idx = actor->ObjectIndex;
if (idx is >= (ushort)ScreenActor.CharacterScreen and <= (ushort)ScreenActor.Card8)
return CreateIndividualUnchecked(IdentifierType.Special, ByteString.Empty, idx, ObjectKind.None, uint.MaxValue);
var kind = (ObjectKind)actor->ObjectKind;
switch (kind)
{
case ObjectKind.Player:
{
var name = new ByteString(actor->Name);
var homeWorld = ((Character*)actor)->HomeWorld;
return check
? CreatePlayer(name, homeWorld)
: CreateIndividualUnchecked(IdentifierType.Player, name, homeWorld, ObjectKind.None, uint.MaxValue);
}
case ObjectKind.BattleNpc:
{
var ownerId = actor->OwnerID;
// 952 -> 780 is a special case for chocobos because they have NameId == 0 otherwise.
var nameId = actor->DataID == 952 ? 780 : ((Character*)actor)->NameID;
if (ownerId != 0xE0000000)
{
owner = HandleCutscene(
(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)(_objects.SearchById(ownerId)?.Address ?? IntPtr.Zero));
if (owner == null)
return ActorIdentifier.Invalid;
var name = new ByteString(owner->Name);
var homeWorld = ((Character*)owner)->HomeWorld;
return check
? CreateOwned(name, homeWorld, ObjectKind.BattleNpc, nameId)
: CreateIndividualUnchecked(IdentifierType.Owned, name, homeWorld, ObjectKind.BattleNpc, nameId);
}
// Hack to support Anamnesis changing ObjectKind for NPC faces.
if (nameId == 0 && allowPlayerNpc)
{
var name = new ByteString(actor->Name);
if (!name.IsEmpty)
{
var homeWorld = ((Character*)actor)->HomeWorld;
return check
? CreatePlayer(name, homeWorld)
: CreateIndividualUnchecked(IdentifierType.Player, name, homeWorld, ObjectKind.None, uint.MaxValue);
}
}
var index = withoutIndex ? ushort.MaxValue : actor->ObjectIndex;
return check
? CreateNpc(ObjectKind.BattleNpc, nameId, index)
: CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty, index, ObjectKind.BattleNpc, nameId);
}
case ObjectKind.EventNpc:
{
var dataId = actor->DataID;
// Special case for squadron that is also in the game functions, cf. E8 ?? ?? ?? ?? 89 87 ?? ?? ?? ?? 4C 89 BF
if (dataId == 0xF845D)
dataId = actor->GetNpcID();
if (MannequinIds.Contains(dataId))
{
static ByteString Get(byte* ptr)
=> ptr == null ? ByteString.Empty : new ByteString(ptr);
var retainerName = Get(actor->Name);
var actualName = _framework.IsInFrameworkUpdateThread ? Get(actor->GetName()) : ByteString.Empty;
if (!actualName.Equals(retainerName))
{
var ident = check
? CreateRetainer(retainerName, ActorIdentifier.RetainerType.Mannequin)
: CreateIndividualUnchecked(IdentifierType.Retainer, retainerName, (ushort)ActorIdentifier.RetainerType.Mannequin,
ObjectKind.EventNpc, dataId);
if (ident.IsValid)
return ident;
}
}
var index = withoutIndex ? ushort.MaxValue : actor->ObjectIndex;
return check
? CreateNpc(ObjectKind.EventNpc, dataId, index)
: CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty, index, ObjectKind.EventNpc, dataId);
}
case ObjectKind.MountType:
case ObjectKind.Companion:
case ObjectKind.Ornament:
{
owner = HandleCutscene(
(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_objects.GetObjectAddress(actor->ObjectIndex - 1));
if (owner == null)
return ActorIdentifier.Invalid;
var dataId = GetCompanionId(actor, (Character*)owner);
var name = new ByteString(owner->Name);
var homeWorld = ((Character*)owner)->HomeWorld;
return check
? CreateOwned(name, homeWorld, kind, dataId)
: CreateIndividualUnchecked(IdentifierType.Owned, name, homeWorld, kind, dataId);
}
case ObjectKind.Retainer:
{
var name = new ByteString(actor->Name);
return check
? CreateRetainer(name, ActorIdentifier.RetainerType.Bell)
: CreateIndividualUnchecked(IdentifierType.Retainer, name, (ushort)ActorIdentifier.RetainerType.Bell, ObjectKind.None,
uint.MaxValue);
}
default:
{
var name = new ByteString(actor->Name);
var index = withoutIndex ? ushort.MaxValue : actor->ObjectIndex;
return CreateIndividualUnchecked(IdentifierType.UnkObject, name, index, ObjectKind.None, 0);
}
}
}
/// <summary>
/// Obtain the current companion ID for an object by its actor and owner.
/// </summary>
private unsafe uint GetCompanionId(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* actor,
Character* owner)
{
return (ObjectKind)actor->ObjectKind switch
{
ObjectKind.MountType => owner->Mount.MountId,
ObjectKind.Ornament => owner->Ornament.OrnamentId,
ObjectKind.Companion => actor->DataID,
_ => actor->DataID,
};
}
public unsafe ActorIdentifier FromObject(GameObject? actor, out FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* owner,
bool allowPlayerNpc, bool check, bool withoutIndex)
=> FromObject((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)(actor?.Address ?? IntPtr.Zero), out owner, allowPlayerNpc,
check, withoutIndex);
public unsafe ActorIdentifier FromObject(GameObject? actor, bool allowPlayerNpc, bool check, bool withoutIndex)
=> FromObject(actor, out _, allowPlayerNpc, check, withoutIndex);
public ActorIdentifier CreateIndividual(IdentifierType type, ByteString name, ushort homeWorld, ObjectKind kind, uint dataId)
=> type switch
{
IdentifierType.Player => CreatePlayer(name, homeWorld),
IdentifierType.Retainer => CreateRetainer(name, (ActorIdentifier.RetainerType)homeWorld),
IdentifierType.Owned => CreateOwned(name, homeWorld, kind, dataId),
IdentifierType.Special => CreateSpecial((ScreenActor)homeWorld),
IdentifierType.Npc => CreateNpc(kind, dataId, homeWorld),
IdentifierType.UnkObject => CreateIndividualUnchecked(IdentifierType.UnkObject, name, homeWorld, ObjectKind.None, 0),
_ => ActorIdentifier.Invalid,
};
/// <summary>
/// Only use this if you are sure the input is valid.
/// </summary>
public ActorIdentifier CreateIndividualUnchecked(IdentifierType type, ByteString name, ushort homeWorld, ObjectKind kind, uint dataId)
=> new(type, kind, homeWorld, dataId, name);
public ActorIdentifier CreatePlayer(ByteString name, ushort homeWorld)
{
if (!VerifyWorld(homeWorld) || !VerifyPlayerName(name.Span))
return ActorIdentifier.Invalid;
return new ActorIdentifier(IdentifierType.Player, ObjectKind.Player, homeWorld, 0, name);
}
public ActorIdentifier CreateRetainer(ByteString name, ActorIdentifier.RetainerType type)
{
if (!VerifyRetainerName(name.Span))
return ActorIdentifier.Invalid;
return new ActorIdentifier(IdentifierType.Retainer, ObjectKind.Retainer, (ushort)type, 0, name);
}
public ActorIdentifier CreateSpecial(ScreenActor actor)
{
if (!VerifySpecial(actor))
return ActorIdentifier.Invalid;
return new ActorIdentifier(IdentifierType.Special, ObjectKind.Player, (ushort)actor, 0, ByteString.Empty);
}
public ActorIdentifier CreateNpc(ObjectKind kind, uint data, ushort index = ushort.MaxValue)
{
if (!VerifyIndex(index) || !VerifyNpcData(kind, data))
return ActorIdentifier.Invalid;
return new ActorIdentifier(IdentifierType.Npc, kind, index, data, ByteString.Empty);
}
public ActorIdentifier CreateOwned(ByteString ownerName, ushort homeWorld, ObjectKind kind, uint dataId)
{
if (!VerifyWorld(homeWorld) || !VerifyPlayerName(ownerName.Span) || !VerifyOwnedData(kind, dataId))
return ActorIdentifier.Invalid;
return new ActorIdentifier(IdentifierType.Owned, kind, homeWorld, dataId, ownerName);
}
/// <summary> Checks SE naming rules. </summary>
public static bool VerifyPlayerName(ReadOnlySpan<byte> name)
{
// Total no more than 20 characters + space.
if (name.Length is < 5 or > 21)
return false;
// Forename and surname, no more spaces.
var splitIndex = name.IndexOf((byte)' ');
if (splitIndex < 0 || name[(splitIndex + 1)..].IndexOf((byte)' ') >= 0)
return false;
return CheckNamePart(name[..splitIndex], 2, 15) && CheckNamePart(name[(splitIndex + 1)..], 2, 15);
}
/// <summary> Checks SE naming rules. </summary>
public static bool VerifyPlayerName(ReadOnlySpan<char> name)
{
// Total no more than 20 characters + space.
if (name.Length is < 5 or > 21)
return false;
// Forename and surname, no more spaces.
var splitIndex = name.IndexOf(' ');
if (splitIndex < 0 || name[(splitIndex + 1)..].IndexOf(' ') >= 0)
return false;
return CheckNamePart(name[..splitIndex], 2, 15) && CheckNamePart(name[(splitIndex + 1)..], 2, 15);
}
/// <summary> Checks SE naming rules. </summary>
public static bool VerifyRetainerName(ReadOnlySpan<byte> name)
=> CheckNamePart(name, 3, 20);
/// <summary> Checks SE naming rules. </summary>
public static bool VerifyRetainerName(ReadOnlySpan<char> name)
=> CheckNamePart(name, 3, 20);
private static bool CheckNamePart(ReadOnlySpan<char> part, int minLength, int maxLength)
{
// Each name part at least 2 and at most 15 characters for players, and at least 3 and at most 20 characters for retainers.
if (part.Length < minLength || part.Length > maxLength)
return false;
// Each part starting with capitalized letter.
if (part[0] is < 'A' or > 'Z')
return false;
// Every other symbol needs to be lowercase letter, hyphen or apostrophe.
var last = '\0';
for (var i = 1; i < part.Length; ++i)
{
var current = part[i];
if (current is not ('\'' or '-' or (>= 'a' and <= 'z')))
return false;
// Hyphens can not be used in succession, after or before apostrophes or as the last symbol.
if (last is '\'' && current is '-')
return false;
if (last is '-' && current is '-' or '\'')
return false;
last = current;
}
return true;
}
private static bool CheckNamePart(ReadOnlySpan<byte> part, int minLength, int maxLength)
{
// Each name part at least 2 and at most 15 characters for players, and at least 3 and at most 20 characters for retainers.
if (part.Length < minLength || part.Length > maxLength)
return false;
// Each part starting with capitalized letter.
if (part[0] is < (byte)'A' or > (byte)'Z')
return false;
// Every other symbol needs to be lowercase letter, hyphen or apostrophe.
var last = (byte)'\0';
for (var i = 1; i < part.Length; ++i)
{
var current = part[i];
if (current is not ((byte)'\'' or (byte)'-' or (>= (byte)'a' and <= (byte)'z')))
return false;
// Hyphens can not be used in succession, after or before apostrophes or as the last symbol.
if (last is (byte)'\'' && current is (byte)'-')
return false;
if (last is (byte)'-' && current is (byte)'-' or (byte)'\'')
return false;
last = current;
}
return true;
}
/// <summary> Checks if the world is a valid public world or ushort.MaxValue (any world). </summary>
public bool VerifyWorld(ushort worldId)
=> worldId == ushort.MaxValue || Data.Worlds.ContainsKey(worldId);
/// <summary> Verify that the enum value is a specific actor and return the name if it is. </summary>
public static bool VerifySpecial(ScreenActor actor)
=> actor is >= ScreenActor.CharacterScreen and <= ScreenActor.Card8;
/// <summary> Verify that the object index is a valid index for an NPC. </summary>
public bool VerifyIndex(ushort index)
{
return index switch
{
ushort.MaxValue => true,
< 200 => index % 2 == 0,
> (ushort)ScreenActor.Card8 => index < _objects.Length,
_ => false,
};
}
/// <summary> Verify that the object kind is a valid owned object, and the corresponding data Id. </summary>
public bool VerifyOwnedData(ObjectKind kind, uint dataId)
{
return kind switch
{
ObjectKind.MountType => Data.Mounts.ContainsKey(dataId),
ObjectKind.Companion => Data.Companions.ContainsKey(dataId),
ObjectKind.Ornament => Data.Ornaments.ContainsKey(dataId),
ObjectKind.BattleNpc => Data.BNpcs.ContainsKey(dataId),
_ => false,
};
}
public bool VerifyNpcData(ObjectKind kind, uint dataId)
=> kind switch
{
ObjectKind.MountType => Data.Mounts.ContainsKey(dataId),
ObjectKind.Companion => Data.Companions.ContainsKey(dataId),
ObjectKind.Ornament => Data.Ornaments.ContainsKey(dataId),
ObjectKind.BattleNpc => Data.BNpcs.ContainsKey(dataId),
ObjectKind.EventNpc => Data.ENpcs.ContainsKey(dataId),
_ => false,
};
}

View file

@ -1,91 +0,0 @@
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
namespace Penumbra;
[StructLayout( LayoutKind.Explicit )]
public unsafe struct AgentBannerInterface
{
[FieldOffset( 0x0 )] public AgentInterface AgentInterface;
[FieldOffset( 0x28 )] public BannerInterfaceStorage* Data;
public BannerInterfaceStorage.CharacterData* Character( int idx )
=> idx switch
{
_ when Data == null => null,
0 => &Data->Character1,
1 => &Data->Character2,
2 => &Data->Character3,
3 => &Data->Character4,
4 => &Data->Character5,
5 => &Data->Character6,
6 => &Data->Character7,
7 => &Data->Character8,
_ => null,
};
}
[StructLayout(LayoutKind.Explicit)]
public unsafe struct AgentBannerParty
{
public static AgentBannerParty* Instance() => ( AgentBannerParty* )Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId( AgentId.BannerParty );
[FieldOffset( 0x0 )] public AgentBannerInterface AgentBannerInterface;
}
[StructLayout( LayoutKind.Explicit )]
public unsafe struct AgentBannerMIP
{
public static AgentBannerMIP* Instance() => ( AgentBannerMIP* )Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId( AgentId.BannerMIP );
[FieldOffset( 0x0 )] public AgentBannerInterface AgentBannerInterface;
}
// Client::UI::Agent::AgentBannerInterface::Storage
// destructed in Client::UI::Agent::AgentBannerInterface::dtor
[StructLayout( LayoutKind.Explicit, Size = 0x3B30 )]
public unsafe struct BannerInterfaceStorage
{
// vtable: 48 8D 05 ?? ?? ?? ?? 48 89 01 48 8B F9 7E
// dtor: E8 ?? ?? ?? ?? 48 83 EF ?? 75 ?? BA ?? ?? ?? ?? 48 8B CE E8 ?? ?? ?? ?? 48 89 7D
[StructLayout( LayoutKind.Explicit, Size = 0x760 )]
public struct CharacterData
{
[FieldOffset( 0x000 )] public void** VTable;
[FieldOffset( 0x018 )] public Utf8String Name1;
[FieldOffset( 0x080 )] public Utf8String Name2;
[FieldOffset( 0x0E8 )] public Utf8String UnkString1;
[FieldOffset( 0x150 )] public Utf8String UnkString2;
[FieldOffset( 0x1C0 )] public Utf8String Job;
[FieldOffset( 0x238 )] public uint WorldId;
[FieldOffset( 0x240 )] public Utf8String UnkString3;
[FieldOffset( 0x2B0 )] public void* CharaView;
[FieldOffset( 0x5D0 )] public AtkTexture AtkTexture;
[FieldOffset( 0x6E0 )] public Utf8String Title;
[FieldOffset( 0x750 )] public void* SomePointer;
}
[FieldOffset( 0x0000 )] public void* Agent; // AgentBannerParty, maybe other Banner agents
[FieldOffset( 0x0008 )] public UIModule* UiModule;
[FieldOffset( 0x0010 )] public uint Unk1; // Maybe count or bitfield, but probably not
[FieldOffset( 0x0014 )] public uint Unk2;
[FieldOffset( 0x0020 )] public CharacterData Character1;
[FieldOffset( 0x0780 )] public CharacterData Character2;
[FieldOffset( 0x0EE0 )] public CharacterData Character3;
[FieldOffset( 0x1640 )] public CharacterData Character4;
[FieldOffset( 0x1DA0 )] public CharacterData Character5;
[FieldOffset( 0x2500 )] public CharacterData Character6;
[FieldOffset( 0x2C60 )] public CharacterData Character7;
[FieldOffset( 0x33C0 )] public CharacterData Character8;
[FieldOffset( 0x3B20 )] public long Unk3;
[FieldOffset( 0x3B28 )] public long Unk4;
}

View file

@ -1,12 +0,0 @@
namespace Penumbra.GameData.Actors;
public enum IdentifierType : byte
{
Invalid,
Player,
Owned,
Special,
Npc,
Retainer,
UnkObject,
};

View file

@ -1,17 +0,0 @@
namespace Penumbra.GameData.Actors;
public enum ScreenActor : ushort
{
CutsceneStart = 200,
GPosePlayer = 201,
CutsceneEnd = 240,
CharacterScreen = CutsceneEnd,
ExamineScreen = 241,
FittingRoom = 242,
DyePreview = 243,
Portrait = 244,
Card6 = 245,
Card7 = 246,
Card8 = 247,
ScreenEnd = Card8 + 1,
}

File diff suppressed because it is too large Load diff

View file

@ -1,92 +0,0 @@
using System;
using System.Threading.Tasks;
using Dalamud;
using Dalamud.Logging;
using Dalamud.Plugin;
namespace Penumbra.GameData.Data;
/// <summary>
/// A container base class that shares data through Dalamud but cares about the used language and version.
/// Inheritors should dispose their Dalamud Shares in DisposeInternal via DisposeTag and add them in their constructor via TryCatchData.
/// </summary>
public abstract class DataSharer : IDisposable
{
protected readonly DalamudPluginInterface PluginInterface;
protected readonly int Version;
protected readonly ClientLanguage Language;
private bool _disposed;
protected DataSharer(DalamudPluginInterface pluginInterface, ClientLanguage language, int version)
{
PluginInterface = pluginInterface;
Language = language;
Version = version;
}
protected virtual void DisposeInternal()
{ }
public void Dispose()
{
if (_disposed)
return;
DisposeInternal();
GC.SuppressFinalize(this);
_disposed = true;
}
~DataSharer()
=> Dispose();
protected void DisposeTag(string tag)
=> PluginInterface.RelinquishData(GetVersionedTag(tag, Language, Version));
protected T TryCatchData<T>(string tag, Func<T> func) where T : class
{
try
{
return PluginInterface.GetOrCreateData(GetVersionedTag(tag, Language, Version), func);
}
catch (Exception ex)
{
PluginLog.Error($"Error creating shared data for {tag}:\n{ex}");
return func();
}
}
protected Task<T> TryCatchDataAsync<T>(string tag, Action<T> fill) where T : class, new()
{
tag = GetVersionedTag(tag, Language, Version);
if (PluginInterface.TryGetData<T>(tag, out var data))
return Task.FromResult(data);
T ret = new();
return Task.Run(() =>
{
fill(ret);
return ret;
});
}
public static void DisposeTag(DalamudPluginInterface pi, string tag, ClientLanguage language, int version)
=> pi.RelinquishData(GetVersionedTag(tag, language, version));
public static T TryCatchData<T>(DalamudPluginInterface pi, string tag, ClientLanguage language, int version, Func<T> func)
where T : class
{
try
{
return pi.GetOrCreateData(GetVersionedTag(tag, language, version), func);
}
catch (Exception ex)
{
PluginLog.Error($"Error creating shared actor data for {tag}:\n{ex}");
return func();
}
}
private static string GetVersionedTag(string tag, ClientLanguage language, int version)
=> $"Penumbra.GameData.{tag}.{language}.V{version}";
}

View file

@ -1,463 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Penumbra.GameData.Interop;
using Penumbra.String;
namespace Penumbra.GameData.Data;
public partial class DisassembledShader
{
public struct ResourceBinding
{
public string Name;
public ResourceType Type;
public Format Format;
public ResourceDimension Dimension;
public uint Slot;
public uint Elements;
public uint RegisterCount;
public VectorComponents[] Used;
public VectorComponents UsedDynamically;
}
// Abbreviated using the uppercased first char of their name
public enum ResourceType : byte
{
Unspecified = 0,
ConstantBuffer = 0x43, // 'C'
Sampler = 0x53, // 'S'
Texture = 0x54, // 'T'
Uav = 0x55, // 'U'
}
// Abbreviated using the uppercased first and last char of their name
public enum Format : ushort
{
Unspecified = 0,
NotApplicable = 0x4E41, // 'NA'
Int = 0x4954, // 'IT'
Int4 = 0x4934, // 'I4'
Float = 0x4654, // 'FT'
Float4 = 0x4634, // 'F4'
}
// Abbreviated using the uppercased first and last char of their name
public enum ResourceDimension : ushort
{
Unspecified = 0,
NotApplicable = 0x4E41, // 'NA'
TwoD = 0x3244, // '2D'
ThreeD = 0x3344, // '3D'
Cube = 0x4345, // 'CE'
}
public struct InputOutput
{
public string Name;
public uint Index;
public VectorComponents Mask;
public uint Register;
public string SystemValue;
public Format Format;
public VectorComponents Used;
}
[Flags]
public enum VectorComponents : byte
{
X = 1,
Y = 2,
Z = 4,
W = 8,
All = 15,
}
public enum ShaderStage : byte
{
Unspecified = 0,
Pixel = 0x50, // 'P'
Vertex = 0x56, // 'V'
}
[GeneratedRegex(@"\s(\w+)(?:\[\d+\])?;\s*//\s*Offset:\s*0\s*Size:\s*(\d+)$", RegexOptions.Multiline | RegexOptions.NonBacktracking)]
private static partial Regex ResourceBindingSizeRegex();
[GeneratedRegex(@"c(\d+)(?:\[([^\]]+)\])?(?:\.([wxyz]+))?", RegexOptions.NonBacktracking)]
private static partial Regex Sm3ConstantBufferUsageRegex();
[GeneratedRegex(@"^\s*texld\S*\s+[^,]+,[^,]+,\s*s(\d+)", RegexOptions.NonBacktracking)]
private static partial Regex Sm3TextureUsageRegex();
[GeneratedRegex(@"cb(\d+)\[([^\]]+)\]\.([wxyz]+)", RegexOptions.NonBacktracking)]
private static partial Regex Sm5ConstantBufferUsageRegex();
[GeneratedRegex(@"^\s*sample_\S*\s+[^.]+\.([wxyz]+),[^,]+,\s*t(\d+)\.([wxyz]+)", RegexOptions.NonBacktracking)]
private static partial Regex Sm5TextureUsageRegex();
private static readonly char[] Digits = Enumerable.Range(0, 10).Select(c => (char)('0' + c)).ToArray();
public readonly ByteString RawDisassembly;
public readonly uint ShaderModel;
public readonly ShaderStage Stage;
public readonly string BufferDefinitions;
public readonly ResourceBinding[] ResourceBindings;
public readonly InputOutput[] InputSignature;
public readonly InputOutput[] OutputSignature;
public readonly IReadOnlyList<ByteString> Instructions;
public DisassembledShader(ByteString rawDisassembly)
{
RawDisassembly = rawDisassembly;
var lines = rawDisassembly.Split((byte) '\n');
Instructions = lines.FindAll(ln => !ln.StartsWith("//"u8) && ln.Length > 0);
var shaderModel = Instructions[0].Trim().Split((byte) '_');
Stage = (ShaderStage)(byte)char.ToUpper((char) shaderModel[0][0]);
ShaderModel = (uint.Parse(shaderModel[1].ToString()) << 8) | uint.Parse(shaderModel[2].ToString());
var header = PreParseHeader(lines.Take(lines.IndexOf(Instructions[0])).Select(l => l.ToString()).ToArray());
switch (ShaderModel >> 8)
{
case 3:
ParseSm3Header(header, out BufferDefinitions, out ResourceBindings, out InputSignature, out OutputSignature);
ParseSm3ResourceUsage(Instructions, ResourceBindings);
break;
case 5:
ParseSm5Header(header, out BufferDefinitions, out ResourceBindings, out InputSignature, out OutputSignature);
ParseSm5ResourceUsage(Instructions, ResourceBindings);
break;
default: throw new NotImplementedException();
}
}
public ResourceBinding? GetResourceBindingByName(ResourceType type, string name)
=> ResourceBindings.FirstOrNull(b => b.Type == type && b.Name == name);
public ResourceBinding? GetResourceBindingBySlot(ResourceType type, uint slot)
=> ResourceBindings.FirstOrNull(b => b.Type == type && b.Slot == slot);
public static DisassembledShader Disassemble(ReadOnlySpan<byte> shaderBlob)
=> new(D3DCompiler.Disassemble(shaderBlob));
private static void ParseSm3Header(Dictionary<string, string[]> header, out string bufferDefinitions,
out ResourceBinding[] resourceBindings, out InputOutput[] inputSignature, out InputOutput[] outputSignature)
{
bufferDefinitions = header.TryGetValue("Parameters", out var rawParameters)
? string.Join('\n', rawParameters)
: string.Empty;
if (header.TryGetValue("Registers", out var rawRegisters))
{
var (_, registers) = ParseTable(rawRegisters);
resourceBindings = Array.ConvertAll(registers, register =>
{
var type = (ResourceType)(byte)char.ToUpper(register[1][0]);
if (type == ResourceType.Sampler)
type = ResourceType.Texture;
var size = uint.Parse(register[2]);
return new ResourceBinding
{
Name = register[0],
Type = type,
Format = Format.Unspecified,
Dimension = ResourceDimension.Unspecified,
Slot = uint.Parse(register[1][1..]),
Elements = 1,
RegisterCount = size,
Used = new VectorComponents[size],
};
});
}
else
{
resourceBindings = Array.Empty<ResourceBinding>();
}
inputSignature = Array.Empty<InputOutput>();
outputSignature = Array.Empty<InputOutput>();
}
private static void ParseSm3ResourceUsage(IReadOnlyList<ByteString> instructions, ResourceBinding[] resourceBindings)
{
var cbIndices = new Dictionary<uint, int>();
var tIndices = new Dictionary<uint, int>();
{
var i = 0;
foreach (var binding in resourceBindings)
{
switch (binding.Type)
{
case ResourceType.ConstantBuffer:
for (var j = 0u; j < binding.RegisterCount; j++)
cbIndices[binding.Slot + j] = i;
break;
case ResourceType.Texture:
tIndices[binding.Slot] = i;
break;
}
++i;
}
}
foreach (var instruction in instructions)
{
var trimmed = instruction.Trim();
if (trimmed.StartsWith("def"u8) || trimmed.StartsWith("dcl"u8))
continue;
var instructionString = instruction.ToString();
foreach (Match cbMatch in Sm3ConstantBufferUsageRegex().Matches(instructionString))
{
var buffer = uint.Parse(cbMatch.Groups[1].Value);
if (cbIndices.TryGetValue(buffer, out var i))
{
var swizzle = cbMatch.Groups[3].Success ? ParseVectorComponents(cbMatch.Groups[3].Value) : VectorComponents.All;
if (cbMatch.Groups[2].Success)
resourceBindings[i].UsedDynamically |= swizzle;
else
resourceBindings[i].Used[buffer - resourceBindings[i].Slot] |= swizzle;
}
}
var tMatch = Sm3TextureUsageRegex().Match(instructionString);
if (tMatch.Success)
{
var texture = uint.Parse(tMatch.Groups[1].Value);
if (tIndices.TryGetValue(texture, out var i))
resourceBindings[i].Used[0] = VectorComponents.All;
}
}
}
private static void ParseSm5Header(Dictionary<string, string[]> header, out string bufferDefinitions,
out ResourceBinding[] resourceBindings, out InputOutput[] inputSignature, out InputOutput[] outputSignature)
{
if (header.TryGetValue("Resource Bindings", out var rawResBindings))
{
var (head, resBindings) = ParseTable(rawResBindings);
resourceBindings = Array.ConvertAll(resBindings, binding =>
{
var type = (ResourceType)(byte)char.ToUpper(binding[1][0]);
return new ResourceBinding
{
Name = binding[0],
Type = type,
Format = (Format)(((byte)char.ToUpper(binding[2][0]) << 8) | (byte)char.ToUpper(binding[2][^1])),
Dimension = (ResourceDimension)(((byte)char.ToUpper(binding[3][0]) << 8) | (byte)char.ToUpper(binding[3][^1])),
Slot = uint.Parse(binding[4][binding[4].IndexOfAny(Digits)..]),
Elements = uint.Parse(binding[5]),
RegisterCount = type == ResourceType.Texture ? 1u : 0u,
Used = type == ResourceType.Texture ? new VectorComponents[1] : Array.Empty<VectorComponents>(),
};
});
}
else
{
resourceBindings = Array.Empty<ResourceBinding>();
}
if (header.TryGetValue("Buffer Definitions", out var rawBufferDefs))
{
bufferDefinitions = string.Join('\n', rawBufferDefs);
foreach (Match match in ResourceBindingSizeRegex().Matches(bufferDefinitions))
{
var name = match.Groups[1].Value;
var bytesSize = uint.Parse(match.Groups[2].Value);
var pos = Array.FindIndex(resourceBindings, binding => binding.Type == ResourceType.ConstantBuffer && binding.Name == name);
if (pos >= 0)
{
resourceBindings[pos].RegisterCount = (bytesSize + 0xF) >> 4;
resourceBindings[pos].Used = new VectorComponents[resourceBindings[pos].RegisterCount];
}
}
}
else
{
bufferDefinitions = string.Empty;
}
static InputOutput ParseInputOutput(string[] inOut)
=> new()
{
Name = inOut[0],
Index = uint.Parse(inOut[1]),
Mask = ParseVectorComponents(inOut[2]),
Register = uint.Parse(inOut[3]),
SystemValue = string.Intern(inOut[4]),
Format = (Format)(((byte)char.ToUpper(inOut[5][0]) << 8) | (byte)char.ToUpper(inOut[5][^1])),
Used = ParseVectorComponents(inOut[6]),
};
if (header.TryGetValue("Input signature", out var rawInputSig))
{
var (_, inputSig) = ParseTable(rawInputSig);
inputSignature = Array.ConvertAll(inputSig, ParseInputOutput);
}
else
{
inputSignature = Array.Empty<InputOutput>();
}
if (header.TryGetValue("Output signature", out var rawOutputSig))
{
var (_, outputSig) = ParseTable(rawOutputSig);
outputSignature = Array.ConvertAll(outputSig, ParseInputOutput);
}
else
{
outputSignature = Array.Empty<InputOutput>();
}
}
private static void ParseSm5ResourceUsage(IReadOnlyList<ByteString> instructions, ResourceBinding[] resourceBindings)
{
var cbIndices = new Dictionary<uint, int>();
var tIndices = new Dictionary<uint, int>();
{
var i = 0;
foreach (var binding in resourceBindings)
{
switch (binding.Type)
{
case ResourceType.ConstantBuffer:
cbIndices[binding.Slot] = i;
break;
case ResourceType.Texture:
tIndices[binding.Slot] = i;
break;
}
++i;
}
}
foreach (var instruction in instructions)
{
var trimmed = instruction.Trim();
if (trimmed.StartsWith("def"u8) || trimmed.StartsWith("dcl"u8))
continue;
var instructionString = instruction.ToString();
foreach (Match cbMatch in Sm5ConstantBufferUsageRegex().Matches(instructionString))
{
var buffer = uint.Parse(cbMatch.Groups[1].Value);
if (cbIndices.TryGetValue(buffer, out var i))
{
var swizzle = ParseVectorComponents(cbMatch.Groups[3].Value);
if (int.TryParse(cbMatch.Groups[2].Value, out var vector))
{
if (vector < resourceBindings[i].Used.Length)
resourceBindings[i].Used[vector] |= swizzle;
}
else
{
resourceBindings[i].UsedDynamically |= swizzle;
}
}
}
var tMatch = Sm5TextureUsageRegex().Match(instructionString);
if (tMatch.Success)
{
var texture = uint.Parse(tMatch.Groups[2].Value);
if (tIndices.TryGetValue(texture, out var i))
{
var outSwizzle = ParseVectorComponents(tMatch.Groups[1].Value);
var rawInSwizzle = tMatch.Groups[3].Value;
var inSwizzle = new StringBuilder(4);
if ((outSwizzle & VectorComponents.X) != 0)
inSwizzle.Append(rawInSwizzle[0]);
if ((outSwizzle & VectorComponents.Y) != 0)
inSwizzle.Append(rawInSwizzle[1]);
if ((outSwizzle & VectorComponents.Z) != 0)
inSwizzle.Append(rawInSwizzle[2]);
if ((outSwizzle & VectorComponents.W) != 0)
inSwizzle.Append(rawInSwizzle[3]);
resourceBindings[i].Used[0] |= ParseVectorComponents(inSwizzle.ToString());
}
}
}
}
private static VectorComponents ParseVectorComponents(string components)
{
components = components.ToUpperInvariant();
return (components.Contains('X') ? VectorComponents.X : 0)
| (components.Contains('Y') ? VectorComponents.Y : 0)
| (components.Contains('Z') ? VectorComponents.Z : 0)
| (components.Contains('W') ? VectorComponents.W : 0);
}
private static Dictionary<string, string[]> PreParseHeader(ReadOnlySpan<string> header)
{
var sections = new Dictionary<string, string[]>();
void AddSection(string name, ReadOnlySpan<string> section)
{
while (section.Length > 0 && section[0].Length <= 3)
section = section[1..];
while (section.Length > 0 && section[^1].Length <= 3)
section = section[..^1];
sections.Add(name, Array.ConvertAll(section.ToArray(), ln => ln.Length <= 3 ? string.Empty : ln[3..]));
}
var lastSectionName = "";
var lastSectionStart = 0;
for (var i = 1; i < header.Length - 1; ++i)
{
string current;
if (header[i - 1].Length <= 3 && header[i + 1].Length <= 3 && (current = header[i].TrimEnd()).EndsWith(':'))
{
AddSection(lastSectionName, header[lastSectionStart..(i - 1)]);
lastSectionName = current[3..^1];
lastSectionStart = i + 2;
++i; // The next line cannot match
}
}
AddSection(lastSectionName, header[lastSectionStart..]);
return sections;
}
private static (string[], string[][]) ParseTable(ReadOnlySpan<string> lines)
{
var columns = new List<Range>();
{
var dashLine = lines[1];
for (var i = 0; true; /* this part intentionally left blank */)
{
var start = dashLine.IndexOf('-', i);
if (start < 0)
break;
var end = dashLine.IndexOf(' ', start + 1);
if (end < 0)
{
columns.Add(start..dashLine.Length);
break;
}
else
{
columns.Add(start..end);
i = end + 1;
}
}
}
var headers = new string[columns.Count];
{
var headerLine = lines[0];
for (var i = 0; i < columns.Count; ++i)
headers[i] = headerLine[columns[i]].Trim();
}
var data = new List<string[]>();
foreach (var line in lines[2..])
{
var row = new string[columns.Count];
for (var i = 0; i < columns.Count; ++i)
row[i] = line[columns[i]].Trim();
data.Add(row);
}
return (headers, data.ToArray());
}
}

View file

@ -1,72 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Dalamud;
using Dalamud.Plugin;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using PseudoEquipItem = System.ValueTuple<string, ulong, ushort, ushort, ushort, byte, byte>;
namespace Penumbra.GameData.Data;
internal sealed class EquipmentIdentificationList : KeyList<PseudoEquipItem>
{
private const string Tag = "EquipmentIdentification";
public EquipmentIdentificationList(DalamudPluginInterface pi, ClientLanguage language, ItemData data)
: base(pi, Tag, language, ObjectIdentification.IdentificationVersion, CreateEquipmentList(data))
{ }
public IEnumerable<EquipItem> Between(SetId modelId, EquipSlot slot = EquipSlot.Unknown, byte variant = 0)
{
if (slot == EquipSlot.Unknown)
return Between(ToKey(modelId, 0, 0), ToKey(modelId, (EquipSlot)0xFF, 0xFF)).Select(e => (EquipItem)e);
if (variant == 0)
return Between(ToKey(modelId, slot, 0), ToKey(modelId, slot, 0xFF)).Select(e => (EquipItem)e);
return Between(ToKey(modelId, slot, variant), ToKey(modelId, slot, variant)).Select(e => (EquipItem)e);
}
public void Dispose(DalamudPluginInterface pi, ClientLanguage language)
=> DataSharer.DisposeTag(pi, Tag, language, ObjectIdentification.IdentificationVersion);
public static ulong ToKey(SetId modelId, EquipSlot slot, byte variant)
=> ((ulong)modelId << 32) | ((ulong)slot << 16) | variant;
public static ulong ToKey(EquipItem i)
=> ToKey(i.ModelId, i.Type.ToSlot(), i.Variant);
protected override IEnumerable<ulong> ToKeys(PseudoEquipItem i)
{
yield return ToKey(i);
}
protected override bool ValidKey(ulong key)
=> key != 0;
protected override int ValueKeySelector(PseudoEquipItem data)
=> (int)data.Item2;
private static IEnumerable<PseudoEquipItem> CreateEquipmentList(ItemData data)
{
return data.Where(kvp => kvp.Key.IsEquipment() || kvp.Key.IsAccessory())
.SelectMany(kvp => kvp.Value)
.Select(i => (PseudoEquipItem)i)
.Concat(CustomList);
}
private static IEnumerable<PseudoEquipItem> CustomList
=> new[]
{
(PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)8100, (WeaponType)0, 01, FullEquipType.Body, "Reaper Shroud"),
(PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9041, (WeaponType)0, 01, FullEquipType.Head, "Cid's Bandana (9041)"),
(PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9041, (WeaponType)0, 01, FullEquipType.Body, "Cid's Body (9041)"),
(PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9903, (WeaponType)0, 01, FullEquipType.Head, "Smallclothes (NPC, 9903)"),
(PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9903, (WeaponType)0, 01, FullEquipType.Body, "Smallclothes (NPC, 9903)"),
(PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9903, (WeaponType)0, 01, FullEquipType.Hands, "Smallclothes (NPC, 9903)"),
(PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9903, (WeaponType)0, 01, FullEquipType.Legs, "Smallclothes (NPC, 9903)"),
(PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9903, (WeaponType)0, 01, FullEquipType.Feet, "Smallclothes (NPC, 9903)"),
(PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9212, (WeaponType)0, 12, FullEquipType.Body, "Ancient Robes (Lahabrea)"),
(PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9212, (WeaponType)0, 01, FullEquipType.Legs, "Ancient Legs"),
(PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9212, (WeaponType)0, 01, FullEquipType.Feet, "Ancient Shoes"),
};
}

View file

@ -1,306 +0,0 @@
using System;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using Dalamud.Logging;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Penumbra.GameData.Data;
public class GamePathParser : IGamePathParser
{
/// <summary> Obtain basic information about a file path. </summary>
public GameObjectInfo GetFileInfo(string path)
{
path = path.ToLowerInvariant().Replace('\\', '/');
var (fileType, objectType, match) = ParseGamePath(path);
if (match is not { Success: true })
return new GameObjectInfo
{
FileType = fileType,
ObjectType = objectType,
};
try
{
var groups = match.Groups;
switch (objectType)
{
case ObjectType.Accessory: return HandleEquipment(fileType, groups);
case ObjectType.Equipment: return HandleEquipment(fileType, groups);
case ObjectType.Weapon: return HandleWeapon(fileType, groups);
case ObjectType.Map: return HandleMap(fileType, groups);
case ObjectType.Monster: return HandleMonster(fileType, groups);
case ObjectType.DemiHuman: return HandleDemiHuman(fileType, groups);
case ObjectType.Character: return HandleCustomization(fileType, groups);
case ObjectType.Icon: return HandleIcon(fileType, groups);
}
}
catch (Exception e)
{
PluginLog.Error($"Could not parse {path}:\n{e}");
}
return new GameObjectInfo
{
FileType = fileType,
ObjectType = objectType,
};
}
/// <summary> Get the key of a VFX symbol. </summary>
/// <returns>The lower-case key or an empty string if no match is found.</returns>
public string VfxToKey(string path)
{
var match = GamePaths.Vfx.Tmb().Match(path);
if (match.Success)
return match.Groups["key"].Value.ToLowerInvariant();
match = GamePaths.Vfx.Pap().Match(path);
return match.Success ? match.Groups["key"].Value.ToLowerInvariant() : string.Empty;
}
/// <summary> Obtain the ObjectType from a given path.</summary>
public ObjectType PathToObjectType(string path)
{
if (path.Length == 0)
return ObjectType.Unknown;
var folders = path.Split('/');
if (folders.Length < 2)
return ObjectType.Unknown;
return folders[0] switch
{
CharacterFolder => folders[1] switch
{
EquipmentFolder => ObjectType.Equipment,
AccessoryFolder => ObjectType.Accessory,
WeaponFolder => ObjectType.Weapon,
PlayerFolder => ObjectType.Character,
DemiHumanFolder => ObjectType.DemiHuman,
MonsterFolder => ObjectType.Monster,
CommonFolder => ObjectType.Character,
_ => ObjectType.Unknown,
},
UiFolder => folders[1] switch
{
IconFolder => ObjectType.Icon,
LoadingFolder => ObjectType.LoadingScreen,
MapFolder => ObjectType.Map,
InterfaceFolder => ObjectType.Interface,
_ => ObjectType.Unknown,
},
CommonFolder => folders[1] switch
{
FontFolder => ObjectType.Font,
_ => ObjectType.Unknown,
},
HousingFolder => ObjectType.Housing,
WorldFolder1 => folders[1] switch
{
HousingFolder => ObjectType.Housing,
_ => ObjectType.World,
},
WorldFolder2 => ObjectType.World,
VfxFolder => ObjectType.Vfx,
_ => ObjectType.Unknown,
};
}
private const string CharacterFolder = "chara";
private const string EquipmentFolder = "equipment";
private const string PlayerFolder = "human";
private const string WeaponFolder = "weapon";
private const string AccessoryFolder = "accessory";
private const string DemiHumanFolder = "demihuman";
private const string MonsterFolder = "monster";
private const string CommonFolder = "common";
private const string UiFolder = "ui";
private const string IconFolder = "icon";
private const string LoadingFolder = "loadingimage";
private const string MapFolder = "map";
private const string InterfaceFolder = "uld";
private const string FontFolder = "font";
private const string HousingFolder = "hou";
private const string VfxFolder = "vfx";
private const string WorldFolder1 = "bgcommon";
private const string WorldFolder2 = "bg";
private (FileType, ObjectType, Match?) ParseGamePath(string path)
{
if (!Names.ExtensionToFileType.TryGetValue(Path.GetExtension(path), out var fileType))
fileType = FileType.Unknown;
var objectType = PathToObjectType(path);
static Match TestCharacterTextures(string path)
{
var regexes = new Regex[]
{
GamePaths.Character.Tex.Regex(),
GamePaths.Character.Tex.FolderRegex(),
GamePaths.Character.Tex.SkinRegex(),
GamePaths.Character.Tex.CatchlightRegex(),
GamePaths.Character.Tex.DecalRegex(),
};
foreach (var regex in regexes)
{
var match = regex.Match(path);
if (match.Success)
return match;
}
return Match.Empty;
}
var match = (fileType, objectType) switch
{
(FileType.Font, ObjectType.Font) => GamePaths.Font.Regex().Match(path),
(FileType.Imc, ObjectType.Weapon) => GamePaths.Weapon.Imc.Regex().Match(path),
(FileType.Imc, ObjectType.Monster) => GamePaths.Monster.Imc.Regex().Match(path),
(FileType.Imc, ObjectType.DemiHuman) => GamePaths.DemiHuman.Imc.Regex().Match(path),
(FileType.Imc, ObjectType.Equipment) => GamePaths.Equipment.Imc.Regex().Match(path),
(FileType.Imc, ObjectType.Accessory) => GamePaths.Accessory.Imc.Regex().Match(path),
(FileType.Model, ObjectType.Weapon) => GamePaths.Weapon.Mdl.Regex().Match(path),
(FileType.Model, ObjectType.Monster) => GamePaths.Monster.Mdl.Regex().Match(path),
(FileType.Model, ObjectType.DemiHuman) => GamePaths.DemiHuman.Mdl.Regex().Match(path),
(FileType.Model, ObjectType.Equipment) => GamePaths.Equipment.Mdl.Regex().Match(path),
(FileType.Model, ObjectType.Accessory) => GamePaths.Accessory.Mdl.Regex().Match(path),
(FileType.Model, ObjectType.Character) => GamePaths.Character.Mdl.Regex().Match(path),
(FileType.Material, ObjectType.Weapon) => GamePaths.Weapon.Mtrl.Regex().Match(path),
(FileType.Material, ObjectType.Monster) => GamePaths.Monster.Mtrl.Regex().Match(path),
(FileType.Material, ObjectType.DemiHuman) => GamePaths.DemiHuman.Mtrl.Regex().Match(path),
(FileType.Material, ObjectType.Equipment) => GamePaths.Equipment.Mtrl.Regex().Match(path),
(FileType.Material, ObjectType.Accessory) => GamePaths.Accessory.Mtrl.Regex().Match(path),
(FileType.Material, ObjectType.Character) => GamePaths.Character.Mtrl.Regex().Match(path),
(FileType.Texture, ObjectType.Weapon) => GamePaths.Weapon.Tex.Regex().Match(path),
(FileType.Texture, ObjectType.Monster) => GamePaths.Monster.Tex.Regex().Match(path),
(FileType.Texture, ObjectType.DemiHuman) => GamePaths.DemiHuman.Tex.Regex().Match(path),
(FileType.Texture, ObjectType.Equipment) => GamePaths.Equipment.Tex.Regex().Match(path),
(FileType.Texture, ObjectType.Accessory) => GamePaths.Accessory.Tex.Regex().Match(path),
(FileType.Texture, ObjectType.Character) => TestCharacterTextures(path),
(FileType.Texture, ObjectType.Icon) => GamePaths.Icon.Regex().Match(path),
(FileType.Texture, ObjectType.Map) => GamePaths.Map.Regex().Match(path),
_ => Match.Empty,
};
return (fileType, objectType, match.Success ? match : null);
}
private static GameObjectInfo HandleEquipment(FileType fileType, GroupCollection groups)
{
var setId = ushort.Parse(groups["id"].Value);
if (fileType == FileType.Imc)
return GameObjectInfo.Equipment(fileType, setId);
var gr = Names.GenderRaceFromCode(groups["race"].Value);
var slot = Names.SuffixToEquipSlot[groups["slot"].Value];
if (fileType == FileType.Model)
return GameObjectInfo.Equipment(fileType, setId, gr, slot);
var variant = byte.Parse(groups["variant"].Value);
return GameObjectInfo.Equipment(fileType, setId, gr, slot, variant);
}
private static GameObjectInfo HandleWeapon(FileType fileType, GroupCollection groups)
{
var weaponId = ushort.Parse(groups["weapon"].Value);
var setId = ushort.Parse(groups["id"].Value);
if (fileType is FileType.Imc or FileType.Model)
return GameObjectInfo.Weapon(fileType, setId, weaponId);
var variant = byte.Parse(groups["variant"].Value);
return GameObjectInfo.Weapon(fileType, setId, weaponId, variant);
}
private static GameObjectInfo HandleMonster(FileType fileType, GroupCollection groups)
{
var monsterId = ushort.Parse(groups["monster"].Value);
var bodyId = ushort.Parse(groups["id"].Value);
if (fileType is FileType.Imc or FileType.Model)
return GameObjectInfo.Monster(fileType, monsterId, bodyId);
var variant = byte.Parse(groups["variant"].Value);
return GameObjectInfo.Monster(fileType, monsterId, bodyId, variant);
}
private static GameObjectInfo HandleDemiHuman(FileType fileType, GroupCollection groups)
{
var demiHumanId = ushort.Parse(groups["id"].Value);
var equipId = ushort.Parse(groups["equip"].Value);
if (fileType == FileType.Imc)
return GameObjectInfo.DemiHuman(fileType, demiHumanId, equipId);
var slot = Names.SuffixToEquipSlot[groups["slot"].Value];
if (fileType == FileType.Model)
return GameObjectInfo.DemiHuman(fileType, demiHumanId, equipId, slot);
var variant = byte.Parse(groups["variant"].Value);
return GameObjectInfo.DemiHuman(fileType, demiHumanId, equipId, slot, variant);
}
private static GameObjectInfo HandleCustomization(FileType fileType, GroupCollection groups)
{
if (groups["catchlight"].Success)
return GameObjectInfo.Customization(fileType, CustomizationType.Iris);
if (groups["skin"].Success)
return GameObjectInfo.Customization(fileType, CustomizationType.Skin);
var id = ushort.Parse(groups["id"].Value);
if (groups["location"].Success)
{
var tmpType = groups["location"].Value == "face" ? CustomizationType.DecalFace
: groups["location"].Value == "equip" ? CustomizationType.DecalEquip : CustomizationType.Unknown;
return GameObjectInfo.Customization(fileType, tmpType, id);
}
var gr = Names.GenderRaceFromCode(groups["race"].Value);
var bodySlot = Names.StringToBodySlot[groups["type"].Value];
var type = groups["slot"].Success
? Names.SuffixToCustomizationType[groups["slot"].Value]
: CustomizationType.Skin;
if (fileType == FileType.Material)
{
var variant = groups["variant"].Success ? byte.Parse(groups["variant"].Value) : (byte)0;
return GameObjectInfo.Customization(fileType, type, id, gr, bodySlot, variant);
}
return GameObjectInfo.Customization(fileType, type, id, gr, bodySlot);
}
private static GameObjectInfo HandleIcon(FileType fileType, GroupCollection groups)
{
var hq = groups["hq"].Success;
var hr = groups["hr"].Success;
var id = uint.Parse(groups["id"].Value);
if (!groups["lang"].Success)
return GameObjectInfo.Icon(fileType, id, hq, hr);
var language = groups["lang"].Value switch
{
"en" => Dalamud.ClientLanguage.English,
"ja" => Dalamud.ClientLanguage.Japanese,
"de" => Dalamud.ClientLanguage.German,
"fr" => Dalamud.ClientLanguage.French,
_ => Dalamud.ClientLanguage.English,
};
return GameObjectInfo.Icon(fileType, id, hq, hr, language);
}
private static GameObjectInfo HandleMap(FileType fileType, GroupCollection groups)
{
var map = Encoding.ASCII.GetBytes(groups["id"].Value);
var variant = byte.Parse(groups["variant"].Value);
if (groups["suffix"].Success)
{
var suffix = Encoding.ASCII.GetBytes(groups["suffix"].Value)[0];
return GameObjectInfo.Map(fileType, map[0], map[1], map[2], map[3], variant, suffix);
}
return GameObjectInfo.Map(fileType, map[0], map[1], map[2], map[3], variant);
}
}

View file

@ -1,408 +0,0 @@
using System.Text.RegularExpressions;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Penumbra.GameData.Data;
public static partial class GamePaths
{
[GeneratedRegex(@"c(?'racecode'\d{4})")]
public static partial Regex RaceCodeParser();
public static GenderRace ParseRaceCode(string path)
{
var match = RaceCodeParser().Match(path);
return match.Success
? Names.GenderRaceFromCode(match.Groups["racecode"].Value)
: GenderRace.Unknown;
}
public static partial class Monster
{
public static partial class Imc
{
[GeneratedRegex(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/b\k'id'\.imc")]
public static partial Regex Regex();
public static string Path(SetId monsterId, SetId bodyId)
=> $"chara/monster/m{monsterId.Value:D4}/obj/body/b{bodyId.Value:D4}/b{bodyId.Value:D4}.imc";
}
public static partial class Mdl
{
[GeneratedRegex(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/model/m\k'monster'b\k'id'\.mdl")]
public static partial Regex Regex();
public static string Path(SetId monsterId, SetId bodyId)
=> $"chara/monster/m{monsterId.Value:D4}/obj/body/b{bodyId.Value:D4}/model/m{monsterId.Value:D4}b{bodyId.Value:D4}.mdl";
}
public static partial class Mtrl
{
[GeneratedRegex(
@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/material/v(?'variant'\d{4})/mt_m\k'monster'b\k'id'_[a-z]+\.mtrl")]
public static partial Regex Regex();
public static string Path(SetId monsterId, SetId bodyId, byte variant, string suffix)
=> $"chara/monster/m{monsterId.Value:D4}/obj/body/b{bodyId.Value:D4}/material/v{variant:D4}/mt_m{monsterId.Value:D4}b{bodyId.Value:D4}_{suffix}.mtrl";
}
public static partial class Tex
{
[GeneratedRegex(
@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/texture/v(?'variant'\d{2})_m\k'monster'b\k'id'(_[a-z])?_[a-z]\.tex")]
public static partial Regex Regex();
public static string Path(SetId monsterId, SetId bodyId, byte variant, char suffix1, char suffix2 = '\0')
=> $"chara/monster/m{monsterId.Value:D4}/obj/body/b{bodyId.Value:D4}/texture/v{variant:D2}_m{monsterId.Value:D4}b{bodyId.Value:D4}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex";
}
public static partial class Sklb
{
public static string Path(SetId monsterId)
=> $"chara/monster/m{monsterId.Value:D4}/skeleton/base/b0001/skl_m{monsterId.Value:D4}b0001.sklb";
}
public static partial class Skp
{
public static string Path(SetId monsterId)
=> $"chara/monster/m{monsterId.Value:D4}/skeleton/base/b0001/skl_m{monsterId.Value:D4}b0001.skp";
}
public static partial class Eid
{
public static string Path(SetId monsterId)
=> $"chara/monster/m{monsterId.Value:D4}/skeleton/base/b0001/eid_m{monsterId.Value:D4}b0001.eid";
}
}
public static partial class Weapon
{
public static partial class Imc
{
[GeneratedRegex(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/b\k'weapon'\.imc")]
public static partial Regex Regex();
public static string Path(SetId weaponId, SetId bodyId)
=> $"chara/weapon/w{weaponId.Value:D4}/obj/body/b{bodyId.Value:D4}/b{bodyId.Value:D4}.imc";
}
public static partial class Mdl
{
[GeneratedRegex(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/model/w\k'id'b\k'weapon'\.mdl")]
public static partial Regex Regex();
public static string Path(SetId weaponId, SetId bodyId)
=> $"chara/weapon/w{weaponId.Value:D4}/obj/body/b{bodyId.Value:D4}/model/w{weaponId.Value:D4}b{bodyId.Value:D4}.mdl";
}
public static partial class Mtrl
{
[GeneratedRegex(
@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/material/v(?'variant'\d{4})/mt_w\k'id'b\k'weapon'_[a-z]+\.mtrl")]
public static partial Regex Regex();
public static string Path(SetId weaponId, SetId bodyId, byte variant, string suffix)
=> $"chara/weapon/w{weaponId.Value:D4}/obj/body/b{bodyId.Value:D4}/material/v{variant:D4}/mt_w{weaponId.Value:D4}b{bodyId.Value:D4}_{suffix}.mtrl";
}
public static partial class Tex
{
[GeneratedRegex(
@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/texture/v(?'variant'\d{2})_w\k'id'b\k'weapon'(_[a-z])?_[a-z]\.tex")]
public static partial Regex Regex();
public static string Path(SetId weaponId, SetId bodyId, byte variant, char suffix1, char suffix2 = '\0')
=> $"chara/weapon/w{weaponId.Value:D4}/obj/body/b{bodyId.Value:D4}/texture/v{variant:D2}_w{weaponId.Value:D4}b{bodyId.Value:D4}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex";
}
}
public static partial class DemiHuman
{
public static partial class Imc
{
[GeneratedRegex(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/e\k'equip'\.imc")]
public static partial Regex Regex();
public static string Path(SetId demiId, SetId equipId)
=> $"chara/demihuman/d{demiId.Value:D4}/obj/equipment/e{equipId.Value:D4}/e{equipId.Value:D4}.imc";
}
public static partial class Mdl
{
[GeneratedRegex(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/model/d\k'id'e\k'equip'_(?'slot'[a-z]{3})\.mdl")]
public static partial Regex Regex();
public static string Path(SetId demiId, SetId equipId, EquipSlot slot)
=> $"chara/demihuman/d{demiId.Value:D4}/obj/equipment/e{equipId.Value:D4}/model/d{demiId.Value:D4}e{equipId.Value:D4}_{slot.ToSuffix()}.mdl";
}
public static partial class Mtrl
{
[GeneratedRegex(
@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/material/v(?'variant'\d{4})/mt_d\k'id'e\k'equip'_(?'slot'[a-z]{3})_[a-z]+\.mtrl")]
public static partial Regex Regex();
public static string Path(SetId demiId, SetId equipId, EquipSlot slot, byte variant, string suffix)
=> $"chara/demihuman/d{demiId.Value:D4}/obj/equipment/e{equipId.Value:D4}/material/v{variant:D4}/mt_d{demiId.Value:D4}e{equipId.Value:D4}_{slot.ToSuffix()}_{suffix}.mtrl";
}
public static partial class Tex
{
[GeneratedRegex(
@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/texture/v(?'variant'\d{2})_d\k'id'e\k'equip'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex")]
public static partial Regex Regex();
public static string Path(SetId demiId, SetId equipId, EquipSlot slot, byte variant, char suffix1, char suffix2 = '\0')
=> $"chara/demihuman/d{demiId.Value:D4}/obj/equipment/e{equipId.Value:D4}/texture/v{variant:D2}_d{demiId.Value:D4}e{equipId.Value:D4}_{slot.ToSuffix()}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex";
}
}
public static partial class Equipment
{
public static partial class Imc
{
[GeneratedRegex(@"chara/equipment/e(?'id'\d{4})/e\k'id'\.imc")]
public static partial Regex Regex();
public static string Path(SetId equipId)
=> $"chara/equipment/e{equipId.Value:D4}/e{equipId.Value:D4}.imc";
}
public static partial class Mdl
{
[GeneratedRegex(@"chara/equipment/e(?'id'\d{4})/model/c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})\.mdl")]
public static partial Regex Regex();
public static string Path(SetId equipId, GenderRace raceCode, EquipSlot slot)
=> $"chara/equipment/e{equipId.Value:D4}/model/c{raceCode.ToRaceCode()}e{equipId.Value:D4}_{slot.ToSuffix()}.mdl";
}
public static partial class Mtrl
{
[GeneratedRegex(
@"chara/equipment/e(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})_[a-z]+\.mtrl")]
public static partial Regex Regex();
public static string Path(SetId equipId, GenderRace raceCode, EquipSlot slot, byte variant, string suffix)
=> $"{FolderPath(equipId, variant)}/mt_c{raceCode.ToRaceCode()}e{equipId.Value:D4}_{slot.ToSuffix()}_{suffix}.mtrl";
public static string FolderPath(SetId equipId, byte variant)
=> $"chara/equipment/e{equipId.Value:D4}/material/v{variant:D4}";
}
public static partial class Tex
{
[GeneratedRegex(
@"chara/equipment/e(?'id'\d{4})/texture/v(?'variant'\d{2})_c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex")]
public static partial Regex Regex();
public static string Path(SetId equipId, GenderRace raceCode, EquipSlot slot, byte variant, char suffix1, char suffix2 = '\0')
=> $"chara/equipment/e{equipId.Value:D4}/texture/v{variant:D2}_c{raceCode.ToRaceCode()}e{equipId.Value:D4}_{slot.ToSuffix()}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex";
}
public static partial class Avfx
{
[GeneratedRegex(@"chara/equipment/e(?'id'\d{4})/vfx/eff/ve(?'variant'\d{4})\.avfx")]
public static partial Regex Regex();
public static string Path(SetId equipId, byte effectId)
=> $"chara/equipment/e{equipId.Value:D4}/vfx/eff/ve{effectId:D4}.avfx";
}
public static partial class Decal
{
[GeneratedRegex(@"chara/common/texture/decal_equip/-decal_(?'decalId'\d{3})\.tex")]
public static partial Regex Regex();
public static string Path(byte decalId)
=> $"chara/common/texture/decal_equip/-decal_{decalId:D3}.tex";
}
}
public static partial class Accessory
{
public static partial class Imc
{
[GeneratedRegex(@"chara/accessory/a(?'id'\d{4})/a\k'id'\.imc")]
public static partial Regex Regex();
public static string Path(SetId accessoryId)
=> $"chara/accessory/a{accessoryId.Value:D4}/a{accessoryId.Value:D4}.imc";
}
public static partial class Mdl
{
[GeneratedRegex(@"chara/accessory/a(?'id'\d{4})/model/c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})\.mdl")]
public static partial Regex Regex();
public static string Path(SetId accessoryId, GenderRace raceCode, EquipSlot slot)
=> $"chara/accessory/a{accessoryId.Value:D4}/model/c{raceCode.ToRaceCode()}a{accessoryId.Value:D4}_{slot.ToSuffix()}.mdl";
}
public static partial class Mtrl
{
[GeneratedRegex(
@"chara/accessory/a(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})_[a-z]+\.mtrl")]
public static partial Regex Regex();
public static string Path(SetId accessoryId, GenderRace raceCode, EquipSlot slot, byte variant, string suffix)
=> $"{FolderPath(accessoryId, variant)}/c{raceCode.ToRaceCode()}a{accessoryId.Value:D4}_{slot.ToSuffix()}_{suffix}.mtrl";
public static string FolderPath(SetId accessoryId, byte variant)
=> $"chara/accessory/a{accessoryId.Value:D4}/material/v{variant:D4}";
}
public static partial class Tex
{
[GeneratedRegex(
@"chara/accessory/a(?'id'\d{4})/texture/v(?'variant'\d{2})_c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex")]
public static partial Regex Regex();
public static string Path(SetId accessoryId, GenderRace raceCode, EquipSlot slot, byte variant, char suffix1, char suffix2 = '\0')
=> $"chara/accessory/a{accessoryId.Value:D4}/texture/v{variant:D2}_c{raceCode.ToRaceCode()}a{accessoryId.Value:D4}_{slot.ToSuffix()}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex";
}
}
public static partial class Skeleton
{
public static partial class Phyb
{
public static string Path(GenderRace raceCode, string slot, SetId slotId)
=> $"chara/human/c{raceCode.ToRaceCode()}/skeleton/{slot}/{slot[0]}{slotId.Value:D4}/phy_c{raceCode.ToRaceCode()}{slot[0]}{slotId.Value:D4}.phyb";
}
public static partial class Sklb
{
public static string Path(GenderRace raceCode, string slot, SetId slotId)
=> $"chara/human/c{raceCode.ToRaceCode()}/skeleton/{slot}/{slot[0]}{slotId.Value:D4}/skl_c{raceCode.ToRaceCode()}{slot[0]}{slotId.Value:D4}.sklb";
}
}
public static partial class Character
{
public static partial class Mdl
{
[GeneratedRegex(
@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/model/c\k'race'\k'typeabr'\k'id'_(?'slot'[a-z]{3})\.mdl")]
public static partial Regex Regex();
public static string Path(GenderRace raceCode, BodySlot slot, SetId slotId, CustomizationType type)
=> $"chara/human/c{raceCode.ToRaceCode()}/obj/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/model/c{raceCode.ToRaceCode()}{slot.ToAbbreviation()}{slotId.Value:D4}_{type.ToSuffix()}.mdl";
}
public static partial class Mtrl
{
[GeneratedRegex(
@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/material(/v(?'variant'\d{4}))?/mt_c\k'race'\k'typeabr'\k'id'(_(?'slot'[a-z]{3}))?_[a-z]+\.mtrl")]
public static partial Regex Regex();
public static string FolderPath(GenderRace raceCode, BodySlot slot, SetId slotId, byte variant = byte.MaxValue)
=> $"chara/human/c{raceCode.ToRaceCode()}/obj/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/material{(variant != byte.MaxValue ? $"/v{variant:D4}" : string.Empty)}";
public static string HairPath(GenderRace raceCode, SetId slotId, string fileName, out GenderRace actualGr)
{
actualGr = MaterialHandling.GetGameGenderRace(raceCode, slotId);
var folder = FolderPath(actualGr, BodySlot.Hair, slotId, 1);
return actualGr == raceCode
? $"{folder}{fileName}"
: $"{folder}/mt_c{actualGr.ToRaceCode()}{fileName[9..]}";
}
public static string TailPath(GenderRace raceCode, SetId slotId, string fileName, byte variant, out SetId actualSlotId)
{
switch (raceCode)
{
case GenderRace.HrothgarMale:
case GenderRace.HrothgarFemale:
case GenderRace.HrothgarMaleNpc:
case GenderRace.HrothgarFemaleNpc:
var folder = FolderPath(raceCode, BodySlot.Tail, 1, variant == byte.MaxValue ? (byte)1 : variant);
actualSlotId = 1;
return $"{folder}{fileName}";
default:
actualSlotId = slotId;
return $"{FolderPath(raceCode, BodySlot.Tail, slotId, variant)}{fileName}";
}
}
public static string Path(GenderRace raceCode, BodySlot slot, SetId slotId, string fileName,
out GenderRace actualGr, out SetId actualSlotId, byte variant = byte.MaxValue)
{
switch (slot)
{
case BodySlot.Hair:
actualSlotId = slotId;
return HairPath(raceCode, slotId, fileName, out actualGr);
case BodySlot.Tail:
actualGr = raceCode;
return TailPath(raceCode, slotId, fileName, variant, out actualSlotId);
default:
actualSlotId = slotId;
actualGr = raceCode;
return $"{FolderPath(raceCode, slot, slotId, variant)}{fileName}";
}
}
}
public static partial class Tex
{
[GeneratedRegex(
@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/texture/(?'minus'(--)?)(v(?'variant'\d{2})_)?c\k'race'\k'typeabr'\k'id'(_(?'slot'[a-z]{3}))?(_[a-z])?_[a-z]\.tex")]
public static partial Regex Regex();
public static string Path(GenderRace raceCode, BodySlot slot, SetId slotId, char suffix1, bool minus = false,
CustomizationType type = CustomizationType.Unknown, byte variant = byte.MaxValue, char suffix2 = '\0')
=> $"chara/human/c{raceCode.ToRaceCode()}/obj/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/texture/"
+ (minus ? "--" : string.Empty)
+ (variant != byte.MaxValue ? $"v{variant:D2}_" : string.Empty)
+ $"c{raceCode.ToRaceCode()}{slot.ToAbbreviation()}{slotId.Value:D4}{(type != CustomizationType.Unknown ? $"_{type.ToSuffix()}" : string.Empty)}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex";
[GeneratedRegex(@"chara/common/texture/(?'catchlight'catchlight)(.*)\.tex")]
public static partial Regex CatchlightRegex();
[GeneratedRegex(@"chara/common/texture/skin(?'skin'.*)\.tex")]
public static partial Regex SkinRegex();
[GeneratedRegex(@"chara/common/texture/decal_(?'location'[a-z]+)/[-_]?decal_(?'id'\d+).tex")]
public static partial Regex DecalRegex();
[GeneratedRegex(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/texture")]
public static partial Regex FolderRegex();
}
}
public static partial class Icon
{
[GeneratedRegex(@"ui/icon/(?'group'\d*)(/(?'lang'[a-z]{2}))?(/(?'hq'hq))?/(?'id'\d*)(?'hr'_hr1)?\.tex")]
public static partial Regex Regex();
}
public static partial class Map
{
[GeneratedRegex(@"ui/map/(?'id'[a-z0-9]{4})/(?'variant'\d{2})/\k'id'\k'variant'(?'suffix'[a-z])?(_[a-z])?\.tex")]
public static partial Regex Regex();
}
public static partial class Font
{
[GeneratedRegex(@"common/font/(?'fontname'.*)_(?'id'\d\d)(_lobby)?\.fdt")]
public static partial Regex Regex();
}
public static partial class Vfx
{
[GeneratedRegex(@"chara[\/]action[\/](?'key'[^\s]+?)\.tmb", RegexOptions.IgnoreCase)]
public static partial Regex Tmb();
[GeneratedRegex(@"chara[\/]human[\/]c0101[\/]animation[\/]a0001[\/][^\s]+?[\/](?'key'[^\s]+?)\.pap", RegexOptions.IgnoreCase)]
public static partial Regex Pap();
}
public static partial class Shader
{
public static string ShpkPath(string name)
=> $"shader/sm5/shpk/{name}";
}
}

View file

@ -1,47 +0,0 @@
using System.Collections;
using System.Linq;
using Dalamud;
using Dalamud.Data;
using Dalamud.Plugin;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Lumina.Excel.GeneratedSheets;
namespace Penumbra.GameData.Data;
public sealed class HumanModelList : DataSharer
{
public const string Tag = "HumanModels";
public const int CurrentVersion = 2;
private readonly BitArray _humanModels;
public HumanModelList(DalamudPluginInterface pluginInterface, DataManager gameData)
: base(pluginInterface, ClientLanguage.English, CurrentVersion)
{
_humanModels = TryCatchData(Tag, () => GetValidHumanModels(gameData));
}
public bool IsHuman(uint modelId)
=> modelId < _humanModels.Count && _humanModels[(int)modelId];
public int Count
=> _humanModels.Count;
protected override void DisposeInternal()
{
DisposeTag(Tag);
}
/// <summary>
/// Go through all ModelChara rows and return a bitfield of those that resolve to human models.
/// </summary>
private static BitArray GetValidHumanModels(DataManager gameData)
{
var sheet = gameData.GetExcelSheet<ModelChara>()!;
var ret = new BitArray((int)sheet.RowCount, false);
foreach (var (_, idx) in sheet.Select((m, i) => (m, i)).Where(p => p.m.Type == (byte)CharacterBase.ModelType.Human))
ret[idx] = true;
return ret;
}
}

View file

@ -1,189 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Dalamud;
using Dalamud.Data;
using Dalamud.Plugin;
using Lumina.Excel.GeneratedSheets;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using PseudoEquipItem = System.ValueTuple<string, ulong, ushort, ushort, ushort, byte, byte>;
namespace Penumbra.GameData.Data;
public sealed class ItemData : DataSharer, IReadOnlyDictionary<FullEquipType, IReadOnlyList<EquipItem>>
{
private readonly IReadOnlyDictionary<uint, PseudoEquipItem> _mainItems;
private readonly IReadOnlyDictionary<uint, PseudoEquipItem> _offItems;
private readonly IReadOnlyDictionary<uint, PseudoEquipItem> _gauntlets;
private readonly IReadOnlyList<IReadOnlyList<PseudoEquipItem>> _byType;
private static IReadOnlyList<IReadOnlyList<PseudoEquipItem>> CreateItems(DataManager dataManager, ClientLanguage language)
{
var tmp = Enum.GetValues<FullEquipType>().Select(_ => new List<EquipItem>(1024)).ToArray();
var itemSheet = dataManager.GetExcelSheet<Item>(language)!;
foreach (var item in itemSheet.Where(i => i.Name.RawData.Length > 1))
{
var type = item.ToEquipType();
if (type.IsWeapon() || type.IsTool())
{
var mh = EquipItem.FromMainhand(item);
if (item.ModelMain != 0)
tmp[(int)type].Add(mh);
if (item.ModelSub != 0)
{
if (type is FullEquipType.Fists && item.ModelSub < 0x100000000)
{
tmp[(int)FullEquipType.Hands].Add(new EquipItem(mh.Name + $" (Gauntlets)", mh.Id, mh.IconId, (SetId)item.ModelSub, 0,
(byte)(item.ModelSub >> 16), FullEquipType.Hands));
tmp[(int)FullEquipType.FistsOff].Add(new EquipItem(mh.Name + FullEquipType.FistsOff.OffhandTypeSuffix(), mh.Id,
mh.IconId, (SetId)(mh.ModelId.Value + 50), mh.WeaponType, mh.Variant, FullEquipType.FistsOff));
}
else
{
tmp[(int)type.ValidOffhand()].Add(EquipItem.FromOffhand(item));
}
}
}
else if (type != FullEquipType.Unknown)
{
tmp[(int)type].Add(EquipItem.FromArmor(item));
}
}
var ret = new IReadOnlyList<PseudoEquipItem>[tmp.Length];
ret[0] = Array.Empty<PseudoEquipItem>();
for (var i = 1; i < tmp.Length; ++i)
ret[i] = tmp[i].OrderBy(item => item.Name).Select(s => (PseudoEquipItem)s).ToArray();
return ret;
}
private static Tuple<IReadOnlyDictionary<uint, PseudoEquipItem>, IReadOnlyDictionary<uint, PseudoEquipItem>> CreateMainItems(
IReadOnlyList<IReadOnlyList<PseudoEquipItem>> items)
{
var dict = new Dictionary<uint, PseudoEquipItem>(1024 * 4);
foreach (var fistWeapon in items[(int)FullEquipType.Fists])
dict.TryAdd((uint)fistWeapon.Item2, fistWeapon);
var gauntlets = items[(int)FullEquipType.Hands].Where(g => dict.ContainsKey((uint)g.Item2)).ToDictionary(g => (uint)g.Item2, g => g);
gauntlets.TrimExcess();
foreach (var type in Enum.GetValues<FullEquipType>().Where(v => !FullEquipTypeExtensions.OffhandTypes.Contains(v)))
{
var list = items[(int)type];
foreach (var item in list)
dict.TryAdd((uint)item.Item2, item);
}
dict.TrimExcess();
return new Tuple<IReadOnlyDictionary<uint, (string, ulong, ushort, ushort, ushort, byte, byte)>,
IReadOnlyDictionary<uint, (string, ulong, ushort, ushort, ushort, byte, byte)>>(dict, gauntlets);
}
private static IReadOnlyDictionary<uint, PseudoEquipItem> CreateOffItems(IReadOnlyList<IReadOnlyList<PseudoEquipItem>> items)
{
var dict = new Dictionary<uint, PseudoEquipItem>(128);
foreach (var type in FullEquipTypeExtensions.OffhandTypes)
{
var list = items[(int)type];
foreach (var item in list)
dict.TryAdd((uint)item.Item2, item);
}
dict.TrimExcess();
return dict;
}
public ItemData(DalamudPluginInterface pluginInterface, DataManager dataManager, ClientLanguage language)
: base(pluginInterface, language, 4)
{
_byType = TryCatchData("ItemList", () => CreateItems(dataManager, language));
(_mainItems, _gauntlets) = TryCatchData("ItemDictMain", () => CreateMainItems(_byType));
_offItems = TryCatchData("ItemDictOff", () => CreateOffItems(_byType));
}
protected override void DisposeInternal()
{
DisposeTag("ItemList");
DisposeTag("ItemDictMain");
DisposeTag("ItemDictOff");
}
public IEnumerator<KeyValuePair<FullEquipType, IReadOnlyList<EquipItem>>> GetEnumerator()
{
for (var i = 1; i < _byType.Count; ++i)
yield return new KeyValuePair<FullEquipType, IReadOnlyList<EquipItem>>((FullEquipType)i, new EquipItemList(_byType[i]));
}
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public int Count
=> _byType.Count - 1;
public bool ContainsKey(FullEquipType key)
=> (int)key < _byType.Count && key != FullEquipType.Unknown;
public bool TryGetValue(FullEquipType key, out IReadOnlyList<EquipItem> value)
{
if (ContainsKey(key))
{
value = new EquipItemList(_byType[(int)key]);
return true;
}
value = Array.Empty<EquipItem>();
return false;
}
public IReadOnlyList<EquipItem> this[FullEquipType key]
=> TryGetValue(key, out var ret) ? ret : throw new IndexOutOfRangeException();
public IEnumerable<(uint, EquipItem)> AllItems(bool main)
=> (main ? _mainItems : _offItems).Select(i => (i.Key, (EquipItem)i.Value));
public int TotalItemCount(bool main)
=> main ? _mainItems.Count : _offItems.Count;
public bool TryGetValue(uint key, EquipSlot slot, out EquipItem value)
{
var dict = slot is EquipSlot.OffHand ? _offItems : _mainItems;
if (slot is EquipSlot.Hands && _gauntlets.TryGetValue(key, out var v) || dict.TryGetValue(key, out v))
{
value = v;
return true;
}
value = default;
return false;
}
public IEnumerable<FullEquipType> Keys
=> Enum.GetValues<FullEquipType>().Skip(1);
public IEnumerable<IReadOnlyList<EquipItem>> Values
=> _byType.Skip(1).Select(l => (IReadOnlyList<EquipItem>)new EquipItemList(l));
private readonly struct EquipItemList : IReadOnlyList<EquipItem>
{
private readonly IReadOnlyList<PseudoEquipItem> _items;
public EquipItemList(IReadOnlyList<PseudoEquipItem> items)
=> _items = items;
public IEnumerator<EquipItem> GetEnumerator()
=> _items.Select(i => (EquipItem)i).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public int Count
=> _items.Count;
public EquipItem this[int index]
=> _items[index];
}
}

View file

@ -1,101 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Dalamud;
using Dalamud.Plugin;
namespace Penumbra.GameData.Data;
/// <summary>
/// A list sorting objects based on a key which then allows efficiently finding all objects between a pair of keys via binary search.
/// </summary>
public abstract class KeyList<T>
{
private readonly List<(ulong Key, T Data)> _list;
public IReadOnlyList<(ulong Key, T Data)> List
=> _list;
/// <summary>
/// Iterate over all objects between the given minimal and maximal keys (inclusive).
/// </summary>
protected IEnumerable<T> Between(ulong minKey, ulong maxKey)
{
var (minIdx, maxIdx) = GetMinMax(minKey, maxKey);
if (minIdx < 0)
yield break;
for (var i = minIdx; i <= maxIdx; ++i)
yield return _list[i].Data;
}
private (int MinIdx, int MaxIdx) GetMinMax(ulong minKey, ulong maxKey)
{
var idx = _list.BinarySearch((minKey, default!), ListComparer);
var minIdx = idx;
if (minIdx < 0)
{
minIdx = ~minIdx;
if (minIdx == _list.Count || _list[minIdx].Key > maxKey)
return (-1, -1);
idx = minIdx;
}
else
{
while (minIdx > 0 && _list[minIdx - 1].Key >= minKey)
--minIdx;
}
if (_list[minIdx].Key < minKey || _list[minIdx].Key > maxKey)
return (-1, -1);
var maxIdx = _list.BinarySearch(idx, _list.Count - idx, (maxKey, default!), ListComparer);
if (maxIdx < 0)
{
maxIdx = ~maxIdx;
return maxIdx > minIdx ? (minIdx, maxIdx - 1) : (-1, -1);
}
while (maxIdx < _list.Count - 1 && _list[maxIdx + 1].Key <= maxKey)
++maxIdx;
if (_list[maxIdx].Key < minKey || _list[maxIdx].Key > maxKey)
return (-1, -1);
return (minIdx, maxIdx);
}
/// <summary>
/// The function turning an object to (potentially multiple) keys. Only used during construction.
/// </summary>
protected abstract IEnumerable<ulong> ToKeys(T data);
/// <summary>
/// Whether a returned key is valid. Only used during construction.
/// </summary>
protected abstract bool ValidKey(ulong key);
/// <summary>
/// How multiple items with the same key should be sorted.
/// </summary>
protected abstract int ValueKeySelector(T data);
protected KeyList(DalamudPluginInterface pi, string tag, ClientLanguage language, int version, IEnumerable<T> data)
{
_list = DataSharer.TryCatchData(pi, tag, language, version,
() => data.SelectMany(d => ToKeys(d).Select(k => (k, d)))
.Where(p => ValidKey(p.k))
.OrderBy(p => p.k)
.ThenBy(p => ValueKeySelector(p.d))
.ToList());
}
private class Comparer : IComparer<(ulong, T)>
{
public int Compare((ulong, T) x, (ulong, T) y)
=> x.Item1.CompareTo(y.Item1);
}
private static readonly Comparer ListComparer = new();
}

View file

@ -1,32 +0,0 @@
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Penumbra.GameData.Data;
public static class MaterialHandling
{
public static GenderRace GetGameGenderRace(GenderRace actualGr, SetId hairId)
{
// Hrothgar do not share hairstyles.
if (actualGr is GenderRace.HrothgarFemale or GenderRace.HrothgarMale)
return actualGr;
// Some hairstyles are miqo'te specific but otherwise shared.
if (hairId.Value is >= 101 and <= 115)
{
if (actualGr is GenderRace.MiqoteFemale or GenderRace.MiqoteMale)
return actualGr;
return actualGr.Split().Item1 == Gender.Female ? GenderRace.MidlanderFemale : GenderRace.MidlanderMale;
}
// All hairstyles above 116 are shared except for Hrothgar
if (hairId.Value is >= 116 and <= 200)
return actualGr.Split().Item1 == Gender.Female ? GenderRace.MidlanderFemale : GenderRace.MidlanderMale;
return actualGr;
}
public static bool IsSpecialCase(GenderRace gr, SetId hairId)
=> gr is GenderRace.MidlanderMale or GenderRace.MidlanderFemale && hairId.Value is >= 101 and <= 200;
}

View file

@ -1,52 +0,0 @@
using System.Collections.Generic;
using Dalamud;
using Dalamud.Data;
using Dalamud.Plugin;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Lumina.Excel.GeneratedSheets;
using Penumbra.GameData.Structs;
namespace Penumbra.GameData.Data;
internal sealed class ModelIdentificationList : KeyList<ModelChara>
{
private const string Tag = "ModelIdentification";
public ModelIdentificationList(DalamudPluginInterface pi, ClientLanguage language, DataManager gameData)
: base(pi, Tag, language, ObjectIdentification.IdentificationVersion, CreateModelList(gameData, language))
{ }
public IEnumerable<ModelChara> Between(CharacterBase.ModelType type, SetId modelId, byte modelBase = 0, byte variant = 0)
{
if (modelBase == 0)
return Between(ToKey(type, modelId, 0, 0), ToKey(type, modelId, 0xFF, 0xFF));
if (variant == 0)
return Between(ToKey(type, modelId, modelBase, 0), ToKey(type, modelId, modelBase, 0xFF));
return Between(ToKey(type, modelId, modelBase, variant), ToKey(type, modelId, modelBase, variant));
}
public void Dispose(DalamudPluginInterface pi, ClientLanguage language)
=> DataSharer.DisposeTag(pi, Tag, language, ObjectIdentification.IdentificationVersion);
public static ulong ToKey(CharacterBase.ModelType type, SetId model, byte modelBase, byte variant)
=> ((ulong)type << 32) | ((ulong)model << 16) | ((ulong)modelBase << 8) | variant;
private static ulong ToKey(ModelChara row)
=> ToKey((CharacterBase.ModelType)row.Type, row.Model, row.Base, row.Variant);
protected override IEnumerable<ulong> ToKeys(ModelChara row)
{
yield return ToKey(row);
}
protected override bool ValidKey(ulong key)
=> key != 0;
protected override int ValueKeySelector(ModelChara data)
=> (int)data.RowId;
private static IEnumerable<ModelChara> CreateModelList(DataManager gameData, ClientLanguage language)
=> gameData.GetExcelSheet<ModelChara>(language)!;
}

View file

@ -1,331 +0,0 @@
using System;
using System.Collections.Concurrent;
using Dalamud;
using Dalamud.Data;
using Lumina.Excel.GeneratedSheets;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Plugin;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Penumbra.GameData.Actors;
using Action = Lumina.Excel.GeneratedSheets.Action;
using ObjectType = Penumbra.GameData.Enums.ObjectType;
namespace Penumbra.GameData.Data;
internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier
{
public const int IdentificationVersion = 3;
public IGamePathParser GamePathParser { get; } = new GamePathParser();
public readonly IReadOnlyList<IReadOnlyList<uint>> BnpcNames;
public readonly IReadOnlyList<IReadOnlyList<(string Name, ObjectKind Kind, uint Id)>> ModelCharaToObjects;
public readonly IReadOnlyDictionary<string, IReadOnlyList<Action>> Actions;
private readonly ActorManager.ActorManagerData _actorData;
private readonly EquipmentIdentificationList _equipment;
private readonly WeaponIdentificationList _weapons;
private readonly ModelIdentificationList _modelIdentifierToModelChara;
public ObjectIdentification(DalamudPluginInterface pluginInterface, DataManager dataManager, ItemData itemData, ClientLanguage language)
: base(pluginInterface, language, IdentificationVersion)
{
_actorData = new ActorManager.ActorManagerData(pluginInterface, dataManager, language);
_equipment = new EquipmentIdentificationList(pluginInterface, language, itemData);
_weapons = new WeaponIdentificationList(pluginInterface, language, itemData);
Actions = TryCatchData("Actions", () => CreateActionList(dataManager));
_modelIdentifierToModelChara = new ModelIdentificationList(pluginInterface, language, dataManager);
BnpcNames = TryCatchData("BNpcNames", NpcNames.CreateNames);
ModelCharaToObjects = TryCatchData("ModelObjects", () => CreateModelObjects(_actorData, dataManager, language));
}
public void Identify(IDictionary<string, object?> set, string path)
{
if (path.EndsWith(".pap", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".tmb", StringComparison.OrdinalIgnoreCase))
if (IdentifyVfx(set, path))
return;
var info = GamePathParser.GetFileInfo(path);
IdentifyParsed(set, info);
}
public Dictionary<string, object?> Identify(string path)
{
Dictionary<string, object?> ret = new();
Identify(ret, path);
return ret;
}
public IEnumerable<EquipItem> Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot)
=> slot switch
{
EquipSlot.MainHand => _weapons.Between(setId, weaponType, (byte)variant),
EquipSlot.OffHand => _weapons.Between(setId, weaponType, (byte)variant),
_ => _equipment.Between(setId, slot, (byte)variant),
};
public IReadOnlyList<uint> GetBnpcNames(uint bNpcId)
=> bNpcId >= BnpcNames.Count ? Array.Empty<uint>() : BnpcNames[(int)bNpcId];
public IReadOnlyList<(string Name, ObjectKind Kind, uint Id)> ModelCharaNames(uint modelId)
=> modelId >= ModelCharaToObjects.Count ? Array.Empty<(string Name, ObjectKind Kind, uint Id)>() : ModelCharaToObjects[(int)modelId];
public int NumModelChara
=> ModelCharaToObjects.Count;
protected override void DisposeInternal()
{
_actorData.Dispose();
_weapons.Dispose(PluginInterface, Language);
_equipment.Dispose(PluginInterface, Language);
DisposeTag("Actions");
DisposeTag("Models");
_modelIdentifierToModelChara.Dispose(PluginInterface, Language);
DisposeTag("BNpcNames");
DisposeTag("ModelObjects");
}
private IReadOnlyDictionary<string, IReadOnlyList<Action>> CreateActionList(DataManager gameData)
{
var sheet = gameData.GetExcelSheet<Action>(Language)!;
var storage = new ConcurrentDictionary<string, ConcurrentBag<Action>>();
void AddAction(string? key, Action action)
{
if (key.IsNullOrEmpty())
return;
key = key.ToLowerInvariant();
if (storage.TryGetValue(key, out var actions))
actions.Add(action);
else
storage[key] = new ConcurrentBag<Action> { action };
}
var options = new ParallelOptions
{
MaxDegreeOfParallelism = Environment.ProcessorCount,
};
Parallel.ForEach(sheet.Where(a => !a.Name.RawData.IsEmpty), options, action =>
{
var startKey = action.AnimationStart?.Value?.Name?.Value?.Key.ToDalamudString().ToString();
var endKey = action.AnimationEnd?.Value?.Key.ToDalamudString().ToString();
var hitKey = action.ActionTimelineHit?.Value?.Key.ToDalamudString().ToString();
AddAction(startKey, action);
AddAction(endKey, action);
AddAction(hitKey, action);
});
return storage.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList<Action>)kvp.Value.ToArray());
}
private void FindEquipment(IDictionary<string, object?> set, GameObjectInfo info)
{
var items = _equipment.Between(info.PrimaryId, info.EquipSlot, info.Variant);
foreach (var item in items)
set[item.Name] = item;
}
private void FindWeapon(IDictionary<string, object?> set, GameObjectInfo info)
{
var items = _weapons.Between(info.PrimaryId, info.SecondaryId, info.Variant);
foreach (var item in items)
set[item.Name] = item;
}
private void FindModel(IDictionary<string, object?> set, GameObjectInfo info)
{
var type = info.ObjectType.ToModelType();
if (type is 0 or CharacterBase.ModelType.Weapon)
return;
var models = _modelIdentifierToModelChara.Between(type, info.PrimaryId, (byte)info.SecondaryId, info.Variant);
foreach (var model in models.Where(m => m.RowId != 0 && m.RowId < ModelCharaToObjects.Count))
{
var objectList = ModelCharaToObjects[(int)model.RowId];
foreach (var (name, kind, _) in objectList)
set[$"{name} ({kind.ToName()})"] = model;
}
}
private static void AddCounterString(IDictionary<string, object?> set, string data)
{
if (set.TryGetValue(data, out var obj) && obj is int counter)
set[data] = counter + 1;
else
set[data] = 1;
}
private void IdentifyParsed(IDictionary<string, object?> set, GameObjectInfo info)
{
switch (info.FileType)
{
case FileType.Sound:
AddCounterString(set, FileType.Sound.ToString());
return;
case FileType.Animation:
case FileType.Pap:
AddCounterString(set, FileType.Animation.ToString());
return;
case FileType.Shader:
AddCounterString(set, FileType.Shader.ToString());
return;
}
switch (info.ObjectType)
{
case ObjectType.LoadingScreen:
case ObjectType.Map:
case ObjectType.Interface:
case ObjectType.Vfx:
case ObjectType.World:
case ObjectType.Housing:
case ObjectType.Font:
AddCounterString(set, info.ObjectType.ToString());
break;
case ObjectType.DemiHuman:
FindModel(set, info);
break;
case ObjectType.Monster:
FindModel(set, info);
break;
case ObjectType.Icon:
set[$"Icon: {info.IconId}"] = null;
break;
case ObjectType.Accessory:
case ObjectType.Equipment:
FindEquipment(set, info);
break;
case ObjectType.Weapon:
FindWeapon(set, info);
break;
case ObjectType.Character:
var (gender, race) = info.GenderRace.Split();
var raceString = race != ModelRace.Unknown ? race.ToName() + " " : "";
var genderString = gender != Gender.Unknown ? gender.ToName() + " " : "Player ";
switch (info.CustomizationType)
{
case CustomizationType.Skin:
set[$"Customization: {raceString}{genderString}Skin Textures"] = null;
break;
case CustomizationType.DecalFace:
set[$"Customization: Face Decal {info.PrimaryId}"] = null;
break;
case CustomizationType.Iris when race == ModelRace.Unknown:
set[$"Customization: All Eyes (Catchlight)"] = null;
break;
case CustomizationType.DecalEquip:
set[$"Equipment Decal {info.PrimaryId}"] = null;
break;
default:
{
var customizationString = race == ModelRace.Unknown
|| info.BodySlot == BodySlot.Unknown
|| info.CustomizationType == CustomizationType.Unknown
? "Customization: Unknown"
: $"Customization: {race} {gender} {info.BodySlot} ({info.CustomizationType}) {info.PrimaryId}";
set[customizationString] = null;
break;
}
}
break;
}
}
private bool IdentifyVfx(IDictionary<string, object?> set, string path)
{
var key = GamePathParser.VfxToKey(path);
if (key.Length == 0 || !Actions.TryGetValue(key, out var actions) || actions.Count == 0)
return false;
foreach (var action in actions)
set[$"Action: {action.Name}"] = action;
return true;
}
private IReadOnlyList<IReadOnlyList<(string Name, ObjectKind Kind, uint Id)>> CreateModelObjects(ActorManager.ActorManagerData actors,
DataManager gameData, ClientLanguage language)
{
var modelSheet = gameData.GetExcelSheet<ModelChara>(language)!;
var ret = new List<ConcurrentBag<(string Name, ObjectKind Kind, uint Id)>>((int)modelSheet.RowCount);
for (var i = -1; i < modelSheet.Last().RowId; ++i)
ret.Add(new ConcurrentBag<(string Name, ObjectKind Kind, uint Id)>());
void AddChara(int modelChara, ObjectKind kind, uint dataId, uint displayId)
{
if (modelChara >= ret.Count)
return;
if (actors.TryGetName(kind, dataId, out var name))
ret[modelChara].Add((name, kind, displayId));
}
var oTask = Task.Run(() =>
{
foreach (var ornament in gameData.GetExcelSheet<Ornament>(language)!)
AddChara(ornament.Model, ObjectKind.Ornament, ornament.RowId, ornament.RowId);
});
var mTask = Task.Run(() =>
{
foreach (var mount in gameData.GetExcelSheet<Mount>(language)!)
AddChara((int)mount.ModelChara.Row, ObjectKind.MountType, mount.RowId, mount.RowId);
});
var cTask = Task.Run(() =>
{
foreach (var companion in gameData.GetExcelSheet<Companion>(language)!)
AddChara((int)companion.Model.Row, ObjectKind.Companion, companion.RowId, companion.RowId);
});
var eTask = Task.Run(() =>
{
foreach (var eNpc in gameData.GetExcelSheet<ENpcBase>(language)!)
AddChara((int)eNpc.ModelChara.Row, ObjectKind.EventNpc, eNpc.RowId, eNpc.RowId);
});
var options = new ParallelOptions()
{
MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2),
};
Parallel.ForEach(gameData.GetExcelSheet<BNpcBase>(language)!.Where(b => b.RowId < BnpcNames.Count), options, bNpc =>
{
foreach (var name in BnpcNames[(int)bNpc.RowId])
AddChara((int)bNpc.ModelChara.Row, ObjectKind.BattleNpc, name, bNpc.RowId);
});
Task.WaitAll(oTask, mTask, cTask, eTask);
return ret.Select(s => !s.IsEmpty
? s.ToArray()
: Array.Empty<(string Name, ObjectKind Kind, uint Id)>()).ToArray();
}
public static unsafe ulong KeyFromCharacterBase(CharacterBase* drawObject)
{
var type = (*(delegate* unmanaged<CharacterBase*, uint>**)drawObject)[Offsets.DrawObjectGetModelTypeVfunc](drawObject);
var unk = (ulong)*((byte*)drawObject + Offsets.DrawObjectModelUnk1) << 8;
return type switch
{
1 => type | unk,
2 => type | unk | ((ulong)*(ushort*)((byte*)drawObject + Offsets.DrawObjectModelUnk3) << 16),
3 => type
| unk
| ((ulong)*(ushort*)((byte*)drawObject + Offsets.DrawObjectModelUnk2) << 16)
| ((ulong)**(ushort**)((byte*)drawObject + Offsets.DrawObjectModelUnk4) << 32)
| ((ulong)**(ushort**)((byte*)drawObject + Offsets.DrawObjectModelUnk3) << 40),
_ => 0u,
};
}
}

View file

@ -1,433 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud;
using Dalamud.Data;
using Dalamud.Logging;
using Dalamud.Plugin;
using Dalamud.Utility;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Race = Penumbra.GameData.Enums.Race;
namespace Penumbra.GameData.Data;
/// <summary>
/// Handle gender- or race-locked gear in the draw model itself.
/// Racial gear gets swapped to the correct current race and gender (it is one set each).
/// Gender-locked gear gets swapped to the equivalent set if it exists (most of them do),
/// with some items getting send to emperor's new clothes and a few funny entries.
/// </summary>
public sealed class RestrictedGear : DataSharer
{
private readonly ExcelSheet<Item> _items;
private readonly ExcelSheet<EquipRaceCategory> _categories;
public readonly IReadOnlySet<uint> RaceGenderSet;
public readonly IReadOnlyDictionary<uint, uint> MaleToFemale;
public readonly IReadOnlyDictionary<uint, uint> FemaleToMale;
public RestrictedGear(DalamudPluginInterface pi, ClientLanguage language, DataManager gameData)
: base(pi, language, 2)
{
_items = gameData.GetExcelSheet<Item>()!;
_categories = gameData.GetExcelSheet<EquipRaceCategory>()!;
(RaceGenderSet, MaleToFemale, FemaleToMale) = TryCatchData("RestrictedGear", CreateRestrictedGear);
}
protected override void DisposeInternal()
=> DisposeTag("RestrictedGear");
/// <summary>
/// Resolve a model given by its model id, variant and slot for your current race and gender.
/// </summary>
/// <param name="armor">The equipment piece.</param>
/// <param name="slot">The equipment slot.</param>
/// <param name="race">The intended race.</param>
/// <param name="gender">The intended gender.</param>
/// <returns>True and the changed-to piece of gear or false and the same piece of gear.</returns>
public (bool Replaced, CharacterArmor Armor) ResolveRestricted(CharacterArmor armor, EquipSlot slot, Race race, Gender gender)
{
var quad = armor.Set.Value | ((uint)armor.Variant << 16);
// Check racial gear, this does not need slots.
if (RaceGenderGroup.Contains(quad))
{
var idx = ((int)race - 1) * 2 + (gender is Gender.Female or Gender.FemaleNpc ? 1 : 0);
var value = RaceGenderGroup[idx];
return (value != quad, new CharacterArmor((ushort)value, (byte)(value >> 16), armor.Stain));
}
// Check gender slots. If current gender is female, check if anything needs to be changed from male to female,
// and vice versa.
// Some items lead to the exact same model- and variant id just gender specified,
// so check for actual difference in the Replaced bool.
var needle = quad | ((uint)slot.ToSlot() << 24);
if (gender is Gender.Female or Gender.FemaleNpc && MaleToFemale.TryGetValue(needle, out var newValue)
|| gender is Gender.Male or Gender.MaleNpc && FemaleToMale.TryGetValue(needle, out newValue))
return (quad != newValue, new CharacterArmor((ushort)newValue, (byte)(newValue >> 16), armor.Stain));
// The gear is not restricted.
return (false, armor);
}
private Tuple<IReadOnlySet<uint>, IReadOnlyDictionary<uint, uint>, IReadOnlyDictionary<uint, uint>> CreateRestrictedGear()
{
var m2f = new Dictionary<uint, uint>();
var f2m = new Dictionary<uint, uint>();
var rg = RaceGenderGroup.Where(c => c is not 0 and not uint.MaxValue).ToHashSet();
AddKnown(m2f, f2m);
UnhandledRestrictedGear(rg, m2f, f2m, false); // Set this to true to create a print of unassigned gear on launch.
return new Tuple<IReadOnlySet<uint>, IReadOnlyDictionary<uint, uint>, IReadOnlyDictionary<uint, uint>>(rg, m2f, f2m);
}
// Add all unknown restricted gear and pair it with emperor's new gear on start up.
// Can also print unhandled items.
private void UnhandledRestrictedGear(IReadOnlySet<uint> rg, Dictionary<uint, uint> m2f, Dictionary<uint, uint> f2m, bool print)
{
if (print)
PluginLog.Information("#### MALE ONLY ######");
void AddEmperor(Item item, bool male, bool female)
{
var slot = ((EquipSlot)item.EquipSlotCategory.Row).ToSlot();
var emperor = slot switch
{
EquipSlot.Head => 10032u,
EquipSlot.Body => 10033u,
EquipSlot.Hands => 10034u,
EquipSlot.Legs => 10035u,
EquipSlot.Feet => 10036u,
EquipSlot.Ears => 09293u,
EquipSlot.Neck => 09292u,
EquipSlot.Wrists => 09294u,
EquipSlot.RFinger => 09295u,
EquipSlot.LFinger => 09295u,
_ => 0u,
};
if (emperor == 0)
return;
if (male)
AddItem(m2f, f2m, item.RowId, emperor, true, false);
if (female)
AddItem(m2f, f2m, emperor, item.RowId, false, true);
}
var unhandled = 0;
foreach (var item in _items.Where(i => i.EquipRestriction == 2))
{
if (m2f.ContainsKey((uint)item.ModelMain | ((uint)((EquipSlot)item.EquipSlotCategory.Row).ToSlot() << 24)))
continue;
++unhandled;
AddEmperor(item, true, false);
if (print)
PluginLog.Information($"{item.RowId:D5} {item.Name.ToDalamudString().TextValue}");
}
if (print)
PluginLog.Information("#### FEMALE ONLY ####");
foreach (var item in _items.Where(i => i.EquipRestriction == 3))
{
if (f2m.ContainsKey((uint)item.ModelMain | ((uint)((EquipSlot)item.EquipSlotCategory.Row).ToSlot() << 24)))
continue;
++unhandled;
AddEmperor(item, false, true);
if (print)
PluginLog.Information($"{item.RowId:D5} {item.Name.ToDalamudString().TextValue}");
}
if (print)
PluginLog.Information("#### OTHER #########");
foreach (var item in _items.Where(i => i.EquipRestriction > 3))
{
if (rg.Contains((uint)item.ModelMain))
continue;
++unhandled;
if (print)
PluginLog.Information(
$"{item.RowId:D5} {item.Name.ToDalamudString().TextValue} RestrictionGroup {_categories.GetRow(item.EquipRestriction)!.RowId:D2}");
}
if (unhandled > 0)
PluginLog.Warning($"There were {unhandled} restricted items not handled and directed to Emperor's New Set.");
}
// Add a item redirection by its item - NOT MODEL - id.
// This uses the items model as well as its slot.
// Creates a <-> redirection by default but can add -> or <- redirections by setting the corresponding bools to false.
// Prints warnings if anything does not make sense.
private void AddItem(Dictionary<uint, uint> m2f, Dictionary<uint, uint> f2m, uint itemIdMale, uint itemIdFemale, bool addMale = true,
bool addFemale = true)
{
if (!addMale && !addFemale)
return;
var mItem = _items.GetRow(itemIdMale);
var fItem = _items.GetRow(itemIdFemale);
if (mItem == null || fItem == null)
{
PluginLog.Warning($"Could not add item {itemIdMale} or {itemIdFemale} to restricted items.");
return;
}
if (mItem.EquipRestriction != 2 && addMale)
{
PluginLog.Warning($"{mItem.Name.ToDalamudString().TextValue} is not restricted anymore.");
return;
}
if (fItem.EquipRestriction != 3 && addFemale)
{
PluginLog.Warning($"{fItem.Name.ToDalamudString().TextValue} is not restricted anymore.");
return;
}
var mSlot = ((EquipSlot)mItem.EquipSlotCategory.Row).ToSlot();
var fSlot = ((EquipSlot)fItem.EquipSlotCategory.Row).ToSlot();
if (!mSlot.IsAccessory() && !mSlot.IsEquipment())
{
PluginLog.Warning($"{mItem.Name.ToDalamudString().TextValue} is not equippable to a known slot.");
return;
}
if (mSlot != fSlot)
{
PluginLog.Warning($"{mItem.Name.ToDalamudString().TextValue} and {fItem.Name.ToDalamudString().TextValue} are not compatible.");
return;
}
var mModelIdSlot = (uint)mItem.ModelMain | ((uint)mSlot << 24);
var fModelIdSlot = (uint)fItem.ModelMain | ((uint)fSlot << 24);
if (addMale)
m2f.TryAdd(mModelIdSlot, fModelIdSlot);
if (addFemale)
f2m.TryAdd(fModelIdSlot, mModelIdSlot);
}
// @formatter:off
// Add all currently existing and known gender restricted items.
private void AddKnown(Dictionary<uint, uint> m2f, Dictionary<uint, uint> f2m)
{
AddItem(m2f, f2m, 02967, 02970); // Lord's Yukata (Blue) <-> Lady's Yukata (Red)
AddItem(m2f, f2m, 02968, 02971); // Lord's Yukata (Green) <-> Lady's Yukata (Blue)
AddItem(m2f, f2m, 02969, 02972); // Lord's Yukata (Grey) <-> Lady's Yukata (Black)
AddItem(m2f, f2m, 02973, 02978); // Red Summer Top <-> Red Summer Halter
AddItem(m2f, f2m, 02974, 02979); // Green Summer Top <-> Green Summer Halter
AddItem(m2f, f2m, 02975, 02980); // Blue Summer Top <-> Blue Summer Halter
AddItem(m2f, f2m, 02976, 02981); // Solar Summer Top <-> Solar Summer Halter
AddItem(m2f, f2m, 02977, 02982); // Lunar Summer Top <-> Lunar Summer Halter
AddItem(m2f, f2m, 02996, 02997); // Hempen Undershirt <-> Hempen Camise
AddItem(m2f, f2m, 03280, 03283); // Lord's Drawers (Black) <-> Lady's Knickers (Black)
AddItem(m2f, f2m, 03281, 03284); // Lord's Drawers (White) <-> Lady's Knickers (White)
AddItem(m2f, f2m, 03282, 03285); // Lord's Drawers (Gold) <-> Lady's Knickers (Gold)
AddItem(m2f, f2m, 03286, 03291); // Red Summer Trunks <-> Red Summer Tanga
AddItem(m2f, f2m, 03287, 03292); // Green Summer Trunks <-> Green Summer Tanga
AddItem(m2f, f2m, 03288, 03293); // Blue Summer Trunks <-> Blue Summer Tanga
AddItem(m2f, f2m, 03289, 03294); // Solar Summer Trunks <-> Solar Summer Tanga
AddItem(m2f, f2m, 03290, 03295); // Lunar Summer Trunks <-> Lunar Summer Tanga
AddItem(m2f, f2m, 03307, 03308); // Hempen Underpants <-> Hempen Pantalettes
AddItem(m2f, f2m, 03748, 03749); // Lord's Clogs <-> Lady's Clogs
AddItem(m2f, f2m, 06045, 06041); // Bohemian's Coat <-> Guardian Corps Coat
AddItem(m2f, f2m, 06046, 06042); // Bohemian's Gloves <-> Guardian Corps Gauntlets
AddItem(m2f, f2m, 06047, 06043); // Bohemian's Trousers <-> Guardian Corps Skirt
AddItem(m2f, f2m, 06048, 06044); // Bohemian's Boots <-> Guardian Corps Boots
AddItem(m2f, f2m, 06094, 06098); // Summer Evening Top <-> Summer Morning Halter
AddItem(m2f, f2m, 06095, 06099); // Summer Evening Trunks <-> Summer Morning Tanga
AddItem(m2f, f2m, 06096, 06100); // Striped Summer Top <-> Striped Summer Halter
AddItem(m2f, f2m, 06097, 06101); // Striped Summer Trunks <-> Striped Summer Tanga
AddItem(m2f, f2m, 06102, 06104); // Black Summer Top <-> Black Summer Halter
AddItem(m2f, f2m, 06103, 06105); // Black Summer Trunks <-> Black Summer Tanga
AddItem(m2f, f2m, 08532, 08535); // Lord's Yukata (Blackflame) <-> Lady's Yukata (Redfly)
AddItem(m2f, f2m, 08533, 08536); // Lord's Yukata (Whiteflame) <-> Lady's Yukata (Bluefly)
AddItem(m2f, f2m, 08534, 08537); // Lord's Yukata (Blueflame) <-> Lady's Yukata (Pinkfly)
AddItem(m2f, f2m, 08542, 08549); // Ti Leaf Lei <-> Coronal Summer Halter
AddItem(m2f, f2m, 08543, 08550); // Red Summer Maro <-> Red Summer Pareo
AddItem(m2f, f2m, 08544, 08551); // South Seas Talisman <-> Sea Breeze Summer Halter
AddItem(m2f, f2m, 08545, 08552); // Blue Summer Maro <-> Sea Breeze Summer Pareo
AddItem(m2f, f2m, 08546, 08553); // Coeurl Talisman <-> Coeurl Beach Halter
AddItem(m2f, f2m, 08547, 08554); // Coeurl Beach Maro <-> Coeurl Beach Pareo
AddItem(m2f, f2m, 08548, 08555); // Coeurl Beach Briefs <-> Coeurl Beach Tanga
AddItem(m2f, f2m, 10316, 10317); // Southern Seas Vest <-> Southern Seas Swimsuit
AddItem(m2f, f2m, 10318, 10319); // Southern Seas Trunks <-> Southern Seas Tanga
AddItem(m2f, f2m, 10320, 10321); // Striped Southern Seas Vest <-> Striped Southern Seas Swimsuit
AddItem(m2f, f2m, 13298, 13567); // Black-feathered Flat Hat <-> Red-feathered Flat Hat
AddItem(m2f, f2m, 13300, 13639); // Lord's Suikan <-> Lady's Suikan
AddItem(m2f, f2m, 13724, 13725); // Little Lord's Clogs <-> Little Lady's Clogs
AddItem(m2f, f2m, 14854, 14857); // Eastern Lord's Togi <-> Eastern Lady's Togi
AddItem(m2f, f2m, 14855, 14858); // Eastern Lord's Trousers <-> Eastern Lady's Loincloth
AddItem(m2f, f2m, 14856, 14859); // Eastern Lord's Crakows <-> Eastern Lady's Crakows
AddItem(m2f, f2m, 15639, 15642); // Far Eastern Patriarch's Hat <-> Far Eastern Matriarch's Sun Hat
AddItem(m2f, f2m, 15640, 15643); // Far Eastern Patriarch's Tunic <-> Far Eastern Matriarch's Dress
AddItem(m2f, f2m, 15641, 15644); // Far Eastern Patriarch's Longboots <-> Far Eastern Matriarch's Boots
AddItem(m2f, f2m, 15922, 15925); // Moonfire Vest <-> Moonfire Halter
AddItem(m2f, f2m, 15923, 15926); // Moonfire Trunks <-> Moonfire Tanga
AddItem(m2f, f2m, 15924, 15927); // Moonfire Caligae <-> Moonfire Sandals
AddItem(m2f, f2m, 16106, 16111); // Makai Mauler's Facemask <-> Makai Manhandler's Facemask
AddItem(m2f, f2m, 16107, 16112); // Makai Mauler's Oilskin <-> Makai Manhandler's Jerkin
AddItem(m2f, f2m, 16108, 16113); // Makai Mauler's Fingerless Gloves <-> Makai Manhandler's Fingerless Gloves
AddItem(m2f, f2m, 16109, 16114); // Makai Mauler's Leggings <-> Makai Manhandler's Quartertights
AddItem(m2f, f2m, 16110, 16115); // Makai Mauler's Boots <-> Makai Manhandler's Longboots
AddItem(m2f, f2m, 16116, 16121); // Makai Marksman's Eyepatch <-> Makai Markswoman's Ribbon
AddItem(m2f, f2m, 16117, 16122); // Makai Marksman's Battlegarb <-> Makai Markswoman's Battledress
AddItem(m2f, f2m, 16118, 16123); // Makai Marksman's Fingerless Gloves <-> Makai Markswoman's Fingerless Gloves
AddItem(m2f, f2m, 16119, 16124); // Makai Marksman's Slops <-> Makai Markswoman's Quartertights
AddItem(m2f, f2m, 16120, 16125); // Makai Marksman's Boots <-> Makai Markswoman's Longboots
AddItem(m2f, f2m, 16126, 16131); // Makai Sun Guide's Circlet <-> Makai Moon Guide's Circlet
AddItem(m2f, f2m, 16127, 16132); // Makai Sun Guide's Oilskin <-> Makai Moon Guide's Gown
AddItem(m2f, f2m, 16128, 16133); // Makai Sun Guide's Fingerless Gloves <-> Makai Moon Guide's Fingerless Gloves
AddItem(m2f, f2m, 16129, 16134); // Makai Sun Guide's Slops <-> Makai Moon Guide's Quartertights
AddItem(m2f, f2m, 16130, 16135); // Makai Sun Guide's Boots <-> Makai Moon Guide's Longboots
AddItem(m2f, f2m, 16136, 16141); // Makai Priest's Coronet <-> Makai Priestess's Headdress
AddItem(m2f, f2m, 16137, 16142); // Makai Priest's Doublet Robe <-> Makai Priestess's Jerkin
AddItem(m2f, f2m, 16138, 16143); // Makai Priest's Fingerless Gloves <-> Makai Priestess's Fingerless Gloves
AddItem(m2f, f2m, 16139, 16144); // Makai Priest's Slops <-> Makai Priestess's Skirt
AddItem(m2f, f2m, 16140, 16145); // Makai Priest's Boots <-> Makai Priestess's Longboots
AddItem(m2f, f2m, 16588, 16592); // Far Eastern Gentleman's Hat <-> Far Eastern Beauty's Hairpin
AddItem(m2f, f2m, 16589, 16593); // Far Eastern Gentleman's Robe <-> Far Eastern Beauty's Robe
AddItem(m2f, f2m, 16590, 16594); // Far Eastern Gentleman's Haidate <-> Far Eastern Beauty's Koshita
AddItem(m2f, f2m, 16591, 16595); // Far Eastern Gentleman's Boots <-> Far Eastern Beauty's Boots
AddItem(m2f, f2m, 17204, 17209); // Common Makai Mauler's Facemask <-> Common Makai Manhandler's Facemask
AddItem(m2f, f2m, 17205, 17210); // Common Makai Mauler's Oilskin <-> Common Makai Manhandler's Jerkin
AddItem(m2f, f2m, 17206, 17211); // Common Makai Mauler's Fingerless Gloves <-> Common Makai Manhandler's Fingerless Glove
AddItem(m2f, f2m, 17207, 17212); // Common Makai Mauler's Leggings <-> Common Makai Manhandler's Quartertights
AddItem(m2f, f2m, 17208, 17213); // Common Makai Mauler's Boots <-> Common Makai Manhandler's Longboots
AddItem(m2f, f2m, 17214, 17219); // Common Makai Marksman's Eyepatch <-> Common Makai Markswoman's Ribbon
AddItem(m2f, f2m, 17215, 17220); // Common Makai Marksman's Battlegarb <-> Common Makai Markswoman's Battledress
AddItem(m2f, f2m, 17216, 17221); // Common Makai Marksman's Fingerless Gloves <-> Common Makai Markswoman's Fingerless Glove
AddItem(m2f, f2m, 17217, 17222); // Common Makai Marksman's Slops <-> Common Makai Markswoman's Quartertights
AddItem(m2f, f2m, 17218, 17223); // Common Makai Marksman's Boots <-> Common Makai Markswoman's Longboots
AddItem(m2f, f2m, 17224, 17229); // Common Makai Sun Guide's Circlet <-> Common Makai Moon Guide's Circlet
AddItem(m2f, f2m, 17225, 17230); // Common Makai Sun Guide's Oilskin <-> Common Makai Moon Guide's Gown
AddItem(m2f, f2m, 17226, 17231); // Common Makai Sun Guide's Fingerless Gloves <-> Common Makai Moon Guide's Fingerless Glove
AddItem(m2f, f2m, 17227, 17232); // Common Makai Sun Guide's Slops <-> Common Makai Moon Guide's Quartertights
AddItem(m2f, f2m, 17228, 17233); // Common Makai Sun Guide's Boots <-> Common Makai Moon Guide's Longboots
AddItem(m2f, f2m, 17234, 17239); // Common Makai Priest's Coronet <-> Common Makai Priestess's Headdress
AddItem(m2f, f2m, 17235, 17240); // Common Makai Priest's Doublet Robe <-> Common Makai Priestess's Jerkin
AddItem(m2f, f2m, 17236, 17241); // Common Makai Priest's Fingerless Gloves <-> Common Makai Priestess's Fingerless Gloves
AddItem(m2f, f2m, 17237, 17242); // Common Makai Priest's Slops <-> Common Makai Priestess's Skirt
AddItem(m2f, f2m, 17238, 17243); // Common Makai Priest's Boots <-> Common Makai Priestess's Longboots
AddItem(m2f, f2m, 20479, 20484); // Star of the Nezha Lord <-> Star of the Nezha Lady
AddItem(m2f, f2m, 20480, 20485); // Nezha Lord's Togi <-> Nezha Lady's Togi
AddItem(m2f, f2m, 20481, 20486); // Nezha Lord's Gloves <-> Nezha Lady's Gloves
AddItem(m2f, f2m, 20482, 20487); // Nezha Lord's Slops <-> Nezha Lady's Slops
AddItem(m2f, f2m, 20483, 20488); // Nezha Lord's Boots <-> Nezha Lady's Kneeboots
AddItem(m2f, f2m, 22367, 22372); // Faerie Tale Prince's Circlet <-> Faerie Tale Princess's Tiara
AddItem(m2f, f2m, 22368, 22373); // Faerie Tale Prince's Vest <-> Faerie Tale Princess's Dress
AddItem(m2f, f2m, 22369, 22374); // Faerie Tale Prince's Gloves <-> Faerie Tale Princess's Gloves
AddItem(m2f, f2m, 22370, 22375); // Faerie Tale Prince's Slops <-> Faerie Tale Princess's Long Skirt
AddItem(m2f, f2m, 22371, 22376); // Faerie Tale Prince's Boots <-> Faerie Tale Princess's Heels
AddItem(m2f, f2m, 24599, 24602); // Far Eastern Schoolboy's Hat <-> Far Eastern Schoolgirl's Hair Ribbon
AddItem(m2f, f2m, 24600, 24603); // Far Eastern Schoolboy's Hakama <-> Far Eastern Schoolgirl's Hakama
AddItem(m2f, f2m, 24601, 24604); // Far Eastern Schoolboy's Zori <-> Far Eastern Schoolgirl's Boots
AddItem(m2f, f2m, 28600, 28605); // Eastern Lord Errant's Hat <-> Eastern Lady Errant's Hat
AddItem(m2f, f2m, 28601, 28606); // Eastern Lord Errant's Jacket <-> Eastern Lady Errant's Coat
AddItem(m2f, f2m, 28602, 28607); // Eastern Lord Errant's Wristbands <-> Eastern Lady Errant's Gloves
AddItem(m2f, f2m, 28603, 28608); // Eastern Lord Errant's Trousers <-> Eastern Lady Errant's Skirt
AddItem(m2f, f2m, 28604, 28609); // Eastern Lord Errant's Shoes <-> Eastern Lady Errant's Boots
AddItem(m2f, f2m, 36336, 36337); // Omega-M Attire <-> Omega-F Attire
AddItem(m2f, f2m, 36338, 36339); // Omega-M Ear Cuffs <-> Omega-F Earrings
AddItem(m2f, f2m, 37442, 37447); // Makai Vanguard's Monocle <-> Makai Vanbreaker's Ribbon
AddItem(m2f, f2m, 37443, 37448); // Makai Vanguard's Battlegarb <-> Makai Vanbreaker's Battledress
AddItem(m2f, f2m, 37444, 37449); // Makai Vanguard's Fingerless Gloves <-> Makai Vanbreaker's Fingerless Gloves
AddItem(m2f, f2m, 37445, 37450); // Makai Vanguard's Leggings <-> Makai Vanbreaker's Quartertights
AddItem(m2f, f2m, 37446, 37451); // Makai Vanguard's Boots <-> Makai Vanbreaker's Longboots
AddItem(m2f, f2m, 37452, 37457); // Makai Harbinger's Facemask <-> Makai Harrower's Facemask
AddItem(m2f, f2m, 37453, 37458); // Makai Harbinger's Battlegarb <-> Makai Harrower's Jerkin
AddItem(m2f, f2m, 37454, 37459); // Makai Harbinger's Fingerless Gloves <-> Makai Harrower's Fingerless Gloves
AddItem(m2f, f2m, 37455, 37460); // Makai Harbinger's Leggings <-> Makai Harrower's Quartertights
AddItem(m2f, f2m, 37456, 37461); // Makai Harbinger's Boots <-> Makai Harrower's Longboots
AddItem(m2f, f2m, 37462, 37467); // Common Makai Vanguard's Monocle <-> Common Makai Vanbreaker's Ribbon
AddItem(m2f, f2m, 37463, 37468); // Common Makai Vanguard's Battlegarb <-> Common Makai Vanbreaker's Battledress
AddItem(m2f, f2m, 37464, 37469); // Common Makai Vanguard's Fingerless Gloves <-> Common Makai Vanbreaker's Fingerless Gloves
AddItem(m2f, f2m, 37465, 37470); // Common Makai Vanguard's Leggings <-> Common Makai Vanbreaker's Quartertights
AddItem(m2f, f2m, 37466, 37471); // Common Makai Vanguard's Boots <-> Common Makai Vanbreaker's Longboots
AddItem(m2f, f2m, 37472, 37477); // Common Makai Harbinger's Facemask <-> Common Makai Harrower's Facemask
AddItem(m2f, f2m, 37473, 37478); // Common Makai Harbinger's Battlegarb <-> Common Makai Harrower's Jerkin
AddItem(m2f, f2m, 37474, 37479); // Common Makai Harbinger's Fingerless Gloves <-> Common Makai Harrower's Fingerless Gloves
AddItem(m2f, f2m, 37475, 37480); // Common Makai Harbinger's Leggings <-> Common Makai Harrower's Quartertights
AddItem(m2f, f2m, 37476, 37481); // Common Makai Harbinger's Boots <-> Common Makai Harrower's Longboots
AddItem(m2f, f2m, 13323, 13322); // Scion Thief's Tunic <-> Scion Conjurer's Dalmatica
AddItem(m2f, f2m, 13693, 10034, true, false); // Scion Thief's Halfgloves -> The Emperor's New Gloves
AddItem(m2f, f2m, 13694, 13691); // Scion Thief's Gaskins <-> Scion Conjurer's Chausses
AddItem(m2f, f2m, 13695, 13692); // Scion Thief's Armored Caligae <-> Scion Conjurer's Pattens
AddItem(m2f, f2m, 13326, 30063); // Scion Thaumaturge's Robe <-> Scion Sorceress's Headdress
AddItem(m2f, f2m, 13696, 30062); // Scion Thaumaturge's Monocle <-> Scion Sorceress's Robe
AddItem(m2f, f2m, 13697, 30064); // Scion Thaumaturge's Gauntlets <-> Scion Sorceress's Shadowtalons
AddItem(m2f, f2m, 13698, 10035, true, false); // Scion Thaumaturge's Gaskins -> The Emperor's New Breeches
AddItem(m2f, f2m, 13699, 30065); // Scion Thaumaturge's Moccasins <-> Scion Sorceress's High Boots
AddItem(m2f, f2m, 13327, 15942); // Scion Chronocler's Cowl <-> Scion Healer's Robe
AddItem(m2f, f2m, 13700, 10034, true, false); // Scion Chronocler's Ringbands -> The Emperor's New Gloves
AddItem(m2f, f2m, 13701, 15943); // Scion Chronocler's Tights <-> Scion Healer's Halftights
AddItem(m2f, f2m, 13702, 15944); // Scion Chronocler's Caligae <-> Scion Healer's Highboots
AddItem(m2f, f2m, 14861, 13324); // Head Engineer's Goggles <-> Scion Striker's Visor
AddItem(m2f, f2m, 14862, 13325); // Head Engineer's Attire <-> Scion Striker's Attire
AddItem(m2f, f2m, 15938, 33751); // Scion Rogue's Jacket <-> Oracle Top
AddItem(m2f, f2m, 15939, 10034, true, false); // Scion Rogue's Armguards -> The Emperor's New Gloves
AddItem(m2f, f2m, 15940, 33752); // Scion Rogue's Gaskins <-> Oracle Leggings
AddItem(m2f, f2m, 15941, 33753); // Scion Rogue's Boots <-> Oracle Pantalettes
AddItem(m2f, f2m, 16042, 16046); // Abes Jacket <-> High Summoner's Dress
AddItem(m2f, f2m, 16043, 16047); // Abes Gloves <-> High Summoner's Armlets
AddItem(m2f, f2m, 16044, 10035, true, false); // Abes Halfslops -> The Emperor's New Breeches
AddItem(m2f, f2m, 16045, 16048); // Abes Boots <-> High Summoner's Boots
AddItem(m2f, f2m, 17473, 28553); // Lord Commander's Coat <-> Majestic Dress
AddItem(m2f, f2m, 17474, 28554); // Lord Commander's Gloves <-> Majestic Wristdresses
AddItem(m2f, f2m, 10036, 28555, false); // Emperor's New Boots <- Majestic Boots
AddItem(m2f, f2m, 21021, 21026); // Werewolf Feet <-> Werewolf Legs
AddItem(m2f, f2m, 22452, 20633); // Cracked Manderville Monocle <-> Blackbosom Hat
AddItem(m2f, f2m, 22453, 20634); // Torn Manderville Coatee <-> Blackbosom Dress
AddItem(m2f, f2m, 22454, 20635); // Singed Manderville Gloves <-> Blackbosom Dress Gloves
AddItem(m2f, f2m, 22455, 10035, true, false); // Stained Manderville Bottoms -> The Emperor's New Breeches
AddItem(m2f, f2m, 22456, 20636); // Scuffed Manderville Gaiters <-> Blackbosom Boots
AddItem(m2f, f2m, 23013, 21302); // Doman Liege's Dogi <-> Scion Liberator's Jacket
AddItem(m2f, f2m, 23014, 21303); // Doman Liege's Kote <-> Scion Liberator's Fingerless Gloves
AddItem(m2f, f2m, 23015, 21304); // Doman Liege's Kyakui <-> Scion Liberator's Pantalettes
AddItem(m2f, f2m, 23016, 21305); // Doman Liege's Kyahan <-> Scion Liberator's Sabatons
AddItem(m2f, f2m, 09293, 21306, false); // The Emperor's New Earrings <- Scion Liberator's Earrings
AddItem(m2f, f2m, 24158, 23008, true, false); // Leal Samurai's Kasa -> Eastern Socialite's Hat
AddItem(m2f, f2m, 24159, 23009, true, false); // Leal Samurai's Dogi -> Eastern Socialite's Cheongsam
AddItem(m2f, f2m, 24160, 23010, true, false); // Leal Samurai's Tekko -> Eastern Socialite's Gloves
AddItem(m2f, f2m, 24161, 23011, true, false); // Leal Samurai's Tsutsu-hakama -> Eastern Socialite's Skirt
AddItem(m2f, f2m, 24162, 23012, true, false); // Leal Samurai's Geta -> Eastern Socialite's Boots
AddItem(m2f, f2m, 02966, 13321, false); // Reindeer Suit <- Antecedent's Attire
AddItem(m2f, f2m, 15479, 36843, false); // Swine Body <- Lyse's Leadership Attire
AddItem(m2f, f2m, 21941, 24999, false); // Ala Mhigan Gown <- Gown of Light
AddItem(m2f, f2m, 30757, 25000, false); // Southern Seas Skirt <- Skirt of Light
AddItem(m2f, f2m, 36821, 27933, false); // Archfiend Helm <- Scion Hearer's Hood
AddItem(m2f, f2m, 36822, 27934, false); // Archfiend Armor <- Scion Hearer's Coat
AddItem(m2f, f2m, 36825, 27935, false); // Archfiend Sabatons <- Scion Hearer's Shoes
AddItem(m2f, f2m, 32393, 39302, false); // Edenmete Gown of Casting <- Gaia's Attire
}
// The racial starter sets are available for all 4 slots each,
// but have no associated accessories or hats.
private static readonly uint[] RaceGenderGroup =
{
0x020054,
0x020055,
0x020056,
0x020057,
0x02005C,
0x02005D,
0x020058,
0x020059,
0x02005A,
0x02005B,
0x020101,
0x020102,
0x010255,
uint.MaxValue, // TODO: Female Hrothgar
0x0102E8,
0x010245,
};
// @Formatter:on
}

View file

@ -1,70 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Dalamud;
using Dalamud.Data;
using Dalamud.Plugin;
using Penumbra.GameData.Structs;
namespace Penumbra.GameData.Data;
public sealed class StainData : DataSharer, IReadOnlyDictionary<StainId, Stain>
{
public readonly IReadOnlyDictionary<byte, (string Name, uint Dye, bool Gloss)> Data;
public StainData(DalamudPluginInterface pluginInterface, DataManager dataManager, ClientLanguage language)
: base(pluginInterface, language, 2)
{
Data = TryCatchData("Stains", () => CreateStainData(dataManager));
}
protected override void DisposeInternal()
=> DisposeTag("Stains");
private IReadOnlyDictionary<byte, (string Name, uint Dye, bool Gloss)> CreateStainData(DataManager dataManager)
{
var stainSheet = dataManager.GetExcelSheet<Lumina.Excel.GeneratedSheets.Stain>(Language)!;
return stainSheet.Where(s => s.Color != 0 && s.Name.RawData.Length > 0)
.ToDictionary(s => (byte)s.RowId, s =>
{
var stain = new Stain(s);
return (stain.Name, stain.RgbaColor, stain.Gloss);
});
}
public IEnumerator<KeyValuePair<StainId, Stain>> GetEnumerator()
=> Data.Select(kvp
=> new KeyValuePair<StainId, Stain>(new StainId(kvp.Key), new Stain(kvp.Value.Name, kvp.Value.Dye, kvp.Key, kvp.Value.Gloss)))
.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public int Count
=> Data.Count;
public bool ContainsKey(StainId key)
=> Data.ContainsKey(key.Value);
public bool TryGetValue(StainId key, out Stain value)
{
if (!Data.TryGetValue(key.Value, out var data))
{
value = default;
return false;
}
value = new Stain(data.Name, data.Dye, key.Value, data.Gloss);
return true;
}
public Stain this[StainId key]
=> TryGetValue(key, out var data) ? data : throw new ArgumentOutOfRangeException(nameof(key));
public IEnumerable<StainId> Keys
=> Data.Keys.Select(k => new StainId(k));
public IEnumerable<Stain> Values
=> Data.Select(kvp => new Stain(kvp.Value.Name, kvp.Value.Dye, kvp.Key, kvp.Value.Gloss));
}

View file

@ -1,57 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Dalamud;
using Dalamud.Plugin;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using PseudoEquipItem = System.ValueTuple<string, ulong, ushort, ushort, ushort, byte, byte>;
namespace Penumbra.GameData.Data;
internal sealed class WeaponIdentificationList : KeyList<PseudoEquipItem>
{
private const string Tag = "WeaponIdentification";
private const int Version = 2;
public WeaponIdentificationList(DalamudPluginInterface pi, ClientLanguage language, ItemData data)
: base(pi, Tag, language, Version, CreateWeaponList(data))
{ }
public IEnumerable<EquipItem> Between(SetId modelId)
=> Between(ToKey(modelId, 0, 0), ToKey(modelId, 0xFFFF, 0xFF)).Select(e => (EquipItem)e);
public IEnumerable<EquipItem> Between(SetId modelId, WeaponType type, byte variant = 0)
{
if (type == 0)
return Between(ToKey(modelId, 0, 0), ToKey(modelId, 0xFFFF, 0xFF)).Select(e => (EquipItem)e);
if (variant == 0)
return Between(ToKey(modelId, type, 0), ToKey(modelId, type, 0xFF)).Select(e => (EquipItem)e);
return Between(ToKey(modelId, type, variant), ToKey(modelId, type, variant)).Select(e => (EquipItem)e);
}
public void Dispose(DalamudPluginInterface pi, ClientLanguage language)
=> DataSharer.DisposeTag(pi, Tag, language, Version);
public static ulong ToKey(SetId modelId, WeaponType type, byte variant)
=> ((ulong)modelId << 32) | ((ulong)type << 16) | variant;
public static ulong ToKey(EquipItem i)
=> ToKey(i.ModelId, i.WeaponType, i.Variant);
protected override IEnumerable<ulong> ToKeys(PseudoEquipItem data)
{
yield return ToKey(data);
}
protected override bool ValidKey(ulong key)
=> key != 0;
protected override int ValueKeySelector(PseudoEquipItem data)
=> (int)data.Item2;
private static IEnumerable<PseudoEquipItem> CreateWeaponList(ItemData data)
=> data.Where(kvp => !kvp.Key.IsEquipment() && !kvp.Key.IsAccessory())
.SelectMany(kvp => kvp.Value)
.Select(i => (PseudoEquipItem)i);
}

View file

@ -1,63 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
namespace Penumbra.GameData.Enums;
public enum BodySlot : byte
{
Unknown,
Hair,
Face,
Tail,
Body,
Zear,
}
public static class BodySlotEnumExtension
{
public static string ToSuffix( this BodySlot value )
=> value switch
{
BodySlot.Zear => "zear",
BodySlot.Face => "face",
BodySlot.Hair => "hair",
BodySlot.Body => "body",
BodySlot.Tail => "tail",
_ => throw new InvalidEnumArgumentException(),
};
public static char ToAbbreviation(this BodySlot value)
=> value switch
{
BodySlot.Hair => 'h',
BodySlot.Face => 'f',
BodySlot.Tail => 't',
BodySlot.Body => 'b',
BodySlot.Zear => 'z',
_ => throw new InvalidEnumArgumentException(),
};
public static CustomizationType ToCustomizationType(this BodySlot value)
=> value switch
{
BodySlot.Hair => CustomizationType.Hair,
BodySlot.Face => CustomizationType.Face,
BodySlot.Tail => CustomizationType.Tail,
BodySlot.Body => CustomizationType.Body,
BodySlot.Zear => CustomizationType.Zear,
_ => throw new ArgumentOutOfRangeException(nameof(value), value, null)
};
}
public static partial class Names
{
public static readonly Dictionary< string, BodySlot > StringToBodySlot = new()
{
{ BodySlot.Zear.ToSuffix(), BodySlot.Zear },
{ BodySlot.Face.ToSuffix(), BodySlot.Face },
{ BodySlot.Hair.ToSuffix(), BodySlot.Hair },
{ BodySlot.Body.ToSuffix(), BodySlot.Body },
{ BodySlot.Tail.ToSuffix(), BodySlot.Tail },
};
}

View file

@ -1,33 +0,0 @@
using System;
using Dalamud.Data;
using Lumina.Excel.GeneratedSheets;
using Penumbra.Api.Enums;
using Action = Lumina.Excel.GeneratedSheets.Action;
namespace Penumbra.GameData.Enums;
public static class ChangedItemExtensions
{
public static (ChangedItemType, uint) ChangedItemToTypeAndId(object? item)
{
return item switch
{
null => (ChangedItemType.None, 0),
Item i => (ChangedItemType.Item, i.RowId),
Action a => (ChangedItemType.Action, a.RowId),
_ => (ChangedItemType.Customization, 0),
};
}
public static object? GetObject(this ChangedItemType type, DataManager manager, uint id)
{
return type switch
{
ChangedItemType.None => null,
ChangedItemType.Item => manager.GetExcelSheet<Item>()?.GetRow(id),
ChangedItemType.Action => manager.GetExcelSheet<Action>()?.GetRow(id),
ChangedItemType.Customization => null,
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null),
};
}
}

View file

@ -1,55 +0,0 @@
using System.Collections.Generic;
using System.ComponentModel;
namespace Penumbra.GameData.Enums
{
public enum CustomizationType : byte
{
Unknown,
Body,
Tail,
Face,
Iris,
Accessory,
Hair,
Zear,
DecalFace,
DecalEquip,
Skin,
Etc,
}
public static class CustomizationTypeEnumExtension
{
public static string ToSuffix( this CustomizationType value )
{
return value switch
{
CustomizationType.Body => "top",
CustomizationType.Face => "fac",
CustomizationType.Iris => "iri",
CustomizationType.Accessory => "acc",
CustomizationType.Hair => "hir",
CustomizationType.Tail => "til",
CustomizationType.Zear => "zer",
CustomizationType.Etc => "etc",
_ => throw new InvalidEnumArgumentException(),
};
}
}
public static partial class Names
{
public static readonly Dictionary< string, CustomizationType > SuffixToCustomizationType = new()
{
{ CustomizationType.Body.ToSuffix(), CustomizationType.Body },
{ CustomizationType.Face.ToSuffix(), CustomizationType.Face },
{ CustomizationType.Iris.ToSuffix(), CustomizationType.Iris },
{ CustomizationType.Accessory.ToSuffix(), CustomizationType.Accessory },
{ CustomizationType.Hair.ToSuffix(), CustomizationType.Hair },
{ CustomizationType.Tail.ToSuffix(), CustomizationType.Tail },
{ CustomizationType.Zear.ToSuffix(), CustomizationType.Zear },
{ CustomizationType.Etc.ToSuffix(), CustomizationType.Etc },
};
}
}

View file

@ -1,232 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Penumbra.GameData.Enums;
public enum EquipSlot : byte
{
Unknown = 0,
MainHand = 1,
OffHand = 2,
Head = 3,
Body = 4,
Hands = 5,
Belt = 6,
Legs = 7,
Feet = 8,
Ears = 9,
Neck = 10,
Wrists = 11,
RFinger = 12,
BothHand = 13,
LFinger = 14, // Not officially existing, means "weapon could be equipped in either hand" for the game.
HeadBody = 15,
BodyHandsLegsFeet = 16,
SoulCrystal = 17,
LegsFeet = 18,
FullBody = 19,
BodyHands = 20,
BodyLegsFeet = 21,
ChestHands = 22,
Nothing = 23,
All = 24, // Not officially existing
}
public static class EquipSlotExtensions
{
public static EquipSlot ToEquipSlot(this uint value)
=> value switch
{
0 => EquipSlot.Head,
1 => EquipSlot.Body,
2 => EquipSlot.Hands,
3 => EquipSlot.Legs,
4 => EquipSlot.Feet,
5 => EquipSlot.Ears,
6 => EquipSlot.Neck,
7 => EquipSlot.Wrists,
8 => EquipSlot.RFinger,
9 => EquipSlot.LFinger,
10 => EquipSlot.MainHand,
11 => EquipSlot.OffHand,
_ => EquipSlot.Unknown,
};
public static uint ToIndex(this EquipSlot slot)
=> slot switch
{
EquipSlot.Head => 0,
EquipSlot.Body => 1,
EquipSlot.Hands => 2,
EquipSlot.Legs => 3,
EquipSlot.Feet => 4,
EquipSlot.Ears => 5,
EquipSlot.Neck => 6,
EquipSlot.Wrists => 7,
EquipSlot.RFinger => 8,
EquipSlot.LFinger => 9,
EquipSlot.MainHand => 10,
EquipSlot.OffHand => 11,
_ => uint.MaxValue,
};
public static string ToSuffix(this EquipSlot value)
{
return value switch
{
EquipSlot.Head => "met",
EquipSlot.Hands => "glv",
EquipSlot.Legs => "dwn",
EquipSlot.Feet => "sho",
EquipSlot.Body => "top",
EquipSlot.Ears => "ear",
EquipSlot.Neck => "nek",
EquipSlot.RFinger => "rir",
EquipSlot.LFinger => "ril",
EquipSlot.Wrists => "wrs",
_ => "unk",
};
}
public static EquipSlot ToSlot(this EquipSlot value)
{
return value switch
{
EquipSlot.MainHand => EquipSlot.MainHand,
EquipSlot.OffHand => EquipSlot.OffHand,
EquipSlot.Head => EquipSlot.Head,
EquipSlot.Body => EquipSlot.Body,
EquipSlot.Hands => EquipSlot.Hands,
EquipSlot.Belt => EquipSlot.Belt,
EquipSlot.Legs => EquipSlot.Legs,
EquipSlot.Feet => EquipSlot.Feet,
EquipSlot.Ears => EquipSlot.Ears,
EquipSlot.Neck => EquipSlot.Neck,
EquipSlot.Wrists => EquipSlot.Wrists,
EquipSlot.RFinger => EquipSlot.RFinger,
EquipSlot.BothHand => EquipSlot.MainHand,
EquipSlot.LFinger => EquipSlot.RFinger,
EquipSlot.HeadBody => EquipSlot.Body,
EquipSlot.BodyHandsLegsFeet => EquipSlot.Body,
EquipSlot.SoulCrystal => EquipSlot.SoulCrystal,
EquipSlot.LegsFeet => EquipSlot.Legs,
EquipSlot.FullBody => EquipSlot.Body,
EquipSlot.BodyHands => EquipSlot.Body,
EquipSlot.BodyLegsFeet => EquipSlot.Body,
EquipSlot.ChestHands => EquipSlot.Body,
_ => EquipSlot.Unknown,
};
}
public static string ToName(this EquipSlot value)
{
return value switch
{
EquipSlot.Head => "Head",
EquipSlot.Hands => "Hands",
EquipSlot.Legs => "Legs",
EquipSlot.Feet => "Feet",
EquipSlot.Body => "Body",
EquipSlot.Ears => "Earrings",
EquipSlot.Neck => "Necklace",
EquipSlot.RFinger => "Right Ring",
EquipSlot.LFinger => "Left Ring",
EquipSlot.Wrists => "Bracelets",
EquipSlot.MainHand => "Primary Weapon",
EquipSlot.OffHand => "Secondary Weapon",
EquipSlot.Belt => "Belt",
EquipSlot.BothHand => "Primary Weapon",
EquipSlot.HeadBody => "Head and Body",
EquipSlot.BodyHandsLegsFeet => "Costume",
EquipSlot.SoulCrystal => "Soul Crystal",
EquipSlot.LegsFeet => "Bottom",
EquipSlot.FullBody => "Costume",
EquipSlot.BodyHands => "Top",
EquipSlot.BodyLegsFeet => "Costume",
EquipSlot.All => "Costume",
_ => "Unknown",
};
}
public static bool IsEquipment(this EquipSlot value)
{
return value switch
{
EquipSlot.Head => true,
EquipSlot.Hands => true,
EquipSlot.Legs => true,
EquipSlot.Feet => true,
EquipSlot.Body => true,
_ => false,
};
}
public static bool IsAccessory(this EquipSlot value)
{
return value switch
{
EquipSlot.Ears => true,
EquipSlot.Neck => true,
EquipSlot.RFinger => true,
EquipSlot.LFinger => true,
EquipSlot.Wrists => true,
_ => false,
};
}
public static bool IsEquipmentPiece(this EquipSlot value)
{
return value switch
{
// Accessories
EquipSlot.RFinger => true,
EquipSlot.Wrists => true,
EquipSlot.Ears => true,
EquipSlot.Neck => true,
// Equipment
EquipSlot.Head => true,
EquipSlot.Body => true,
EquipSlot.Hands => true,
EquipSlot.Legs => true,
EquipSlot.Feet => true,
EquipSlot.BodyHands => true,
EquipSlot.BodyHandsLegsFeet => true,
EquipSlot.BodyLegsFeet => true,
EquipSlot.FullBody => true,
EquipSlot.HeadBody => true,
EquipSlot.LegsFeet => true,
EquipSlot.ChestHands => true,
_ => false,
};
}
public static readonly EquipSlot[] EquipmentSlots = Enum.GetValues<EquipSlot>().Where(e => e.IsEquipment()).ToArray();
public static readonly EquipSlot[] AccessorySlots = Enum.GetValues<EquipSlot>().Where(e => e.IsAccessory()).ToArray();
public static readonly EquipSlot[] EqdpSlots = EquipmentSlots.Concat(AccessorySlots).ToArray();
public static readonly EquipSlot[] WeaponSlots =
{
EquipSlot.MainHand,
EquipSlot.OffHand,
};
public static readonly EquipSlot[] FullSlots = WeaponSlots.Concat(EqdpSlots).ToArray();
}
public static partial class Names
{
public static readonly Dictionary<string, EquipSlot> SuffixToEquipSlot = new()
{
{ EquipSlot.Head.ToSuffix(), EquipSlot.Head },
{ EquipSlot.Hands.ToSuffix(), EquipSlot.Hands },
{ EquipSlot.Legs.ToSuffix(), EquipSlot.Legs },
{ EquipSlot.Feet.ToSuffix(), EquipSlot.Feet },
{ EquipSlot.Body.ToSuffix(), EquipSlot.Body },
{ EquipSlot.Ears.ToSuffix(), EquipSlot.Ears },
{ EquipSlot.Neck.ToSuffix(), EquipSlot.Neck },
{ EquipSlot.RFinger.ToSuffix(), EquipSlot.RFinger },
{ EquipSlot.LFinger.ToSuffix(), EquipSlot.LFinger },
{ EquipSlot.Wrists.ToSuffix(), EquipSlot.Wrists },
};
}

View file

@ -1,44 +0,0 @@
using System.Collections.Generic;
namespace Penumbra.GameData.Enums;
public enum FileType : byte
{
Unknown,
Sound,
Imc,
Vfx,
Animation,
Pap,
MetaInfo,
Material,
Texture,
Model,
Shader,
Font,
Environment,
}
public static partial class Names
{
public static readonly Dictionary< string, FileType > ExtensionToFileType = new()
{
{ ".mdl", FileType.Model },
{ ".tex", FileType.Texture },
{ ".mtrl", FileType.Material },
{ ".atex", FileType.Animation },
{ ".avfx", FileType.Vfx },
{ ".scd", FileType.Sound },
{ ".imc", FileType.Imc },
{ ".pap", FileType.Pap },
{ ".eqp", FileType.MetaInfo },
{ ".eqdp", FileType.MetaInfo },
{ ".est", FileType.MetaInfo },
{ ".exd", FileType.MetaInfo },
{ ".exh", FileType.MetaInfo },
{ ".shpk", FileType.Shader },
{ ".shcd", FileType.Shader },
{ ".fdt", FileType.Font },
{ ".envb", FileType.Environment },
};
}

View file

@ -1,450 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Lumina.Excel.GeneratedSheets;
namespace Penumbra.GameData.Enums;
public enum FullEquipType : byte
{
Unknown,
Head,
Body,
Hands,
Legs,
Feet,
Ears,
Neck,
Wrists,
Finger,
Fists, // PGL, MNK
FistsOff,
Sword, // GLA, PLD Main
Axe, // MRD, WAR
Bow, // ARC, BRD
BowOff,
Lance, // LNC, DRG,
Staff, // THM, BLM, CNJ, WHM
Wand, // THM, BLM, CNJ, WHM Main
Book, // ACN, SMN, SCH
Daggers, // ROG, NIN
DaggersOff,
Broadsword, // DRK,
Gun, // MCH,
GunOff,
Orrery, // AST,
OrreryOff,
Katana, // SAM
KatanaOff,
Rapier, // RDM
RapierOff,
Cane, // BLU
Gunblade, // GNB,
Glaives, // DNC,
GlaivesOff,
Scythe, // RPR,
Nouliths, // SGE
Shield, // GLA, PLD, THM, BLM, CNJ, WHM Off
Saw, // CRP
CrossPeinHammer, // BSM
RaisingHammer, // ARM
LapidaryHammer, // GSM
Knife, // LTW
Needle, // WVR
Alembic, // ALC
Frypan, // CUL
Pickaxe, // MIN
Hatchet, // BTN
FishingRod, // FSH
ClawHammer, // CRP Off
File, // BSM Off
Pliers, // ARM Off
GrindingWheel, // GSM Off
Awl, // LTW Off
SpinningWheel, // WVR Off
Mortar, // ALC Off
CulinaryKnife, // CUL Off
Sledgehammer, // MIN Off
GardenScythe, // BTN Off
Gig, // FSH Off
}
public static class FullEquipTypeExtensions
{
internal static FullEquipType ToEquipType(this Item item)
{
var slot = (EquipSlot)item.EquipSlotCategory.Row;
var weapon = (WeaponCategory)item.ItemUICategory.Row;
return slot.ToEquipType(weapon);
}
public static bool IsWeapon(this FullEquipType type)
=> type switch
{
FullEquipType.Fists => true,
FullEquipType.Sword => true,
FullEquipType.Axe => true,
FullEquipType.Bow => true,
FullEquipType.Lance => true,
FullEquipType.Staff => true,
FullEquipType.Wand => true,
FullEquipType.Book => true,
FullEquipType.Daggers => true,
FullEquipType.Broadsword => true,
FullEquipType.Gun => true,
FullEquipType.Orrery => true,
FullEquipType.Katana => true,
FullEquipType.Rapier => true,
FullEquipType.Cane => true,
FullEquipType.Gunblade => true,
FullEquipType.Glaives => true,
FullEquipType.Scythe => true,
FullEquipType.Nouliths => true,
FullEquipType.Shield => true,
_ => false,
};
public static bool IsTool(this FullEquipType type)
=> type switch
{
FullEquipType.Saw => true,
FullEquipType.CrossPeinHammer => true,
FullEquipType.RaisingHammer => true,
FullEquipType.LapidaryHammer => true,
FullEquipType.Knife => true,
FullEquipType.Needle => true,
FullEquipType.Alembic => true,
FullEquipType.Frypan => true,
FullEquipType.Pickaxe => true,
FullEquipType.Hatchet => true,
FullEquipType.FishingRod => true,
FullEquipType.ClawHammer => true,
FullEquipType.File => true,
FullEquipType.Pliers => true,
FullEquipType.GrindingWheel => true,
FullEquipType.Awl => true,
FullEquipType.SpinningWheel => true,
FullEquipType.Mortar => true,
FullEquipType.CulinaryKnife => true,
FullEquipType.Sledgehammer => true,
FullEquipType.GardenScythe => true,
FullEquipType.Gig => true,
_ => false,
};
public static bool IsEquipment(this FullEquipType type)
=> type switch
{
FullEquipType.Head => true,
FullEquipType.Body => true,
FullEquipType.Hands => true,
FullEquipType.Legs => true,
FullEquipType.Feet => true,
_ => false,
};
public static bool IsAccessory(this FullEquipType type)
=> type switch
{
FullEquipType.Ears => true,
FullEquipType.Neck => true,
FullEquipType.Wrists => true,
FullEquipType.Finger => true,
_ => false,
};
public static string ToName(this FullEquipType type)
=> type switch
{
FullEquipType.Head => EquipSlot.Head.ToName(),
FullEquipType.Body => EquipSlot.Body.ToName(),
FullEquipType.Hands => EquipSlot.Hands.ToName(),
FullEquipType.Legs => EquipSlot.Legs.ToName(),
FullEquipType.Feet => EquipSlot.Feet.ToName(),
FullEquipType.Ears => EquipSlot.Ears.ToName(),
FullEquipType.Neck => EquipSlot.Neck.ToName(),
FullEquipType.Wrists => EquipSlot.Wrists.ToName(),
FullEquipType.Finger => "Ring",
FullEquipType.Fists => "Fist Weapon",
FullEquipType.FistsOff => "Fist Weapon (Offhand)",
FullEquipType.Sword => "Sword",
FullEquipType.Axe => "Axe",
FullEquipType.Bow => "Bow",
FullEquipType.BowOff => "Quiver",
FullEquipType.Lance => "Lance",
FullEquipType.Staff => "Staff",
FullEquipType.Wand => "Mace",
FullEquipType.Book => "Book",
FullEquipType.Daggers => "Dagger",
FullEquipType.DaggersOff => "Dagger (Offhand)",
FullEquipType.Broadsword => "Broadsword",
FullEquipType.Gun => "Gun",
FullEquipType.GunOff => "Aetherotransformer",
FullEquipType.Orrery => "Orrery",
FullEquipType.OrreryOff => "Card Holder",
FullEquipType.Katana => "Katana",
FullEquipType.KatanaOff => "Sheathe",
FullEquipType.Rapier => "Rapier",
FullEquipType.RapierOff => "Focus",
FullEquipType.Cane => "Cane",
FullEquipType.Gunblade => "Gunblade",
FullEquipType.Glaives => "Glaive",
FullEquipType.GlaivesOff => "Glaive (Offhand)",
FullEquipType.Scythe => "Scythe",
FullEquipType.Nouliths => "Nouliths",
FullEquipType.Shield => "Shield",
FullEquipType.Saw => "Saw",
FullEquipType.CrossPeinHammer => "Cross Pein Hammer",
FullEquipType.RaisingHammer => "Raising Hammer",
FullEquipType.LapidaryHammer => "Lapidary Hammer",
FullEquipType.Knife => "Round Knife",
FullEquipType.Needle => "Needle",
FullEquipType.Alembic => "Alembic",
FullEquipType.Frypan => "Frypan",
FullEquipType.Pickaxe => "Pickaxe",
FullEquipType.Hatchet => "Hatchet",
FullEquipType.FishingRod => "Fishing Rod",
FullEquipType.ClawHammer => "Clawhammer",
FullEquipType.File => "File",
FullEquipType.Pliers => "Pliers",
FullEquipType.GrindingWheel => "Grinding Wheel",
FullEquipType.Awl => "Awl",
FullEquipType.SpinningWheel => "Spinning Wheel",
FullEquipType.Mortar => "Mortar",
FullEquipType.CulinaryKnife => "Culinary Knife",
FullEquipType.Sledgehammer => "Sledgehammer",
FullEquipType.GardenScythe => "Garden Scythe",
FullEquipType.Gig => "Gig",
_ => "Unknown",
};
public static EquipSlot ToSlot(this FullEquipType type)
=> type switch
{
FullEquipType.Head => EquipSlot.Head,
FullEquipType.Body => EquipSlot.Body,
FullEquipType.Hands => EquipSlot.Hands,
FullEquipType.Legs => EquipSlot.Legs,
FullEquipType.Feet => EquipSlot.Feet,
FullEquipType.Ears => EquipSlot.Ears,
FullEquipType.Neck => EquipSlot.Neck,
FullEquipType.Wrists => EquipSlot.Wrists,
FullEquipType.Finger => EquipSlot.RFinger,
FullEquipType.Fists => EquipSlot.MainHand,
FullEquipType.FistsOff => EquipSlot.OffHand,
FullEquipType.Sword => EquipSlot.MainHand,
FullEquipType.Axe => EquipSlot.MainHand,
FullEquipType.Bow => EquipSlot.MainHand,
FullEquipType.BowOff => EquipSlot.OffHand,
FullEquipType.Lance => EquipSlot.MainHand,
FullEquipType.Staff => EquipSlot.MainHand,
FullEquipType.Wand => EquipSlot.MainHand,
FullEquipType.Book => EquipSlot.MainHand,
FullEquipType.Daggers => EquipSlot.MainHand,
FullEquipType.DaggersOff => EquipSlot.OffHand,
FullEquipType.Broadsword => EquipSlot.MainHand,
FullEquipType.Gun => EquipSlot.MainHand,
FullEquipType.GunOff => EquipSlot.OffHand,
FullEquipType.Orrery => EquipSlot.MainHand,
FullEquipType.OrreryOff => EquipSlot.OffHand,
FullEquipType.Katana => EquipSlot.MainHand,
FullEquipType.KatanaOff => EquipSlot.OffHand,
FullEquipType.Rapier => EquipSlot.MainHand,
FullEquipType.RapierOff => EquipSlot.OffHand,
FullEquipType.Cane => EquipSlot.MainHand,
FullEquipType.Gunblade => EquipSlot.MainHand,
FullEquipType.Glaives => EquipSlot.MainHand,
FullEquipType.GlaivesOff => EquipSlot.OffHand,
FullEquipType.Scythe => EquipSlot.MainHand,
FullEquipType.Nouliths => EquipSlot.MainHand,
FullEquipType.Shield => EquipSlot.OffHand,
FullEquipType.Saw => EquipSlot.MainHand,
FullEquipType.CrossPeinHammer => EquipSlot.MainHand,
FullEquipType.RaisingHammer => EquipSlot.MainHand,
FullEquipType.LapidaryHammer => EquipSlot.MainHand,
FullEquipType.Knife => EquipSlot.MainHand,
FullEquipType.Needle => EquipSlot.MainHand,
FullEquipType.Alembic => EquipSlot.MainHand,
FullEquipType.Frypan => EquipSlot.MainHand,
FullEquipType.Pickaxe => EquipSlot.MainHand,
FullEquipType.Hatchet => EquipSlot.MainHand,
FullEquipType.FishingRod => EquipSlot.MainHand,
FullEquipType.ClawHammer => EquipSlot.OffHand,
FullEquipType.File => EquipSlot.OffHand,
FullEquipType.Pliers => EquipSlot.OffHand,
FullEquipType.GrindingWheel => EquipSlot.OffHand,
FullEquipType.Awl => EquipSlot.OffHand,
FullEquipType.SpinningWheel => EquipSlot.OffHand,
FullEquipType.Mortar => EquipSlot.OffHand,
FullEquipType.CulinaryKnife => EquipSlot.OffHand,
FullEquipType.Sledgehammer => EquipSlot.OffHand,
FullEquipType.GardenScythe => EquipSlot.OffHand,
FullEquipType.Gig => EquipSlot.OffHand,
_ => EquipSlot.Unknown,
};
public static FullEquipType ToEquipType(this EquipSlot slot, WeaponCategory category = WeaponCategory.Unknown, bool mainhand = true)
=> slot switch
{
EquipSlot.Head => FullEquipType.Head,
EquipSlot.Body => FullEquipType.Body,
EquipSlot.Hands => FullEquipType.Hands,
EquipSlot.Legs => FullEquipType.Legs,
EquipSlot.Feet => FullEquipType.Feet,
EquipSlot.Ears => FullEquipType.Ears,
EquipSlot.Neck => FullEquipType.Neck,
EquipSlot.Wrists => FullEquipType.Wrists,
EquipSlot.RFinger => FullEquipType.Finger,
EquipSlot.LFinger => FullEquipType.Finger,
EquipSlot.HeadBody => FullEquipType.Body,
EquipSlot.BodyHandsLegsFeet => FullEquipType.Body,
EquipSlot.LegsFeet => FullEquipType.Legs,
EquipSlot.FullBody => FullEquipType.Body,
EquipSlot.BodyHands => FullEquipType.Body,
EquipSlot.BodyLegsFeet => FullEquipType.Body,
EquipSlot.ChestHands => FullEquipType.Body,
EquipSlot.MainHand => category.ToEquipType(mainhand),
EquipSlot.OffHand => category.ToEquipType(mainhand),
EquipSlot.BothHand => category.ToEquipType(mainhand),
_ => FullEquipType.Unknown,
};
public static FullEquipType ToEquipType(this WeaponCategory category, bool mainhand = true)
=> category switch
{
WeaponCategory.Pugilist when mainhand => FullEquipType.Fists,
WeaponCategory.Pugilist => FullEquipType.FistsOff,
WeaponCategory.Gladiator => FullEquipType.Sword,
WeaponCategory.Marauder => FullEquipType.Axe,
WeaponCategory.Archer when mainhand => FullEquipType.Bow,
WeaponCategory.Archer => FullEquipType.BowOff,
WeaponCategory.Lancer => FullEquipType.Lance,
WeaponCategory.Thaumaturge1 => FullEquipType.Wand,
WeaponCategory.Thaumaturge2 => FullEquipType.Staff,
WeaponCategory.Conjurer1 => FullEquipType.Wand,
WeaponCategory.Conjurer2 => FullEquipType.Staff,
WeaponCategory.Arcanist => FullEquipType.Book,
WeaponCategory.Shield => FullEquipType.Shield,
WeaponCategory.CarpenterMain => FullEquipType.Saw,
WeaponCategory.CarpenterOff => FullEquipType.ClawHammer,
WeaponCategory.BlacksmithMain => FullEquipType.CrossPeinHammer,
WeaponCategory.BlacksmithOff => FullEquipType.File,
WeaponCategory.ArmorerMain => FullEquipType.RaisingHammer,
WeaponCategory.ArmorerOff => FullEquipType.Pliers,
WeaponCategory.GoldsmithMain => FullEquipType.LapidaryHammer,
WeaponCategory.GoldsmithOff => FullEquipType.GrindingWheel,
WeaponCategory.LeatherworkerMain => FullEquipType.Knife,
WeaponCategory.LeatherworkerOff => FullEquipType.Awl,
WeaponCategory.WeaverMain => FullEquipType.Needle,
WeaponCategory.WeaverOff => FullEquipType.SpinningWheel,
WeaponCategory.AlchemistMain => FullEquipType.Alembic,
WeaponCategory.AlchemistOff => FullEquipType.Mortar,
WeaponCategory.CulinarianMain => FullEquipType.Frypan,
WeaponCategory.CulinarianOff => FullEquipType.CulinaryKnife,
WeaponCategory.MinerMain => FullEquipType.Pickaxe,
WeaponCategory.MinerOff => FullEquipType.Sledgehammer,
WeaponCategory.BotanistMain => FullEquipType.Hatchet,
WeaponCategory.BotanistOff => FullEquipType.GardenScythe,
WeaponCategory.FisherMain => FullEquipType.FishingRod,
WeaponCategory.FisherOff => FullEquipType.Gig,
WeaponCategory.Rogue when mainhand => FullEquipType.Daggers,
WeaponCategory.Rogue => FullEquipType.DaggersOff,
WeaponCategory.DarkKnight => FullEquipType.Broadsword,
WeaponCategory.Machinist when mainhand => FullEquipType.Gun,
WeaponCategory.Machinist => FullEquipType.GunOff,
WeaponCategory.Astrologian when mainhand => FullEquipType.Orrery,
WeaponCategory.Astrologian => FullEquipType.OrreryOff,
WeaponCategory.Samurai when mainhand => FullEquipType.Katana,
WeaponCategory.Samurai => FullEquipType.KatanaOff,
WeaponCategory.RedMage when mainhand => FullEquipType.Rapier,
WeaponCategory.RedMage => FullEquipType.RapierOff,
WeaponCategory.Scholar => FullEquipType.Book,
WeaponCategory.BlueMage => FullEquipType.Cane,
WeaponCategory.Gunbreaker => FullEquipType.Gunblade,
WeaponCategory.Dancer when mainhand => FullEquipType.Glaives,
WeaponCategory.Dancer => FullEquipType.GlaivesOff,
WeaponCategory.Reaper => FullEquipType.Scythe,
WeaponCategory.Sage => FullEquipType.Nouliths,
_ => FullEquipType.Unknown,
};
public static FullEquipType ValidOffhand(this FullEquipType type)
=> type switch
{
FullEquipType.Fists => FullEquipType.FistsOff,
FullEquipType.Sword => FullEquipType.Shield,
FullEquipType.Wand => FullEquipType.Shield,
FullEquipType.Daggers => FullEquipType.DaggersOff,
FullEquipType.Gun => FullEquipType.GunOff,
FullEquipType.Orrery => FullEquipType.OrreryOff,
FullEquipType.Rapier => FullEquipType.RapierOff,
FullEquipType.Glaives => FullEquipType.GlaivesOff,
FullEquipType.Bow => FullEquipType.BowOff,
FullEquipType.Katana => FullEquipType.KatanaOff,
_ => FullEquipType.Unknown,
};
public static FullEquipType Offhand(this FullEquipType type)
=> type switch
{
FullEquipType.Fists => FullEquipType.FistsOff,
FullEquipType.Sword => FullEquipType.Shield,
FullEquipType.Wand => FullEquipType.Shield,
FullEquipType.Daggers => FullEquipType.DaggersOff,
FullEquipType.Gun => FullEquipType.GunOff,
FullEquipType.Orrery => FullEquipType.OrreryOff,
FullEquipType.Rapier => FullEquipType.RapierOff,
FullEquipType.Glaives => FullEquipType.GlaivesOff,
FullEquipType.Bow => FullEquipType.BowOff,
FullEquipType.Katana => FullEquipType.KatanaOff,
FullEquipType.Saw => FullEquipType.ClawHammer,
FullEquipType.CrossPeinHammer => FullEquipType.File,
FullEquipType.RaisingHammer => FullEquipType.Pliers,
FullEquipType.LapidaryHammer => FullEquipType.GrindingWheel,
FullEquipType.Knife => FullEquipType.Awl,
FullEquipType.Needle => FullEquipType.SpinningWheel,
FullEquipType.Alembic => FullEquipType.Mortar,
FullEquipType.Frypan => FullEquipType.CulinaryKnife,
FullEquipType.Pickaxe => FullEquipType.Sledgehammer,
FullEquipType.Hatchet => FullEquipType.GardenScythe,
FullEquipType.FishingRod => FullEquipType.Gig,
_ => FullEquipType.Unknown,
};
internal static string OffhandTypeSuffix(this FullEquipType type)
=> type switch
{
FullEquipType.FistsOff => " (Offhand)",
FullEquipType.DaggersOff => " (Offhand)",
FullEquipType.GunOff => " (Aetherotransformer)",
FullEquipType.OrreryOff => " (Card Holder)",
FullEquipType.RapierOff => " (Focus)",
FullEquipType.GlaivesOff => " (Offhand)",
FullEquipType.BowOff => " (Quiver)",
FullEquipType.KatanaOff => " (Sheathe)",
_ => string.Empty,
};
public static bool IsOffhandType(this FullEquipType type)
=> type.OffhandTypeSuffix().Length > 0;
public static readonly IReadOnlyList<FullEquipType> WeaponTypes
= Enum.GetValues<FullEquipType>().Where(v => v.IsWeapon()).ToArray();
public static readonly IReadOnlyList<FullEquipType> ToolTypes
= Enum.GetValues<FullEquipType>().Where(v => v.IsTool()).ToArray();
public static readonly IReadOnlyList<FullEquipType> EquipmentTypes
= Enum.GetValues<FullEquipType>().Where(v => v.IsEquipment()).ToArray();
public static readonly IReadOnlyList<FullEquipType> AccessoryTypes
= Enum.GetValues<FullEquipType>().Where(v => v.IsAccessory()).ToArray();
public static readonly IReadOnlyList<FullEquipType> OffhandTypes
= Enum.GetValues<FullEquipType>().Where(v => v.OffhandTypeSuffix().Length > 0).ToArray();
}

View file

@ -1,26 +0,0 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
namespace Penumbra.GameData.Enums;
public static class ModelTypeExtensions
{
public static string ToName(this CharacterBase.ModelType type)
=> type switch
{
CharacterBase.ModelType.DemiHuman => "Demihuman",
CharacterBase.ModelType.Monster => "Monster",
CharacterBase.ModelType.Human => "Human",
CharacterBase.ModelType.Weapon => "Weapon",
_ => string.Empty,
};
public static CharacterBase.ModelType ToModelType(this ObjectType type)
=> type switch
{
ObjectType.DemiHuman => CharacterBase.ModelType.DemiHuman,
ObjectType.Monster => CharacterBase.ModelType.Monster,
ObjectType.Character => CharacterBase.ModelType.Human,
ObjectType.Weapon => CharacterBase.ModelType.Weapon,
_ => 0,
};
}

View file

@ -1,53 +0,0 @@
namespace Penumbra.GameData.Enums;
public enum ObjectType : byte
{
Unknown,
Vfx,
DemiHuman,
Accessory,
World,
Housing,
Monster,
Icon,
LoadingScreen,
Map,
Interface,
Equipment,
Character,
Weapon,
Font,
}
public static class ObjectTypeExtensions
{
public static string ToName( this ObjectType type )
=> type switch
{
ObjectType.Vfx => "Visual Effect",
ObjectType.DemiHuman => "Demi Human",
ObjectType.Accessory => "Accessory",
ObjectType.World => "Doodad",
ObjectType.Housing => "Housing Object",
ObjectType.Monster => "Monster",
ObjectType.Icon => "Icon",
ObjectType.LoadingScreen => "Loading Screen",
ObjectType.Map => "Map",
ObjectType.Interface => "UI Element",
ObjectType.Equipment => "Equipment",
ObjectType.Character => "Character",
ObjectType.Weapon => "Weapon",
ObjectType.Font => "Font",
_ => "Unknown",
};
public static readonly ObjectType[] ValidImcTypes =
{
ObjectType.Equipment,
ObjectType.Accessory,
ObjectType.DemiHuman,
ObjectType.Monster,
ObjectType.Weapon,
};
}

View file

@ -1,510 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using static Penumbra.GameData.Enums.GenderRace;
namespace Penumbra.GameData.Enums;
public enum Race : byte
{
Unknown,
Hyur,
Elezen,
Lalafell,
Miqote,
Roegadyn,
AuRa,
Hrothgar,
Viera,
}
public enum Gender : byte
{
Unknown,
Male,
Female,
MaleNpc,
FemaleNpc,
}
public enum ModelRace : byte
{
Unknown,
Midlander,
Highlander,
Elezen,
Lalafell,
Miqote,
Roegadyn,
AuRa,
Hrothgar,
Viera,
}
public enum SubRace : byte
{
Unknown,
Midlander,
Highlander,
Wildwood,
Duskwight,
Plainsfolk,
Dunesfolk,
SeekerOfTheSun,
KeeperOfTheMoon,
Seawolf,
Hellsguard,
Raen,
Xaela,
Helion,
Lost,
Rava,
Veena,
}
// The combined gender-race-npc numerical code as used by the game.
public enum GenderRace : ushort
{
Unknown = 0,
MidlanderMale = 0101,
MidlanderMaleNpc = 0104,
MidlanderFemale = 0201,
MidlanderFemaleNpc = 0204,
HighlanderMale = 0301,
HighlanderMaleNpc = 0304,
HighlanderFemale = 0401,
HighlanderFemaleNpc = 0404,
ElezenMale = 0501,
ElezenMaleNpc = 0504,
ElezenFemale = 0601,
ElezenFemaleNpc = 0604,
MiqoteMale = 0701,
MiqoteMaleNpc = 0704,
MiqoteFemale = 0801,
MiqoteFemaleNpc = 0804,
RoegadynMale = 0901,
RoegadynMaleNpc = 0904,
RoegadynFemale = 1001,
RoegadynFemaleNpc = 1004,
LalafellMale = 1101,
LalafellMaleNpc = 1104,
LalafellFemale = 1201,
LalafellFemaleNpc = 1204,
AuRaMale = 1301,
AuRaMaleNpc = 1304,
AuRaFemale = 1401,
AuRaFemaleNpc = 1404,
HrothgarMale = 1501,
HrothgarMaleNpc = 1504,
HrothgarFemale = 1601,
HrothgarFemaleNpc = 1604,
VieraMale = 1701,
VieraMaleNpc = 1704,
VieraFemale = 1801,
VieraFemaleNpc = 1804,
UnknownMaleNpc = 9104,
UnknownFemaleNpc = 9204,
}
public static class RaceEnumExtensions
{
public static Race ToRace(this ModelRace race)
{
return race switch
{
ModelRace.Unknown => Race.Unknown,
ModelRace.Midlander => Race.Hyur,
ModelRace.Highlander => Race.Hyur,
ModelRace.Elezen => Race.Elezen,
ModelRace.Lalafell => Race.Lalafell,
ModelRace.Miqote => Race.Miqote,
ModelRace.Roegadyn => Race.Roegadyn,
ModelRace.AuRa => Race.AuRa,
ModelRace.Hrothgar => Race.Hrothgar,
ModelRace.Viera => Race.Viera,
_ => throw new ArgumentOutOfRangeException(nameof(race), race, null),
};
}
public static Race ToRace(this SubRace subRace)
{
return subRace switch
{
SubRace.Unknown => Race.Unknown,
SubRace.Midlander => Race.Hyur,
SubRace.Highlander => Race.Hyur,
SubRace.Wildwood => Race.Elezen,
SubRace.Duskwight => Race.Elezen,
SubRace.Plainsfolk => Race.Lalafell,
SubRace.Dunesfolk => Race.Lalafell,
SubRace.SeekerOfTheSun => Race.Miqote,
SubRace.KeeperOfTheMoon => Race.Miqote,
SubRace.Seawolf => Race.Roegadyn,
SubRace.Hellsguard => Race.Roegadyn,
SubRace.Raen => Race.AuRa,
SubRace.Xaela => Race.AuRa,
SubRace.Helion => Race.Hrothgar,
SubRace.Lost => Race.Hrothgar,
SubRace.Rava => Race.Viera,
SubRace.Veena => Race.Viera,
_ => throw new ArgumentOutOfRangeException(nameof(subRace), subRace, null),
};
}
public static string ToName(this ModelRace modelRace)
{
return modelRace switch
{
ModelRace.Midlander => SubRace.Midlander.ToName(),
ModelRace.Highlander => SubRace.Highlander.ToName(),
ModelRace.Elezen => Race.Elezen.ToName(),
ModelRace.Lalafell => Race.Lalafell.ToName(),
ModelRace.Miqote => Race.Miqote.ToName(),
ModelRace.Roegadyn => Race.Roegadyn.ToName(),
ModelRace.AuRa => Race.AuRa.ToName(),
ModelRace.Hrothgar => Race.Hrothgar.ToName(),
ModelRace.Viera => Race.Viera.ToName(),
_ => Race.Unknown.ToName(),
};
}
public static string ToName(this Race race)
{
return race switch
{
Race.Hyur => "Hyur",
Race.Elezen => "Elezen",
Race.Lalafell => "Lalafell",
Race.Miqote => "Miqo'te",
Race.Roegadyn => "Roegadyn",
Race.AuRa => "Au Ra",
Race.Hrothgar => "Hrothgar",
Race.Viera => "Viera",
_ => "Unknown",
};
}
public static string ToName(this Gender gender)
{
return gender switch
{
Gender.Male => "Male",
Gender.Female => "Female",
Gender.MaleNpc => "Male (NPC)",
Gender.FemaleNpc => "Female (NPC)",
_ => "Unknown",
};
}
public static string ToName(this SubRace subRace)
{
return subRace switch
{
SubRace.Midlander => "Midlander",
SubRace.Highlander => "Highlander",
SubRace.Wildwood => "Wildwood",
SubRace.Duskwight => "Duskwight",
SubRace.Plainsfolk => "Plainsfolk",
SubRace.Dunesfolk => "Dunesfolk",
SubRace.SeekerOfTheSun => "Seeker Of The Sun",
SubRace.KeeperOfTheMoon => "Keeper Of The Moon",
SubRace.Seawolf => "Seawolf",
SubRace.Hellsguard => "Hellsguard",
SubRace.Raen => "Raen",
SubRace.Xaela => "Xaela",
SubRace.Helion => "Hellion",
SubRace.Lost => "Lost",
SubRace.Rava => "Rava",
SubRace.Veena => "Veena",
_ => "Unknown",
};
}
public static string ToShortName(this SubRace subRace)
{
return subRace switch
{
SubRace.SeekerOfTheSun => "Sunseeker",
SubRace.KeeperOfTheMoon => "Moonkeeper",
_ => subRace.ToName(),
};
}
public static bool FitsRace(this SubRace subRace, Race race)
=> subRace.ToRace() == race;
public static byte ToByte(this Gender gender, ModelRace modelRace)
=> (byte)((int)gender | ((int)modelRace << 3));
public static byte ToByte(this ModelRace modelRace, Gender gender)
=> gender.ToByte(modelRace);
public static byte ToByte(this GenderRace value)
{
var (gender, race) = value.Split();
return gender.ToByte(race);
}
public static (Gender, ModelRace) Split(this GenderRace value)
{
return value switch
{
Unknown => (Gender.Unknown, ModelRace.Unknown),
MidlanderMale => (Gender.Male, ModelRace.Midlander),
MidlanderMaleNpc => (Gender.MaleNpc, ModelRace.Midlander),
MidlanderFemale => (Gender.Female, ModelRace.Midlander),
MidlanderFemaleNpc => (Gender.FemaleNpc, ModelRace.Midlander),
HighlanderMale => (Gender.Male, ModelRace.Highlander),
HighlanderMaleNpc => (Gender.MaleNpc, ModelRace.Highlander),
HighlanderFemale => (Gender.Female, ModelRace.Highlander),
HighlanderFemaleNpc => (Gender.FemaleNpc, ModelRace.Highlander),
ElezenMale => (Gender.Male, ModelRace.Elezen),
ElezenMaleNpc => (Gender.MaleNpc, ModelRace.Elezen),
ElezenFemale => (Gender.Female, ModelRace.Elezen),
ElezenFemaleNpc => (Gender.FemaleNpc, ModelRace.Elezen),
LalafellMale => (Gender.Male, ModelRace.Lalafell),
LalafellMaleNpc => (Gender.MaleNpc, ModelRace.Lalafell),
LalafellFemale => (Gender.Female, ModelRace.Lalafell),
LalafellFemaleNpc => (Gender.FemaleNpc, ModelRace.Lalafell),
MiqoteMale => (Gender.Male, ModelRace.Miqote),
MiqoteMaleNpc => (Gender.MaleNpc, ModelRace.Miqote),
MiqoteFemale => (Gender.Female, ModelRace.Miqote),
MiqoteFemaleNpc => (Gender.FemaleNpc, ModelRace.Miqote),
RoegadynMale => (Gender.Male, ModelRace.Roegadyn),
RoegadynMaleNpc => (Gender.MaleNpc, ModelRace.Roegadyn),
RoegadynFemale => (Gender.Female, ModelRace.Roegadyn),
RoegadynFemaleNpc => (Gender.FemaleNpc, ModelRace.Roegadyn),
AuRaMale => (Gender.Male, ModelRace.AuRa),
AuRaMaleNpc => (Gender.MaleNpc, ModelRace.AuRa),
AuRaFemale => (Gender.Female, ModelRace.AuRa),
AuRaFemaleNpc => (Gender.FemaleNpc, ModelRace.AuRa),
HrothgarMale => (Gender.Male, ModelRace.Hrothgar),
HrothgarMaleNpc => (Gender.MaleNpc, ModelRace.Hrothgar),
HrothgarFemale => (Gender.Female, ModelRace.Hrothgar),
HrothgarFemaleNpc => (Gender.FemaleNpc, ModelRace.Hrothgar),
VieraMale => (Gender.Male, ModelRace.Viera),
VieraMaleNpc => (Gender.Male, ModelRace.Viera),
VieraFemale => (Gender.Female, ModelRace.Viera),
VieraFemaleNpc => (Gender.FemaleNpc, ModelRace.Viera),
UnknownMaleNpc => (Gender.MaleNpc, ModelRace.Unknown),
UnknownFemaleNpc => (Gender.FemaleNpc, ModelRace.Unknown),
_ => throw new InvalidEnumArgumentException(),
};
}
public static bool IsValid(this GenderRace value)
=> value != Unknown && Enum.IsDefined(typeof(GenderRace), value);
public static string ToRaceCode(this GenderRace value)
{
return value switch
{
MidlanderMale => "0101",
MidlanderMaleNpc => "0104",
MidlanderFemale => "0201",
MidlanderFemaleNpc => "0204",
HighlanderMale => "0301",
HighlanderMaleNpc => "0304",
HighlanderFemale => "0401",
HighlanderFemaleNpc => "0404",
ElezenMale => "0501",
ElezenMaleNpc => "0504",
ElezenFemale => "0601",
ElezenFemaleNpc => "0604",
MiqoteMale => "0701",
MiqoteMaleNpc => "0704",
MiqoteFemale => "0801",
MiqoteFemaleNpc => "0804",
RoegadynMale => "0901",
RoegadynMaleNpc => "0904",
RoegadynFemale => "1001",
RoegadynFemaleNpc => "1004",
LalafellMale => "1101",
LalafellMaleNpc => "1104",
LalafellFemale => "1201",
LalafellFemaleNpc => "1204",
AuRaMale => "1301",
AuRaMaleNpc => "1304",
AuRaFemale => "1401",
AuRaFemaleNpc => "1404",
HrothgarMale => "1501",
HrothgarMaleNpc => "1504",
HrothgarFemale => "1601",
HrothgarFemaleNpc => "1604",
VieraMale => "1701",
VieraMaleNpc => "1704",
VieraFemale => "1801",
VieraFemaleNpc => "1804",
UnknownMaleNpc => "9104",
UnknownFemaleNpc => "9204",
_ => string.Empty,
};
}
public static GenderRace[] Dependencies(this GenderRace raceCode)
=> DependencyList.TryGetValue(raceCode, out var dep) ? dep : Array.Empty<GenderRace>();
public static IEnumerable<GenderRace> OnlyDependencies(this GenderRace raceCode)
=> DependencyList.TryGetValue(raceCode, out var dep) ? dep.Skip(1) : Array.Empty<GenderRace>();
private static readonly Dictionary<GenderRace, GenderRace[]> DependencyList = new()
{
// @formatter:off
[MidlanderMale] = new[]{ MidlanderMale },
[HighlanderMale] = new[]{ HighlanderMale, MidlanderMale },
[ElezenMale] = new[]{ ElezenMale, MidlanderMale },
[MiqoteMale] = new[]{ MiqoteMale, MidlanderMale },
[RoegadynMale] = new[]{ RoegadynMale, MidlanderMale },
[LalafellMale] = new[]{ LalafellMale, MidlanderMale },
[AuRaMale] = new[]{ AuRaMale, MidlanderMale },
[HrothgarMale] = new[]{ HrothgarMale, RoegadynMale, MidlanderMale },
[VieraMale] = new[]{ VieraMale, MidlanderMale },
[MidlanderFemale] = new[]{ MidlanderFemale, MidlanderMale },
[HighlanderFemale] = new[]{ HighlanderFemale, MidlanderFemale, MidlanderMale },
[ElezenFemale] = new[]{ ElezenFemale, MidlanderFemale, MidlanderMale },
[MiqoteFemale] = new[]{ MiqoteFemale, MidlanderFemale, MidlanderMale },
[RoegadynFemale] = new[]{ RoegadynFemale, MidlanderFemale, MidlanderMale },
[LalafellFemale] = new[]{ LalafellFemale, LalafellMale, MidlanderMale },
[AuRaFemale] = new[]{ AuRaFemale, MidlanderFemale, MidlanderMale },
[HrothgarFemale] = new[]{ HrothgarFemale, RoegadynFemale, MidlanderFemale, MidlanderMale },
[VieraFemale] = new[]{ VieraFemale, MidlanderFemale, MidlanderMale },
[MidlanderMaleNpc] = new[]{ MidlanderMaleNpc, MidlanderMale },
[HighlanderMaleNpc] = new[]{ HighlanderMaleNpc, HighlanderMale, MidlanderMaleNpc, MidlanderMale },
[ElezenMaleNpc] = new[]{ ElezenMaleNpc, ElezenMale, MidlanderMaleNpc, MidlanderMale },
[MiqoteMaleNpc] = new[]{ MiqoteMaleNpc, MiqoteMale, MidlanderMaleNpc, MidlanderMale },
[RoegadynMaleNpc] = new[]{ RoegadynMaleNpc, RoegadynMale, MidlanderMaleNpc, MidlanderMale },
[LalafellMaleNpc] = new[]{ LalafellMaleNpc, LalafellMale, MidlanderMaleNpc, MidlanderMale },
[AuRaMaleNpc] = new[]{ AuRaMaleNpc, AuRaMale, MidlanderMaleNpc, MidlanderMale },
[HrothgarMaleNpc] = new[]{ HrothgarMaleNpc, HrothgarMale, RoegadynMaleNpc, RoegadynMale, MidlanderMaleNpc, MidlanderMale },
[VieraMaleNpc] = new[]{ VieraMaleNpc, VieraMale, MidlanderMaleNpc, MidlanderMale },
[MidlanderFemaleNpc] = new[]{ MidlanderFemaleNpc, MidlanderFemale, MidlanderMaleNpc, MidlanderMale },
[HighlanderFemaleNpc] = new[]{ HighlanderFemaleNpc, HighlanderFemale, MidlanderFemaleNpc, MidlanderFemale, MidlanderMaleNpc, MidlanderMale },
[ElezenFemaleNpc] = new[]{ ElezenFemaleNpc, ElezenFemale, MidlanderFemaleNpc, MidlanderFemale, MidlanderMaleNpc, MidlanderMale },
[MiqoteFemaleNpc] = new[]{ MiqoteFemaleNpc, MiqoteFemale, MidlanderFemaleNpc, MidlanderFemale, MidlanderMaleNpc, MidlanderMale },
[RoegadynFemaleNpc] = new[]{ RoegadynFemaleNpc, RoegadynFemale, MidlanderFemaleNpc, MidlanderFemale, MidlanderMaleNpc, MidlanderMale },
[LalafellFemaleNpc] = new[]{ LalafellFemaleNpc, LalafellFemale, LalafellMaleNpc, LalafellMale, MidlanderMaleNpc, MidlanderMale },
[AuRaFemaleNpc] = new[]{ AuRaFemaleNpc, AuRaFemale, MidlanderFemaleNpc, MidlanderFemale, MidlanderMaleNpc, MidlanderMale },
[HrothgarFemaleNpc] = new[]{ HrothgarFemaleNpc, HrothgarFemale, RoegadynFemaleNpc, RoegadynFemale, MidlanderFemaleNpc, MidlanderFemale, MidlanderMaleNpc, MidlanderMale },
[VieraFemaleNpc] = new[]{ VieraFemaleNpc, VieraFemale, MidlanderFemaleNpc, MidlanderFemale, MidlanderMaleNpc, MidlanderMale },
[UnknownMaleNpc] = new[]{ UnknownMaleNpc, MidlanderMaleNpc, MidlanderMale },
[UnknownFemaleNpc] = new[]{ UnknownFemaleNpc, MidlanderFemaleNpc, MidlanderFemale, MidlanderMaleNpc, MidlanderMale },
// @formatter:on
};
}
public static partial class Names
{
public static GenderRace GenderRaceFromCode(string code)
{
return code switch
{
"0101" => MidlanderMale,
"0104" => MidlanderMaleNpc,
"0201" => MidlanderFemale,
"0204" => MidlanderFemaleNpc,
"0301" => HighlanderMale,
"0304" => HighlanderMaleNpc,
"0401" => HighlanderFemale,
"0404" => HighlanderFemaleNpc,
"0501" => ElezenMale,
"0504" => ElezenMaleNpc,
"0601" => ElezenFemale,
"0604" => ElezenFemaleNpc,
"0701" => MiqoteMale,
"0704" => MiqoteMaleNpc,
"0801" => MiqoteFemale,
"0804" => MiqoteFemaleNpc,
"0901" => RoegadynMale,
"0904" => RoegadynMaleNpc,
"1001" => RoegadynFemale,
"1004" => RoegadynFemaleNpc,
"1101" => LalafellMale,
"1104" => LalafellMaleNpc,
"1201" => LalafellFemale,
"1204" => LalafellFemaleNpc,
"1301" => AuRaMale,
"1304" => AuRaMaleNpc,
"1401" => AuRaFemale,
"1404" => AuRaFemaleNpc,
"1501" => HrothgarMale,
"1504" => HrothgarMaleNpc,
"1601" => HrothgarFemale,
"1604" => HrothgarFemaleNpc,
"1701" => VieraMale,
"1704" => VieraMaleNpc,
"1801" => VieraFemale,
"1804" => VieraFemaleNpc,
"9104" => UnknownMaleNpc,
"9204" => UnknownFemaleNpc,
_ => Unknown,
};
}
public static GenderRace GenderRaceFromByte(byte value)
{
var gender = (Gender)(value & 0b111);
var race = (ModelRace)(value >> 3);
return CombinedRace(gender, race);
}
public static GenderRace CombinedRace(Gender gender, ModelRace modelRace)
{
return gender switch
{
Gender.Male => modelRace switch
{
ModelRace.Midlander => MidlanderMale,
ModelRace.Highlander => HighlanderMale,
ModelRace.Elezen => ElezenMale,
ModelRace.Lalafell => LalafellMale,
ModelRace.Miqote => MiqoteMale,
ModelRace.Roegadyn => RoegadynMale,
ModelRace.AuRa => AuRaMale,
ModelRace.Hrothgar => HrothgarMale,
ModelRace.Viera => VieraMale,
_ => Unknown,
},
Gender.MaleNpc => modelRace switch
{
ModelRace.Midlander => MidlanderMaleNpc,
ModelRace.Highlander => HighlanderMaleNpc,
ModelRace.Elezen => ElezenMaleNpc,
ModelRace.Lalafell => LalafellMaleNpc,
ModelRace.Miqote => MiqoteMaleNpc,
ModelRace.Roegadyn => RoegadynMaleNpc,
ModelRace.AuRa => AuRaMaleNpc,
ModelRace.Hrothgar => HrothgarMaleNpc,
ModelRace.Viera => VieraMaleNpc,
_ => Unknown,
},
Gender.Female => modelRace switch
{
ModelRace.Midlander => MidlanderFemale,
ModelRace.Highlander => HighlanderFemale,
ModelRace.Elezen => ElezenFemale,
ModelRace.Lalafell => LalafellFemale,
ModelRace.Miqote => MiqoteFemale,
ModelRace.Roegadyn => RoegadynFemale,
ModelRace.AuRa => AuRaFemale,
ModelRace.Hrothgar => HrothgarFemale,
ModelRace.Viera => VieraFemale,
_ => Unknown,
},
Gender.FemaleNpc => modelRace switch
{
ModelRace.Midlander => MidlanderFemaleNpc,
ModelRace.Highlander => HighlanderFemaleNpc,
ModelRace.Elezen => ElezenFemaleNpc,
ModelRace.Lalafell => LalafellFemaleNpc,
ModelRace.Miqote => MiqoteFemaleNpc,
ModelRace.Roegadyn => RoegadynFemaleNpc,
ModelRace.AuRa => AuRaFemaleNpc,
ModelRace.Hrothgar => HrothgarFemaleNpc,
ModelRace.Viera => VieraFemaleNpc,
_ => Unknown,
},
_ => Unknown,
};
}
}

View file

@ -1,319 +0,0 @@
using System;
using System.IO;
using System.Linq;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.String;
using Penumbra.String.Functions;
namespace Penumbra.GameData.Enums;
public enum ResourceType : uint
{
Unknown = 0,
Aet = 0x00616574,
Amb = 0x00616D62,
Atch = 0x61746368,
Atex = 0x61746578,
Avfx = 0x61766678,
Awt = 0x00617774,
Cmp = 0x00636D70,
Dic = 0x00646963,
Eid = 0x00656964,
Envb = 0x656E7662,
Eqdp = 0x65716470,
Eqp = 0x00657170,
Essb = 0x65737362,
Est = 0x00657374,
Evp = 0x00657670,
Exd = 0x00657864,
Exh = 0x00657868,
Exl = 0x0065786C,
Fdt = 0x00666474,
Gfd = 0x00676664,
Ggd = 0x00676764,
Gmp = 0x00676D70,
Gzd = 0x00677A64,
Imc = 0x00696D63,
Lcb = 0x006C6362,
Lgb = 0x006C6762,
Luab = 0x6C756162,
Lvb = 0x006C7662,
Mdl = 0x006D646C,
Mlt = 0x006D6C74,
Mtrl = 0x6D74726C,
Obsb = 0x6F627362,
Pap = 0x00706170,
Pbd = 0x00706264,
Pcb = 0x00706362,
Phyb = 0x70687962,
Plt = 0x00706C74,
Scd = 0x00736364,
Sgb = 0x00736762,
Shcd = 0x73686364,
Shpk = 0x7368706B,
Sklb = 0x736B6C62,
Skp = 0x00736B70,
Stm = 0x0073746D,
Svb = 0x00737662,
Tera = 0x74657261,
Tex = 0x00746578,
Tmb = 0x00746D62,
Ugd = 0x00756764,
Uld = 0x00756C64,
Waoe = 0x77616F65,
Wtd = 0x00777464,
}
[Flags]
public enum ResourceTypeFlag : ulong
{
Aet = 0x0000_0000_0000_0001,
Amb = 0x0000_0000_0000_0002,
Atch = 0x0000_0000_0000_0004,
Atex = 0x0000_0000_0000_0008,
Avfx = 0x0000_0000_0000_0010,
Awt = 0x0000_0000_0000_0020,
Cmp = 0x0000_0000_0000_0040,
Dic = 0x0000_0000_0000_0080,
Eid = 0x0000_0000_0000_0100,
Envb = 0x0000_0000_0000_0200,
Eqdp = 0x0000_0000_0000_0400,
Eqp = 0x0000_0000_0000_0800,
Essb = 0x0000_0000_0000_1000,
Est = 0x0000_0000_0000_2000,
Evp = 0x0000_0000_0000_4000,
Exd = 0x0000_0000_0000_8000,
Exh = 0x0000_0000_0001_0000,
Exl = 0x0000_0000_0002_0000,
Fdt = 0x0000_0000_0004_0000,
Gfd = 0x0000_0000_0008_0000,
Ggd = 0x0000_0000_0010_0000,
Gmp = 0x0000_0000_0020_0000,
Gzd = 0x0000_0000_0040_0000,
Imc = 0x0000_0000_0080_0000,
Lcb = 0x0000_0000_0100_0000,
Lgb = 0x0000_0000_0200_0000,
Luab = 0x0000_0000_0400_0000,
Lvb = 0x0000_0000_0800_0000,
Mdl = 0x0000_0000_1000_0000,
Mlt = 0x0000_0000_2000_0000,
Mtrl = 0x0000_0000_4000_0000,
Obsb = 0x0000_0000_8000_0000,
Pap = 0x0000_0001_0000_0000,
Pbd = 0x0000_0002_0000_0000,
Pcb = 0x0000_0004_0000_0000,
Phyb = 0x0000_0008_0000_0000,
Plt = 0x0000_0010_0000_0000,
Scd = 0x0000_0020_0000_0000,
Sgb = 0x0000_0040_0000_0000,
Shcd = 0x0000_0080_0000_0000,
Shpk = 0x0000_0100_0000_0000,
Sklb = 0x0000_0200_0000_0000,
Skp = 0x0000_0400_0000_0000,
Stm = 0x0000_0800_0000_0000,
Svb = 0x0000_1000_0000_0000,
Tera = 0x0000_2000_0000_0000,
Tex = 0x0000_4000_0000_0000,
Tmb = 0x0000_8000_0000_0000,
Ugd = 0x0001_0000_0000_0000,
Uld = 0x0002_0000_0000_0000,
Waoe = 0x0004_0000_0000_0000,
Wtd = 0x0008_0000_0000_0000,
}
[Flags]
public enum ResourceCategoryFlag : ushort
{
Common = 0x0001,
BgCommon = 0x0002,
Bg = 0x0004,
Cut = 0x0008,
Chara = 0x0010,
Shader = 0x0020,
Ui = 0x0040,
Sound = 0x0080,
Vfx = 0x0100,
UiScript = 0x0200,
Exd = 0x0400,
GameScript = 0x0800,
Music = 0x1000,
SqpackTest = 0x2000,
}
public static class ResourceExtensions
{
public static readonly ResourceTypeFlag AllResourceTypes = Enum.GetValues<ResourceTypeFlag>().Aggregate((v, f) => v | f);
public static readonly ResourceCategoryFlag AllResourceCategories = Enum.GetValues<ResourceCategoryFlag>().Aggregate((v, f) => v | f);
public static ResourceTypeFlag ToFlag(this ResourceType type)
=> type switch
{
ResourceType.Aet => ResourceTypeFlag.Aet,
ResourceType.Amb => ResourceTypeFlag.Amb,
ResourceType.Atch => ResourceTypeFlag.Atch,
ResourceType.Atex => ResourceTypeFlag.Atex,
ResourceType.Avfx => ResourceTypeFlag.Avfx,
ResourceType.Awt => ResourceTypeFlag.Awt,
ResourceType.Cmp => ResourceTypeFlag.Cmp,
ResourceType.Dic => ResourceTypeFlag.Dic,
ResourceType.Eid => ResourceTypeFlag.Eid,
ResourceType.Envb => ResourceTypeFlag.Envb,
ResourceType.Eqdp => ResourceTypeFlag.Eqdp,
ResourceType.Eqp => ResourceTypeFlag.Eqp,
ResourceType.Essb => ResourceTypeFlag.Essb,
ResourceType.Est => ResourceTypeFlag.Est,
ResourceType.Evp => ResourceTypeFlag.Evp,
ResourceType.Exd => ResourceTypeFlag.Exd,
ResourceType.Exh => ResourceTypeFlag.Exh,
ResourceType.Exl => ResourceTypeFlag.Exl,
ResourceType.Fdt => ResourceTypeFlag.Fdt,
ResourceType.Gfd => ResourceTypeFlag.Gfd,
ResourceType.Ggd => ResourceTypeFlag.Ggd,
ResourceType.Gmp => ResourceTypeFlag.Gmp,
ResourceType.Gzd => ResourceTypeFlag.Gzd,
ResourceType.Imc => ResourceTypeFlag.Imc,
ResourceType.Lcb => ResourceTypeFlag.Lcb,
ResourceType.Lgb => ResourceTypeFlag.Lgb,
ResourceType.Luab => ResourceTypeFlag.Luab,
ResourceType.Lvb => ResourceTypeFlag.Lvb,
ResourceType.Mdl => ResourceTypeFlag.Mdl,
ResourceType.Mlt => ResourceTypeFlag.Mlt,
ResourceType.Mtrl => ResourceTypeFlag.Mtrl,
ResourceType.Obsb => ResourceTypeFlag.Obsb,
ResourceType.Pap => ResourceTypeFlag.Pap,
ResourceType.Pbd => ResourceTypeFlag.Pbd,
ResourceType.Pcb => ResourceTypeFlag.Pcb,
ResourceType.Phyb => ResourceTypeFlag.Phyb,
ResourceType.Plt => ResourceTypeFlag.Plt,
ResourceType.Scd => ResourceTypeFlag.Scd,
ResourceType.Sgb => ResourceTypeFlag.Sgb,
ResourceType.Shcd => ResourceTypeFlag.Shcd,
ResourceType.Shpk => ResourceTypeFlag.Shpk,
ResourceType.Sklb => ResourceTypeFlag.Sklb,
ResourceType.Skp => ResourceTypeFlag.Skp,
ResourceType.Stm => ResourceTypeFlag.Stm,
ResourceType.Svb => ResourceTypeFlag.Svb,
ResourceType.Tera => ResourceTypeFlag.Tera,
ResourceType.Tex => ResourceTypeFlag.Tex,
ResourceType.Tmb => ResourceTypeFlag.Tmb,
ResourceType.Ugd => ResourceTypeFlag.Ugd,
ResourceType.Uld => ResourceTypeFlag.Uld,
ResourceType.Waoe => ResourceTypeFlag.Waoe,
ResourceType.Wtd => ResourceTypeFlag.Wtd,
_ => 0,
};
public static bool FitsFlag(this ResourceType type, ResourceTypeFlag flags)
=> (type.ToFlag() & flags) != 0;
public static ResourceCategoryFlag ToFlag(this ResourceCategory type)
=> type switch
{
ResourceCategory.Common => ResourceCategoryFlag.Common,
ResourceCategory.BgCommon => ResourceCategoryFlag.BgCommon,
ResourceCategory.Bg => ResourceCategoryFlag.Bg,
ResourceCategory.Cut => ResourceCategoryFlag.Cut,
ResourceCategory.Chara => ResourceCategoryFlag.Chara,
ResourceCategory.Shader => ResourceCategoryFlag.Shader,
ResourceCategory.Ui => ResourceCategoryFlag.Ui,
ResourceCategory.Sound => ResourceCategoryFlag.Sound,
ResourceCategory.Vfx => ResourceCategoryFlag.Vfx,
ResourceCategory.UiScript => ResourceCategoryFlag.UiScript,
ResourceCategory.Exd => ResourceCategoryFlag.Exd,
ResourceCategory.GameScript => ResourceCategoryFlag.GameScript,
ResourceCategory.Music => ResourceCategoryFlag.Music,
ResourceCategory.SqpackTest => ResourceCategoryFlag.SqpackTest,
_ => 0,
};
public static bool FitsFlag(this ResourceCategory type, ResourceCategoryFlag flags)
=> (type.ToFlag() & flags) != 0;
public static ResourceType FromBytes(byte a1, byte a2, byte a3)
=> (ResourceType)(((uint)ByteStringFunctions.AsciiToLower(a1) << 16)
| ((uint)ByteStringFunctions.AsciiToLower(a2) << 8)
| ByteStringFunctions.AsciiToLower(a3));
public static ResourceType FromBytes(byte a1, byte a2, byte a3, byte a4)
=> (ResourceType)(((uint)ByteStringFunctions.AsciiToLower(a1) << 24)
| ((uint)ByteStringFunctions.AsciiToLower(a2) << 16)
| ((uint)ByteStringFunctions.AsciiToLower(a3) << 8)
| ByteStringFunctions.AsciiToLower(a4));
public static ResourceType FromBytes(char a1, char a2, char a3)
=> FromBytes((byte)a1, (byte)a2, (byte)a3);
public static ResourceType FromBytes(char a1, char a2, char a3, char a4)
=> FromBytes((byte)a1, (byte)a2, (byte)a3, (byte)a4);
public static ResourceType Type(string path)
{
var ext = Path.GetExtension(path.AsSpan());
ext = ext.Length == 0 ? path.AsSpan() : ext[1..];
return ext.Length switch
{
0 => 0,
1 => (ResourceType)ext[^1],
2 => FromBytes('\0', ext[^2], ext[^1]),
3 => FromBytes(ext[^3], ext[^2], ext[^1]),
_ => FromBytes(ext[^4], ext[^3], ext[^2], ext[^1]),
};
}
public static ResourceType Type(ByteString path)
{
var extIdx = path.LastIndexOf((byte)'.');
var ext = extIdx == -1 ? path : extIdx == path.Length - 1 ? ByteString.Empty : path.Substring(extIdx + 1);
return ext.Length switch
{
0 => 0,
1 => (ResourceType)ext[^1],
2 => FromBytes(0, ext[^2], ext[^1]),
3 => FromBytes(ext[^3], ext[^2], ext[^1]),
_ => FromBytes(ext[^4], ext[^3], ext[^2], ext[^1]),
};
}
public static ResourceCategory Category(ByteString path)
{
if (path.Length < 3)
return ResourceCategory.Debug;
return ByteStringFunctions.AsciiToUpper(path[0]) switch
{
(byte)'C' => ByteStringFunctions.AsciiToUpper(path[1]) switch
{
(byte)'O' => ResourceCategory.Common,
(byte)'U' => ResourceCategory.Cut,
(byte)'H' => ResourceCategory.Chara,
_ => ResourceCategory.Debug,
},
(byte)'B' => ByteStringFunctions.AsciiToUpper(path[2]) switch
{
(byte)'C' => ResourceCategory.BgCommon,
(byte)'/' => ResourceCategory.Bg,
_ => ResourceCategory.Debug,
},
(byte)'S' => ByteStringFunctions.AsciiToUpper(path[1]) switch
{
(byte)'H' => ResourceCategory.Shader,
(byte)'O' => ResourceCategory.Sound,
(byte)'Q' => ResourceCategory.SqpackTest,
_ => ResourceCategory.Debug,
},
(byte)'U' => ByteStringFunctions.AsciiToUpper(path[2]) switch
{
(byte)'/' => ResourceCategory.Ui,
(byte)'S' => ResourceCategory.UiScript,
_ => ResourceCategory.Debug,
},
(byte)'V' => ResourceCategory.Vfx,
(byte)'E' => ResourceCategory.Exd,
(byte)'G' => ResourceCategory.GameScript,
(byte)'M' => ResourceCategory.Music,
_ => ResourceCategory.Debug,
};
}
}

View file

@ -1,91 +0,0 @@
using System.ComponentModel;
namespace Penumbra.GameData.Enums;
public enum RspAttribute : byte
{
MaleMinSize,
MaleMaxSize,
MaleMinTail,
MaleMaxTail,
FemaleMinSize,
FemaleMaxSize,
FemaleMinTail,
FemaleMaxTail,
BustMinX,
BustMinY,
BustMinZ,
BustMaxX,
BustMaxY,
BustMaxZ,
NumAttributes,
}
public static class RspAttributeExtensions
{
public static Gender ToGender( this RspAttribute attribute )
{
return attribute switch
{
RspAttribute.MaleMinSize => Gender.Male,
RspAttribute.MaleMaxSize => Gender.Male,
RspAttribute.MaleMinTail => Gender.Male,
RspAttribute.MaleMaxTail => Gender.Male,
RspAttribute.FemaleMinSize => Gender.Female,
RspAttribute.FemaleMaxSize => Gender.Female,
RspAttribute.FemaleMinTail => Gender.Female,
RspAttribute.FemaleMaxTail => Gender.Female,
RspAttribute.BustMinX => Gender.Female,
RspAttribute.BustMinY => Gender.Female,
RspAttribute.BustMinZ => Gender.Female,
RspAttribute.BustMaxX => Gender.Female,
RspAttribute.BustMaxY => Gender.Female,
RspAttribute.BustMaxZ => Gender.Female,
_ => Gender.Unknown,
};
}
public static string ToUngenderedString( this RspAttribute attribute )
{
return attribute switch
{
RspAttribute.MaleMinSize => "MinSize",
RspAttribute.MaleMaxSize => "MaxSize",
RspAttribute.MaleMinTail => "MinTail",
RspAttribute.MaleMaxTail => "MaxTail",
RspAttribute.FemaleMinSize => "MinSize",
RspAttribute.FemaleMaxSize => "MaxSize",
RspAttribute.FemaleMinTail => "MinTail",
RspAttribute.FemaleMaxTail => "MaxTail",
RspAttribute.BustMinX => "BustMinX",
RspAttribute.BustMinY => "BustMinY",
RspAttribute.BustMinZ => "BustMinZ",
RspAttribute.BustMaxX => "BustMaxX",
RspAttribute.BustMaxY => "BustMaxY",
RspAttribute.BustMaxZ => "BustMaxZ",
_ => "",
};
}
public static string ToFullString( this RspAttribute attribute )
{
return attribute switch
{
RspAttribute.MaleMinSize => "Male Minimum Size",
RspAttribute.MaleMaxSize => "Male Maximum Size",
RspAttribute.FemaleMinSize => "Female Minimum Size",
RspAttribute.FemaleMaxSize => "Female Maximum Size",
RspAttribute.BustMinX => "Bust Minimum X-Axis",
RspAttribute.BustMaxX => "Bust Maximum X-Axis",
RspAttribute.BustMinY => "Bust Minimum Y-Axis",
RspAttribute.BustMaxY => "Bust Maximum Y-Axis",
RspAttribute.BustMinZ => "Bust Minimum Z-Axis",
RspAttribute.BustMaxZ => "Bust Maximum Z-Axis",
RspAttribute.MaleMinTail => "Male Minimum Tail Length",
RspAttribute.MaleMaxTail => "Male Maximum Tail Length",
RspAttribute.FemaleMinTail => "Female Minimum Tail Length",
RspAttribute.FemaleMaxTail => "Female Maximum Tail Length",
_ => throw new InvalidEnumArgumentException(),
};
}
}

View file

@ -1,51 +0,0 @@
namespace Penumbra.GameData.Enums;
public enum WeaponCategory : byte
{
Unknown = 0,
Pugilist,
Gladiator,
Marauder,
Archer,
Lancer,
Thaumaturge1,
Thaumaturge2,
Conjurer1,
Conjurer2,
Arcanist,
Shield,
CarpenterMain,
CarpenterOff,
BlacksmithMain,
BlacksmithOff,
ArmorerMain,
ArmorerOff,
GoldsmithMain,
GoldsmithOff,
LeatherworkerMain,
LeatherworkerOff,
WeaverMain,
WeaverOff,
AlchemistMain,
AlchemistOff,
CulinarianMain,
CulinarianOff,
MinerMain,
MinerOff,
BotanistMain,
BotanistOff,
FisherMain,
Rogue = 84,
DarkKnight = 87,
Machinist = 88,
Astrologian = 89,
Samurai = 96,
RedMage = 97,
Scholar = 98,
FisherOff = 99,
BlueMage = 105,
Gunbreaker = 106,
Dancer = 107,
Reaper = 108,
Sage = 109,
}

View file

@ -1,283 +0,0 @@
using System;
using System.IO;
using System.Numerics;
using System.Text;
namespace Penumbra.GameData.Files;
public class AvfxFile : IWritable
{
public struct Block
{
public uint Name;
public uint Size;
public byte[] Data;
public Block(BinaryReader r)
{
Name = r.ReadUInt32();
Size = r.ReadUInt32();
Data = r.ReadBytes((int)Size.RoundTo4());
}
public byte ToBool()
=> BitConverter.ToBoolean(Data) ? (byte)1 : (byte)0;
public uint ToUint()
=> BitConverter.ToUInt32(Data);
public float ToFloat()
=> BitConverter.ToSingle(Data);
public new string ToString()
{
var span = Data.AsSpan(0, (int)Size - 1);
return Encoding.UTF8.GetString(span);
}
}
public static readonly Vector3 BadVector = new(float.NaN);
public Vector3 ClipBox = BadVector;
public Vector3 ClipBoxSize = BadVector;
public Vector3 RevisedValuesPos = BadVector;
public Vector3 RevisedValuesRot = BadVector;
public Vector3 RevisedValuesScale = BadVector;
public Vector3 RevisedValuesColor = BadVector;
public uint Version = uint.MaxValue;
public uint DrawLayerType = uint.MaxValue;
public uint DrawOrderType = uint.MaxValue;
public uint DirectionalLightSourceType = uint.MaxValue;
public uint PointLightsType1 = uint.MaxValue;
public uint PointLightsType2 = uint.MaxValue;
public float BiasZmaxScale = float.NaN;
public float BiasZmaxDistance = float.NaN;
public float NearClipBegin = float.NaN;
public float NearClipEnd = float.NaN;
public float FadeInnerX = float.NaN;
public float FadeOuterX = float.NaN;
public float FadeInnerY = float.NaN;
public float FadeOuterY = float.NaN;
public float FadeInnerZ = float.NaN;
public float FadeOuterZ = float.NaN;
public float FarClipBegin = float.NaN;
public float FarClipEnd = float.NaN;
public float SoftParticleFadeRange = float.NaN;
public float SoftKeyOffset = float.NaN;
public float GlobalFogInfluence = float.NaN;
public byte IsDelayFastParticle = byte.MaxValue;
public byte IsFitGround = byte.MaxValue;
public byte IsTransformSkip = byte.MaxValue;
public byte IsAllStopOnHide = byte.MaxValue;
public byte CanBeClippedOut = byte.MaxValue;
public byte ClipBoxEnabled = byte.MaxValue;
public byte IsCameraSpace = byte.MaxValue;
public byte IsFullEnvLight = byte.MaxValue;
public byte IsClipOwnSetting = byte.MaxValue;
public byte FadeEnabledX = byte.MaxValue;
public byte FadeEnabledY = byte.MaxValue;
public byte FadeEnabledZ = byte.MaxValue;
public byte GlobalFogEnabled = byte.MaxValue;
public byte LtsEnabled = byte.MaxValue;
public Block[] Schedulers = Array.Empty<Block>();
public Block[] Timelines = Array.Empty<Block>();
public Block[] Emitters = Array.Empty<Block>();
public Block[] Particles = Array.Empty<Block>();
public Block[] Effectors = Array.Empty<Block>();
public Block[] Binders = Array.Empty<Block>();
public string[] Textures = Array.Empty<string>();
public Block[] Models = Array.Empty<Block>();
public bool Valid
=> true;
public AvfxFile(byte[] data)
{
using var stream = new MemoryStream(data);
using var r = new BinaryReader(stream);
var name = r.ReadUInt32();
var size = r.ReadUInt32();
var schedulerCount = 0;
var timelineCount = 0;
var emitterCount = 0;
var particleCount = 0;
var effectorCount = 0;
var binderCount = 0;
var textureCount = 0;
var modelCount = 0;
while (r.BaseStream.Position < size)
{
var block = new Block(r);
switch (block.Name)
{
// @formatter:off
case AvfxMagic.Version: Version = block.ToUint(); break;
case AvfxMagic.IsDelayFastParticle: IsDelayFastParticle = block.ToBool(); break;
case AvfxMagic.IsFitGround: IsFitGround = block.ToBool(); break;
case AvfxMagic.IsTransformSkip: IsTransformSkip = block.ToBool(); break;
case AvfxMagic.IsAllStopOnHide: IsAllStopOnHide = block.ToBool(); break;
case AvfxMagic.CanBeClippedOut: CanBeClippedOut = block.ToBool(); break;
case AvfxMagic.ClipBoxEnabled: ClipBoxEnabled = block.ToBool(); break;
case AvfxMagic.ClipBoxX: ClipBox.X = block.ToFloat(); break;
case AvfxMagic.ClipBoxY: ClipBox.Y = block.ToFloat(); break;
case AvfxMagic.ClipBoxZ: ClipBox.Z = block.ToFloat(); break;
case AvfxMagic.ClipBoxSizeX: ClipBoxSize.X = block.ToFloat(); break;
case AvfxMagic.ClipBoxSizeY: ClipBoxSize.Y = block.ToFloat(); break;
case AvfxMagic.ClipBoxSizeZ: ClipBoxSize.Z = block.ToFloat(); break;
case AvfxMagic.BiasZmaxScale: BiasZmaxScale = block.ToFloat(); break;
case AvfxMagic.BiasZmaxDistance: BiasZmaxDistance = block.ToFloat(); break;
case AvfxMagic.IsCameraSpace: IsCameraSpace = block.ToBool(); break;
case AvfxMagic.IsFullEnvLight: IsFullEnvLight = block.ToBool(); break;
case AvfxMagic.IsClipOwnSetting: IsClipOwnSetting = block.ToBool(); break;
case AvfxMagic.NearClipBegin: NearClipBegin = block.ToFloat(); break;
case AvfxMagic.NearClipEnd: NearClipEnd = block.ToFloat(); break;
case AvfxMagic.FarClipBegin: FarClipBegin = block.ToFloat(); break;
case AvfxMagic.FarClipEnd: FarClipEnd = block.ToFloat(); break;
case AvfxMagic.SoftParticleFadeRange: SoftParticleFadeRange = block.ToFloat(); break;
case AvfxMagic.SoftKeyOffset: SoftKeyOffset = block.ToFloat(); break;
case AvfxMagic.DrawLayerType: DrawLayerType = block.ToUint(); break;
case AvfxMagic.DrawOrderType: DrawOrderType = block.ToUint(); break;
case AvfxMagic.DirectionalLightSourceType: DirectionalLightSourceType = block.ToUint(); break;
case AvfxMagic.PointLightsType1: PointLightsType1 = block.ToUint(); break;
case AvfxMagic.PointLightsType2: PointLightsType2 = block.ToUint(); break;
case AvfxMagic.RevisedValuesPosX: RevisedValuesPos.X = block.ToFloat(); break;
case AvfxMagic.RevisedValuesPosY: RevisedValuesPos.Y = block.ToFloat(); break;
case AvfxMagic.RevisedValuesPosZ: RevisedValuesPos.Z = block.ToFloat(); break;
case AvfxMagic.RevisedValuesRotX: RevisedValuesRot.X = block.ToFloat(); break;
case AvfxMagic.RevisedValuesRotY: RevisedValuesRot.Y = block.ToFloat(); break;
case AvfxMagic.RevisedValuesRotZ: RevisedValuesRot.Z = block.ToFloat(); break;
case AvfxMagic.RevisedValuesScaleX: RevisedValuesScale.X = block.ToFloat(); break;
case AvfxMagic.RevisedValuesScaleY: RevisedValuesScale.Y = block.ToFloat(); break;
case AvfxMagic.RevisedValuesScaleZ: RevisedValuesScale.Z = block.ToFloat(); break;
case AvfxMagic.RevisedValuesColorR: RevisedValuesColor.X = block.ToFloat(); break;
case AvfxMagic.RevisedValuesColorG: RevisedValuesColor.Y = block.ToFloat(); break;
case AvfxMagic.RevisedValuesColorB: RevisedValuesColor.Z = block.ToFloat(); break;
case AvfxMagic.FadeEnabledX: FadeEnabledX = block.ToBool(); break;
case AvfxMagic.FadeInnerX: FadeInnerX = block.ToFloat(); break;
case AvfxMagic.FadeOuterX: FadeOuterX = block.ToFloat(); break;
case AvfxMagic.FadeEnabledY: FadeEnabledY = block.ToBool(); break;
case AvfxMagic.FadeInnerY: FadeInnerY = block.ToFloat(); break;
case AvfxMagic.FadeOuterY: FadeOuterY = block.ToFloat(); break;
case AvfxMagic.FadeEnabledZ: FadeEnabledZ = block.ToBool(); break;
case AvfxMagic.FadeInnerZ: FadeInnerZ = block.ToFloat(); break;
case AvfxMagic.FadeOuterZ: FadeOuterZ = block.ToFloat(); break;
case AvfxMagic.GlobalFogEnabled: GlobalFogEnabled = block.ToBool(); break;
case AvfxMagic.GlobalFogInfluence: GlobalFogInfluence = block.ToFloat(); break;
case AvfxMagic.LtsEnabled: LtsEnabled = block.ToBool(); break;
case AvfxMagic.NumSchedulers: Schedulers = new Block[block.ToUint()]; break;
case AvfxMagic.NumTimelines: Timelines = new Block[block.ToUint()]; break;
case AvfxMagic.NumEmitters: Emitters = new Block[block.ToUint()]; break;
case AvfxMagic.NumParticles: Particles = new Block[block.ToUint()]; break;
case AvfxMagic.NumEffectors: Effectors = new Block[block.ToUint()]; break;
case AvfxMagic.NumBinders: Binders = new Block[block.ToUint()]; break;
case AvfxMagic.NumTextures: Textures = new string[block.ToUint()]; break;
case AvfxMagic.NumModels: Models = new Block[block.ToUint()]; break;
case AvfxMagic.Scheduler: Schedulers[schedulerCount++] = block; break;
case AvfxMagic.Timeline: Timelines[timelineCount++] = block; break;
case AvfxMagic.Emitter: Emitters[emitterCount++] = block; break;
case AvfxMagic.Particle: Particles[particleCount++] = block; break;
case AvfxMagic.Effector: Effectors[effectorCount++] = block; break;
case AvfxMagic.Binder: Binders[binderCount++] = block; break;
case AvfxMagic.Texture: Textures[textureCount++] = block.ToString(); break;
case AvfxMagic.Model: Models[modelCount++] = block; break;
// @formatter:on
}
}
}
public byte[] Write()
{
using var m = new MemoryStream(512 * 1024);
using var w = new BinaryWriter(m);
w.Write(AvfxMagic.AvfxBase);
var sizePos = w.BaseStream.Position;
w.Write(0u);
w.WriteBlock(AvfxMagic.Version, Version)
.WriteBlock(AvfxMagic.IsDelayFastParticle, IsDelayFastParticle)
.WriteBlock(AvfxMagic.IsFitGround, IsFitGround)
.WriteBlock(AvfxMagic.IsTransformSkip, IsTransformSkip)
.WriteBlock(AvfxMagic.IsAllStopOnHide, IsAllStopOnHide)
.WriteBlock(AvfxMagic.CanBeClippedOut, CanBeClippedOut)
.WriteBlock(AvfxMagic.ClipBoxEnabled, ClipBoxEnabled)
.WriteBlock(AvfxMagic.ClipBoxX, ClipBox.X)
.WriteBlock(AvfxMagic.ClipBoxY, ClipBox.Y)
.WriteBlock(AvfxMagic.ClipBoxZ, ClipBox.Z)
.WriteBlock(AvfxMagic.ClipBoxSizeX, ClipBoxSize.X)
.WriteBlock(AvfxMagic.ClipBoxSizeY, ClipBoxSize.Y)
.WriteBlock(AvfxMagic.ClipBoxSizeZ, ClipBoxSize.Z)
.WriteBlock(AvfxMagic.BiasZmaxScale, BiasZmaxScale)
.WriteBlock(AvfxMagic.BiasZmaxDistance, BiasZmaxDistance)
.WriteBlock(AvfxMagic.IsCameraSpace, IsCameraSpace)
.WriteBlock(AvfxMagic.IsFullEnvLight, IsFullEnvLight)
.WriteBlock(AvfxMagic.IsClipOwnSetting, IsClipOwnSetting)
.WriteBlock(AvfxMagic.NearClipBegin, NearClipBegin)
.WriteBlock(AvfxMagic.NearClipEnd, NearClipEnd)
.WriteBlock(AvfxMagic.FarClipBegin, FarClipBegin)
.WriteBlock(AvfxMagic.FarClipEnd, FarClipEnd)
.WriteBlock(AvfxMagic.SoftParticleFadeRange, SoftParticleFadeRange)
.WriteBlock(AvfxMagic.SoftKeyOffset, SoftKeyOffset)
.WriteBlock(AvfxMagic.DrawLayerType, DrawLayerType)
.WriteBlock(AvfxMagic.DrawOrderType, DrawOrderType)
.WriteBlock(AvfxMagic.DirectionalLightSourceType, DirectionalLightSourceType)
.WriteBlock(AvfxMagic.PointLightsType1, PointLightsType1)
.WriteBlock(AvfxMagic.PointLightsType2, PointLightsType2)
.WriteBlock(AvfxMagic.RevisedValuesPosX, RevisedValuesPos.X)
.WriteBlock(AvfxMagic.RevisedValuesPosY, RevisedValuesPos.Y)
.WriteBlock(AvfxMagic.RevisedValuesPosZ, RevisedValuesPos.Z)
.WriteBlock(AvfxMagic.RevisedValuesRotX, RevisedValuesRot.X)
.WriteBlock(AvfxMagic.RevisedValuesRotY, RevisedValuesRot.Y)
.WriteBlock(AvfxMagic.RevisedValuesRotZ, RevisedValuesRot.Z)
.WriteBlock(AvfxMagic.RevisedValuesScaleX, RevisedValuesScale.X)
.WriteBlock(AvfxMagic.RevisedValuesScaleY, RevisedValuesScale.Y)
.WriteBlock(AvfxMagic.RevisedValuesScaleZ, RevisedValuesScale.Z)
.WriteBlock(AvfxMagic.RevisedValuesColorR, RevisedValuesColor.X)
.WriteBlock(AvfxMagic.RevisedValuesColorG, RevisedValuesColor.Y)
.WriteBlock(AvfxMagic.RevisedValuesColorB, RevisedValuesColor.Z)
.WriteBlock(AvfxMagic.FadeEnabledX, FadeEnabledX)
.WriteBlock(AvfxMagic.FadeInnerX, FadeInnerX)
.WriteBlock(AvfxMagic.FadeOuterX, FadeOuterX)
.WriteBlock(AvfxMagic.FadeEnabledY, FadeEnabledY)
.WriteBlock(AvfxMagic.FadeInnerY, FadeInnerY)
.WriteBlock(AvfxMagic.FadeOuterY, FadeOuterY)
.WriteBlock(AvfxMagic.FadeEnabledZ, FadeEnabledZ)
.WriteBlock(AvfxMagic.FadeInnerZ, FadeInnerZ)
.WriteBlock(AvfxMagic.FadeOuterZ, FadeOuterZ)
.WriteBlock(AvfxMagic.GlobalFogEnabled, GlobalFogEnabled)
.WriteBlock(AvfxMagic.GlobalFogInfluence, GlobalFogInfluence)
.WriteBlock(AvfxMagic.LtsEnabled, LtsEnabled)
.WriteBlock(AvfxMagic.NumSchedulers, (uint)Schedulers.Length)
.WriteBlock(AvfxMagic.NumTimelines, (uint)Timelines.Length)
.WriteBlock(AvfxMagic.NumEmitters, (uint)Emitters.Length)
.WriteBlock(AvfxMagic.NumParticles, (uint)Particles.Length)
.WriteBlock(AvfxMagic.NumEffectors, (uint)Effectors.Length)
.WriteBlock(AvfxMagic.NumBinders, (uint)Binders.Length)
.WriteBlock(AvfxMagic.NumTextures, (uint)Textures.Length)
.WriteBlock(AvfxMagic.NumModels, (uint)Models.Length);
foreach (var block in Schedulers)
w.WriteBlock(block);
foreach (var block in Timelines)
w.WriteBlock(block);
foreach (var block in Emitters)
w.WriteBlock(block);
foreach (var block in Particles)
w.WriteBlock(block);
foreach (var block in Effectors)
w.WriteBlock(block);
foreach (var block in Binders)
w.WriteBlock(block);
foreach (var texture in Textures)
w.WriteTextureBlock(texture);
foreach (var block in Models)
w.WriteBlock(block);
w.Seek((int)sizePos, SeekOrigin.Begin);
w.Write((uint)w.BaseStream.Length - 8u);
return m.ToArray();
}
}

View file

@ -1,149 +0,0 @@
using System.IO;
using System.Numerics;
using System.Text;
// ReSharper disable ShiftExpressionZeroLeftOperand
namespace Penumbra.GameData.Files;
public static class AvfxMagic
{
public const uint AvfxBase = ('A' << 24) | ('V' << 16) | ('F' << 8) | (uint)'X';
public const uint Version = (000 << 24) | ('V' << 16) | ('e' << 8) | (uint)'r';
public const uint IsDelayFastParticle = ('b' << 24) | ('D' << 16) | ('F' << 8) | (uint)'P';
public const uint IsFitGround = (000 << 24) | ('b' << 16) | ('F' << 8) | (uint)'G';
public const uint IsTransformSkip = (000 << 24) | ('b' << 16) | ('T' << 8) | (uint)'S';
public const uint IsAllStopOnHide = ('b' << 24) | ('A' << 16) | ('S' << 8) | (uint)'H';
public const uint CanBeClippedOut = ('b' << 24) | ('C' << 16) | ('B' << 8) | (uint)'C';
public const uint ClipBoxEnabled = ('b' << 24) | ('C' << 16) | ('u' << 8) | (uint)'l';
public const uint ClipBoxX = ('C' << 24) | ('B' << 16) | ('P' << 8) | (uint)'x';
public const uint ClipBoxY = ('C' << 24) | ('B' << 16) | ('P' << 8) | (uint)'y';
public const uint ClipBoxZ = ('C' << 24) | ('B' << 16) | ('P' << 8) | (uint)'z';
public const uint ClipBoxSizeX = ('C' << 24) | ('B' << 16) | ('S' << 8) | (uint)'x';
public const uint ClipBoxSizeY = ('C' << 24) | ('B' << 16) | ('S' << 8) | (uint)'y';
public const uint ClipBoxSizeZ = ('C' << 24) | ('B' << 16) | ('S' << 8) | (uint)'z';
public const uint BiasZmaxScale = ('Z' << 24) | ('B' << 16) | ('M' << 8) | (uint)'s';
public const uint BiasZmaxDistance = ('Z' << 24) | ('B' << 16) | ('M' << 8) | (uint)'d';
public const uint IsCameraSpace = ('b' << 24) | ('C' << 16) | ('m' << 8) | (uint)'S';
public const uint IsFullEnvLight = ('b' << 24) | ('F' << 16) | ('E' << 8) | (uint)'L';
public const uint IsClipOwnSetting = ('b' << 24) | ('O' << 16) | ('S' << 8) | (uint)'t';
public const uint NearClipBegin = (000 << 24) | ('N' << 16) | ('C' << 8) | (uint)'B';
public const uint NearClipEnd = (000 << 24) | ('N' << 16) | ('C' << 8) | (uint)'E';
public const uint FarClipBegin = (000 << 24) | ('F' << 16) | ('C' << 8) | (uint)'B';
public const uint FarClipEnd = (000 << 24) | ('F' << 16) | ('C' << 8) | (uint)'E';
public const uint SoftParticleFadeRange = ('S' << 24) | ('P' << 16) | ('F' << 8) | (uint)'R';
public const uint SoftKeyOffset = (000 << 24) | ('S' << 16) | ('K' << 8) | (uint)'O';
public const uint DrawLayerType = ('D' << 24) | ('w' << 16) | ('L' << 8) | (uint)'y';
public const uint DrawOrderType = ('D' << 24) | ('w' << 16) | ('O' << 8) | (uint)'T';
public const uint DirectionalLightSourceType = ('D' << 24) | ('L' << 16) | ('S' << 8) | (uint)'T';
public const uint PointLightsType1 = ('P' << 24) | ('L' << 16) | ('1' << 8) | (uint)'S';
public const uint PointLightsType2 = ('P' << 24) | ('L' << 16) | ('2' << 8) | (uint)'S';
public const uint RevisedValuesPosX = ('R' << 24) | ('v' << 16) | ('P' << 8) | (uint)'x';
public const uint RevisedValuesPosY = ('R' << 24) | ('v' << 16) | ('P' << 8) | (uint)'y';
public const uint RevisedValuesPosZ = ('R' << 24) | ('v' << 16) | ('P' << 8) | (uint)'z';
public const uint RevisedValuesRotX = ('R' << 24) | ('v' << 16) | ('R' << 8) | (uint)'x';
public const uint RevisedValuesRotY = ('R' << 24) | ('v' << 16) | ('R' << 8) | (uint)'y';
public const uint RevisedValuesRotZ = ('R' << 24) | ('v' << 16) | ('R' << 8) | (uint)'z';
public const uint RevisedValuesScaleX = ('R' << 24) | ('v' << 16) | ('S' << 8) | (uint)'x';
public const uint RevisedValuesScaleY = ('R' << 24) | ('v' << 16) | ('S' << 8) | (uint)'y';
public const uint RevisedValuesScaleZ = ('R' << 24) | ('v' << 16) | ('S' << 8) | (uint)'z';
public const uint RevisedValuesColorR = (000 << 24) | ('R' << 16) | ('v' << 8) | (uint)'R';
public const uint RevisedValuesColorG = (000 << 24) | ('R' << 16) | ('v' << 8) | (uint)'G';
public const uint RevisedValuesColorB = (000 << 24) | ('R' << 16) | ('v' << 8) | (uint)'B';
public const uint FadeEnabledX = ('A' << 24) | ('F' << 16) | ('X' << 8) | (uint)'e';
public const uint FadeInnerX = ('A' << 24) | ('F' << 16) | ('X' << 8) | (uint)'i';
public const uint FadeOuterX = ('A' << 24) | ('F' << 16) | ('X' << 8) | (uint)'o';
public const uint FadeEnabledY = ('A' << 24) | ('F' << 16) | ('Y' << 8) | (uint)'e';
public const uint FadeInnerY = ('A' << 24) | ('F' << 16) | ('Y' << 8) | (uint)'i';
public const uint FadeOuterY = ('A' << 24) | ('F' << 16) | ('Y' << 8) | (uint)'o';
public const uint FadeEnabledZ = ('A' << 24) | ('F' << 16) | ('Z' << 8) | (uint)'e';
public const uint FadeInnerZ = ('A' << 24) | ('F' << 16) | ('Z' << 8) | (uint)'i';
public const uint FadeOuterZ = ('A' << 24) | ('F' << 16) | ('Z' << 8) | (uint)'o';
public const uint GlobalFogEnabled = ('b' << 24) | ('G' << 16) | ('F' << 8) | (uint)'E';
public const uint GlobalFogInfluence = ('G' << 24) | ('F' << 16) | ('I' << 8) | (uint)'M';
public const uint LtsEnabled = ('b' << 24) | ('L' << 16) | ('T' << 8) | (uint)'S';
public const uint NumSchedulers = ('S' << 24) | ('c' << 16) | ('C' << 8) | (uint)'n';
public const uint NumTimelines = ('T' << 24) | ('l' << 16) | ('C' << 8) | (uint)'n';
public const uint NumEmitters = ('E' << 24) | ('m' << 16) | ('C' << 8) | (uint)'n';
public const uint NumParticles = ('P' << 24) | ('r' << 16) | ('C' << 8) | (uint)'n';
public const uint NumEffectors = ('E' << 24) | ('f' << 16) | ('C' << 8) | (uint)'n';
public const uint NumBinders = ('B' << 24) | ('d' << 16) | ('C' << 8) | (uint)'n';
public const uint NumTextures = ('T' << 24) | ('x' << 16) | ('C' << 8) | (uint)'n';
public const uint NumModels = ('M' << 24) | ('d' << 16) | ('C' << 8) | (uint)'n';
public const uint Scheduler = ('S' << 24) | ('c' << 16) | ('h' << 8) | (uint)'d';
public const uint Timeline = ('T' << 24) | ('m' << 16) | ('L' << 8) | (uint)'n';
public const uint Emitter = ('E' << 24) | ('m' << 16) | ('i' << 8) | (uint)'t';
public const uint Particle = ('P' << 24) | ('t' << 16) | ('c' << 8) | (uint)'l';
public const uint Effector = ('E' << 24) | ('f' << 16) | ('c' << 8) | (uint)'t';
public const uint Binder = ('B' << 24) | ('i' << 16) | ('n' << 8) | (uint)'d';
public const uint Texture = (000 << 24) | ('T' << 16) | ('e' << 8) | (uint)'x';
public const uint Model = ('M' << 24) | ('o' << 16) | ('d' << 8) | (uint)'l';
internal static uint RoundTo4(this uint size)
{
var rest = size & 0b11u;
return rest > 0 ? (size & ~0b11u) + 4u : size;
}
internal static BinaryWriter WriteTextureBlock(this BinaryWriter bw, string texture)
{
bw.Write(Texture);
var bytes = Encoding.UTF8.GetBytes(texture);
var size = (uint)bytes.Length + 1u;
bw.Write(size);
bw.Write(bytes);
bw.Write((byte)0);
for (var end = size.RoundTo4(); size < end; ++size)
bw.Write((byte)0);
return bw;
}
internal static BinaryWriter WriteBlock(this BinaryWriter bw, AvfxFile.Block block)
{
bw.Write(block.Name);
bw.Write(block.Size);
bw.Write(block.Data);
return bw;
}
internal static BinaryWriter WriteBlock(this BinaryWriter bw, uint magic, uint value)
{
if (value != uint.MaxValue)
{
bw.Write(magic);
bw.Write(4u);
bw.Write(value);
}
return bw;
}
internal static BinaryWriter WriteBlock(this BinaryWriter bw, uint magic, byte value)
{
if (value != byte.MaxValue)
{
bw.Write(magic);
bw.Write(4u);
bw.Write(value == 1 ? 1u : 0u);
}
return bw;
}
internal static BinaryWriter WriteBlock(this BinaryWriter bw, uint magic, float value)
{
if (!float.IsNaN(value))
{
bw.Write(magic);
bw.Write(4u);
bw.Write(value);
}
return bw;
}
internal static BinaryWriter WriteBlock(this BinaryWriter bw, uint magicX, uint magicY, uint magicZ, Vector3 value)
=> bw.WriteBlock(magicX, value.X)
.WriteBlock(magicY, value.Y)
.WriteBlock(magicZ, value.Z);
}

View file

@ -1,7 +0,0 @@
namespace Penumbra.GameData.Files;
public interface IWritable
{
public bool Valid { get; }
public byte[] Write();
}

View file

@ -1,285 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Lumina.Data.Parsing;
namespace Penumbra.GameData.Files;
public partial class MdlFile
{
private static uint Write(BinaryWriter w, string s, long basePos)
{
var currentPos = w.BaseStream.Position;
w.Write(Encoding.UTF8.GetBytes(s));
w.Write((byte)0);
return (uint)(currentPos - basePos);
}
private List<uint> WriteStrings(BinaryWriter w)
{
var startPos = (int)w.BaseStream.Position;
var basePos = startPos + 8;
var count = (ushort)(Attributes.Length + Bones.Length + Materials.Length + Shapes.Length);
w.Write(count);
w.Seek(basePos, SeekOrigin.Begin);
var ret = Attributes.Concat(Bones)
.Concat(Materials)
.Concat(Shapes.Select(s => s.ShapeName))
.Select(attribute => Write(w, attribute, basePos)).ToList();
var padding = (w.BaseStream.Position & 0b111) > 0 ? (w.BaseStream.Position & ~0b111) + 8 : w.BaseStream.Position;
for (var i = w.BaseStream.Position; i < padding; ++i)
w.Write((byte)0);
var size = (int)w.BaseStream.Position - basePos;
w.Seek(startPos + 4, SeekOrigin.Begin);
w.Write((uint)size);
w.Seek(basePos + size, SeekOrigin.Begin);
return ret;
}
private void WriteModelFileHeader(BinaryWriter w, uint runtimeSize)
{
w.Write(Version);
w.Write(StackSize);
w.Write(runtimeSize);
w.Write((ushort)VertexDeclarations.Length);
w.Write((ushort)Materials.Length);
w.Write(VertexOffset[0] > 0 ? VertexOffset[0] + runtimeSize : 0u);
w.Write(VertexOffset[1] > 0 ? VertexOffset[1] + runtimeSize : 0u);
w.Write(VertexOffset[2] > 0 ? VertexOffset[2] + runtimeSize : 0u);
w.Write(IndexOffset[0] > 0 ? IndexOffset[0] + runtimeSize : 0u);
w.Write(IndexOffset[1] > 0 ? IndexOffset[1] + runtimeSize : 0u);
w.Write(IndexOffset[2] > 0 ? IndexOffset[2] + runtimeSize : 0u);
w.Write(VertexBufferSize[0]);
w.Write(VertexBufferSize[1]);
w.Write(VertexBufferSize[2]);
w.Write(IndexBufferSize[0]);
w.Write(IndexBufferSize[1]);
w.Write(IndexBufferSize[2]);
w.Write(LodCount);
w.Write(EnableIndexBufferStreaming);
w.Write(EnableEdgeGeometry);
w.Write((byte)0); // Padding
}
private void WriteModelHeader(BinaryWriter w)
{
w.Write(Radius);
w.Write((ushort)Meshes.Length);
w.Write((ushort)Attributes.Length);
w.Write((ushort)SubMeshes.Length);
w.Write((ushort)Materials.Length);
w.Write((ushort)Bones.Length);
w.Write((ushort)BoneTables.Length);
w.Write((ushort)Shapes.Length);
w.Write((ushort)ShapeMeshes.Length);
w.Write((ushort)ShapeValues.Length);
w.Write(LodCount);
w.Write((byte)Flags1);
w.Write((ushort)ElementIds.Length);
w.Write((byte)TerrainShadowMeshes.Length);
w.Write((byte)Flags2);
w.Write(ModelClipOutDistance);
w.Write(ShadowClipOutDistance);
w.Write(Unknown4);
w.Write((ushort)TerrainShadowSubMeshes.Length);
w.Write(Unknown5);
w.Write(BgChangeMaterialIndex);
w.Write(BgCrestChangeMaterialIndex);
w.Write(Unknown6);
w.Write(Unknown7);
w.Write(Unknown8);
w.Write(Unknown9);
w.Write((uint)0); // 6 byte padding
w.Write((ushort)0);
}
private static void Write(BinaryWriter w, in MdlStructs.VertexElement vertex)
{
w.Write(vertex.Stream);
w.Write(vertex.Offset);
w.Write(vertex.Type);
w.Write(vertex.Usage);
w.Write(vertex.UsageIndex);
w.Write((ushort)0); // 3 byte padding
w.Write((byte)0);
}
private static void Write(BinaryWriter w, in MdlStructs.VertexDeclarationStruct vertexDecl)
{
foreach (var vertex in vertexDecl.VertexElements)
Write(w, vertex);
Write(w, new MdlStructs.VertexElement() { Stream = 255 });
w.Seek((int)(NumVertices - 1 - vertexDecl.VertexElements.Length) * 8, SeekOrigin.Current);
}
private static void Write(BinaryWriter w, in MdlStructs.ElementIdStruct elementId)
{
w.Write(elementId.ElementId);
w.Write(elementId.ParentBoneName);
w.Write(elementId.Translate[0]);
w.Write(elementId.Translate[1]);
w.Write(elementId.Translate[2]);
w.Write(elementId.Rotate[0]);
w.Write(elementId.Rotate[1]);
w.Write(elementId.Rotate[2]);
}
private static unsafe void Write<T>(BinaryWriter w, in T data) where T : unmanaged
{
fixed (T* ptr = &data)
{
var bytePtr = (byte*)ptr;
var size = sizeof(T);
var span = new ReadOnlySpan<byte>(bytePtr, size);
w.Write(span);
}
}
private static void Write(BinaryWriter w, MdlStructs.MeshStruct mesh)
{
w.Write(mesh.VertexCount);
w.Write((ushort)0); // padding
w.Write(mesh.IndexCount);
w.Write(mesh.MaterialIndex);
w.Write(mesh.SubMeshIndex);
w.Write(mesh.SubMeshCount);
w.Write(mesh.BoneTableIndex);
w.Write(mesh.StartIndex);
w.Write(mesh.VertexBufferOffset[0]);
w.Write(mesh.VertexBufferOffset[1]);
w.Write(mesh.VertexBufferOffset[2]);
w.Write(mesh.VertexBufferStride[0]);
w.Write(mesh.VertexBufferStride[1]);
w.Write(mesh.VertexBufferStride[2]);
w.Write(mesh.VertexStreamCount);
}
private static void Write(BinaryWriter w, MdlStructs.BoneTableStruct bone)
{
foreach (var index in bone.BoneIndex)
w.Write(index);
w.Write(bone.BoneCount);
w.Write((ushort)0); // 3 bytes padding
w.Write((byte)0);
}
private void Write(BinaryWriter w, int shapeIdx, IReadOnlyList<uint> offsets)
{
var shape = Shapes[shapeIdx];
var offset = offsets[Attributes.Length + Bones.Length + Materials.Length + shapeIdx];
w.Write(offset);
w.Write(shape.ShapeMeshStartIndex[0]);
w.Write(shape.ShapeMeshStartIndex[1]);
w.Write(shape.ShapeMeshStartIndex[2]);
w.Write(shape.ShapeMeshCount[0]);
w.Write(shape.ShapeMeshCount[1]);
w.Write(shape.ShapeMeshCount[2]);
}
private static void Write(BinaryWriter w, MdlStructs.BoundingBoxStruct box)
{
w.Write(box.Min[0]);
w.Write(box.Min[1]);
w.Write(box.Min[2]);
w.Write(box.Min[3]);
w.Write(box.Max[0]);
w.Write(box.Max[1]);
w.Write(box.Max[2]);
w.Write(box.Max[3]);
}
public byte[] Write()
{
using var stream = new MemoryStream();
using (var w = new BinaryWriter(stream))
{
// Skip and write this later when we actually know it.
w.Seek((int)FileHeaderSize, SeekOrigin.Begin);
foreach (var vertexDecl in VertexDeclarations)
Write(w, vertexDecl);
var offsets = WriteStrings(w);
WriteModelHeader(w);
foreach (var elementId in ElementIds)
Write(w, elementId);
foreach (var lod in Lods)
Write(w, lod);
if (Flags2.HasFlag(MdlStructs.ModelFlags2.ExtraLodEnabled))
foreach (var extraLod in ExtraLods)
Write(w, extraLod);
foreach (var mesh in Meshes)
Write(w, mesh);
for (var i = 0; i < Attributes.Length; ++i)
w.Write(offsets[i]);
foreach (var terrainShadowMesh in TerrainShadowMeshes)
Write(w, terrainShadowMesh);
foreach (var subMesh in SubMeshes)
Write(w, subMesh);
foreach (var terrainShadowSubMesh in TerrainShadowSubMeshes)
Write(w, terrainShadowSubMesh);
for (var i = 0; i < Materials.Length; ++i)
w.Write(offsets[Attributes.Length + Bones.Length + i]);
for (var i = 0; i < Bones.Length; ++i)
w.Write(offsets[Attributes.Length + i]);
foreach (var boneTable in BoneTables)
Write(w, boneTable);
for (var i = 0; i < Shapes.Length; ++i)
Write(w, i, offsets);
foreach (var shapeMesh in ShapeMeshes)
Write(w, shapeMesh);
foreach (var shapeValue in ShapeValues)
Write(w, shapeValue);
w.Write(SubMeshBoneMap.Length * 2);
foreach (var bone in SubMeshBoneMap)
w.Write(bone);
var pos = w.BaseStream.Position + 1;
var padding = (byte) (pos & 0b111);
if (padding > 0)
padding = (byte) (8 - padding);
w.Write(padding);
for (var i = 0; i < padding; ++i)
w.Write((byte) (0xDEADBEEFF00DCAFEu >> (8 * (7 - i))));
Write(w, BoundingBoxes);
Write(w, ModelBoundingBoxes);
Write(w, WaterBoundingBoxes);
Write(w, VerticalFogBoundingBoxes);
foreach (var box in BoneBoundingBoxes)
Write(w, box);
var totalSize = w.BaseStream.Position;
var runtimeSize = (uint)(totalSize - StackSize - FileHeaderSize);
w.Write(RemainingData);
// Write header data.
w.Seek(0, SeekOrigin.Begin);
WriteModelFileHeader(w, runtimeSize);
}
return stream.ToArray();
}
}

View file

@ -1,249 +0,0 @@
using System;
using System.IO;
using System.Reflection;
using System.Text;
using Lumina.Data;
using Lumina.Data.Parsing;
using Lumina.Extensions;
namespace Penumbra.GameData.Files;
public partial class MdlFile : IWritable
{
public const uint NumVertices = 17;
public const uint FileHeaderSize = 0x44;
// Refers to string, thus not Lumina struct.
public struct Shape
{
public string ShapeName = string.Empty;
public ushort[] ShapeMeshStartIndex;
public ushort[] ShapeMeshCount;
public Shape(MdlStructs.ShapeStruct data, uint[] offsets, string[] strings)
{
var idx = offsets.AsSpan().IndexOf(data.StringOffset);
ShapeName = idx >= 0 ? strings[idx] : string.Empty;
ShapeMeshStartIndex = data.ShapeMeshStartIndex;
ShapeMeshCount = data.ShapeMeshCount;
}
}
// Raw data to write back.
public uint Version;
public float Radius;
public float ModelClipOutDistance;
public float ShadowClipOutDistance;
public byte BgChangeMaterialIndex;
public byte BgCrestChangeMaterialIndex;
public ushort Unknown4;
public byte Unknown5;
public byte Unknown6;
public ushort Unknown7;
public ushort Unknown8;
public ushort Unknown9;
// Offsets are stored relative to RuntimeSize instead of file start.
public uint[] VertexOffset;
public uint[] IndexOffset;
public uint[] VertexBufferSize;
public uint[] IndexBufferSize;
public byte LodCount;
public bool EnableIndexBufferStreaming;
public bool EnableEdgeGeometry;
public MdlStructs.ModelFlags1 Flags1;
public MdlStructs.ModelFlags2 Flags2;
public MdlStructs.BoundingBoxStruct BoundingBoxes;
public MdlStructs.BoundingBoxStruct ModelBoundingBoxes;
public MdlStructs.BoundingBoxStruct WaterBoundingBoxes;
public MdlStructs.BoundingBoxStruct VerticalFogBoundingBoxes;
public MdlStructs.VertexDeclarationStruct[] VertexDeclarations;
public MdlStructs.ElementIdStruct[] ElementIds;
public MdlStructs.MeshStruct[] Meshes;
public MdlStructs.BoneTableStruct[] BoneTables;
public MdlStructs.BoundingBoxStruct[] BoneBoundingBoxes;
public MdlStructs.SubmeshStruct[] SubMeshes;
public MdlStructs.ShapeMeshStruct[] ShapeMeshes;
public MdlStructs.ShapeValueStruct[] ShapeValues;
public MdlStructs.TerrainShadowMeshStruct[] TerrainShadowMeshes;
public MdlStructs.TerrainShadowSubmeshStruct[] TerrainShadowSubMeshes;
public MdlStructs.LodStruct[] Lods;
public MdlStructs.ExtraLodStruct[] ExtraLods;
public ushort[] SubMeshBoneMap;
// Strings are written in order
public string[] Attributes;
public string[] Bones;
public string[] Materials;
public Shape[] Shapes;
// Raw, unparsed data.
public byte[] RemainingData;
public bool Valid { get; }
public MdlFile(byte[] data)
{
using var stream = new MemoryStream(data);
using var r = new LuminaBinaryReader(stream);
var header = LoadModelFileHeader(r);
LodCount = header.LodCount;
VertexBufferSize = header.VertexBufferSize;
IndexBufferSize = header.IndexBufferSize;
VertexOffset = header.VertexOffset;
IndexOffset = header.IndexOffset;
for (var i = 0; i < 3; ++i)
{
if (VertexOffset[i] > 0)
VertexOffset[i] -= header.RuntimeSize;
if (IndexOffset[i] > 0)
IndexOffset[i] -= header.RuntimeSize;
}
VertexDeclarations = new MdlStructs.VertexDeclarationStruct[header.VertexDeclarationCount];
for (var i = 0; i < header.VertexDeclarationCount; ++i)
VertexDeclarations[i] = MdlStructs.VertexDeclarationStruct.Read(r);
var (offsets, strings) = LoadStrings(r);
var modelHeader = LoadModelHeader(r);
ElementIds = new MdlStructs.ElementIdStruct[modelHeader.ElementIdCount];
for (var i = 0; i < modelHeader.ElementIdCount; i++)
ElementIds[i] = MdlStructs.ElementIdStruct.Read(r);
Lods = r.ReadStructuresAsArray<MdlStructs.LodStruct>(3);
ExtraLods = modelHeader.ExtraLodEnabled
? r.ReadStructuresAsArray<MdlStructs.ExtraLodStruct>(3)
: Array.Empty<MdlStructs.ExtraLodStruct>();
Meshes = new MdlStructs.MeshStruct[modelHeader.MeshCount];
for (var i = 0; i < modelHeader.MeshCount; i++)
Meshes[i] = MdlStructs.MeshStruct.Read(r);
Attributes = new string[modelHeader.AttributeCount];
for (var i = 0; i < modelHeader.AttributeCount; ++i)
{
var offset = r.ReadUInt32();
var stringIdx = offsets.AsSpan().IndexOf(offset);
Attributes[i] = stringIdx >= 0 ? strings[stringIdx] : string.Empty;
}
TerrainShadowMeshes = r.ReadStructuresAsArray<MdlStructs.TerrainShadowMeshStruct>(modelHeader.TerrainShadowMeshCount);
SubMeshes = r.ReadStructuresAsArray<MdlStructs.SubmeshStruct>(modelHeader.SubmeshCount);
TerrainShadowSubMeshes = r.ReadStructuresAsArray<MdlStructs.TerrainShadowSubmeshStruct>(modelHeader.TerrainShadowSubmeshCount);
Materials = new string[modelHeader.MaterialCount];
for (var i = 0; i < modelHeader.MaterialCount; ++i)
{
var offset = r.ReadUInt32();
var stringIdx = offsets.AsSpan().IndexOf(offset);
Materials[i] = stringIdx >= 0 ? strings[stringIdx] : string.Empty;
}
Bones = new string[modelHeader.BoneCount];
for (var i = 0; i < modelHeader.BoneCount; ++i)
{
var offset = r.ReadUInt32();
var stringIdx = offsets.AsSpan().IndexOf(offset);
Bones[i] = stringIdx >= 0 ? strings[stringIdx] : string.Empty;
}
BoneTables = new MdlStructs.BoneTableStruct[modelHeader.BoneTableCount];
for (var i = 0; i < modelHeader.BoneTableCount; i++)
BoneTables[i] = MdlStructs.BoneTableStruct.Read(r);
Shapes = new Shape[modelHeader.ShapeCount];
for (var i = 0; i < modelHeader.ShapeCount; i++)
Shapes[i] = new Shape(MdlStructs.ShapeStruct.Read(r), offsets, strings);
ShapeMeshes = r.ReadStructuresAsArray<MdlStructs.ShapeMeshStruct>(modelHeader.ShapeMeshCount);
ShapeValues = r.ReadStructuresAsArray<MdlStructs.ShapeValueStruct>(modelHeader.ShapeValueCount);
var submeshBoneMapSize = r.ReadUInt32();
SubMeshBoneMap = r.ReadStructures<ushort>((int)submeshBoneMapSize / 2).ToArray();
var paddingAmount = r.ReadByte();
r.Seek(r.BaseStream.Position + paddingAmount);
// Dunno what this first one is for?
BoundingBoxes = MdlStructs.BoundingBoxStruct.Read(r);
ModelBoundingBoxes = MdlStructs.BoundingBoxStruct.Read(r);
WaterBoundingBoxes = MdlStructs.BoundingBoxStruct.Read(r);
VerticalFogBoundingBoxes = MdlStructs.BoundingBoxStruct.Read(r);
BoneBoundingBoxes = new MdlStructs.BoundingBoxStruct[modelHeader.BoneCount];
for (var i = 0; i < modelHeader.BoneCount; i++)
BoneBoundingBoxes[i] = MdlStructs.BoundingBoxStruct.Read(r);
var runtimePadding = header.RuntimeSize + FileHeaderSize + header.StackSize - r.BaseStream.Position;
if (runtimePadding > 0)
r.ReadBytes((int)runtimePadding);
RemainingData = r.ReadBytes((int)(r.BaseStream.Length - r.BaseStream.Position));
Valid = true;
}
private MdlStructs.ModelFileHeader LoadModelFileHeader(LuminaBinaryReader r)
{
var header = MdlStructs.ModelFileHeader.Read(r);
Version = header.Version;
EnableIndexBufferStreaming = header.EnableIndexBufferStreaming;
EnableEdgeGeometry = header.EnableEdgeGeometry;
return header;
}
private MdlStructs.ModelHeader LoadModelHeader(BinaryReader r)
{
var modelHeader = r.ReadStructure<MdlStructs.ModelHeader>();
Radius = modelHeader.Radius;
Flags1 = (MdlStructs.ModelFlags1)(modelHeader.GetType()
.GetField("Flags1", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(modelHeader)
?? 0);
Flags2 = (MdlStructs.ModelFlags2)(modelHeader.GetType()
.GetField("Flags2", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(modelHeader)
?? 0);
ModelClipOutDistance = modelHeader.ModelClipOutDistance;
ShadowClipOutDistance = modelHeader.ShadowClipOutDistance;
Unknown4 = modelHeader.Unknown4;
Unknown5 = (byte)(modelHeader.GetType()
.GetField("Unknown5", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(modelHeader)
?? 0);
Unknown6 = modelHeader.Unknown6;
Unknown7 = modelHeader.Unknown7;
Unknown8 = modelHeader.Unknown8;
Unknown9 = modelHeader.Unknown9;
BgChangeMaterialIndex = modelHeader.BGChangeMaterialIndex;
BgCrestChangeMaterialIndex = modelHeader.BGCrestChangeMaterialIndex;
return modelHeader;
}
private static (uint[], string[]) LoadStrings(BinaryReader r)
{
var stringCount = r.ReadUInt16();
r.ReadUInt16();
var stringSize = (int)r.ReadUInt32();
var stringData = r.ReadBytes(stringSize);
var start = 0;
var strings = new string[stringCount];
var offsets = new uint[stringCount];
for (var i = 0; i < stringCount; ++i)
{
var span = stringData.AsSpan(start);
var idx = span.IndexOf((byte)'\0');
strings[i] = Encoding.UTF8.GetString(span[..idx]);
offsets[i] = (uint)start;
start = start + idx + 1;
}
return (offsets, strings);
}
public unsafe uint StackSize
=> (uint)(VertexDeclarations.Length * NumVertices * sizeof(MdlStructs.VertexElement));
}

View file

@ -1,90 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace Penumbra.GameData.Files;
public partial class MtrlFile
{
public unsafe struct ColorDyeSet
{
public struct Row
{
private ushort _data;
public ushort Template
{
get => (ushort)(_data >> 5);
set => _data = (ushort)((_data & 0x1F) | (value << 5));
}
public bool Diffuse
{
get => (_data & 0x01) != 0;
set => _data = (ushort)(value ? _data | 0x01 : _data & 0xFFFE);
}
public bool Specular
{
get => (_data & 0x02) != 0;
set => _data = (ushort)(value ? _data | 0x02 : _data & 0xFFFD);
}
public bool Emissive
{
get => (_data & 0x04) != 0;
set => _data = (ushort)(value ? _data | 0x04 : _data & 0xFFFB);
}
public bool Gloss
{
get => (_data & 0x08) != 0;
set => _data = (ushort)(value ? _data | 0x08 : _data & 0xFFF7);
}
public bool SpecularStrength
{
get => (_data & 0x10) != 0;
set => _data = (ushort)(value ? _data | 0x10 : _data & 0xFFEF);
}
}
public struct RowArray : IEnumerable<Row>
{
public const int NumRows = 16;
private fixed ushort _rowData[NumRows];
public ref Row this[int i]
{
get
{
fixed (ushort* ptr = _rowData)
{
return ref ((Row*)ptr)[i];
}
}
}
public IEnumerator<Row> GetEnumerator()
{
for (var i = 0; i < NumRows; ++i)
yield return this[i];
}
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public ReadOnlySpan<byte> AsBytes()
{
fixed (ushort* ptr = _rowData)
{
return new ReadOnlySpan<byte>(ptr, NumRows * sizeof(ushort));
}
}
}
public RowArray Rows;
public string Name;
public ushort Index;
}
}

View file

@ -1,135 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Numerics;
namespace Penumbra.GameData.Files;
public partial class MtrlFile
{
public unsafe struct ColorSet
{
public struct Row
{
public const int Size = 32;
private fixed ushort _data[16];
public Vector3 Diffuse
{
get => new(ToFloat(0), ToFloat(1), ToFloat(2));
set
{
_data[0] = FromFloat(value.X);
_data[1] = FromFloat(value.Y);
_data[2] = FromFloat(value.Z);
}
}
public Vector3 Specular
{
get => new(ToFloat(4), ToFloat(5), ToFloat(6));
set
{
_data[4] = FromFloat(value.X);
_data[5] = FromFloat(value.Y);
_data[6] = FromFloat(value.Z);
}
}
public Vector3 Emissive
{
get => new(ToFloat(8), ToFloat(9), ToFloat(10));
set
{
_data[8] = FromFloat(value.X);
_data[9] = FromFloat(value.Y);
_data[10] = FromFloat(value.Z);
}
}
public Vector2 MaterialRepeat
{
get => new(ToFloat(12), ToFloat(15));
set
{
_data[12] = FromFloat(value.X);
_data[15] = FromFloat(value.Y);
}
}
public Vector2 MaterialSkew
{
get => new(ToFloat(13), ToFloat(14));
set
{
_data[13] = FromFloat(value.X);
_data[14] = FromFloat(value.Y);
}
}
public float SpecularStrength
{
get => ToFloat(3);
set => _data[3] = FromFloat(value);
}
public float GlossStrength
{
get => ToFloat(7);
set => _data[7] = FromFloat(value);
}
public ushort TileSet
{
get => (ushort)(ToFloat(11) * 64f);
set => _data[11] = FromFloat(value / 64f);
}
private float ToFloat(int idx)
=> (float)BitConverter.UInt16BitsToHalf(_data[idx]);
private static ushort FromFloat(float x)
=> BitConverter.HalfToUInt16Bits((Half)x);
}
public struct RowArray : IEnumerable<Row>
{
public const int NumRows = 16;
private fixed byte _rowData[NumRows * Row.Size];
public ref Row this[int i]
{
get
{
fixed (byte* ptr = _rowData)
{
return ref ((Row*)ptr)[i];
}
}
}
public IEnumerator<Row> GetEnumerator()
{
for (var i = 0; i < NumRows; ++i)
yield return this[i];
}
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public ReadOnlySpan<byte> AsBytes()
{
fixed (byte* ptr = _rowData)
{
return new ReadOnlySpan<byte>(ptr, NumRows * Row.Size);
}
}
}
public RowArray Rows;
public string Name;
public ushort Index;
public bool HasRows;
}
}

View file

@ -1,116 +0,0 @@
using System.IO;
using System.Linq;
using System.Text;
namespace Penumbra.GameData.Files;
public partial class MtrlFile
{
public byte[] Write()
{
using var stream = new MemoryStream();
using( var w = new BinaryWriter( stream ) )
{
const int materialHeaderSize = 4 + 2 + 2 + 2 + 2 + 1 + 1 + 1 + 1;
w.BaseStream.Seek( materialHeaderSize, SeekOrigin.Begin );
ushort cumulativeStringOffset = 0;
foreach( var texture in Textures )
{
w.Write( cumulativeStringOffset );
w.Write( texture.Flags );
cumulativeStringOffset += ( ushort )( texture.Path.Length + 1 );
}
foreach( var set in UvSets )
{
w.Write( cumulativeStringOffset );
w.Write( set.Index );
cumulativeStringOffset += ( ushort )( set.Name.Length + 1 );
}
foreach( var set in ColorSets )
{
w.Write( cumulativeStringOffset );
w.Write( set.Index );
cumulativeStringOffset += ( ushort )( set.Name.Length + 1 );
}
foreach( var text in Textures.Select( t => t.Path )
.Concat( UvSets.Select( c => c.Name ) )
.Concat( ColorSets.Select( c => c.Name ) )
.Append( ShaderPackage.Name ) )
{
w.Write( Encoding.UTF8.GetBytes( text ) );
w.Write( ( byte )'\0' );
}
w.Write( AdditionalData );
var dataSetSize = 0;
foreach( var row in ColorSets.Where( c => c.HasRows ).Select( c => c.Rows ) )
{
var span = row.AsBytes();
w.Write( span );
dataSetSize += span.Length;
}
foreach( var row in ColorDyeSets.Select( c => c.Rows ) )
{
var span = row.AsBytes();
w.Write( span );
dataSetSize += span.Length;
}
w.Write( ( ushort )( ShaderPackage.ShaderValues.Length * 4 ) );
w.Write( ( ushort )ShaderPackage.ShaderKeys.Length );
w.Write( ( ushort )ShaderPackage.Constants.Length );
w.Write( ( ushort )ShaderPackage.Samplers.Length );
w.Write( ShaderPackage.Flags );
foreach( var key in ShaderPackage.ShaderKeys )
{
w.Write( key.Category );
w.Write( key.Value );
}
foreach( var constant in ShaderPackage.Constants )
{
w.Write( constant.Id );
w.Write( constant.ByteOffset );
w.Write( constant.ByteSize );
}
foreach( var sampler in ShaderPackage.Samplers )
{
w.Write( sampler.SamplerId );
w.Write( sampler.Flags );
w.Write( sampler.TextureIndex );
w.Write( ( ushort )0 );
w.Write( ( byte )0 );
}
foreach( var value in ShaderPackage.ShaderValues )
{
w.Write( value );
}
WriteHeader( w, ( ushort )w.BaseStream.Position, dataSetSize, cumulativeStringOffset );
}
return stream.ToArray();
}
private void WriteHeader( BinaryWriter w, ushort fileSize, int dataSetSize, ushort shaderPackageNameOffset )
{
w.BaseStream.Seek( 0, SeekOrigin.Begin );
w.Write( Version );
w.Write( fileSize );
w.Write( ( ushort )dataSetSize );
w.Write( ( ushort )( shaderPackageNameOffset + ShaderPackage.Name.Length + 1 ) );
w.Write( shaderPackageNameOffset );
w.Write( ( byte )Textures.Length );
w.Write( ( byte )UvSets.Length );
w.Write( ( byte )ColorSets.Length );
w.Write( ( byte )AdditionalData.Length );
}
}

View file

@ -1,243 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Lumina.Data.Parsing;
using Lumina.Extensions;
using Penumbra.GameData.Structs;
namespace Penumbra.GameData.Files;
public partial class MtrlFile : IWritable
{
public readonly uint Version;
public bool Valid
=> CheckTextures();
public Texture[] Textures;
public UvSet[] UvSets;
public ColorSet[] ColorSets;
public ColorDyeSet[] ColorDyeSets;
public ShaderPackageData ShaderPackage;
public byte[] AdditionalData;
public bool ApplyDyeTemplate(StmFile stm, int colorSetIdx, int rowIdx, StainId stainId)
{
if (colorSetIdx < 0 || colorSetIdx >= ColorDyeSets.Length || rowIdx is < 0 or >= ColorSet.RowArray.NumRows)
return false;
var dyeSet = ColorDyeSets[colorSetIdx].Rows[rowIdx];
if (!stm.TryGetValue(dyeSet.Template, stainId, out var dyes))
return false;
var ret = false;
if (dyeSet.Diffuse && ColorSets[colorSetIdx].Rows[rowIdx].Diffuse != dyes.Diffuse)
{
ColorSets[colorSetIdx].Rows[rowIdx].Diffuse = dyes.Diffuse;
ret = true;
}
if (dyeSet.Specular && ColorSets[colorSetIdx].Rows[rowIdx].Specular != dyes.Specular)
{
ColorSets[colorSetIdx].Rows[rowIdx].Specular = dyes.Specular;
ret = true;
}
if (dyeSet.SpecularStrength && ColorSets[colorSetIdx].Rows[rowIdx].SpecularStrength != dyes.SpecularPower)
{
ColorSets[colorSetIdx].Rows[rowIdx].SpecularStrength = dyes.SpecularPower;
ret = true;
}
if (dyeSet.Emissive && ColorSets[colorSetIdx].Rows[rowIdx].Emissive != dyes.Emissive)
{
ColorSets[colorSetIdx].Rows[rowIdx].Emissive = dyes.Emissive;
ret = true;
}
if (dyeSet.Gloss && ColorSets[colorSetIdx].Rows[rowIdx].GlossStrength != dyes.Gloss)
{
ColorSets[colorSetIdx].Rows[rowIdx].GlossStrength = dyes.Gloss;
ret = true;
}
return ret;
}
public Span<float> GetConstantValues(Constant constant)
{
if ((constant.ByteOffset & 0x3) != 0
|| (constant.ByteSize & 0x3) != 0
|| (constant.ByteOffset + constant.ByteSize) >> 2 > ShaderPackage.ShaderValues.Length)
return null;
return ShaderPackage.ShaderValues.AsSpan().Slice(constant.ByteOffset >> 2, constant.ByteSize >> 2);
}
public List<(Sampler?, ShpkFile.Resource?)> GetSamplersByTexture(ShpkFile? shpk)
{
var samplers = new List<(Sampler?, ShpkFile.Resource?)>();
for (var i = 0; i < Textures.Length; ++i)
{
samplers.Add((null, null));
}
foreach (var sampler in ShaderPackage.Samplers)
{
samplers[sampler.TextureIndex] = (sampler, shpk?.GetSamplerById(sampler.SamplerId));
}
return samplers;
}
public MtrlFile(byte[] data)
{
using var stream = new MemoryStream(data);
using var r = new BinaryReader(stream);
Version = r.ReadUInt32();
r.ReadUInt16(); // file size
var dataSetSize = r.ReadUInt16();
var stringTableSize = r.ReadUInt16();
var shaderPackageNameOffset = r.ReadUInt16();
var textureCount = r.ReadByte();
var uvSetCount = r.ReadByte();
var colorSetCount = r.ReadByte();
var additionalDataSize = r.ReadByte();
Textures = ReadTextureOffsets(r, textureCount, out var textureOffsets);
UvSets = ReadUvSetOffsets(r, uvSetCount, out var uvOffsets);
ColorSets = ReadColorSetOffsets(r, colorSetCount, out var colorOffsets);
var strings = r.ReadBytes(stringTableSize);
for (var i = 0; i < textureCount; ++i)
Textures[i].Path = UseOffset(strings, textureOffsets[i]);
for (var i = 0; i < uvSetCount; ++i)
UvSets[i].Name = UseOffset(strings, uvOffsets[i]);
for (var i = 0; i < colorSetCount; ++i)
ColorSets[i].Name = UseOffset(strings, colorOffsets[i]);
ColorDyeSets = ColorSets.Length * ColorSet.RowArray.NumRows * ColorSet.Row.Size < dataSetSize
? ColorSets.Select(c => new ColorDyeSet
{
Index = c.Index,
Name = c.Name,
}).ToArray()
: Array.Empty<ColorDyeSet>();
ShaderPackage.Name = UseOffset(strings, shaderPackageNameOffset);
AdditionalData = r.ReadBytes(additionalDataSize);
for (var i = 0; i < ColorSets.Length; ++i)
{
if (stream.Position + ColorSet.RowArray.NumRows * ColorSet.Row.Size <= stream.Length)
{
ColorSets[i].Rows = r.ReadStructure<ColorSet.RowArray>();
ColorSets[i].HasRows = true;
}
else
{
ColorSets[i].HasRows = false;
}
}
for (var i = 0; i < ColorDyeSets.Length; ++i)
ColorDyeSets[i].Rows = r.ReadStructure<ColorDyeSet.RowArray>();
var shaderValueListSize = r.ReadUInt16();
var shaderKeyCount = r.ReadUInt16();
var constantCount = r.ReadUInt16();
var samplerCount = r.ReadUInt16();
ShaderPackage.Flags = r.ReadUInt32();
ShaderPackage.ShaderKeys = r.ReadStructuresAsArray<ShaderKey>(shaderKeyCount);
ShaderPackage.Constants = r.ReadStructuresAsArray<Constant>(constantCount);
ShaderPackage.Samplers = r.ReadStructuresAsArray<Sampler>(samplerCount);
ShaderPackage.ShaderValues = r.ReadStructuresAsArray<float>(shaderValueListSize / 4);
}
private static Texture[] ReadTextureOffsets(BinaryReader r, int count, out ushort[] offsets)
{
var ret = new Texture[count];
offsets = new ushort[count];
for (var i = 0; i < count; ++i)
{
offsets[i] = r.ReadUInt16();
ret[i].Flags = r.ReadUInt16();
}
return ret;
}
private static UvSet[] ReadUvSetOffsets(BinaryReader r, int count, out ushort[] offsets)
{
var ret = new UvSet[count];
offsets = new ushort[count];
for (var i = 0; i < count; ++i)
{
offsets[i] = r.ReadUInt16();
ret[i].Index = r.ReadUInt16();
}
return ret;
}
private static ColorSet[] ReadColorSetOffsets(BinaryReader r, int count, out ushort[] offsets)
{
var ret = new ColorSet[count];
offsets = new ushort[count];
for (var i = 0; i < count; ++i)
{
offsets[i] = r.ReadUInt16();
ret[i].Index = r.ReadUInt16();
}
return ret;
}
private static string UseOffset(ReadOnlySpan<byte> strings, ushort offset)
{
strings = strings[offset..];
var end = strings.IndexOf((byte)'\0');
return Encoding.UTF8.GetString(end == -1 ? strings : strings[..end]);
}
private bool CheckTextures()
=> Textures.All(texture => texture.Path.Contains('/'));
public struct UvSet
{
public string Name;
public ushort Index;
}
public struct Texture
{
public string Path;
public ushort Flags;
public bool DX11
=> (Flags & 0x8000) != 0;
}
public struct Constant
{
public uint Id;
public ushort ByteOffset;
public ushort ByteSize;
}
public struct ShaderPackageData
{
public string Name;
public ShaderKey[] ShaderKeys;
public Constant[] Constants;
public Sampler[] Samplers;
public float[] ShaderValues;
public uint Flags;
}
}

View file

@ -1,220 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Lumina.Misc;
using Penumbra.GameData.Data;
namespace Penumbra.GameData.Files;
public partial class ShpkFile
{
public struct Shader
{
public DisassembledShader.ShaderStage Stage;
public DxVersion DirectXVersion;
public Resource[] Constants;
public Resource[] Samplers;
public Resource[] Uavs;
public byte[] AdditionalHeader;
private byte[] _byteData;
private DisassembledShader? _disassembly;
public byte[] Blob
{
get => _byteData;
set
{
if (_byteData == value)
return;
if (Stage != DisassembledShader.ShaderStage.Unspecified)
{
// Reject the blob entirely if we can't disassemble it or if we find inconsistencies.
var disasm = DisassembledShader.Disassemble(value);
if (disasm.Stage != Stage || (disasm.ShaderModel >> 8) + 6 != (uint)DirectXVersion)
throw new ArgumentException(
$"The supplied blob is a DirectX {(disasm.ShaderModel >> 8) + 6} {disasm.Stage} shader ; expected a DirectX {(uint)DirectXVersion} {Stage} shader.",
nameof(value));
if (disasm.ShaderModel >= 0x0500)
{
var samplers = new Dictionary<uint, string>();
var textures = new Dictionary<uint, string>();
foreach (var binding in disasm.ResourceBindings)
{
switch (binding.Type)
{
case DisassembledShader.ResourceType.Texture:
textures[binding.Slot] = NormalizeResourceName(binding.Name);
break;
case DisassembledShader.ResourceType.Sampler:
samplers[binding.Slot] = NormalizeResourceName(binding.Name);
break;
}
}
if (samplers.Count != textures.Count
|| !samplers.All(pair => textures.TryGetValue(pair.Key, out var texName) && pair.Value == texName))
throw new ArgumentException($"The supplied blob has inconsistent sampler and texture allocation.");
}
_byteData = value;
_disassembly = disasm;
}
else
{
_byteData = value;
_disassembly = null;
}
UpdateUsed();
}
}
public DisassembledShader? Disassembly
=> _disassembly;
public Resource? GetConstantById(uint id)
=> Constants.FirstOrNull(res => res.Id == id);
public Resource? GetConstantByName(string name)
=> Constants.FirstOrNull(res => res.Name == name);
public Resource? GetSamplerById(uint id)
=> Samplers.FirstOrNull(s => s.Id == id);
public Resource? GetSamplerByName(string name)
=> Samplers.FirstOrNull(s => s.Name == name);
public Resource? GetUavById(uint id)
=> Uavs.FirstOrNull(u => u.Id == id);
public Resource? GetUavByName(string name)
=> Uavs.FirstOrNull(u => u.Name == name);
public void UpdateResources(ShpkFile file)
{
if (_disassembly == null)
throw new InvalidOperationException();
var constants = new List<Resource>();
var samplers = new List<Resource>();
var uavs = new List<Resource>();
foreach (var binding in _disassembly.ResourceBindings)
{
switch (binding.Type)
{
case DisassembledShader.ResourceType.ConstantBuffer:
var name = NormalizeResourceName(binding.Name);
// We want to preserve IDs as much as possible, and to deterministically generate new ones in a way that's most compliant with the native ones, to maximize compatibility.
var id = GetConstantByName(name)?.Id ?? file.GetConstantByName(name)?.Id ?? Crc32.Get(name, 0xFFFFFFFFu);
constants.Add(new Resource
{
Id = id,
Name = name,
Slot = (ushort)binding.Slot,
Size = (ushort)binding.RegisterCount,
Used = binding.Used,
UsedDynamically = binding.UsedDynamically,
});
break;
case DisassembledShader.ResourceType.Texture:
name = NormalizeResourceName(binding.Name);
id = GetSamplerByName(name)?.Id ?? file.GetSamplerByName(name)?.Id ?? Crc32.Get(name, 0xFFFFFFFFu);
samplers.Add(new Resource
{
Id = id,
Name = name,
Slot = (ushort)binding.Slot,
Size = (ushort)binding.Slot,
Used = binding.Used,
UsedDynamically = binding.UsedDynamically,
});
break;
case DisassembledShader.ResourceType.Uav:
name = NormalizeResourceName(binding.Name);
id = GetUavByName(name)?.Id ?? file.GetUavByName(name)?.Id ?? Crc32.Get(name, 0xFFFFFFFFu);
uavs.Add(new Resource
{
Id = id,
Name = name,
Slot = (ushort)binding.Slot,
Size = (ushort)binding.Slot,
Used = binding.Used,
UsedDynamically = binding.UsedDynamically,
});
break;
}
}
Constants = constants.ToArray();
Samplers = samplers.ToArray();
Uavs = uavs.ToArray();
}
private void UpdateUsed()
{
if (_disassembly != null)
{
var cbUsage = new Dictionary<string, (DisassembledShader.VectorComponents[], DisassembledShader.VectorComponents)>();
var tUsage = new Dictionary<string, (DisassembledShader.VectorComponents[], DisassembledShader.VectorComponents)>();
var uUsage = new Dictionary<string, (DisassembledShader.VectorComponents[], DisassembledShader.VectorComponents)>();
foreach (var binding in _disassembly.ResourceBindings)
{
switch (binding.Type)
{
case DisassembledShader.ResourceType.ConstantBuffer:
cbUsage[NormalizeResourceName(binding.Name)] = (binding.Used, binding.UsedDynamically);
break;
case DisassembledShader.ResourceType.Texture:
tUsage[NormalizeResourceName(binding.Name)] = (binding.Used, binding.UsedDynamically);
break;
case DisassembledShader.ResourceType.Uav:
uUsage[NormalizeResourceName(binding.Name)] = (binding.Used, binding.UsedDynamically);
break;
}
}
static void CopyUsed(Resource[] resources,
Dictionary<string, (DisassembledShader.VectorComponents[], DisassembledShader.VectorComponents)> used)
{
for (var i = 0; i < resources.Length; ++i)
{
if (used.TryGetValue(resources[i].Name, out var usage))
{
resources[i].Used = usage.Item1;
resources[i].UsedDynamically = usage.Item2;
}
else
{
resources[i].Used = null;
resources[i].UsedDynamically = null;
}
}
}
CopyUsed(Constants, cbUsage);
CopyUsed(Samplers, tUsage);
CopyUsed(Uavs, uUsage);
}
else
{
ClearUsed(Constants);
ClearUsed(Samplers);
ClearUsed(Uavs);
}
}
private static string NormalizeResourceName(string resourceName)
{
var dot = resourceName.IndexOf('.');
if (dot >= 0)
return resourceName[..dot];
if (resourceName.Length > 1 && resourceName[^2] is '_' && resourceName[^1] is 'S' or 'T')
return resourceName[..^2];
return resourceName;
}
}
}

View file

@ -1,79 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace Penumbra.GameData.Files;
public partial class ShpkFile
{
public class StringPool
{
public MemoryStream Data;
public List<int> StartingOffsets;
public StringPool(ReadOnlySpan<byte> bytes)
{
Data = new MemoryStream();
Data.Write(bytes);
StartingOffsets = new List<int>
{
0,
};
for (var i = 0; i < bytes.Length; ++i)
{
if (bytes[i] == 0)
StartingOffsets.Add(i + 1);
}
if (StartingOffsets[^1] == bytes.Length)
StartingOffsets.RemoveAt(StartingOffsets.Count - 1);
else
Data.WriteByte(0);
}
public string GetString(int offset, int size)
=> Encoding.UTF8.GetString(Data.GetBuffer().AsSpan().Slice(offset, size));
public string GetNullTerminatedString(int offset)
{
var str = Data.GetBuffer().AsSpan()[offset..];
var size = str.IndexOf((byte)0);
if (size >= 0)
str = str[..size];
return Encoding.UTF8.GetString(str);
}
public (int, int) FindOrAddString(string str)
{
var dataSpan = Data.GetBuffer().AsSpan();
var bytes = Encoding.UTF8.GetBytes(str);
foreach (var offset in StartingOffsets)
{
if (offset + bytes.Length > Data.Length)
break;
var strSpan = dataSpan[offset..];
var match = true;
for (var i = 0; i < bytes.Length; ++i)
{
if (strSpan[i] != bytes[i])
{
match = false;
break;
}
}
if (match && strSpan[bytes.Length] == 0)
return (offset, bytes.Length);
}
Data.Seek(0L, SeekOrigin.End);
var newOffset = (int)Data.Position;
StartingOffsets.Add(newOffset);
Data.Write(bytes);
Data.WriteByte(0);
return (newOffset, bytes.Length);
}
}
}

View file

@ -1,165 +0,0 @@
using System;
using System.IO;
namespace Penumbra.GameData.Files;
public partial class ShpkFile
{
public byte[] Write()
{
if (SubViewKeys.Length != 2)
throw new InvalidDataException();
using var stream = new MemoryStream();
using var blobs = new MemoryStream();
var strings = new StringPool(ReadOnlySpan<byte>.Empty);
using (var w = new BinaryWriter(stream))
{
w.Write(ShPkMagic);
w.Write(Version);
w.Write(DirectXVersion switch
{
DxVersion.DirectX9 => Dx9Magic,
DxVersion.DirectX11 => Dx11Magic,
_ => throw new NotImplementedException(),
});
var offsetsPosition = stream.Position;
w.Write(0u); // Placeholder for file size
w.Write(0u); // Placeholder for blobs offset
w.Write(0u); // Placeholder for strings offset
w.Write((uint)VertexShaders.Length);
w.Write((uint)PixelShaders.Length);
w.Write(MaterialParamsSize);
w.Write((uint)MaterialParams.Length);
w.Write((uint)Constants.Length);
w.Write((uint)Samplers.Length);
w.Write((uint)Uavs.Length);
w.Write((uint)SystemKeys.Length);
w.Write((uint)SceneKeys.Length);
w.Write((uint)MaterialKeys.Length);
w.Write((uint)Nodes.Length);
w.Write((uint)Items.Length);
WriteShaderArray(w, VertexShaders, blobs, strings);
WriteShaderArray(w, PixelShaders, blobs, strings);
foreach (var materialParam in MaterialParams)
{
w.Write(materialParam.Id);
w.Write(materialParam.ByteOffset);
w.Write(materialParam.ByteSize);
}
WriteResourceArray(w, Constants, strings);
WriteResourceArray(w, Samplers, strings);
WriteResourceArray(w, Uavs, strings);
foreach (var key in SystemKeys)
{
w.Write(key.Id);
w.Write(key.DefaultValue);
}
foreach (var key in SceneKeys)
{
w.Write(key.Id);
w.Write(key.DefaultValue);
}
foreach (var key in MaterialKeys)
{
w.Write(key.Id);
w.Write(key.DefaultValue);
}
foreach (var key in SubViewKeys)
w.Write(key.DefaultValue);
foreach (var node in Nodes)
{
if (node.PassIndices.Length != 16
|| node.SystemKeys.Length != SystemKeys.Length
|| node.SceneKeys.Length != SceneKeys.Length
|| node.MaterialKeys.Length != MaterialKeys.Length
|| node.SubViewKeys.Length != SubViewKeys.Length)
throw new InvalidDataException();
w.Write(node.Id);
w.Write(node.Passes.Length);
w.Write(node.PassIndices);
foreach (var key in node.SystemKeys)
w.Write(key);
foreach (var key in node.SceneKeys)
w.Write(key);
foreach (var key in node.MaterialKeys)
w.Write(key);
foreach (var key in node.SubViewKeys)
w.Write(key);
foreach (var pass in node.Passes)
{
w.Write(pass.Id);
w.Write(pass.VertexShader);
w.Write(pass.PixelShader);
}
}
foreach (var item in Items)
{
w.Write(item.Id);
w.Write(item.Node);
}
w.Write(AdditionalData);
var blobsOffset = (int)stream.Position;
blobs.WriteTo(stream);
var stringsOffset = (int)stream.Position;
strings.Data.WriteTo(stream);
var fileSize = (int)stream.Position;
stream.Seek(offsetsPosition, SeekOrigin.Begin);
w.Write(fileSize);
w.Write(blobsOffset);
w.Write(stringsOffset);
}
return stream.ToArray();
}
private static void WriteResourceArray(BinaryWriter w, Resource[] array, StringPool strings)
{
foreach (var buf in array)
{
var (strOffset, strSize) = strings.FindOrAddString(buf.Name);
w.Write(buf.Id);
w.Write(strOffset);
w.Write(strSize);
w.Write(buf.Slot);
w.Write(buf.Size);
}
}
private static void WriteShaderArray(BinaryWriter w, Shader[] array, MemoryStream blobs, StringPool strings)
{
foreach (var shader in array)
{
var blobOffset = (int)blobs.Position;
blobs.Write(shader.AdditionalHeader);
blobs.Write(shader.Blob);
var blobSize = (int)blobs.Position - blobOffset;
w.Write(blobOffset);
w.Write(blobSize);
w.Write((ushort)shader.Constants.Length);
w.Write((ushort)shader.Samplers.Length);
w.Write((ushort)shader.Uavs.Length);
w.Write((ushort)0);
WriteResourceArray(w, shader.Constants, strings);
WriteResourceArray(w, shader.Samplers, strings);
WriteResourceArray(w, shader.Uavs, strings);
}
}
}

View file

@ -1,486 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Lumina.Extensions;
using Penumbra.GameData.Data;
namespace Penumbra.GameData.Files;
public partial class ShpkFile : IWritable
{
private const uint ShPkMagic = 0x6B506853u; // bytes of ShPk
private const uint Dx9Magic = 0x00395844u; // bytes of DX9\0
private const uint Dx11Magic = 0x31315844u; // bytes of DX11
public const uint MaterialParamsConstantId = 0x64D12851u;
public uint Version;
public DxVersion DirectXVersion;
public Shader[] VertexShaders;
public Shader[] PixelShaders;
public uint MaterialParamsSize;
public MaterialParam[] MaterialParams;
public Resource[] Constants;
public Resource[] Samplers;
public Resource[] Uavs;
public Key[] SystemKeys;
public Key[] SceneKeys;
public Key[] MaterialKeys;
public Key[] SubViewKeys;
public Node[] Nodes;
public Item[] Items;
public byte[] AdditionalData;
public bool Valid { get; private set; }
private bool _changed;
public MaterialParam? GetMaterialParamById(uint id)
=> MaterialParams.FirstOrNull(m => m.Id == id);
public Resource? GetConstantById(uint id)
=> Constants.FirstOrNull(c => c.Id == id);
public Resource? GetConstantByName(string name)
=> Constants.FirstOrNull(c => c.Name == name);
public Resource? GetSamplerById(uint id)
=> Samplers.FirstOrNull(s => s.Id == id);
public Resource? GetSamplerByName(string name)
=> Samplers.FirstOrNull(s => s.Name == name);
public Resource? GetUavById(uint id)
=> Uavs.FirstOrNull(u => u.Id == id);
public Resource? GetUavByName(string name)
=> Uavs.FirstOrNull(u => u.Name == name);
public Key? GetSystemKeyById(uint id)
=> SystemKeys.FirstOrNull(k => k.Id == id);
public Key? GetSceneKeyById(uint id)
=> SceneKeys.FirstOrNull(k => k.Id == id);
public Key? GetMaterialKeyById(uint id)
=> MaterialKeys.FirstOrNull(k => k.Id == id);
public Node? GetNodeById(uint id)
=> Nodes.FirstOrNull(n => n.Id == id);
public Item? GetItemById(uint id)
=> Items.FirstOrNull(i => i.Id == id);
public ShpkFile(byte[] data, bool disassemble = false)
{
using var stream = new MemoryStream(data);
using var r = new BinaryReader(stream);
if (r.ReadUInt32() != ShPkMagic)
throw new InvalidDataException();
Version = r.ReadUInt32();
DirectXVersion = r.ReadUInt32() switch
{
Dx9Magic => DxVersion.DirectX9,
Dx11Magic => DxVersion.DirectX11,
_ => throw new InvalidDataException(),
};
if (r.ReadUInt32() != data.Length)
throw new InvalidDataException();
var blobsOffset = r.ReadUInt32();
var stringsOffset = r.ReadUInt32();
var vertexShaderCount = r.ReadUInt32();
var pixelShaderCount = r.ReadUInt32();
MaterialParamsSize = r.ReadUInt32();
var materialParamCount = r.ReadUInt32();
var constantCount = r.ReadUInt32();
var samplerCount = r.ReadUInt32();
var uavCount = r.ReadUInt32();
var systemKeyCount = r.ReadUInt32();
var sceneKeyCount = r.ReadUInt32();
var materialKeyCount = r.ReadUInt32();
var nodeCount = r.ReadUInt32();
var itemCount = r.ReadUInt32();
var blobs = new ReadOnlySpan<byte>(data, (int)blobsOffset, (int)(stringsOffset - blobsOffset));
var strings = new StringPool(new ReadOnlySpan<byte>(data, (int)stringsOffset, (int)(data.Length - stringsOffset)));
VertexShaders = ReadShaderArray(r, (int)vertexShaderCount, DisassembledShader.ShaderStage.Vertex, DirectXVersion, disassemble, blobs,
strings);
PixelShaders = ReadShaderArray(r, (int)pixelShaderCount, DisassembledShader.ShaderStage.Pixel, DirectXVersion, disassemble, blobs,
strings);
MaterialParams = r.ReadStructuresAsArray<MaterialParam>((int)materialParamCount);
Constants = ReadResourceArray(r, (int)constantCount, strings);
Samplers = ReadResourceArray(r, (int)samplerCount, strings);
Uavs = ReadResourceArray(r, (int)uavCount, strings);
SystemKeys = ReadKeyArray(r, (int)systemKeyCount);
SceneKeys = ReadKeyArray(r, (int)sceneKeyCount);
MaterialKeys = ReadKeyArray(r, (int)materialKeyCount);
var subViewKey1Default = r.ReadUInt32();
var subViewKey2Default = r.ReadUInt32();
SubViewKeys = new Key[]
{
new()
{
Id = 1,
DefaultValue = subViewKey1Default,
Values = Array.Empty<uint>(),
},
new()
{
Id = 2,
DefaultValue = subViewKey2Default,
Values = Array.Empty<uint>(),
},
};
Nodes = ReadNodeArray(r, (int)nodeCount, SystemKeys.Length, SceneKeys.Length, MaterialKeys.Length, SubViewKeys.Length);
Items = r.ReadStructuresAsArray<Item>((int)itemCount);
AdditionalData = r.ReadBytes((int)(blobsOffset - r.BaseStream.Position)); // This should be empty, but just in case.
if (disassemble)
UpdateUsed();
UpdateKeyValues();
Valid = true;
_changed = false;
}
public void UpdateResources()
{
var constants = new Dictionary<uint, Resource>();
var samplers = new Dictionary<uint, Resource>();
var uavs = new Dictionary<uint, Resource>();
static void CollectResources(Dictionary<uint, Resource> resources, Resource[] shaderResources, Func<uint, Resource?> getExistingById,
DisassembledShader.ResourceType type)
{
foreach (var resource in shaderResources)
{
if (resources.TryGetValue(resource.Id, out var carry) && type != DisassembledShader.ResourceType.ConstantBuffer)
continue;
var existing = getExistingById(resource.Id);
resources[resource.Id] = new Resource
{
Id = resource.Id,
Name = resource.Name,
Slot = existing?.Slot ?? (type == DisassembledShader.ResourceType.ConstantBuffer ? (ushort)65535 : (ushort)2),
Size = type == DisassembledShader.ResourceType.ConstantBuffer ? Math.Max(carry.Size, resource.Size) : existing?.Size ?? 0,
Used = null,
UsedDynamically = null,
};
}
}
foreach (var shader in VertexShaders)
{
CollectResources(constants, shader.Constants, GetConstantById, DisassembledShader.ResourceType.ConstantBuffer);
CollectResources(samplers, shader.Samplers, GetSamplerById, DisassembledShader.ResourceType.Sampler);
CollectResources(uavs, shader.Uavs, GetUavById, DisassembledShader.ResourceType.Uav);
}
foreach (var shader in PixelShaders)
{
CollectResources(constants, shader.Constants, GetConstantById, DisassembledShader.ResourceType.ConstantBuffer);
CollectResources(samplers, shader.Samplers, GetSamplerById, DisassembledShader.ResourceType.Sampler);
CollectResources(uavs, shader.Uavs, GetUavById, DisassembledShader.ResourceType.Uav);
}
Constants = constants.Values.ToArray();
Samplers = samplers.Values.ToArray();
Uavs = uavs.Values.ToArray();
UpdateUsed();
// Ceil required size to a multiple of 16 bytes.
// Offsets can be skipped, MaterialParamsConstantId's size is the count.
MaterialParamsSize = (GetConstantById(MaterialParamsConstantId)?.Size ?? 0u) << 4;
foreach (var param in MaterialParams)
MaterialParamsSize = Math.Max(MaterialParamsSize, (uint)param.ByteOffset + param.ByteSize);
MaterialParamsSize = (MaterialParamsSize + 0xFu) & ~0xFu;
}
private void UpdateUsed()
{
var cUsage = new Dictionary<uint, (DisassembledShader.VectorComponents[], DisassembledShader.VectorComponents)>();
var sUsage = new Dictionary<uint, (DisassembledShader.VectorComponents[], DisassembledShader.VectorComponents)>();
var uUsage = new Dictionary<uint, (DisassembledShader.VectorComponents[], DisassembledShader.VectorComponents)>();
static void CollectUsed(Dictionary<uint, (DisassembledShader.VectorComponents[], DisassembledShader.VectorComponents)> usage,
Resource[] resources)
{
foreach (var resource in resources)
{
if (resource.Used == null)
continue;
usage.TryGetValue(resource.Id, out var carry);
carry.Item1 ??= Array.Empty<DisassembledShader.VectorComponents>();
var combined = new DisassembledShader.VectorComponents[Math.Max(carry.Item1.Length, resource.Used.Length)];
for (var i = 0; i < combined.Length; ++i)
combined[i] = (i < carry.Item1.Length ? carry.Item1[i] : 0) | (i < resource.Used.Length ? resource.Used[i] : 0);
usage[resource.Id] = (combined, carry.Item2 | (resource.UsedDynamically ?? 0));
}
}
static void CopyUsed(Resource[] resources,
Dictionary<uint, (DisassembledShader.VectorComponents[], DisassembledShader.VectorComponents)> used)
{
for (var i = 0; i < resources.Length; ++i)
{
if (used.TryGetValue(resources[i].Id, out var usage))
{
resources[i].Used = usage.Item1;
resources[i].UsedDynamically = usage.Item2;
}
else
{
resources[i].Used = null;
resources[i].UsedDynamically = null;
}
}
}
foreach (var shader in VertexShaders)
{
CollectUsed(cUsage, shader.Constants);
CollectUsed(sUsage, shader.Samplers);
CollectUsed(uUsage, shader.Uavs);
}
foreach (var shader in PixelShaders)
{
CollectUsed(cUsage, shader.Constants);
CollectUsed(sUsage, shader.Samplers);
CollectUsed(uUsage, shader.Uavs);
}
CopyUsed(Constants, cUsage);
CopyUsed(Samplers, sUsage);
CopyUsed(Uavs, uUsage);
}
public void UpdateKeyValues()
{
static HashSet<uint>[] InitializeValueSet(Key[] keys)
=> Array.ConvertAll(keys, key => new HashSet<uint>()
{
key.DefaultValue,
});
static void CollectValues(HashSet<uint>[] valueSets, uint[] values)
{
for (var i = 0; i < valueSets.Length; ++i)
valueSets[i].Add(values[i]);
}
static void CopyValues(Key[] keys, HashSet<uint>[] valueSets)
{
for (var i = 0; i < keys.Length; ++i)
keys[i].Values = valueSets[i].ToArray();
}
var systemKeyValues = InitializeValueSet(SystemKeys);
var sceneKeyValues = InitializeValueSet(SceneKeys);
var materialKeyValues = InitializeValueSet(MaterialKeys);
var subViewKeyValues = InitializeValueSet(SubViewKeys);
foreach (var node in Nodes)
{
CollectValues(systemKeyValues, node.SystemKeys);
CollectValues(sceneKeyValues, node.SceneKeys);
CollectValues(materialKeyValues, node.MaterialKeys);
CollectValues(subViewKeyValues, node.SubViewKeys);
}
CopyValues(SystemKeys, systemKeyValues);
CopyValues(SceneKeys, sceneKeyValues);
CopyValues(MaterialKeys, materialKeyValues);
CopyValues(SubViewKeys, subViewKeyValues);
}
public void SetInvalid()
=> Valid = false;
public void SetChanged()
=> _changed = true;
public bool IsChanged()
{
var changed = _changed;
_changed = false;
return changed;
}
private static void ClearUsed(Resource[] resources)
{
for (var i = 0; i < resources.Length; ++i)
{
resources[i].Used = null;
resources[i].UsedDynamically = null;
}
}
private static Resource[] ReadResourceArray(BinaryReader r, int count, StringPool strings)
{
var ret = new Resource[count];
for (var i = 0; i < count; ++i)
{
var id = r.ReadUInt32();
var strOffset = r.ReadUInt32();
var strSize = r.ReadUInt32();
ret[i] = new Resource
{
Id = id,
Name = strings.GetString((int)strOffset, (int)strSize),
Slot = r.ReadUInt16(),
Size = r.ReadUInt16(),
};
}
return ret;
}
private static Shader[] ReadShaderArray(BinaryReader r, int count, DisassembledShader.ShaderStage stage, DxVersion directX,
bool disassemble, ReadOnlySpan<byte> blobs, StringPool strings)
{
var extraHeaderSize = stage switch
{
DisassembledShader.ShaderStage.Vertex => directX switch
{
DxVersion.DirectX9 => 4,
DxVersion.DirectX11 => 8,
_ => throw new NotImplementedException(),
},
_ => 0,
};
var ret = new Shader[count];
for (var i = 0; i < count; ++i)
{
var blobOffset = r.ReadUInt32();
var blobSize = r.ReadUInt32();
var constantCount = r.ReadUInt16();
var samplerCount = r.ReadUInt16();
var uavCount = r.ReadUInt16();
if (r.ReadUInt16() != 0)
throw new NotImplementedException();
var rawBlob = blobs.Slice((int)blobOffset, (int)blobSize);
ret[i] = new Shader
{
Stage = disassemble ? stage : DisassembledShader.ShaderStage.Unspecified,
DirectXVersion = directX,
Constants = ReadResourceArray(r, constantCount, strings),
Samplers = ReadResourceArray(r, samplerCount, strings),
Uavs = ReadResourceArray(r, uavCount, strings),
AdditionalHeader = rawBlob[..extraHeaderSize].ToArray(),
Blob = rawBlob[extraHeaderSize..].ToArray(),
};
}
return ret;
}
private static Key[] ReadKeyArray(BinaryReader r, int count)
{
var ret = new Key[count];
for (var i = 0; i < count; ++i)
{
ret[i] = new Key
{
Id = r.ReadUInt32(),
DefaultValue = r.ReadUInt32(),
Values = Array.Empty<uint>(),
};
}
return ret;
}
private static Node[] ReadNodeArray(BinaryReader r, int count, int systemKeyCount, int sceneKeyCount, int materialKeyCount,
int subViewKeyCount)
{
var ret = new Node[count];
for (var i = 0; i < count; ++i)
{
var id = r.ReadUInt32();
var passCount = r.ReadUInt32();
ret[i] = new Node
{
Id = id,
PassIndices = r.ReadBytes(16),
SystemKeys = r.ReadStructuresAsArray<uint>(systemKeyCount),
SceneKeys = r.ReadStructuresAsArray<uint>(sceneKeyCount),
MaterialKeys = r.ReadStructuresAsArray<uint>(materialKeyCount),
SubViewKeys = r.ReadStructuresAsArray<uint>(subViewKeyCount),
Passes = r.ReadStructuresAsArray<Pass>((int)passCount),
};
}
return ret;
}
public enum DxVersion : uint
{
DirectX9 = 9,
DirectX11 = 11,
}
public struct Resource
{
public uint Id;
public string Name;
public ushort Slot;
public ushort Size;
public DisassembledShader.VectorComponents[]? Used;
public DisassembledShader.VectorComponents? UsedDynamically;
}
public struct MaterialParam
{
public uint Id;
public ushort ByteOffset;
public ushort ByteSize;
}
public struct Pass
{
public uint Id;
public uint VertexShader;
public uint PixelShader;
}
public struct Key
{
public uint Id;
public uint DefaultValue;
public uint[] Values;
}
public struct Node
{
public uint Id;
public byte[] PassIndices;
public uint[] SystemKeys;
public uint[] SceneKeys;
public uint[] MaterialKeys;
public uint[] SubViewKeys;
public Pass[] Passes;
}
public struct Item
{
public uint Id;
public uint Node;
}
}

View file

@ -1,174 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using Lumina.Extensions;
using Penumbra.GameData.Structs;
namespace Penumbra.GameData.Files;
public partial class StmFile
{
public readonly struct StainingTemplateEntry
{
/// <summary>
/// The number of stains is capped at 128 at the moment
/// </summary>
public const int NumElements = 128;
// ColorSet row information for each stain.
public readonly IReadOnlyList<(Half R, Half G, Half B)> DiffuseEntries;
public readonly IReadOnlyList<(Half R, Half G, Half B)> SpecularEntries;
public readonly IReadOnlyList<(Half R, Half G, Half B)> EmissiveEntries;
public readonly IReadOnlyList<Half> GlossEntries;
public readonly IReadOnlyList<Half> SpecularPowerEntries;
public DyePack this[StainId idx]
=> this[(int)idx.Value];
public DyePack this[int idx]
{
get
{
// The 0th index is skipped.
if (idx is <= 0 or > NumElements)
return default;
--idx;
var (dr, dg, db) = DiffuseEntries[idx];
var (sr, sg, sb) = SpecularEntries[idx];
var (er, eg, eb) = EmissiveEntries[idx];
var g = GlossEntries[idx];
var sp = SpecularPowerEntries[idx];
// Convert to DyePack using floats.
return new DyePack
{
Diffuse = new Vector3((float)dr, (float)dg, (float)db),
Specular = new Vector3((float)sr, (float)sg, (float)sb),
Emissive = new Vector3((float)er, (float)eg, (float)eb),
Gloss = (float)g,
SpecularPower = (float)sp,
};
}
}
private static IReadOnlyList<T> ReadArray<T>(BinaryReader br, int offset, int size, Func<BinaryReader, T> read, int entrySize)
{
br.Seek(offset);
var arraySize = size / entrySize;
// The actual amount of entries informs which type of list we use.
switch (arraySize)
{
case 0: return new RepeatingList<T>(default!, NumElements); // All default
case 1: return new RepeatingList<T>(read(br), NumElements); // All single entry
case NumElements: // 1-to-1 entries
var ret = new T[NumElements];
for (var i = 0; i < NumElements; ++i)
ret[i] = read(br);
return ret;
// Indexed access.
case < NumElements: return new IndexedList<T>(br, arraySize - NumElements / entrySize, NumElements, read);
// Should not happen.
case > NumElements: throw new InvalidDataException($"Stain Template can not have more than {NumElements} elements.");
}
}
// Read functions
private static (Half, Half, Half) ReadTriple(BinaryReader br)
=> (br.ReadHalf(), br.ReadHalf(), br.ReadHalf());
private static Half ReadSingle(BinaryReader br)
=> br.ReadHalf();
// Actually parse an entry.
public unsafe StainingTemplateEntry(BinaryReader br, int offset)
{
br.Seek(offset);
// 5 different lists of values.
Span<ushort> ends = stackalloc ushort[5];
for (var i = 0; i < ends.Length; ++i)
ends[i] = (ushort)(br.ReadUInt16() * 2); // because the ends are in terms of ushort.
offset += ends.Length * 2;
DiffuseEntries = ReadArray(br, offset, ends[0], ReadTriple, 6);
SpecularEntries = ReadArray(br, offset + ends[0], ends[1] - ends[0], ReadTriple, 6);
EmissiveEntries = ReadArray(br, offset + ends[1], ends[2] - ends[1], ReadTriple, 6);
GlossEntries = ReadArray(br, offset + ends[2], ends[3] - ends[2], ReadSingle, 2);
SpecularPowerEntries = ReadArray(br, offset + ends[3], ends[4] - ends[3], ReadSingle, 2);
}
/// <summary>
/// Used if a single value is used for all entries of a list.
/// </summary>
private sealed class RepeatingList<T> : IReadOnlyList<T>
{
private readonly T _value;
public int Count { get; }
public RepeatingList(T value, int size)
{
_value = value;
Count = size;
}
public IEnumerator<T> GetEnumerator()
{
for (var i = 0; i < Count; ++i)
yield return _value;
}
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public T this[int index]
=> index >= 0 && index < Count ? _value : throw new IndexOutOfRangeException();
}
/// <summary>
/// Used if there is a small set of values for a bigger list, accessed via index information.
/// </summary>
private sealed class IndexedList<T> : IReadOnlyList<T>
{
private readonly T[] _values;
private readonly byte[] _indices;
/// <summary>
/// Reads <paramref name="count"/> values from <paramref name="br"/> via <paramref name="read"/>, then reads <paramref name="indexCount"/> byte indices.
/// </summary>
public IndexedList(BinaryReader br, int count, int indexCount, Func<BinaryReader, T> read)
{
_values = new T[count + 1];
_indices = new byte[indexCount];
_values[0] = default!;
for (var i = 1; i < count + 1; ++i)
_values[i] = read(br);
// Seems to be an unused 0xFF byte marker.
// Necessary for correct offsets.
br.ReadByte();
for (var i = 0; i < indexCount; ++i)
{
_indices[i] = br.ReadByte();
if (_indices[i] > count)
_indices[i] = 0;
}
}
public IEnumerator<T> GetEnumerator()
{
for (var i = 0; i < NumElements; ++i)
yield return _values[_indices[i]];
}
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public int Count
=> _indices.Length;
public T this[int index]
=> index >= 0 && index < Count ? _values[_indices[index]] : default!;
}
}
}

View file

@ -1,98 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using Dalamud.Data;
using Penumbra.GameData.Structs;
namespace Penumbra.GameData.Files;
public partial class StmFile
{
public const string Path = "chara/base_material/stainingtemplate.stm";
/// <summary>
/// All dye-able color set information for a row.
/// </summary>
public record struct DyePack
{
public Vector3 Diffuse;
public Vector3 Specular;
public Vector3 Emissive;
public float Gloss;
public float SpecularPower;
}
/// <summary>
/// All currently available dyeing templates with their IDs.
/// </summary>
public readonly IReadOnlyDictionary<ushort, StainingTemplateEntry> Entries;
/// <summary>
/// Access a specific dye pack.
/// </summary>
/// <param name="template">The ID of the accessed template.</param>
/// <param name="idx">The ID of the Stain.</param>
/// <returns>The corresponding color set information or a defaulted DyePack of 0-entries.</returns>
public DyePack this[ushort template, int idx]
=> Entries.TryGetValue(template, out var entry) ? entry[idx] : default;
/// <inheritdoc cref="this[ushort, StainId]"/>
public DyePack this[ushort template, StainId idx]
=> this[template, (int)idx.Value];
/// <summary>
/// Try to access a specific dye pack.
/// </summary>
/// <param name="template">The ID of the accessed template.</param>
/// <param name="idx">The ID of the Stain.</param>
/// <param name="dyes">On success, the corresponding color set information, otherwise a defaulted DyePack.</param>
/// <returns>True on success, false otherwise.</returns>
public bool TryGetValue(ushort template, StainId idx, out DyePack dyes)
{
if (idx.Value is > 0 and <= StainingTemplateEntry.NumElements && Entries.TryGetValue(template, out var entry))
{
dyes = entry[idx];
return true;
}
dyes = default;
return false;
}
/// <summary>
/// Create a STM file from the given data array.
/// </summary>
public StmFile(byte[] data)
{
using var stream = new MemoryStream(data);
using var br = new BinaryReader(stream);
br.ReadUInt32();
var numEntries = br.ReadInt32();
var keys = new ushort[numEntries];
var offsets = new ushort[numEntries];
for (var i = 0; i < numEntries; ++i)
keys[i] = br.ReadUInt16();
for (var i = 0; i < numEntries; ++i)
offsets[i] = br.ReadUInt16();
var entries = new Dictionary<ushort, StainingTemplateEntry>(numEntries);
Entries = entries;
for (var i = 0; i < numEntries; ++i)
{
var offset = offsets[i] * 2 + 8 + 4 * numEntries;
entries.Add(keys[i], new StainingTemplateEntry(br, offset));
}
}
/// <summary>
/// Try to read and parse the default STM file given by Lumina.
/// </summary>
public StmFile(DataManager gameData)
: this(gameData.GetFile(Path)?.Data ?? Array.Empty<byte>())
{ }
}

View file

@ -1,81 +0,0 @@
using System;
using System.Collections.Generic;
using Dalamud;
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Plugin;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Penumbra.GameData;
public static class GameData
{
/// <summary>
/// Obtain an object identifier that can link a game path to game objects that use it, using your client language.
/// </summary>
public static IObjectIdentifier GetIdentifier(DalamudPluginInterface pluginInterface, DataManager dataManager, ItemData itemData)
=> new ObjectIdentification(pluginInterface, dataManager, itemData, dataManager.Language);
/// <summary>
/// Obtain an object identifier that can link a game path to game objects that use it using the given language.
/// </summary>
public static IObjectIdentifier GetIdentifier(DalamudPluginInterface pluginInterface, DataManager dataManager, ItemData itemData, ClientLanguage language)
=> new ObjectIdentification(pluginInterface, dataManager, itemData, language);
/// <summary>
/// Obtain a parser for game paths.
/// </summary>
public static IGamePathParser GetGamePathParser()
=> new GamePathParser();
}
public interface IObjectIdentifier : IDisposable
{
/// <summary>
/// An accessible parser for game paths.
/// </summary>
public IGamePathParser GamePathParser { get; }
/// <summary>
/// Add all known game objects using the given game path to the dictionary.
/// </summary>
/// <param name="set">A pre-existing dictionary to which names (and optional linked objects) can be added.</param>
/// <param name="path">The game path to identify.</param>
public void Identify(IDictionary<string, object?> set, string path);
/// <summary>
/// Return named information and possibly linked objects for all known game objects using the given path.
/// </summary>
/// <param name="path">The game path to identify.</param>
public Dictionary<string, object?> Identify(string path);
/// <summary>
/// Identify an equippable item by its model values.
/// </summary>
/// <param name="setId">The primary model ID for the piece of equipment.</param>
/// <param name="weaponType">The secondary model ID for weapons, WeaponType.Zero for equipment and accessories.</param>
/// <param name="variant">The variant ID of the model.</param>
/// <param name="slot">The equipment slot the piece of equipment uses.</param>
public IEnumerable<EquipItem> Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot);
/// <inheritdoc cref="Identify(SetId, WeaponType, ushort, EquipSlot)"/>
public IEnumerable<EquipItem> Identify(SetId setId, ushort variant, EquipSlot slot)
=> Identify(setId, 0, variant, slot);
/// <summary> Obtain a list of BNPC Name Ids associated with a BNPC Id. </summary>
public IReadOnlyList<uint> GetBnpcNames(uint bNpcId);
/// <summary> Obtain a list of Names and Object Kinds associated with a ModelChara ID. </summary>
public IReadOnlyList<(string Name, ObjectKind Kind, uint Id)> ModelCharaNames(uint modelId);
public int NumModelChara { get; }
}
public interface IGamePathParser
{
public ObjectType PathToObjectType(string path);
public GameObjectInfo GetFileInfo(string path);
public string VfxToKey(string path);
}

View file

@ -1,64 +0,0 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
using Penumbra.String;
namespace Penumbra.GameData.Interop;
internal static class D3DCompiler
{
[Guid("8BA5FB08-5195-40e2-AC58-0D989C3A0102")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface ID3DBlob
{
[PreserveSig]
public unsafe void* GetBufferPointer();
[PreserveSig]
public UIntPtr GetBufferSize();
}
[Flags]
public enum DisassembleFlags : uint
{
EnableColorCode = 1,
EnableDefaultValuePrints = 2,
EnableInstructionNumbering = 4,
EnableInstructionCycle = 8,
DisableDebugInfo = 16,
EnableInstructionOffset = 32,
InstructionOnly = 64,
PrintHexLiterals = 128,
}
public static unsafe ByteString Disassemble(ReadOnlySpan<byte> blob, DisassembleFlags flags = 0, string comments = "")
{
ID3DBlob? disassembly = null;
try
{
fixed (byte* pSrcData = blob)
{
var hr = D3DDisassemble(pSrcData, new UIntPtr((uint)blob.Length), (uint)flags, comments, out disassembly);
Marshal.ThrowExceptionForHR(hr);
}
return disassembly == null
? ByteString.Empty
: new ByteString((byte*)disassembly.GetBufferPointer()).Clone();
}
finally
{
if (disassembly != null)
Marshal.FinalReleaseComObject(disassembly);
}
}
[PreserveSig]
[DllImport("D3DCompiler_47.dll")]
private static extern unsafe int D3DDisassemble(
[In] byte* pSrcData,
[In] UIntPtr srcDataSize,
uint flags,
[MarshalAs(UnmanagedType.LPStr)] string szComments,
out ID3DBlob? ppDisassembly);
}

View file

@ -1,35 +0,0 @@
namespace Penumbra.GameData;
public static class Offsets
{
// ActorManager.Data
public const int AgentCharaCardDataWorldId = 0xC0;
// ObjectIdentification
public const int DrawObjectGetModelTypeVfunc = 50;
private const int DrawObjectModelBase = 0x8E8;
public const int DrawObjectModelUnk1 = DrawObjectModelBase;
public const int DrawObjectModelUnk2 = DrawObjectModelBase + 0x08;
public const int DrawObjectModelUnk3 = DrawObjectModelBase + 0x20;
public const int DrawObjectModelUnk4 = DrawObjectModelBase + 0x28;
// PathResolver.AnimationState
public const int GetGameObjectIdxVfunc = 28;
public const int TimeLinePtr = 0x50;
// PathResolver.Meta
public const int UpdateModelSkip = 0x90c;
public const int GetEqpIndirectSkip1 = 0xA30;
public const int GetEqpIndirectSkip2 = 0xA28;
// FontReloader
public const int ReloadFontsVfunc = 43;
// ObjectReloader
public const int EnableDrawVfunc = 16;
public const int DisableDrawVfunc = 17;
// ResourceHandle
public const int ResourceHandleGetDataVfunc = 23;
public const int ResourceHandleGetLengthVfunc = 17;
}

View file

@ -1,64 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<LangVersion>preview</LangVersion>
<PlatformTarget>x64</PlatformTarget>
<AssemblyTitle>Penumbra.GameData</AssemblyTitle>
<Company>absolute gangstas</Company>
<Product>Penumbra</Product>
<Copyright>Copyright © 2022</Copyright>
<FileVersion>1.0.0.0</FileVersion>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<OutputPath>bin\$(Configuration)\</OutputPath>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugType>full</DebugType>
<DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
</PropertyGroup>
<PropertyGroup>
<MSBuildWarningsAsMessages>$(MSBuildWarningsAsMessages);MSB3277</MSBuildWarningsAsMessages>
</PropertyGroup>
<PropertyGroup>
<DalamudLibPath>$(AppData)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Penumbra.Api\Penumbra.Api.csproj" />
<ProjectReference Include="..\Penumbra.String\Penumbra.String.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Dalamud">
<HintPath>$(DalamudLibPath)Dalamud.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Lumina">
<HintPath>$(DalamudLibPath)Lumina.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>$(DalamudLibPath)Lumina.Excel.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="FFXIVClientStructs">
<HintPath>$(DalamudLibPath)FFXIVClientStructs.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>$(DalamudLibPath)Newtonsoft.Json.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
</Project>

View file

@ -1,90 +0,0 @@
namespace Penumbra.GameData;
public static class Sigs
{
// ResourceLoader.Debug
public const string ResourceManager = "48 8B 05 ?? ?? ?? ?? 33 ED F0";
// ResourceLoader.Replacement
public const string GetResourceSync = "E8 ?? ?? 00 00 48 8D 8F ?? ?? 00 00 48 89 87 ?? ?? 00 00";
public const string GetResourceAsync = "E8 ?? ?? ?? 00 48 8B D8 EB ?? F0 FF 83 ?? ?? 00 00";
public const string ReadFile = "E8 ?? ?? ?? ?? 84 C0 0F 84 ?? 00 00 00 4C 8B C3 BA 05";
public const string ReadSqPack = "E8 ?? ?? ?? ?? EB 05 E8 ?? ?? ?? ?? 84 C0 0F 84 ?? 00 00 00 4C 8B C3";
// ResourceLoader.TexMdl
public const string CheckFileState = "E8 ?? ?? ?? ?? 48 85 c0 74 ?? 45 0f b6 ce 48 89 44 24";
public const string LoadTexFileLocal = "48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 48 83 EC 30 49 8B F0 44 88 4C 24 20";
public const string LoadMdlFileLocal = "40 55 53 56 57 41 56 41 57 48 8D 6C 24 D1 48 81 EC 98 00 00 00";
public const string LoadTexFileExtern = "E8 ?? ?? ?? ?? 0F B6 E8 48 8B CB E8";
public const string LoadMdlFileExtern = "E8 ?? ?? ?? ?? EB 02 B0 F1";
// GameEventManager
public const string ResourceHandleDestructor = "48 89 5C 24 ?? 57 48 83 EC ?? 48 8D 05 ?? ?? ?? ?? 48 8B D9 48 89 01 B8";
public const string CopyCharacter = "E8 ?? ?? ?? ?? 0F B6 9F ?? ?? ?? ?? 48 8D 8F";
public const string CharacterDestructor =
"48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 48 8D 05 ?? ?? ?? ?? 48 8B D9 48 89 01 48 8D 05 ?? ?? ?? ?? 48 89 81 ?? ?? ?? ?? 48 8D 05";
// PathResolver.AnimationState
public const string LoadCharacterSound = "4C 89 4C 24 ?? 55 57 41 56";
public const string LoadTimelineResources = "E8 ?? ?? ?? ?? 83 7F ?? ?? 75 ?? 0F B6 87";
public const string CharacterBaseLoadAnimation = "E8 ?? ?? ?? ?? BA ?? ?? ?? ?? 48 8B CF 44 8B C2 E8 ?? ?? ?? ?? 48 8B 05";
public const string LoadSomePap = "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC ?? 41 8B D9 89 51";
public const string LoadSomeAction = "E8 ?? ?? ?? ?? C6 83 ?? ?? ?? ?? ?? 8B 8E";
public const string LoadCharacterVfx = "E8 ?? ?? ?? ?? 48 8B F8 48 8D 93";
public const string LoadAreaVfx = "48 8B C4 53 55 56 57 41 56 48 81 EC";
public const string ScheduleClipUpdate = "40 53 55 56 57 41 56 48 81 EC ?? ?? ?? ?? 48 8B F9";
// PathResolver.DrawObjectState
public const string CharacterBaseCreate = "E8 ?? ?? ?? ?? 48 85 C0 74 21 C7 40";
public const string CharacterBaseDestructor =
"E8 ?? ?? ?? ?? 40 F6 C7 01 74 3A 40 F6 C7 04 75 27 48 85 DB 74 2F 48 8B 05 ?? ?? ?? ?? 48 8B D3 48 8B 48 30";
public const string EnableDraw = "E8 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 85 C9 74 33 45 33 C0";
public const string WeaponReload = "E8 ?? ?? ?? ?? 33 DB BE";
// PathResolver.Meta
public const string UpdateModel = "48 8B ?? 56 48 83 ?? ?? ?? B9";
public const string GetEqpIndirect = "40 ?? 48 83 ?? ?? ?? 81 ?? ?? ?? ?? ?? 48 8B ?? 74 ?? ?? 83 ?? ?? ?? ?? ?? ?? 74 ?? 4C";
public const string SetupVisor = "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B";
public const string RspSetupCharacter = "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 88 54 24 ?? 57 41 56";
public const string ChangeCustomize = "E8 ?? ?? ?? ?? 41 0F B6 C5 66 41 89 86";
// PathResolver.PathState
public const string HumanVTable = "48 8D 05 ?? ?? ?? ?? 48 89 03 48 B8 ?? ?? ?? ?? ?? ?? ?? ?? 89 8B";
public const string WeaponVTable =
"48 8D 05 ?? ?? ?? ?? 48 89 03 B8 ?? ?? ?? ?? 66 89 83 ?? ?? ?? ?? 48 8B C3 48 89 8B ?? ?? ?? ?? 48 89 8B";
public const string DemiHumanVTable = "48 8D 05 ?? ?? ?? ?? 48 89 03 48 8B C3 89 8B";
public const string MonsterVTable = "48 8D 05 ?? ?? ?? ?? 48 89 03 33 C0 48 89 83 ?? ?? ?? ?? 48 89 83 ?? ?? ?? ?? C7 83";
// PathResolver.Subfiles
public const string LoadMtrlTex = "4C 8B DC 49 89 5B ?? 49 89 73 ?? 55 57 41 55";
public const string LoadMtrlShpk =
"48 89 5C 24 ?? 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 44 0F B7 89";
public const string ApricotResourceLoad = "48 89 74 24 ?? 57 48 83 EC ?? 41 0F B6 F0 48 8B F9";
// CharacterUtility
public const string CharacterUtility = "48 8B 05 ?? ?? ?? ?? 83 B9";
public const string LoadCharacterResources = "E8 ?? ?? ?? ?? 48 8D 8F ?? ?? ?? ?? E8 ?? ?? ?? ?? 33 D2 45 33 C0";
// MetaFileManager
public const string GetFileSpace = "E8 ?? ?? ?? ?? 41 B9 ?? ?? ?? ?? 4C 8B C0";
// ResidentResourceManager
public const string ResidentResourceManager = "0F 44 FE 48 8B 0D ?? ?? ?? ?? 48 85 C9 74 05";
public const string LoadPlayerResources =
"E8 ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? BA ?? ?? ?? ?? 41 B8 ?? ?? ?? ?? 48 8B 48 30 48 8B 01 FF 50 10 48 85 C0 74 0A";
public const string UnloadPlayerResources =
"41 55 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 4C 8B E9 48 83 C1 08";
// ActorManager
public const string InspectTitleId = "0F B7 0D ?? ?? ?? ?? C7 85";
public const string InspectWorldId = "0F B7 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 8B D0";
}

View file

@ -1,59 +0,0 @@
using System;
using System.Runtime.InteropServices;
namespace Penumbra.GameData.Structs;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public struct CharacterArmor : IEquatable<CharacterArmor>
{
public const int Size = 4;
[FieldOffset(0)]
public readonly uint Value;
[FieldOffset(0)]
public SetId Set;
[FieldOffset(2)]
public byte Variant;
[FieldOffset(3)]
public StainId Stain;
public CharacterArmor(SetId set, byte variant, StainId stain)
{
Value = 0;
Set = set;
Variant = variant;
Stain = stain;
}
public readonly CharacterArmor With(StainId stain)
=> new(Set, Variant, stain);
public readonly CharacterWeapon ToWeapon(WeaponType type)
=> new(Set, type, Variant, Stain);
public readonly CharacterWeapon ToWeapon(WeaponType type, StainId stain)
=> new(Set, type, Variant, stain);
public override readonly string ToString()
=> $"{Set},{Variant},{Stain}";
public static readonly CharacterArmor Empty;
public readonly bool Equals(CharacterArmor other)
=> Value == other.Value;
public override readonly bool Equals(object? obj)
=> obj is CharacterArmor other && Equals(other);
public override readonly int GetHashCode()
=> (int)Value;
public static bool operator ==(CharacterArmor left, CharacterArmor right)
=> left.Value == right.Value;
public static bool operator !=(CharacterArmor left, CharacterArmor right)
=> left.Value != right.Value;
}

View file

@ -1,68 +0,0 @@
using System;
using System.Runtime.InteropServices;
namespace Penumbra.GameData.Structs;
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 7)]
public struct CharacterWeapon : IEquatable<CharacterWeapon>
{
[FieldOffset(0)]
public SetId Set;
[FieldOffset(2)]
public WeaponType Type;
[FieldOffset(4)]
public ushort Variant;
[FieldOffset(6)]
public StainId Stain;
public readonly ulong Value
=> (ulong)Set | ((ulong)Type << 16) | ((ulong)Variant << 32) | ((ulong)Stain << 48);
public override readonly string ToString()
=> $"{Set},{Type},{Variant},{Stain}";
public CharacterWeapon(SetId set, WeaponType type, ushort variant, StainId stain)
{
Set = set;
Type = type;
Variant = variant;
Stain = stain;
}
public CharacterWeapon(ulong value)
{
Set = (SetId)value;
Type = (WeaponType)(value >> 16);
Variant = (ushort)(value >> 32);
Stain = (StainId)(value >> 48);
}
public readonly CharacterWeapon With(StainId stain)
=> new(Set, Type, Variant, stain);
public readonly CharacterArmor ToArmor()
=> new(Set, (byte)Variant, Stain);
public readonly CharacterArmor ToArmor(StainId stain)
=> new(Set, (byte)Variant, stain);
public static readonly CharacterWeapon Empty = new(0, 0, 0, 0);
public readonly bool Equals(CharacterWeapon other)
=> Value == other.Value;
public override readonly bool Equals(object? obj)
=> obj is CharacterWeapon other && Equals(other);
public override readonly int GetHashCode()
=> Value.GetHashCode();
public static bool operator ==(CharacterWeapon left, CharacterWeapon right)
=> left.Value == right.Value;
public static bool operator !=(CharacterWeapon left, CharacterWeapon right)
=> left.Value != right.Value;
}

View file

@ -1,118 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using Penumbra.String.Functions;
namespace Penumbra.GameData.Structs;
[StructLayout(LayoutKind.Sequential, Size = Size)]
public unsafe struct CustomizeData : IEquatable<CustomizeData>, IReadOnlyCollection<byte>
{
public const int Size = 26;
public fixed byte Data[Size];
public int Count
=> Size;
public IEnumerator<byte> GetEnumerator()
{
for (var i = 0; i < Size; ++i)
yield return At(i);
}
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public byte At(int index)
=> Data[index];
public void Set(int index, byte value)
=> Data[index] = value;
public void Read(void* source)
{
fixed (byte* ptr = Data)
{
MemoryUtility.MemCpyUnchecked(ptr, source, Size);
}
}
public readonly void Write(void* target)
{
fixed (byte* ptr = Data)
{
MemoryUtility.MemCpyUnchecked(target, ptr, Size);
}
}
public readonly CustomizeData Clone()
{
var ret = new CustomizeData();
Write(ret.Data);
return ret;
}
public readonly bool Equals(CustomizeData other)
{
fixed (byte* ptr = Data)
{
return MemoryUtility.MemCmpUnchecked(ptr, other.Data, Size) == 0;
}
}
public override bool Equals(object? obj)
=> obj is CustomizeData other && Equals(other);
public static bool Equals(CustomizeData* lhs, CustomizeData* rhs)
=> MemoryUtility.MemCmpUnchecked(lhs, rhs, Size) == 0;
/// <remarks>Compare Gender and then only from Height onwards, because all screen actors are set to Height 50,
/// the Race is implicitly included in the subrace (after height),
/// and the body type is irrelevant for players.</remarks>>
public static bool ScreenActorEquals(CustomizeData* lhs, CustomizeData* rhs)
=> lhs->Data[1] == rhs->Data[1] && MemoryUtility.MemCmpUnchecked(lhs->Data + 4, rhs->Data + 4, Size - 4) == 0;
public override int GetHashCode()
{
fixed (byte* ptr = Data)
{
var p = (int*)ptr;
var u = *(ushort*)(p + 6);
return HashCode.Combine(*p, p[1], p[2], p[3], p[4], p[5], u);
}
}
public readonly string WriteBase64()
{
fixed (byte* ptr = Data)
{
var data = new ReadOnlySpan<byte>(ptr, Size);
return Convert.ToBase64String(data);
}
}
public override string ToString()
{
var sb = new StringBuilder(Size * 3);
for (var i = 0; i < Size - 1; ++i)
sb.Append($"{Data[i]:X2} ");
sb.Append($"{Data[Size - 1]:X2}");
return sb.ToString();
}
public bool LoadBase64(string base64)
{
var buffer = stackalloc byte[Size];
var span = new Span<byte>(buffer, Size);
if (!Convert.TryFromBase64String(base64, span, out var written) || written != Size)
return false;
Read(buffer);
return true;
}
}

View file

@ -1,120 +0,0 @@
using System;
using System.ComponentModel;
using Penumbra.GameData.Enums;
namespace Penumbra.GameData.Structs;
[Flags]
public enum EqdpEntry : ushort
{
Invalid = 0,
Head1 = 0b0000000001,
Head2 = 0b0000000010,
HeadMask = 0b0000000011,
Body1 = 0b0000000100,
Body2 = 0b0000001000,
BodyMask = 0b0000001100,
Hands1 = 0b0000010000,
Hands2 = 0b0000100000,
HandsMask = 0b0000110000,
Legs1 = 0b0001000000,
Legs2 = 0b0010000000,
LegsMask = 0b0011000000,
Feet1 = 0b0100000000,
Feet2 = 0b1000000000,
FeetMask = 0b1100000000,
Ears1 = 0b0000000001,
Ears2 = 0b0000000010,
EarsMask = 0b0000000011,
Neck1 = 0b0000000100,
Neck2 = 0b0000001000,
NeckMask = 0b0000001100,
Wrists1 = 0b0000010000,
Wrists2 = 0b0000100000,
WristsMask = 0b0000110000,
RingR1 = 0b0001000000,
RingR2 = 0b0010000000,
RingRMask = 0b0011000000,
RingL1 = 0b0100000000,
RingL2 = 0b1000000000,
RingLMask = 0b1100000000,
}
public static class Eqdp
{
public static int Offset( EquipSlot slot )
=> slot switch
{
EquipSlot.Head => 0,
EquipSlot.Body => 2,
EquipSlot.Hands => 4,
EquipSlot.Legs => 6,
EquipSlot.Feet => 8,
EquipSlot.Ears => 0,
EquipSlot.Neck => 2,
EquipSlot.Wrists => 4,
EquipSlot.RFinger => 6,
EquipSlot.LFinger => 8,
_ => throw new InvalidEnumArgumentException(),
};
public static (bool, bool) ToBits( this EqdpEntry entry, EquipSlot slot )
=> slot switch
{
EquipSlot.Head => ( entry.HasFlag( EqdpEntry.Head1 ), entry.HasFlag( EqdpEntry.Head2 ) ),
EquipSlot.Body => ( entry.HasFlag( EqdpEntry.Body1 ), entry.HasFlag( EqdpEntry.Body2 ) ),
EquipSlot.Hands => ( entry.HasFlag( EqdpEntry.Hands1 ), entry.HasFlag( EqdpEntry.Hands2 ) ),
EquipSlot.Legs => ( entry.HasFlag( EqdpEntry.Legs1 ), entry.HasFlag( EqdpEntry.Legs2 ) ),
EquipSlot.Feet => ( entry.HasFlag( EqdpEntry.Feet1 ), entry.HasFlag( EqdpEntry.Feet2 ) ),
EquipSlot.Ears => ( entry.HasFlag( EqdpEntry.Ears1 ), entry.HasFlag( EqdpEntry.Ears2 ) ),
EquipSlot.Neck => ( entry.HasFlag( EqdpEntry.Neck1 ), entry.HasFlag( EqdpEntry.Neck2 ) ),
EquipSlot.Wrists => ( entry.HasFlag( EqdpEntry.Wrists1 ), entry.HasFlag( EqdpEntry.Wrists2 ) ),
EquipSlot.RFinger => ( entry.HasFlag( EqdpEntry.RingR1 ), entry.HasFlag( EqdpEntry.RingR2 ) ),
EquipSlot.LFinger => ( entry.HasFlag( EqdpEntry.RingL1 ), entry.HasFlag( EqdpEntry.RingL2 ) ),
_ => throw new InvalidEnumArgumentException(),
};
public static EqdpEntry FromSlotAndBits( EquipSlot slot, bool bit1, bool bit2 )
{
EqdpEntry ret = 0;
var offset = Offset( slot );
if( bit1 )
{
ret |= ( EqdpEntry )( 1 << offset );
}
if( bit2 )
{
ret |= ( EqdpEntry )( 1 << ( offset + 1 ) );
}
return ret;
}
public static EqdpEntry Mask( EquipSlot slot )
{
return slot switch
{
EquipSlot.Head => EqdpEntry.HeadMask,
EquipSlot.Body => EqdpEntry.BodyMask,
EquipSlot.Hands => EqdpEntry.HandsMask,
EquipSlot.Legs => EqdpEntry.LegsMask,
EquipSlot.Feet => EqdpEntry.FeetMask,
EquipSlot.Ears => EqdpEntry.EarsMask,
EquipSlot.Neck => EqdpEntry.NeckMask,
EquipSlot.Wrists => EqdpEntry.WristsMask,
EquipSlot.RFinger => EqdpEntry.RingRMask,
EquipSlot.LFinger => EqdpEntry.RingLMask,
_ => 0,
};
}
}

View file

@ -1,316 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.ComponentModel;
using Penumbra.GameData.Enums;
namespace Penumbra.GameData.Structs;
[Flags]
public enum EqpEntry : ulong
{
BodyEnabled = 0x00_01ul,
BodyHideWaist = 0x00_02ul,
BodyHideThighs = 0x00_04ul,
BodyHideGlovesS = 0x00_08ul,
_4 = 0x00_10ul,
BodyHideGlovesM = 0x00_20ul,
BodyHideGlovesL = 0x00_40ul,
BodyHideGorget = 0x00_80ul,
BodyShowLeg = 0x01_00ul,
BodyShowHand = 0x02_00ul,
BodyShowHead = 0x04_00ul,
BodyShowNecklace = 0x08_00ul,
BodyShowBracelet = 0x10_00ul,
BodyShowTail = 0x20_00ul,
BodyDisableBreastPhysics = 0x40_00ul,
BodyUsesEvpTable = 0x80_00ul,
BodyMask = 0xFF_FFul,
LegsEnabled = 0x01ul << 16,
LegsHideKneePads = 0x02ul << 16,
LegsHideBootsS = 0x04ul << 16,
LegsHideBootsM = 0x08ul << 16,
_20 = 0x10ul << 16,
LegsShowFoot = 0x20ul << 16,
LegsShowTail = 0x40ul << 16,
_23 = 0x80ul << 16,
LegsMask = 0xFFul << 16,
HandsEnabled = 0x01ul << 24,
HandsHideElbow = 0x02ul << 24,
HandsHideForearm = 0x04ul << 24,
_27 = 0x08ul << 24,
HandShowBracelet = 0x10ul << 24,
HandShowRingL = 0x20ul << 24,
HandShowRingR = 0x40ul << 24,
_31 = 0x80ul << 24,
HandsMask = 0xFFul << 24,
FeetEnabled = 0x01ul << 32,
FeetHideKnee = 0x02ul << 32,
FeetHideCalf = 0x04ul << 32,
FeetHideAnkle = 0x08ul << 32,
_36 = 0x10ul << 32,
_37 = 0x20ul << 32,
_38 = 0x40ul << 32,
_39 = 0x80ul << 32,
FeetMask = 0xFFul << 32,
HeadEnabled = 0x00_00_01ul << 40,
HeadHideScalp = 0x00_00_02ul << 40,
HeadHideHair = 0x00_00_04ul << 40,
HeadShowHairOverride = 0x00_00_08ul << 40,
HeadHideNeck = 0x00_00_10ul << 40,
HeadShowNecklace = 0x00_00_20ul << 40,
_46 = 0x00_00_40ul << 40,
HeadShowEarrings = 0x00_00_80ul << 40,
HeadShowEarringsHuman = 0x00_01_00ul << 40,
HeadShowEarringsAura = 0x00_02_00ul << 40,
HeadShowEarHuman = 0x00_04_00ul << 40,
HeadShowEarMiqote = 0x00_08_00ul << 40,
HeadShowEarAuRa = 0x00_10_00ul << 40,
HeadShowEarViera = 0x00_20_00ul << 40,
_54 = 0x00_40_00ul << 40,
_55 = 0x00_80_00ul << 40,
HeadShowHrothgarHat = 0x01_00_00ul << 40,
HeadShowVieraHat = 0x02_00_00ul << 40,
HeadUsesEvpTable = 0x04_00_00ul << 40,
_59 = 0x08_00_00ul << 40,
_60 = 0x10_00_00ul << 40,
_61 = 0x20_00_00ul << 40,
_62 = 0x40_00_00ul << 40,
_63 = 0x80_00_00ul << 40,
HeadMask = 0xFF_FF_FFul << 40,
}
public static class Eqp
{
// cf. Client::Graphics::Scene::CharacterUtility.GetSlotEqpFlags
public const EqpEntry DefaultEntry = (EqpEntry)0x3fe00070603f00;
public static (int, int) BytesAndOffset(EquipSlot slot)
{
return slot switch
{
EquipSlot.Body => (2, 0),
EquipSlot.Legs => (1, 2),
EquipSlot.Hands => (1, 3),
EquipSlot.Feet => (1, 4),
EquipSlot.Head => (3, 5),
_ => throw new InvalidEnumArgumentException(),
};
}
public static EqpEntry ShiftAndMask(this EqpEntry entry, EquipSlot slot)
{
var (_, offset) = BytesAndOffset(slot);
var mask = Mask(slot);
return (EqpEntry)((ulong)(entry & mask) >> (offset * 8));
}
public static EqpEntry FromSlotAndBytes(EquipSlot slot, byte[] value)
{
EqpEntry ret = 0;
var (bytes, offset) = BytesAndOffset(slot);
if (bytes != value.Length)
throw new ArgumentException();
for (var i = 0; i < bytes; ++i)
ret |= (EqpEntry)((ulong)value[i] << ((offset + i) * 8));
return ret;
}
public static EqpEntry Mask(EquipSlot slot)
{
return slot switch
{
EquipSlot.Body => EqpEntry.BodyMask,
EquipSlot.Head => EqpEntry.HeadMask,
EquipSlot.Legs => EqpEntry.LegsMask,
EquipSlot.Feet => EqpEntry.FeetMask,
EquipSlot.Hands => EqpEntry.HandsMask,
_ => 0,
};
}
public static EquipSlot ToEquipSlot(this EqpEntry entry)
{
return entry switch
{
EqpEntry.BodyEnabled => EquipSlot.Body,
EqpEntry.BodyHideWaist => EquipSlot.Body,
EqpEntry.BodyHideThighs => EquipSlot.Body,
EqpEntry.BodyHideGlovesS => EquipSlot.Body,
EqpEntry._4 => EquipSlot.Body,
EqpEntry.BodyHideGlovesM => EquipSlot.Body,
EqpEntry.BodyHideGlovesL => EquipSlot.Body,
EqpEntry.BodyHideGorget => EquipSlot.Body,
EqpEntry.BodyShowLeg => EquipSlot.Body,
EqpEntry.BodyShowHand => EquipSlot.Body,
EqpEntry.BodyShowHead => EquipSlot.Body,
EqpEntry.BodyShowNecklace => EquipSlot.Body,
EqpEntry.BodyShowBracelet => EquipSlot.Body,
EqpEntry.BodyShowTail => EquipSlot.Body,
EqpEntry.BodyDisableBreastPhysics => EquipSlot.Body,
EqpEntry.BodyUsesEvpTable => EquipSlot.Body,
EqpEntry.LegsEnabled => EquipSlot.Legs,
EqpEntry.LegsHideKneePads => EquipSlot.Legs,
EqpEntry.LegsHideBootsS => EquipSlot.Legs,
EqpEntry.LegsHideBootsM => EquipSlot.Legs,
EqpEntry._20 => EquipSlot.Legs,
EqpEntry.LegsShowFoot => EquipSlot.Legs,
EqpEntry.LegsShowTail => EquipSlot.Legs,
EqpEntry._23 => EquipSlot.Legs,
EqpEntry.HandsEnabled => EquipSlot.Hands,
EqpEntry.HandsHideElbow => EquipSlot.Hands,
EqpEntry.HandsHideForearm => EquipSlot.Hands,
EqpEntry._27 => EquipSlot.Hands,
EqpEntry.HandShowBracelet => EquipSlot.Hands,
EqpEntry.HandShowRingL => EquipSlot.Hands,
EqpEntry.HandShowRingR => EquipSlot.Hands,
EqpEntry._31 => EquipSlot.Hands,
EqpEntry.FeetEnabled => EquipSlot.Feet,
EqpEntry.FeetHideKnee => EquipSlot.Feet,
EqpEntry.FeetHideCalf => EquipSlot.Feet,
EqpEntry.FeetHideAnkle => EquipSlot.Feet,
EqpEntry._36 => EquipSlot.Feet,
EqpEntry._37 => EquipSlot.Feet,
EqpEntry._38 => EquipSlot.Feet,
EqpEntry._39 => EquipSlot.Feet,
EqpEntry.HeadEnabled => EquipSlot.Head,
EqpEntry.HeadHideScalp => EquipSlot.Head,
EqpEntry.HeadHideHair => EquipSlot.Head,
EqpEntry.HeadShowHairOverride => EquipSlot.Head,
EqpEntry.HeadHideNeck => EquipSlot.Head,
EqpEntry.HeadShowNecklace => EquipSlot.Head,
EqpEntry._46 => EquipSlot.Head,
EqpEntry.HeadShowEarrings => EquipSlot.Head,
EqpEntry.HeadShowEarringsHuman => EquipSlot.Head,
EqpEntry.HeadShowEarringsAura => EquipSlot.Head,
EqpEntry.HeadShowEarHuman => EquipSlot.Head,
EqpEntry.HeadShowEarMiqote => EquipSlot.Head,
EqpEntry.HeadShowEarAuRa => EquipSlot.Head,
EqpEntry.HeadShowEarViera => EquipSlot.Head,
EqpEntry._54 => EquipSlot.Head,
EqpEntry._55 => EquipSlot.Head,
EqpEntry.HeadShowHrothgarHat => EquipSlot.Head,
EqpEntry.HeadShowVieraHat => EquipSlot.Head,
EqpEntry.HeadUsesEvpTable => EquipSlot.Head,
// currently unused
EqpEntry._59 => EquipSlot.Unknown,
EqpEntry._60 => EquipSlot.Unknown,
EqpEntry._61 => EquipSlot.Unknown,
EqpEntry._62 => EquipSlot.Unknown,
EqpEntry._63 => EquipSlot.Unknown,
_ => EquipSlot.Unknown,
};
}
public static string ToLocalName(this EqpEntry entry)
{
return entry switch
{
EqpEntry.BodyEnabled => "Enabled",
EqpEntry.BodyHideWaist => "Hide Waist",
EqpEntry.BodyHideThighs => "Hide Thigh Pads",
EqpEntry.BodyHideGlovesS => "Hide Small Gloves",
EqpEntry._4 => "Unknown 4",
EqpEntry.BodyHideGlovesM => "Hide Medium Gloves",
EqpEntry.BodyHideGlovesL => "Hide Large Gloves",
EqpEntry.BodyHideGorget => "Hide Gorget",
EqpEntry.BodyShowLeg => "Show Legs",
EqpEntry.BodyShowHand => "Show Hands",
EqpEntry.BodyShowHead => "Show Head",
EqpEntry.BodyShowNecklace => "Show Necklace",
EqpEntry.BodyShowBracelet => "Show Bracelet",
EqpEntry.BodyShowTail => "Show Tail",
EqpEntry.BodyDisableBreastPhysics => "Disable Breast Physics",
EqpEntry.BodyUsesEvpTable => "Uses EVP Table",
EqpEntry.LegsEnabled => "Enabled",
EqpEntry.LegsHideKneePads => "Hide Knee Pads",
EqpEntry.LegsHideBootsS => "Hide Small Boots",
EqpEntry.LegsHideBootsM => "Hide Medium Boots",
EqpEntry._20 => "Unknown 20",
EqpEntry.LegsShowFoot => "Show Foot",
EqpEntry.LegsShowTail => "Show Tail",
EqpEntry._23 => "Unknown 23",
EqpEntry.HandsEnabled => "Enabled",
EqpEntry.HandsHideElbow => "Hide Elbow",
EqpEntry.HandsHideForearm => "Hide Forearm",
EqpEntry._27 => "Unknown 27",
EqpEntry.HandShowBracelet => "Show Bracelet",
EqpEntry.HandShowRingL => "Show Left Ring",
EqpEntry.HandShowRingR => "Show Right Ring",
EqpEntry._31 => "Unknown 31",
EqpEntry.FeetEnabled => "Enabled",
EqpEntry.FeetHideKnee => "Hide Knees",
EqpEntry.FeetHideCalf => "Hide Calves",
EqpEntry.FeetHideAnkle => "Hide Ankles",
EqpEntry._36 => "Unknown 36",
EqpEntry._37 => "Unknown 37",
EqpEntry._38 => "Unknown 38",
EqpEntry._39 => "Unknown 39",
EqpEntry.HeadEnabled => "Enabled",
EqpEntry.HeadHideScalp => "Hide Scalp",
EqpEntry.HeadHideHair => "Hide Hair",
EqpEntry.HeadShowHairOverride => "Show Hair Override",
EqpEntry.HeadHideNeck => "Hide Neck",
EqpEntry.HeadShowNecklace => "Show Necklace",
EqpEntry._46 => "Unknown 46",
EqpEntry.HeadShowEarrings => "Show Earrings",
EqpEntry.HeadShowEarringsHuman => "Show Earrings (Human)",
EqpEntry.HeadShowEarringsAura => "Show Earrings (Au Ra)",
EqpEntry.HeadShowEarHuman => "Show Ears (Human)",
EqpEntry.HeadShowEarMiqote => "Show Ears (Miqo'te)",
EqpEntry.HeadShowEarAuRa => "Show Ears (Au Ra)",
EqpEntry.HeadShowEarViera => "Show Ears (Viera)",
EqpEntry._54 => "Unknown 54",
EqpEntry._55 => "Unknown 55",
EqpEntry.HeadShowHrothgarHat => "Show on Hrothgar",
EqpEntry.HeadShowVieraHat => "Show on Viera",
EqpEntry.HeadUsesEvpTable => "Uses EVP Table",
EqpEntry._59 => "Unknown 59",
EqpEntry._60 => "Unknown 60",
EqpEntry._61 => "Unknown 61",
EqpEntry._62 => "Unknown 62",
EqpEntry._63 => "Unknown 63",
_ => throw new InvalidEnumArgumentException(),
};
}
private static EqpEntry[] GetEntriesForSlot(EquipSlot slot)
{
return ((EqpEntry[])Enum.GetValues(typeof(EqpEntry)))
.Where(e => e.ToEquipSlot() == slot)
.ToArray();
}
public static readonly EqpEntry[] EqpAttributesBody = GetEntriesForSlot(EquipSlot.Body);
public static readonly EqpEntry[] EqpAttributesLegs = GetEntriesForSlot(EquipSlot.Legs);
public static readonly EqpEntry[] EqpAttributesHands = GetEntriesForSlot(EquipSlot.Hands);
public static readonly EqpEntry[] EqpAttributesFeet = GetEntriesForSlot(EquipSlot.Feet);
public static readonly EqpEntry[] EqpAttributesHead = GetEntriesForSlot(EquipSlot.Head);
public static readonly IReadOnlyDictionary<EquipSlot, EqpEntry[]> EqpAttributes = new Dictionary<EquipSlot, EqpEntry[]>()
{
[EquipSlot.Body] = EqpAttributesBody,
[EquipSlot.Legs] = EqpAttributesLegs,
[EquipSlot.Hands] = EqpAttributesHands,
[EquipSlot.Feet] = EqpAttributesFeet,
[EquipSlot.Head] = EqpAttributesHead,
};
}

View file

@ -1,120 +0,0 @@
using System.Runtime.InteropServices;
using Dalamud.Utility;
using Lumina.Excel.GeneratedSheets;
using Penumbra.GameData.Enums;
using PseudoEquipItem = System.ValueTuple<string, ulong, ushort, ushort, ushort, byte, byte>;
namespace Penumbra.GameData.Structs;
[StructLayout(LayoutKind.Sequential)]
public readonly struct EquipItem
{
public readonly string Name;
public readonly ulong Id;
public readonly ushort IconId;
public readonly SetId ModelId;
public readonly WeaponType WeaponType;
public readonly byte Variant;
public readonly FullEquipType Type;
public uint ItemId
=> (uint)Id;
public bool Valid
=> Type != FullEquipType.Unknown;
public CharacterArmor Armor()
=> new(ModelId, Variant, 0);
public CharacterArmor Armor(StainId stain)
=> new(ModelId, Variant, stain);
public CharacterWeapon Weapon()
=> new(ModelId, WeaponType, Variant, 0);
public CharacterWeapon Weapon(StainId stain)
=> new(ModelId, WeaponType, Variant, stain);
public EquipItem()
=> Name = string.Empty;
public EquipItem(string name, ulong id, ushort iconId, SetId modelId, WeaponType weaponType, byte variant, FullEquipType type)
{
Name = string.Intern(name);
Id = id;
IconId = iconId;
ModelId = modelId;
WeaponType = weaponType;
Variant = variant;
Type = type;
}
public string ModelString
=> WeaponType == 0 ? $"{ModelId.Value}-{Variant}" : $"{ModelId.Value}-{WeaponType.Value}-{Variant}";
public static implicit operator EquipItem(PseudoEquipItem it)
=> new(it.Item1, it.Item2, it.Item3, it.Item4, it.Item5, it.Item6, (FullEquipType)it.Item7);
public static explicit operator PseudoEquipItem(EquipItem it)
=> (it.Name, it.ItemId, it.IconId, (ushort)it.ModelId, (ushort)it.WeaponType, it.Variant, (byte)it.Type);
public static EquipItem FromArmor(Item item)
{
var type = item.ToEquipType();
var name = item.Name.ToDalamudString().TextValue;
var id = item.RowId;
var icon = item.Icon;
var model = (SetId)item.ModelMain;
var weapon = (WeaponType)0;
var variant = (byte)(item.ModelMain >> 16);
return new EquipItem(name, id, icon, model, weapon, variant, type);
}
public static EquipItem FromMainhand(Item item)
{
var type = item.ToEquipType();
var name = item.Name.ToDalamudString().TextValue;
var id = item.RowId;
var icon = item.Icon;
var model = (SetId)item.ModelMain;
var weapon = (WeaponType)(item.ModelMain >> 16);
var variant = (byte)(item.ModelMain >> 32);
return new EquipItem(name, id, icon, model, weapon, variant, type);
}
public static EquipItem FromOffhand(Item item)
{
var type = item.ToEquipType().ValidOffhand();
var name = item.Name.ToDalamudString().TextValue + type.OffhandTypeSuffix();
var id = item.RowId;
var icon = item.Icon;
var model = (SetId)item.ModelSub;
var weapon = (WeaponType)(item.ModelSub >> 16);
var variant = (byte)(item.ModelSub >> 32);
return new EquipItem(name, id, icon, model, weapon, variant, type);
}
public static EquipItem FromIds(uint itemId, ushort iconId, SetId modelId, WeaponType type, byte variant,
FullEquipType equipType = FullEquipType.Unknown, string? name = null)
{
name ??= $"Unknown ({modelId.Value}-{(type.Value != 0 ? $"{type.Value}-" : string.Empty)}{variant})";
var fullId = itemId == 0
? modelId.Value | ((ulong)type.Value << 16) | ((ulong)variant << 32) | ((ulong)equipType << 40) | (1ul << 48)
: itemId;
return new EquipItem(name, fullId, iconId, modelId, type, variant, equipType);
}
public static EquipItem FromId(ulong id)
{
var setId = (SetId)id;
var type = (WeaponType)(id >> 16);
var variant = (byte)(id >> 32);
var equipType = (FullEquipType)(id >> 40);
return new EquipItem($"Unknown ({setId.Value}-{(type.Value != 0 ? $"{type.Value}-" : string.Empty)}{variant})", id, 0, setId, type,
variant, equipType);
}
public override string ToString()
=> Name;
}

View file

@ -1,159 +0,0 @@
using System;
using System.Runtime.InteropServices;
using Dalamud;
using Penumbra.GameData.Enums;
namespace Penumbra.GameData.Structs;
[StructLayout( LayoutKind.Explicit )]
public struct GameObjectInfo : IComparable
{
public static GameObjectInfo Equipment( FileType type, ushort setId, GenderRace gr = GenderRace.Unknown
, EquipSlot slot = EquipSlot.Unknown, byte variant = 0 )
=> new()
{
FileType = type,
ObjectType = slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment,
PrimaryId = setId,
GenderRace = gr,
Variant = variant,
EquipSlot = slot,
};
public static GameObjectInfo Weapon( FileType type, ushort setId, ushort weaponId, byte variant = 0 )
=> new()
{
FileType = type,
ObjectType = ObjectType.Weapon,
PrimaryId = setId,
SecondaryId = weaponId,
Variant = variant,
};
public static GameObjectInfo Customization( FileType type, CustomizationType customizationType, ushort id = 0
, GenderRace gr = GenderRace.Unknown, BodySlot bodySlot = BodySlot.Unknown, byte variant = 0 )
=> new()
{
FileType = type,
ObjectType = ObjectType.Character,
PrimaryId = id,
GenderRace = gr,
BodySlot = bodySlot,
Variant = variant,
CustomizationType = customizationType,
};
public static GameObjectInfo Monster( FileType type, ushort monsterId, ushort bodyId, byte variant = 0 )
=> new()
{
FileType = type,
ObjectType = ObjectType.Monster,
PrimaryId = monsterId,
SecondaryId = bodyId,
Variant = variant,
};
public static GameObjectInfo DemiHuman( FileType type, ushort demiHumanId, ushort bodyId, EquipSlot slot = EquipSlot.Unknown,
byte variant = 0
)
=> new()
{
FileType = type,
ObjectType = ObjectType.DemiHuman,
PrimaryId = demiHumanId,
SecondaryId = bodyId,
Variant = variant,
EquipSlot = slot,
};
public static GameObjectInfo Map( FileType type, byte c1, byte c2, byte c3, byte c4, byte variant, byte suffix = 0 )
=> new()
{
FileType = type,
ObjectType = ObjectType.Map,
MapC1 = c1,
MapC2 = c2,
MapC3 = c3,
MapC4 = c4,
MapSuffix = suffix,
Variant = variant,
};
public static GameObjectInfo Icon( FileType type, uint iconId, bool hq, bool hr, ClientLanguage lang = ClientLanguage.English )
=> new()
{
FileType = type,
ObjectType = ObjectType.Icon,
IconId = iconId,
IconHqHr = ( byte )( hq ? hr ? 3 : 1 : hr ? 2 : 0 ),
Language = lang,
};
[FieldOffset( 0 )]
public readonly ulong Identifier;
[FieldOffset( 0 )]
public FileType FileType;
[FieldOffset( 1 )]
public ObjectType ObjectType;
[FieldOffset( 2 )]
public ushort PrimaryId; // Equipment, Weapon, Customization, Monster, DemiHuman
[FieldOffset( 2 )]
public uint IconId; // Icon
[FieldOffset( 2 )]
public byte MapC1; // Map
[FieldOffset( 3 )]
public byte MapC2; // Map
[FieldOffset( 4 )]
public ushort SecondaryId; // Weapon, Monster, Demihuman
[FieldOffset( 4 )]
public byte MapC3; // Map
[FieldOffset( 4 )]
private byte _genderRaceByte; // Equipment, Customization
public GenderRace GenderRace
{
get => Names.GenderRaceFromByte( _genderRaceByte );
set => _genderRaceByte = value.ToByte();
}
[FieldOffset( 5 )]
public BodySlot BodySlot; // Customization
[FieldOffset( 5 )]
public byte MapC4; // Map
[FieldOffset( 6 )]
public byte Variant; // Equipment, Weapon, Customization, Map, Monster, Demihuman
[FieldOffset( 6 )]
public byte IconHqHr; // Icon
[FieldOffset( 7 )]
public EquipSlot EquipSlot; // Equipment, Demihuman
[FieldOffset( 7 )]
public CustomizationType CustomizationType; // Customization
[FieldOffset( 7 )]
public ClientLanguage Language; // Icon
[FieldOffset( 7 )]
public byte MapSuffix;
public override int GetHashCode()
=> Identifier.GetHashCode();
public int CompareTo( object? r )
=> Identifier.CompareTo( r );
}

View file

@ -1,103 +0,0 @@
using System;
using System.IO;
namespace Penumbra.GameData.Structs;
public struct GmpEntry : IEquatable< GmpEntry >
{
public static readonly GmpEntry Default = new();
public bool Enabled
{
get => ( Value & 1 ) == 1;
set
{
if( value )
{
Value |= 1ul;
}
else
{
Value &= ~1ul;
}
}
}
public bool Animated
{
get => ( Value & 2 ) == 2;
set
{
if( value )
{
Value |= 2ul;
}
else
{
Value &= ~2ul;
}
}
}
public ushort RotationA
{
get => ( ushort )( ( Value >> 2 ) & 0x3FF );
set => Value = ( Value & ~0xFFCul ) | ( ( value & 0x3FFul ) << 2 );
}
public ushort RotationB
{
get => ( ushort )( ( Value >> 12 ) & 0x3FF );
set => Value = ( Value & ~0x3FF000ul ) | ( ( value & 0x3FFul ) << 12 );
}
public ushort RotationC
{
get => ( ushort )( ( Value >> 22 ) & 0x3FF );
set => Value = ( Value & ~0xFFC00000ul ) | ( ( value & 0x3FFul ) << 22 );
}
public byte UnknownA
{
get => ( byte )( ( Value >> 32 ) & 0x0F );
set => Value = ( Value & ~0x0F00000000ul ) | ( ( value & 0x0Ful ) << 32 );
}
public byte UnknownB
{
get => ( byte )( ( Value >> 36 ) & 0x0F );
set => Value = ( Value & ~0xF000000000ul ) | ( ( value & 0x0Ful ) << 36 );
}
public byte UnknownTotal
{
get => ( byte )( ( Value >> 32 ) & 0xFF );
set => Value = ( Value & ~0xFF00000000ul ) | ( ( value & 0xFFul ) << 32 );
}
public ulong Value { get; set; }
public static GmpEntry FromTexToolsMeta( byte[] data )
{
GmpEntry ret = new();
using var reader = new BinaryReader( new MemoryStream( data ) );
ret.Value = reader.ReadUInt32();
ret.UnknownTotal = data[ 4 ];
return ret;
}
public static implicit operator ulong( GmpEntry entry )
=> entry.Value;
public static explicit operator GmpEntry( ulong entry )
=> new() { Value = entry };
public bool Equals( GmpEntry other )
=> Value == other.Value;
public override bool Equals( object? obj )
=> obj is GmpEntry other && Equals( other );
public override int GetHashCode()
=> Value.GetHashCode();
}

View file

@ -1,50 +0,0 @@
using System;
using Newtonsoft.Json;
namespace Penumbra.GameData.Structs;
public readonly struct ImcEntry : IEquatable<ImcEntry>
{
public byte MaterialId { get; init; }
public byte DecalId { get; init; }
public readonly ushort AttributeAndSound;
public byte VfxId { get; init; }
public byte MaterialAnimationId { get; init; }
public ushort AttributeMask
{
get => (ushort)(AttributeAndSound & 0x3FF);
init => AttributeAndSound = (ushort)((AttributeAndSound & ~0x3FF) | (value & 0x3FF));
}
public byte SoundId
{
get => (byte)(AttributeAndSound >> 10);
init => AttributeAndSound = (ushort)(AttributeMask | (value << 10));
}
public bool Equals(ImcEntry other)
=> MaterialId == other.MaterialId
&& DecalId == other.DecalId
&& AttributeAndSound == other.AttributeAndSound
&& VfxId == other.VfxId
&& MaterialAnimationId == other.MaterialAnimationId;
public override bool Equals(object? obj)
=> obj is ImcEntry other && Equals(other);
public override int GetHashCode()
=> HashCode.Combine(MaterialId, DecalId, AttributeAndSound, VfxId, MaterialAnimationId);
[JsonConstructor]
public ImcEntry(byte materialId, byte decalId, ushort attributeMask, byte soundId, byte vfxId, byte materialAnimationId)
{
MaterialId = materialId;
DecalId = decalId;
AttributeAndSound = 0;
VfxId = vfxId;
MaterialAnimationId = materialAnimationId;
AttributeMask = attributeMask;
SoundId = soundId;
}
}

View file

@ -1,57 +0,0 @@
using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using Penumbra.GameData.Enums;
namespace Penumbra.GameData.Structs;
[StructLayout( LayoutKind.Sequential, Pack = 1 )]
public readonly struct RspEntry
{
public const int ByteSize = ( int )RspAttribute.NumAttributes * 4;
private readonly float[] Attributes;
public RspEntry( RspEntry copy )
=> Attributes = ( float[] )copy.Attributes.Clone();
public RspEntry( byte[] bytes, int offset )
{
if( offset < 0 || offset + ByteSize > bytes.Length )
{
throw new ArgumentOutOfRangeException();
}
Attributes = new float[( int )RspAttribute.NumAttributes];
using MemoryStream s = new(bytes) { Position = offset };
using BinaryReader br = new(s);
for( var i = 0; i < ( int )RspAttribute.NumAttributes; ++i )
{
Attributes[ i ] = br.ReadSingle();
}
}
private static int ToIndex( RspAttribute attribute )
=> attribute < RspAttribute.NumAttributes && attribute >= 0
? ( int )attribute
: throw new InvalidEnumArgumentException();
public float this[ RspAttribute attribute ]
{
get => Attributes[ ToIndex( attribute ) ];
set => Attributes[ ToIndex( attribute ) ] = value;
}
public byte[] ToBytes()
{
using var s = new MemoryStream( ByteSize );
using var bw = new BinaryWriter( s );
foreach( var attribute in Attributes )
{
bw.Write( attribute );
}
return s.ToArray();
}
}

View file

@ -1,29 +0,0 @@
using System;
namespace Penumbra.GameData.Structs;
public readonly struct SetId : IComparable< SetId >, IEquatable<SetId>, IEquatable<ushort>
{
public readonly ushort Value;
public SetId( ushort value )
=> Value = value;
public static implicit operator SetId( ushort id )
=> new(id);
public static explicit operator ushort( SetId id )
=> id.Value;
public bool Equals(SetId other)
=> Value == other.Value;
public bool Equals(ushort other)
=> Value == other;
public override string ToString()
=> Value.ToString();
public int CompareTo( SetId other )
=> Value.CompareTo( other.Value );
}

View file

@ -1,52 +0,0 @@
using Dalamud.Utility;
namespace Penumbra.GameData.Structs;
// A wrapper for the clothing dyes the game provides with their RGBA color value, game ID, unmodified color value and name.
public readonly struct Stain
{
// An empty stain with transparent color.
public static readonly Stain None = new("None");
public readonly string Name;
public readonly uint RgbaColor;
public readonly byte RowIndex;
public readonly bool Gloss;
public byte R
=> (byte)(RgbaColor & 0xFF);
public byte G
=> (byte)((RgbaColor >> 8) & 0xFF);
public byte B
=> (byte)((RgbaColor >> 16) & 0xFF);
public byte Intensity
=> (byte)((1 + R + G + B) / 3);
// R and B need to be shuffled and Alpha set to max.
private static uint SeColorToRgba(uint color)
=> ((color & 0xFF) << 16) | ((color >> 16) & 0xFF) | (color & 0xFF00) | 0xFF000000;
public Stain(Lumina.Excel.GeneratedSheets.Stain stain)
: this(stain.Name.ToDalamudString().ToString(), SeColorToRgba(stain.Color), (byte)stain.RowId, stain.Unknown5)
{ }
internal Stain(string name, uint dye, byte index, bool gloss)
{
Name = name;
RowIndex = index;
Gloss = gloss;
RgbaColor = dye;
}
// Only used by None.
private Stain(string name)
{
Name = name;
RowIndex = 0;
RgbaColor = 0;
Gloss = false;
}
}

View file

@ -1,36 +0,0 @@
using System;
using System.Numerics;
namespace Penumbra.GameData.Structs;
public readonly struct StainId : IEquatable< StainId >, IEqualityOperators<StainId, StainId, bool>
{
public readonly byte Value;
public StainId( byte value )
=> Value = value;
public static implicit operator StainId( byte id )
=> new(id);
public static explicit operator byte( StainId id )
=> id.Value;
public override string ToString()
=> Value.ToString();
public bool Equals( StainId other )
=> Value == other.Value;
public override bool Equals( object? obj )
=> obj is StainId other && Equals( other );
public override int GetHashCode()
=> Value.GetHashCode();
public static bool operator ==(StainId left, StainId right)
=> left.Value == right.Value;
public static bool operator !=(StainId left, StainId right)
=> left.Value != right.Value;
}

View file

@ -1,35 +0,0 @@
using System;
namespace Penumbra.GameData.Structs;
public readonly struct WeaponType : IEquatable< WeaponType >
{
public readonly ushort Value;
public WeaponType( ushort value )
=> Value = value;
public static implicit operator WeaponType( ushort id )
=> new(id);
public static explicit operator ushort( WeaponType id )
=> id.Value;
public override string ToString()
=> Value.ToString();
public bool Equals( WeaponType other )
=> Value == other.Value;
public override bool Equals( object? obj )
=> obj is WeaponType other && Equals( other );
public override int GetHashCode()
=> Value.GetHashCode();
public static bool operator ==( WeaponType lhs, WeaponType rhs )
=> lhs.Value == rhs.Value;
public static bool operator !=( WeaponType lhs, WeaponType rhs )
=> lhs.Value != rhs.Value;
}

View file

@ -1,35 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
namespace Penumbra.GameData;
public static class UtilityFunctions
{
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static T? FirstOrNull<T>(this IEnumerable<T> values, Func<T, bool> predicate) where T : struct
=> values.Cast<T?>().FirstOrDefault(v => predicate(v!.Value));
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static T[] AddItem<T>(this T[] array, T element, int count = 1)
{
var length = array.Length;
var newArray = new T[array.Length + count];
Array.Copy(array, newArray, length);
for (var i = length; i < newArray.Length; ++i)
newArray[i] = element;
return newArray;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static T[] RemoveItems<T>(this T[] array, int offset, int count = 1)
{
var newArray = new T[array.Length - count];
Array.Copy(array, newArray, offset);
Array.Copy(array, offset + count, newArray, offset, newArray.Length - offset);
return newArray;
}
}