mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Actor Stuff.
This commit is contained in:
parent
1353e591b8
commit
878f69fd91
8 changed files with 513 additions and 459 deletions
2
OtterGui
2
OtterGui
|
|
@ -1 +1 @@
|
||||||
Subproject commit 0d2284a82504aac0bff797fa3355f750a3e68834
|
Subproject commit 49f5aaa7733fc74d77435e9b84ce347eb06f61be
|
||||||
|
|
@ -6,8 +6,8 @@ using Penumbra.String;
|
||||||
|
|
||||||
namespace Penumbra.GameData.Actors;
|
namespace Penumbra.GameData.Actors;
|
||||||
|
|
||||||
[StructLayout( LayoutKind.Explicit )]
|
[StructLayout(LayoutKind.Explicit)]
|
||||||
public readonly struct ActorIdentifier : IEquatable< ActorIdentifier >
|
public readonly struct ActorIdentifier : IEquatable<ActorIdentifier>
|
||||||
{
|
{
|
||||||
public static ActorManager? Manager;
|
public static ActorManager? Manager;
|
||||||
|
|
||||||
|
|
@ -26,36 +26,40 @@ public readonly struct ActorIdentifier : IEquatable< ActorIdentifier >
|
||||||
public ActorIdentifier CreatePermanent()
|
public ActorIdentifier CreatePermanent()
|
||||||
=> new(Type, Kind, Index, DataId, PlayerName.Clone());
|
=> new(Type, Kind, Index, DataId, PlayerName.Clone());
|
||||||
|
|
||||||
public bool Equals( ActorIdentifier other )
|
public bool Equals(ActorIdentifier other)
|
||||||
{
|
{
|
||||||
if( Type != other.Type )
|
if (Type != other.Type)
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
return Type switch
|
return Type switch
|
||||||
{
|
{
|
||||||
IdentifierType.Player => HomeWorld == other.HomeWorld && PlayerName.EqualsCi( other.PlayerName ),
|
IdentifierType.Player => HomeWorld == other.HomeWorld && PlayerName.EqualsCi(other.PlayerName),
|
||||||
IdentifierType.Owned => HomeWorld == other.HomeWorld && PlayerName.EqualsCi( other.PlayerName ) && Manager.DataIdEquals( this, other ),
|
IdentifierType.Owned => HomeWorld == other.HomeWorld && PlayerName.EqualsCi(other.PlayerName) && Manager.DataIdEquals(this, other),
|
||||||
IdentifierType.Special => Special == other.Special,
|
IdentifierType.Special => Special == other.Special,
|
||||||
IdentifierType.Npc => Index == other.Index && DataId == other.DataId && Manager.DataIdEquals( this, other ),
|
IdentifierType.Npc => Index == other.Index && DataId == other.DataId && Manager.DataIdEquals(this, other),
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool Equals( object? obj )
|
public override bool Equals(object? obj)
|
||||||
=> obj is ActorIdentifier other && Equals( other );
|
=> 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
|
public bool IsValid
|
||||||
=> Type != IdentifierType.Invalid;
|
=> Type != IdentifierType.Invalid;
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
=> Manager?.ToString( this )
|
=> Manager?.ToString(this)
|
||||||
?? Type switch
|
?? Type switch
|
||||||
{
|
{
|
||||||
IdentifierType.Player => $"{PlayerName} ({HomeWorld})",
|
IdentifierType.Player => $"{PlayerName} ({HomeWorld})",
|
||||||
IdentifierType.Owned => $"{PlayerName}s {Kind} {DataId} ({HomeWorld})",
|
IdentifierType.Owned => $"{PlayerName}s {Kind} {DataId} ({HomeWorld})",
|
||||||
IdentifierType.Special => ActorManager.ToName( Special ),
|
IdentifierType.Special => ActorManager.ToName(Special),
|
||||||
IdentifierType.Npc =>
|
IdentifierType.Npc =>
|
||||||
Index == ushort.MaxValue
|
Index == ushort.MaxValue
|
||||||
? $"{Kind} #{DataId}"
|
? $"{Kind} #{DataId}"
|
||||||
|
|
@ -66,18 +70,18 @@ public readonly struct ActorIdentifier : IEquatable< ActorIdentifier >
|
||||||
public override int GetHashCode()
|
public override int GetHashCode()
|
||||||
=> Type switch
|
=> Type switch
|
||||||
{
|
{
|
||||||
IdentifierType.Player => HashCode.Combine( IdentifierType.Player, PlayerName, HomeWorld ),
|
IdentifierType.Player => HashCode.Combine(IdentifierType.Player, PlayerName, HomeWorld),
|
||||||
IdentifierType.Owned => HashCode.Combine( IdentifierType.Owned, Kind, PlayerName, HomeWorld, DataId ),
|
IdentifierType.Owned => HashCode.Combine(IdentifierType.Owned, Kind, PlayerName, HomeWorld, DataId),
|
||||||
IdentifierType.Special => HashCode.Combine( IdentifierType.Special, Special ),
|
IdentifierType.Special => HashCode.Combine(IdentifierType.Special, Special),
|
||||||
IdentifierType.Npc => HashCode.Combine( IdentifierType.Npc, Kind, Index, DataId ),
|
IdentifierType.Npc => HashCode.Combine(IdentifierType.Npc, Kind, Index, DataId),
|
||||||
_ => 0,
|
_ => 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
internal ActorIdentifier( IdentifierType type, ObjectKind kind, ushort index, uint data, ByteString playerName )
|
internal ActorIdentifier(IdentifierType type, ObjectKind kind, ushort index, uint data, ByteString playerName)
|
||||||
{
|
{
|
||||||
Type = type;
|
Type = type;
|
||||||
Kind = kind;
|
Kind = kind;
|
||||||
Special = ( SpecialActor )index;
|
Special = (SpecialActor)index;
|
||||||
HomeWorld = Index = index;
|
HomeWorld = Index = index;
|
||||||
DataId = data;
|
DataId = data;
|
||||||
PlayerName = playerName;
|
PlayerName = playerName;
|
||||||
|
|
@ -86,26 +90,26 @@ public readonly struct ActorIdentifier : IEquatable< ActorIdentifier >
|
||||||
|
|
||||||
public JObject ToJson()
|
public JObject ToJson()
|
||||||
{
|
{
|
||||||
var ret = new JObject { { nameof( Type ), Type.ToString() } };
|
var ret = new JObject { { nameof(Type), Type.ToString() } };
|
||||||
switch( Type )
|
switch (Type)
|
||||||
{
|
{
|
||||||
case IdentifierType.Player:
|
case IdentifierType.Player:
|
||||||
ret.Add( nameof( PlayerName ), PlayerName.ToString() );
|
ret.Add(nameof(PlayerName), PlayerName.ToString());
|
||||||
ret.Add( nameof( HomeWorld ), HomeWorld );
|
ret.Add(nameof(HomeWorld), HomeWorld);
|
||||||
return ret;
|
return ret;
|
||||||
case IdentifierType.Owned:
|
case IdentifierType.Owned:
|
||||||
ret.Add( nameof( PlayerName ), PlayerName.ToString() );
|
ret.Add(nameof(PlayerName), PlayerName.ToString());
|
||||||
ret.Add( nameof( HomeWorld ), HomeWorld );
|
ret.Add(nameof(HomeWorld), HomeWorld);
|
||||||
ret.Add( nameof( Kind ), Kind.ToString() );
|
ret.Add(nameof(Kind), Kind.ToString());
|
||||||
ret.Add( nameof( DataId ), DataId );
|
ret.Add(nameof(DataId), DataId);
|
||||||
return ret;
|
return ret;
|
||||||
case IdentifierType.Special:
|
case IdentifierType.Special:
|
||||||
ret.Add( nameof( Special ), Special.ToString() );
|
ret.Add(nameof(Special), Special.ToString());
|
||||||
return ret;
|
return ret;
|
||||||
case IdentifierType.Npc:
|
case IdentifierType.Npc:
|
||||||
ret.Add( nameof( Kind ), Kind.ToString() );
|
ret.Add(nameof(Kind), Kind.ToString());
|
||||||
ret.Add( nameof( Index ), Index );
|
ret.Add(nameof(Index), Index);
|
||||||
ret.Add( nameof( DataId ), DataId );
|
ret.Add(nameof(DataId), DataId);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,38 +119,32 @@ public readonly struct ActorIdentifier : IEquatable< ActorIdentifier >
|
||||||
|
|
||||||
public static class ActorManagerExtensions
|
public static class ActorManagerExtensions
|
||||||
{
|
{
|
||||||
public static bool DataIdEquals( this ActorManager? manager, ActorIdentifier lhs, ActorIdentifier rhs )
|
public static bool DataIdEquals(this ActorManager? manager, ActorIdentifier lhs, ActorIdentifier rhs)
|
||||||
{
|
{
|
||||||
if( lhs.Kind != rhs.Kind )
|
if (lhs.Kind != rhs.Kind)
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
if( lhs.DataId == rhs.DataId )
|
if (lhs.DataId == rhs.DataId)
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
|
|
||||||
if( manager == null )
|
if (manager == null)
|
||||||
{
|
|
||||||
return lhs.Kind == rhs.Kind && lhs.DataId == rhs.DataId || lhs.DataId == uint.MaxValue || rhs.DataId == uint.MaxValue;
|
return lhs.Kind == rhs.Kind && lhs.DataId == rhs.DataId || lhs.DataId == uint.MaxValue || rhs.DataId == uint.MaxValue;
|
||||||
}
|
|
||||||
|
|
||||||
return lhs.Kind switch
|
return lhs.Kind switch
|
||||||
{
|
{
|
||||||
ObjectKind.MountType => manager.Mounts.TryGetValue( lhs.DataId, out var lhsName )
|
ObjectKind.MountType => manager.Mounts.TryGetValue(lhs.DataId, out var lhsName)
|
||||||
&& manager.Mounts.TryGetValue( rhs.DataId, out var rhsName )
|
&& manager.Mounts.TryGetValue(rhs.DataId, out var rhsName)
|
||||||
&& lhsName.Equals( rhsName, StringComparison.OrdinalIgnoreCase ),
|
&& lhsName.Equals(rhsName, StringComparison.OrdinalIgnoreCase),
|
||||||
ObjectKind.Companion => manager.Companions.TryGetValue( lhs.DataId, out var lhsName )
|
ObjectKind.Companion => manager.Companions.TryGetValue(lhs.DataId, out var lhsName)
|
||||||
&& manager.Companions.TryGetValue( rhs.DataId, out var rhsName )
|
&& manager.Companions.TryGetValue(rhs.DataId, out var rhsName)
|
||||||
&& lhsName.Equals( rhsName, StringComparison.OrdinalIgnoreCase ),
|
&& lhsName.Equals(rhsName, StringComparison.OrdinalIgnoreCase),
|
||||||
ObjectKind.BattleNpc => manager.BNpcs.TryGetValue( lhs.DataId, out var lhsName )
|
ObjectKind.BattleNpc => manager.BNpcs.TryGetValue(lhs.DataId, out var lhsName)
|
||||||
&& manager.BNpcs.TryGetValue( rhs.DataId, out var rhsName )
|
&& manager.BNpcs.TryGetValue(rhs.DataId, out var rhsName)
|
||||||
&& lhsName.Equals( rhsName, StringComparison.OrdinalIgnoreCase ),
|
&& lhsName.Equals(rhsName, StringComparison.OrdinalIgnoreCase),
|
||||||
ObjectKind.EventNpc => manager.ENpcs.TryGetValue( lhs.DataId, out var lhsName )
|
ObjectKind.EventNpc => manager.ENpcs.TryGetValue(lhs.DataId, out var lhsName)
|
||||||
&& manager.ENpcs.TryGetValue( rhs.DataId, out var rhsName )
|
&& manager.ENpcs.TryGetValue(rhs.DataId, out var rhsName)
|
||||||
&& lhsName.Equals( rhsName, StringComparison.OrdinalIgnoreCase ),
|
&& lhsName.Equals(rhsName, StringComparison.OrdinalIgnoreCase),
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
124
Penumbra.GameData/Actors/ActorManager.Data.cs
Normal file
124
Penumbra.GameData/Actors/ActorManager.Data.cs
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using Dalamud;
|
||||||
|
using Dalamud.Data;
|
||||||
|
using Dalamud.Game.ClientState;
|
||||||
|
using Dalamud.Game.ClientState.Objects;
|
||||||
|
using Dalamud.Logging;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using Dalamud.Utility;
|
||||||
|
using Lumina.Excel.GeneratedSheets;
|
||||||
|
|
||||||
|
namespace Penumbra.GameData.Actors;
|
||||||
|
|
||||||
|
public partial class ActorManager : IDisposable
|
||||||
|
{
|
||||||
|
/// <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 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 ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, DataManager gameData, Func<ushort, short> toParentIdx)
|
||||||
|
: this(pluginInterface, objects, state, gameData, gameData.Language, toParentIdx)
|
||||||
|
{}
|
||||||
|
|
||||||
|
public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, DataManager gameData,
|
||||||
|
ClientLanguage language, Func<ushort, short> toParentIdx)
|
||||||
|
{
|
||||||
|
_pluginInterface = pluginInterface;
|
||||||
|
_objects = objects;
|
||||||
|
_clientState = state;
|
||||||
|
_gameData = gameData;
|
||||||
|
_language = language;
|
||||||
|
_toParentIdx = toParentIdx;
|
||||||
|
|
||||||
|
Worlds = TryCatchData("Worlds", CreateWorldData);
|
||||||
|
Mounts = TryCatchData("Mounts", CreateMountData);
|
||||||
|
Companions = TryCatchData("Companions", CreateCompanionData);
|
||||||
|
BNpcs = TryCatchData("BNpcs", CreateBNpcData);
|
||||||
|
ENpcs = TryCatchData("ENpcs", CreateENpcData);
|
||||||
|
|
||||||
|
ActorIdentifier.Manager = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
_pluginInterface.RelinquishData(GetVersionedTag("Worlds"));
|
||||||
|
_pluginInterface.RelinquishData(GetVersionedTag("Mounts"));
|
||||||
|
_pluginInterface.RelinquishData(GetVersionedTag("Companions"));
|
||||||
|
_pluginInterface.RelinquishData(GetVersionedTag("BNpcs"));
|
||||||
|
_pluginInterface.RelinquishData(GetVersionedTag("ENpcs"));
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
~ActorManager()
|
||||||
|
=> Dispose();
|
||||||
|
|
||||||
|
private const int Version = 1;
|
||||||
|
|
||||||
|
private readonly DalamudPluginInterface _pluginInterface;
|
||||||
|
private readonly ObjectTable _objects;
|
||||||
|
private readonly ClientState _clientState;
|
||||||
|
private readonly DataManager _gameData;
|
||||||
|
private readonly ClientLanguage _language;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
private readonly Func<ushort, short> _toParentIdx;
|
||||||
|
|
||||||
|
private IReadOnlyDictionary<ushort, string> CreateWorldData()
|
||||||
|
=> _gameData.GetExcelSheet<World>(_language)!
|
||||||
|
.Where(w => w.IsPublic && !w.Name.RawData.IsEmpty)
|
||||||
|
.ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString());
|
||||||
|
|
||||||
|
private IReadOnlyDictionary<uint, string> CreateMountData()
|
||||||
|
=> _gameData.GetExcelSheet<Mount>(_language)!
|
||||||
|
.Where(m => m.Singular.RawData.Length > 0 && m.Order >= 0)
|
||||||
|
.ToDictionary(m => m.RowId, m => CultureInfo.InvariantCulture.TextInfo.ToTitleCase(m.Singular.ToDalamudString().ToString()));
|
||||||
|
|
||||||
|
private IReadOnlyDictionary<uint, string> CreateCompanionData()
|
||||||
|
=> _gameData.GetExcelSheet<Companion>(_language)!
|
||||||
|
.Where(c => c.Singular.RawData.Length > 0 && c.Order < ushort.MaxValue)
|
||||||
|
.ToDictionary(c => c.RowId, c => CultureInfo.InvariantCulture.TextInfo.ToTitleCase(c.Singular.ToDalamudString().ToString()));
|
||||||
|
|
||||||
|
private IReadOnlyDictionary<uint, string> CreateBNpcData()
|
||||||
|
=> _gameData.GetExcelSheet<BNpcName>(_language)!
|
||||||
|
.Where(n => n.Singular.RawData.Length > 0)
|
||||||
|
.ToDictionary(n => n.RowId, n => CultureInfo.InvariantCulture.TextInfo.ToTitleCase(n.Singular.ToDalamudString().ToString()));
|
||||||
|
|
||||||
|
private IReadOnlyDictionary<uint, string> CreateENpcData()
|
||||||
|
=> _gameData.GetExcelSheet<ENpcResident>(_language)!
|
||||||
|
.Where(e => e.Singular.RawData.Length > 0)
|
||||||
|
.ToDictionary(e => e.RowId, e => CultureInfo.InvariantCulture.TextInfo.ToTitleCase(e.Singular.ToDalamudString().ToString()));
|
||||||
|
|
||||||
|
private string GetVersionedTag(string tag)
|
||||||
|
=> $"Penumbra.Actors.{tag}.{_language}.V{Version}";
|
||||||
|
|
||||||
|
private T TryCatchData<T>(string tag, Func<T> func) where T : class
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return _pluginInterface.GetOrCreateData(GetVersionedTag(tag), func);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
PluginLog.Error($"Error creating shared actor data for {tag}:\n{ex}");
|
||||||
|
return func();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
302
Penumbra.GameData/Actors/ActorManager.Identifiers.cs
Normal file
302
Penumbra.GameData/Actors/ActorManager.Identifiers.cs
Normal file
|
|
@ -0,0 +1,302 @@
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Linq;
|
||||||
|
using Dalamud.Game.ClientState.Objects.Enums;
|
||||||
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Penumbra.String;
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var type = data[nameof(ActorIdentifier.Type)]?.Value<IdentifierType>() ?? IdentifierType.Invalid;
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case IdentifierType.Player:
|
||||||
|
{
|
||||||
|
var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.Value<string>(), false);
|
||||||
|
var homeWorld = data[nameof(ActorIdentifier.HomeWorld)]?.Value<ushort>() ?? 0;
|
||||||
|
return CreatePlayer(name, homeWorld);
|
||||||
|
}
|
||||||
|
case IdentifierType.Owned:
|
||||||
|
{
|
||||||
|
var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.Value<string>(), false);
|
||||||
|
var homeWorld = data[nameof(ActorIdentifier.HomeWorld)]?.Value<ushort>() ?? 0;
|
||||||
|
var kind = data[nameof(ActorIdentifier.Kind)]?.Value<ObjectKind>() ?? ObjectKind.CardStand;
|
||||||
|
var dataId = data[nameof(ActorIdentifier.DataId)]?.Value<uint>() ?? 0;
|
||||||
|
return CreateOwned(name, homeWorld, kind, dataId);
|
||||||
|
}
|
||||||
|
case IdentifierType.Special:
|
||||||
|
{
|
||||||
|
var special = data[nameof(ActorIdentifier.Special)]?.Value<SpecialActor>() ?? 0;
|
||||||
|
return CreateSpecial(special);
|
||||||
|
}
|
||||||
|
case IdentifierType.Npc:
|
||||||
|
{
|
||||||
|
var index = data[nameof(ActorIdentifier.Index)]?.Value<ushort>() ?? ushort.MaxValue;
|
||||||
|
var kind = data[nameof(ActorIdentifier.Kind)]?.Value<ObjectKind>() ?? ObjectKind.CardStand;
|
||||||
|
var dataId = data[nameof(ActorIdentifier.DataId)]?.Value<uint>() ?? 0;
|
||||||
|
return CreateNpc(kind, dataId, index);
|
||||||
|
}
|
||||||
|
case IdentifierType.Invalid:
|
||||||
|
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} ({Worlds[id.HomeWorld]})"
|
||||||
|
: id.PlayerName.ToString(),
|
||||||
|
IdentifierType.Owned => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id
|
||||||
|
? $"{id.PlayerName} ({Worlds[id.HomeWorld]})'s {ToName(id.Kind, id.DataId)}"
|
||||||
|
: $"{id.PlayerName}s {ToName(id.Kind, id.DataId)}",
|
||||||
|
IdentifierType.Special => ToName(id.Special),
|
||||||
|
IdentifierType.Npc =>
|
||||||
|
id.Index == ushort.MaxValue
|
||||||
|
? ToName(id.Kind, id.DataId)
|
||||||
|
: $"{ToName(id.Kind, id.DataId)} at {id.Index}",
|
||||||
|
_ => "Invalid",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fixed names for special actors.
|
||||||
|
/// </summary>
|
||||||
|
public static string ToName(SpecialActor actor)
|
||||||
|
=> actor switch
|
||||||
|
{
|
||||||
|
SpecialActor.CharacterScreen => "Character Screen Actor",
|
||||||
|
SpecialActor.ExamineScreen => "Examine Screen Actor",
|
||||||
|
SpecialActor.FittingRoom => "Fitting Room Actor",
|
||||||
|
SpecialActor.DyePreview => "Dye Preview Actor",
|
||||||
|
SpecialActor.Portrait => "Portrait Actor",
|
||||||
|
_ => "Invalid",
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <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.BattleNpc => BNpcs.TryGetValue(dataId, out name),
|
||||||
|
ObjectKind.EventNpc => ENpcs.TryGetValue(dataId, out name),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compute an ActorIdentifier from a GameObject.
|
||||||
|
/// </summary>
|
||||||
|
public unsafe ActorIdentifier FromObject(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* actor)
|
||||||
|
{
|
||||||
|
if (actor == null)
|
||||||
|
return ActorIdentifier.Invalid;
|
||||||
|
|
||||||
|
var idx = actor->ObjectIndex;
|
||||||
|
if (idx is >= (ushort)SpecialActor.CutsceneStart and < (ushort)SpecialActor.CutsceneEnd)
|
||||||
|
{
|
||||||
|
var parentIdx = _toParentIdx(idx);
|
||||||
|
if (parentIdx >= 0)
|
||||||
|
return FromObject((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_objects.GetObjectAddress(parentIdx));
|
||||||
|
}
|
||||||
|
else if (idx is >= (ushort)SpecialActor.CharacterScreen and <= (ushort)SpecialActor.Portrait)
|
||||||
|
{
|
||||||
|
return CreateSpecial((SpecialActor)idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ((ObjectKind)actor->ObjectKind)
|
||||||
|
{
|
||||||
|
case ObjectKind.Player:
|
||||||
|
{
|
||||||
|
var name = new ByteString(actor->Name);
|
||||||
|
var homeWorld = ((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor)->HomeWorld;
|
||||||
|
return CreatePlayer(name, homeWorld);
|
||||||
|
}
|
||||||
|
case ObjectKind.BattleNpc:
|
||||||
|
{
|
||||||
|
var ownerId = actor->OwnerID;
|
||||||
|
if (ownerId != 0xE0000000)
|
||||||
|
{
|
||||||
|
var owner =
|
||||||
|
(FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)(_objects.SearchById(ownerId)?.Address ?? IntPtr.Zero);
|
||||||
|
if (owner == null)
|
||||||
|
return ActorIdentifier.Invalid;
|
||||||
|
|
||||||
|
var name = new ByteString(owner->GameObject.Name);
|
||||||
|
var homeWorld = owner->HomeWorld;
|
||||||
|
return CreateOwned(name, homeWorld, ObjectKind.BattleNpc,
|
||||||
|
((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor)->NameID);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateNpc(ObjectKind.BattleNpc, ((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor)->NameID, actor->ObjectIndex);
|
||||||
|
}
|
||||||
|
case ObjectKind.EventNpc: return CreateNpc(ObjectKind.EventNpc, actor->DataID, actor->ObjectIndex);
|
||||||
|
case ObjectKind.MountType:
|
||||||
|
case ObjectKind.Companion:
|
||||||
|
{
|
||||||
|
if (actor->ObjectIndex % 2 == 0)
|
||||||
|
return ActorIdentifier.Invalid;
|
||||||
|
|
||||||
|
var owner = (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)_objects.GetObjectAddress(actor->ObjectIndex - 1);
|
||||||
|
if (owner == null)
|
||||||
|
return ActorIdentifier.Invalid;
|
||||||
|
|
||||||
|
var dataId = GetCompanionId(actor, &owner->GameObject);
|
||||||
|
return CreateOwned(new ByteString(owner->GameObject.Name), owner->HomeWorld, (ObjectKind)actor->ObjectKind, dataId);
|
||||||
|
}
|
||||||
|
default: return ActorIdentifier.Invalid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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,
|
||||||
|
FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* owner)
|
||||||
|
{
|
||||||
|
return (ObjectKind)actor->ObjectKind switch
|
||||||
|
{
|
||||||
|
ObjectKind.MountType => *(ushort*)((byte*)owner + 0x668),
|
||||||
|
ObjectKind.Companion => *(ushort*)((byte*)actor + 0x1AAC),
|
||||||
|
_ => actor->DataID,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe ActorIdentifier FromObject(GameObject? actor)
|
||||||
|
=> FromObject((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)(actor?.Address ?? IntPtr.Zero));
|
||||||
|
|
||||||
|
public ActorIdentifier CreatePlayer(ByteString name, ushort homeWorld)
|
||||||
|
{
|
||||||
|
if (!VerifyWorld(homeWorld) || !VerifyPlayerName(name))
|
||||||
|
return ActorIdentifier.Invalid;
|
||||||
|
|
||||||
|
return new ActorIdentifier(IdentifierType.Player, ObjectKind.Player, homeWorld, 0, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ActorIdentifier CreateSpecial(SpecialActor 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) || !VerifyOwnedData(kind, dataId))
|
||||||
|
return ActorIdentifier.Invalid;
|
||||||
|
|
||||||
|
return new ActorIdentifier(IdentifierType.Owned, kind, homeWorld, dataId, ownerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Checks SE naming rules. </summary>
|
||||||
|
private static bool VerifyPlayerName(ByteString name)
|
||||||
|
{
|
||||||
|
// Total no more than 20 characters + space.
|
||||||
|
if (name.Length is < 5 or > 21)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var split = name.Split((byte)' ');
|
||||||
|
|
||||||
|
// Forename and surname, no more spaces.
|
||||||
|
if (split.Count != 2)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
static bool CheckNamePart(ByteString part)
|
||||||
|
{
|
||||||
|
// Each name part at least 2 and at most 15 characters.
|
||||||
|
if (part.Length is < 2 or > 15)
|
||||||
|
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.
|
||||||
|
if (part.Skip(1).Any(c => c != (byte)'\'' && c != (byte)'-' && c is < (byte)'a' or > (byte)'z'))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var hyphens = part.Split((byte)'-');
|
||||||
|
// Apostrophes can not be used in succession, after or before apostrophes.
|
||||||
|
return !hyphens.Any(p => p.Length == 0 || p[0] == (byte)'\'' || p.Last() == (byte)'\'');
|
||||||
|
}
|
||||||
|
|
||||||
|
return CheckNamePart(split[0]) && CheckNamePart(split[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Checks if the world is a valid public world or ushort.MaxValue (any world). </summary>
|
||||||
|
private bool VerifyWorld(ushort worldId)
|
||||||
|
=> Worlds.ContainsKey(worldId);
|
||||||
|
|
||||||
|
/// <summary> Verify that the enum value is a specific actor and return the name if it is. </summary>
|
||||||
|
private static bool VerifySpecial(SpecialActor actor)
|
||||||
|
=> actor is >= SpecialActor.CharacterScreen and <= SpecialActor.Portrait;
|
||||||
|
|
||||||
|
/// <summary> Verify that the object index is a valid index for an NPC. </summary>
|
||||||
|
private static bool VerifyIndex(ushort index)
|
||||||
|
{
|
||||||
|
return index switch
|
||||||
|
{
|
||||||
|
< 200 => index % 2 == 0,
|
||||||
|
> (ushort)SpecialActor.Portrait => index < 426,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Verify that the object kind is a valid owned object, and the corresponding data Id. </summary>
|
||||||
|
private bool VerifyOwnedData(ObjectKind kind, uint dataId)
|
||||||
|
{
|
||||||
|
return kind switch
|
||||||
|
{
|
||||||
|
ObjectKind.MountType => Mounts.ContainsKey(dataId),
|
||||||
|
ObjectKind.Companion => Companions.ContainsKey(dataId),
|
||||||
|
ObjectKind.BattleNpc => BNpcs.ContainsKey(dataId),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool VerifyNpcData(ObjectKind kind, uint dataId)
|
||||||
|
=> kind switch
|
||||||
|
{
|
||||||
|
ObjectKind.BattleNpc => BNpcs.ContainsKey(dataId),
|
||||||
|
ObjectKind.EventNpc => ENpcs.ContainsKey(dataId),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,352 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using Dalamud.Data;
|
|
||||||
using Dalamud.Game.ClientState;
|
|
||||||
using Dalamud.Game.ClientState.Objects;
|
|
||||||
using Dalamud.Game.ClientState.Objects.Enums;
|
|
||||||
using Dalamud.Game.ClientState.Objects.Types;
|
|
||||||
using Dalamud.Utility;
|
|
||||||
using Lumina.Excel.GeneratedSheets;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using Penumbra.String;
|
|
||||||
|
|
||||||
namespace Penumbra.GameData.Actors;
|
|
||||||
|
|
||||||
public class ActorManager
|
|
||||||
{
|
|
||||||
private readonly ObjectTable _objects;
|
|
||||||
private readonly ClientState _clientState;
|
|
||||||
|
|
||||||
public readonly IReadOnlyDictionary< ushort, string > Worlds;
|
|
||||||
public readonly IReadOnlyDictionary< uint, string > Mounts;
|
|
||||||
public readonly IReadOnlyDictionary< uint, string > Companions;
|
|
||||||
public readonly IReadOnlyDictionary< uint, string > BNpcs;
|
|
||||||
public readonly IReadOnlyDictionary< uint, string > ENpcs;
|
|
||||||
|
|
||||||
public IEnumerable< KeyValuePair< ushort, string > > AllWorlds
|
|
||||||
=> Worlds.OrderBy( kvp => kvp.Key ).Prepend( new KeyValuePair< ushort, string >( ushort.MaxValue, "Any World" ) );
|
|
||||||
|
|
||||||
private readonly Func< ushort, short > _toParentIdx;
|
|
||||||
|
|
||||||
public ActorManager( ObjectTable objects, ClientState state, DataManager gameData, Func< ushort, short > toParentIdx )
|
|
||||||
{
|
|
||||||
_objects = objects;
|
|
||||||
_clientState = state;
|
|
||||||
Worlds = gameData.GetExcelSheet< World >()!
|
|
||||||
.Where( w => w.IsPublic && !w.Name.RawData.IsEmpty )
|
|
||||||
.ToDictionary( w => ( ushort )w.RowId, w => w.Name.ToString() );
|
|
||||||
|
|
||||||
Mounts = gameData.GetExcelSheet< Mount >()!
|
|
||||||
.Where( m => m.Singular.RawData.Length > 0 && m.Order >= 0 )
|
|
||||||
.ToDictionary( m => m.RowId, m => CultureInfo.InvariantCulture.TextInfo.ToTitleCase( m.Singular.ToDalamudString().ToString() ) );
|
|
||||||
Companions = gameData.GetExcelSheet< Companion >()!
|
|
||||||
.Where( c => c.Singular.RawData.Length > 0 && c.Order < ushort.MaxValue )
|
|
||||||
.ToDictionary( c => c.RowId, c => CultureInfo.InvariantCulture.TextInfo.ToTitleCase( c.Singular.ToDalamudString().ToString() ) );
|
|
||||||
|
|
||||||
BNpcs = gameData.GetExcelSheet< BNpcName >()!
|
|
||||||
.Where( n => n.Singular.RawData.Length > 0 )
|
|
||||||
.ToDictionary( n => n.RowId, n => CultureInfo.InvariantCulture.TextInfo.ToTitleCase( n.Singular.ToDalamudString().ToString() ) );
|
|
||||||
|
|
||||||
ENpcs = gameData.GetExcelSheet< ENpcResident >()!
|
|
||||||
.Where( e => e.Singular.RawData.Length > 0 )
|
|
||||||
.ToDictionary( e => e.RowId, e => CultureInfo.InvariantCulture.TextInfo.ToTitleCase( e.Singular.ToDalamudString().ToString() ) );
|
|
||||||
|
|
||||||
_toParentIdx = toParentIdx;
|
|
||||||
|
|
||||||
ActorIdentifier.Manager = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ActorIdentifier FromJson( JObject data )
|
|
||||||
{
|
|
||||||
var type = data[ nameof( ActorIdentifier.Type ) ]?.Value< IdentifierType >() ?? IdentifierType.Invalid;
|
|
||||||
switch( type )
|
|
||||||
{
|
|
||||||
case IdentifierType.Player:
|
|
||||||
{
|
|
||||||
var name = ByteString.FromStringUnsafe( data[ nameof( ActorIdentifier.PlayerName ) ]?.Value< string >(), false );
|
|
||||||
var homeWorld = data[ nameof( ActorIdentifier.HomeWorld ) ]?.Value< ushort >() ?? 0;
|
|
||||||
return CreatePlayer( name, homeWorld );
|
|
||||||
}
|
|
||||||
case IdentifierType.Owned:
|
|
||||||
{
|
|
||||||
var name = ByteString.FromStringUnsafe( data[ nameof( ActorIdentifier.PlayerName ) ]?.Value< string >(), false );
|
|
||||||
var homeWorld = data[ nameof( ActorIdentifier.HomeWorld ) ]?.Value< ushort >() ?? 0;
|
|
||||||
var kind = data[ nameof( ActorIdentifier.Kind ) ]?.Value< ObjectKind >() ?? ObjectKind.CardStand;
|
|
||||||
var dataId = data[ nameof( ActorIdentifier.DataId ) ]?.Value< uint >() ?? 0;
|
|
||||||
return CreateOwned( name, homeWorld, kind, dataId );
|
|
||||||
}
|
|
||||||
case IdentifierType.Special:
|
|
||||||
{
|
|
||||||
var special = data[ nameof( ActorIdentifier.Special ) ]?.Value< SpecialActor >() ?? 0;
|
|
||||||
return CreateSpecial( special );
|
|
||||||
}
|
|
||||||
case IdentifierType.Npc:
|
|
||||||
{
|
|
||||||
var index = data[ nameof( ActorIdentifier.Index ) ]?.Value< ushort >() ?? 0;
|
|
||||||
var kind = data[ nameof( ActorIdentifier.Kind ) ]?.Value< ObjectKind >() ?? ObjectKind.CardStand;
|
|
||||||
var dataId = data[ nameof( ActorIdentifier.DataId ) ]?.Value< uint >() ?? 0;
|
|
||||||
return CreateNpc( kind, index, dataId );
|
|
||||||
}
|
|
||||||
case IdentifierType.Invalid:
|
|
||||||
default:
|
|
||||||
return ActorIdentifier.Invalid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ToString( ActorIdentifier id )
|
|
||||||
{
|
|
||||||
return id.Type switch
|
|
||||||
{
|
|
||||||
IdentifierType.Player => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id
|
|
||||||
? $"{id.PlayerName} ({Worlds[ id.HomeWorld ]})"
|
|
||||||
: id.PlayerName.ToString(),
|
|
||||||
IdentifierType.Owned => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id
|
|
||||||
? $"{id.PlayerName} ({Worlds[ id.HomeWorld ]})'s {ToName( id.Kind, id.DataId )}"
|
|
||||||
: $"{id.PlayerName}s {ToName( id.Kind, id.DataId )}",
|
|
||||||
IdentifierType.Special => ToName( id.Special ),
|
|
||||||
IdentifierType.Npc =>
|
|
||||||
id.Index == ushort.MaxValue
|
|
||||||
? ToName( id.Kind, id.DataId )
|
|
||||||
: $"{ToName( id.Kind, id.DataId )} at {id.Index}",
|
|
||||||
_ => "Invalid",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string ToName( SpecialActor actor )
|
|
||||||
=> actor switch
|
|
||||||
{
|
|
||||||
SpecialActor.CharacterScreen => "Character Screen Actor",
|
|
||||||
SpecialActor.ExamineScreen => "Examine Screen Actor",
|
|
||||||
SpecialActor.FittingRoom => "Fitting Room Actor",
|
|
||||||
SpecialActor.DyePreview => "Dye Preview Actor",
|
|
||||||
SpecialActor.Portrait => "Portrait Actor",
|
|
||||||
_ => "Invalid",
|
|
||||||
};
|
|
||||||
|
|
||||||
public string ToName( ObjectKind kind, uint dataId )
|
|
||||||
=> TryGetName( kind, dataId, out var ret ) ? ret : "Invalid";
|
|
||||||
|
|
||||||
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.BattleNpc => BNpcs.TryGetValue( dataId, out name ),
|
|
||||||
ObjectKind.EventNpc => ENpcs.TryGetValue( dataId, out name ),
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe ActorIdentifier FromObject( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* actor )
|
|
||||||
{
|
|
||||||
if( actor == null )
|
|
||||||
{
|
|
||||||
return ActorIdentifier.Invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
var idx = actor->ObjectIndex;
|
|
||||||
if( idx is >= ( ushort )SpecialActor.CutsceneStart and < ( ushort )SpecialActor.CutsceneEnd )
|
|
||||||
{
|
|
||||||
var parentIdx = _toParentIdx( idx );
|
|
||||||
if( parentIdx >= 0 )
|
|
||||||
{
|
|
||||||
return FromObject( ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )_objects.GetObjectAddress( parentIdx ) );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if( idx is >= ( ushort )SpecialActor.CharacterScreen and <= ( ushort )SpecialActor.Portrait )
|
|
||||||
{
|
|
||||||
return CreateSpecial( ( SpecialActor )idx );
|
|
||||||
}
|
|
||||||
|
|
||||||
switch( ( ObjectKind )actor->ObjectKind )
|
|
||||||
{
|
|
||||||
case ObjectKind.Player:
|
|
||||||
{
|
|
||||||
var name = new ByteString( actor->Name );
|
|
||||||
var homeWorld = ( ( FFXIVClientStructs.FFXIV.Client.Game.Character.Character* )actor )->HomeWorld;
|
|
||||||
return CreatePlayer( name, homeWorld );
|
|
||||||
}
|
|
||||||
case ObjectKind.BattleNpc:
|
|
||||||
{
|
|
||||||
var ownerId = actor->OwnerID;
|
|
||||||
if( ownerId != 0xE0000000 )
|
|
||||||
{
|
|
||||||
var owner = ( FFXIVClientStructs.FFXIV.Client.Game.Character.Character* )( _objects.SearchById( ownerId )?.Address ?? IntPtr.Zero );
|
|
||||||
if( owner == null )
|
|
||||||
{
|
|
||||||
return ActorIdentifier.Invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
var name = new ByteString( owner->GameObject.Name );
|
|
||||||
var homeWorld = owner->HomeWorld;
|
|
||||||
return CreateOwned( name, homeWorld, ObjectKind.BattleNpc, ( ( FFXIVClientStructs.FFXIV.Client.Game.Character.Character* )actor )->NameID );
|
|
||||||
}
|
|
||||||
|
|
||||||
return CreateNpc( ObjectKind.BattleNpc, actor->ObjectIndex, ( ( FFXIVClientStructs.FFXIV.Client.Game.Character.Character* )actor )->NameID );
|
|
||||||
}
|
|
||||||
case ObjectKind.EventNpc: return CreateNpc( ObjectKind.EventNpc, actor->ObjectIndex, actor->DataID );
|
|
||||||
case ObjectKind.MountType:
|
|
||||||
case ObjectKind.Companion:
|
|
||||||
{
|
|
||||||
if( actor->ObjectIndex % 2 == 0 )
|
|
||||||
{
|
|
||||||
return ActorIdentifier.Invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
var owner = ( FFXIVClientStructs.FFXIV.Client.Game.Character.Character* )_objects.GetObjectAddress( actor->ObjectIndex - 1 );
|
|
||||||
if( owner == null )
|
|
||||||
{
|
|
||||||
return ActorIdentifier.Invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
var dataId = GetCompanionId( actor, &owner->GameObject );
|
|
||||||
return CreateOwned( new ByteString( owner->GameObject.Name ), owner->HomeWorld, ( ObjectKind )actor->ObjectKind, dataId );
|
|
||||||
}
|
|
||||||
default: return ActorIdentifier.Invalid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe uint GetCompanionId( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* actor, FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* owner )
|
|
||||||
{
|
|
||||||
return ( ObjectKind )actor->ObjectKind switch
|
|
||||||
{
|
|
||||||
ObjectKind.MountType => *( ushort* )( ( byte* )owner + 0x668 ),
|
|
||||||
ObjectKind.Companion => *( ushort* )( ( byte* )actor + 0x1AAC ),
|
|
||||||
_ => actor->DataID,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe ActorIdentifier FromObject( GameObject? actor )
|
|
||||||
=> FromObject( ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )( actor?.Address ?? IntPtr.Zero ) );
|
|
||||||
|
|
||||||
|
|
||||||
public ActorIdentifier CreatePlayer( ByteString name, ushort homeWorld )
|
|
||||||
{
|
|
||||||
if( !VerifyWorld( homeWorld ) || !VerifyPlayerName( name ) )
|
|
||||||
{
|
|
||||||
return ActorIdentifier.Invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ActorIdentifier( IdentifierType.Player, ObjectKind.Player, homeWorld, 0, name );
|
|
||||||
}
|
|
||||||
|
|
||||||
public ActorIdentifier CreateSpecial( SpecialActor actor )
|
|
||||||
{
|
|
||||||
if( !VerifySpecial( actor ) )
|
|
||||||
{
|
|
||||||
return ActorIdentifier.Invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ActorIdentifier( IdentifierType.Special, ObjectKind.Player, ( ushort )actor, 0, ByteString.Empty );
|
|
||||||
}
|
|
||||||
|
|
||||||
public ActorIdentifier CreateNpc( ObjectKind kind, ushort index = ushort.MaxValue, uint data = uint.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 ) || !VerifyOwnedData( kind, dataId ) )
|
|
||||||
{
|
|
||||||
return ActorIdentifier.Invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ActorIdentifier( IdentifierType.Owned, kind, homeWorld, dataId, ownerName );
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary> Checks SE naming rules. </summary>
|
|
||||||
private static bool VerifyPlayerName( ByteString name )
|
|
||||||
{
|
|
||||||
// Total no more than 20 characters + space.
|
|
||||||
if( name.Length is < 5 or > 21 )
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var split = name.Split( ( byte )' ' );
|
|
||||||
|
|
||||||
// Forename and surname, no more spaces.
|
|
||||||
if( split.Count != 2 )
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool CheckNamePart( ByteString part )
|
|
||||||
{
|
|
||||||
// Each name part at least 2 and at most 15 characters.
|
|
||||||
if( part.Length is < 2 or > 15 )
|
|
||||||
{
|
|
||||||
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.
|
|
||||||
if( part.Skip( 1 ).Any( c => c != ( byte )'\'' && c != ( byte )'-' && c is < ( byte )'a' or > ( byte )'z' ) )
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var hyphens = part.Split( ( byte )'-' );
|
|
||||||
// Apostrophes can not be used in succession, after or before apostrophes.
|
|
||||||
return !hyphens.Any( p => p.Length == 0 || p[ 0 ] == ( byte )'\'' || p.Last() == ( byte )'\'' );
|
|
||||||
}
|
|
||||||
|
|
||||||
return CheckNamePart( split[ 0 ] ) && CheckNamePart( split[ 1 ] );
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary> Checks if the world is a valid public world or ushort.MaxValue (any world). </summary>
|
|
||||||
private bool VerifyWorld( ushort worldId )
|
|
||||||
=> Worlds.ContainsKey( worldId );
|
|
||||||
|
|
||||||
/// <summary> Verify that the enum value is a specific actor and return the name if it is. </summary>
|
|
||||||
private static bool VerifySpecial( SpecialActor actor )
|
|
||||||
=> actor is >= SpecialActor.CharacterScreen and <= SpecialActor.Portrait;
|
|
||||||
|
|
||||||
/// <summary> Verify that the object index is a valid index for an NPC. </summary>
|
|
||||||
private static bool VerifyIndex( ushort index )
|
|
||||||
{
|
|
||||||
return index switch
|
|
||||||
{
|
|
||||||
< 200 => index % 2 == 0,
|
|
||||||
> ( ushort )SpecialActor.Portrait => index < 426,
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary> Verify that the object kind is a valid owned object, and the corresponding data Id. </summary>
|
|
||||||
private bool VerifyOwnedData( ObjectKind kind, uint dataId )
|
|
||||||
{
|
|
||||||
return kind switch
|
|
||||||
{
|
|
||||||
ObjectKind.MountType => Mounts.ContainsKey( dataId ),
|
|
||||||
ObjectKind.Companion => Companions.ContainsKey( dataId ),
|
|
||||||
ObjectKind.BattleNpc => BNpcs.ContainsKey( dataId ),
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool VerifyNpcData( ObjectKind kind, uint dataId )
|
|
||||||
=> kind switch
|
|
||||||
{
|
|
||||||
ObjectKind.BattleNpc => BNpcs.ContainsKey( dataId ),
|
|
||||||
ObjectKind.EventNpc => ENpcs.ContainsKey( dataId ),
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -57,11 +57,10 @@ public interface IObjectIdentifier : IDisposable
|
||||||
/// <param name="weaponType">The secondary model ID for weapons, WeaponType.Zero for equipment and accessories.</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="variant">The variant ID of the model.</param>
|
||||||
/// <param name="slot">The equipment slot the piece of equipment uses.</param>
|
/// <param name="slot">The equipment slot the piece of equipment uses.</param>
|
||||||
/// <returns></returns>
|
public IReadOnlyList<Item> Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot);
|
||||||
public IReadOnlyList<Item>? Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot);
|
|
||||||
|
|
||||||
/// <inheritdoc cref="Identify(SetId, WeaponType, ushort, EquipSlot)"/>
|
/// <inheritdoc cref="Identify(SetId, WeaponType, ushort, EquipSlot)"/>
|
||||||
public IReadOnlyList<Item>? Identify(SetId setId, ushort variant, EquipSlot slot)
|
public IReadOnlyList<Item> Identify(SetId setId, ushort variant, EquipSlot slot)
|
||||||
=> Identify(setId, 0, variant, slot);
|
=> Identify(setId, 0, variant, slot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ namespace Penumbra.GameData;
|
||||||
|
|
||||||
internal class ObjectIdentification : IObjectIdentifier
|
internal class ObjectIdentification : IObjectIdentifier
|
||||||
{
|
{
|
||||||
public IGamePathParser GamePathParser { get; } = new GamePathParser();
|
public IGamePathParser GamePathParser { get; } = new GamePathParser();
|
||||||
|
|
||||||
public void Identify(IDictionary<string, object?> set, string path)
|
public void Identify(IDictionary<string, object?> set, string path)
|
||||||
{
|
{
|
||||||
|
|
@ -38,7 +38,7 @@ internal class ObjectIdentification : IObjectIdentifier
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IReadOnlyList<Item>? Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot)
|
public IReadOnlyList<Item> Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot)
|
||||||
{
|
{
|
||||||
switch (slot)
|
switch (slot)
|
||||||
{
|
{
|
||||||
|
|
@ -55,7 +55,7 @@ internal class ObjectIdentification : IObjectIdentifier
|
||||||
var (begin, _) = FindIndexRange((List<(ulong, IReadOnlyList<Item>)>)_equipment,
|
var (begin, _) = FindIndexRange((List<(ulong, IReadOnlyList<Item>)>)_equipment,
|
||||||
((ulong)setId << 32) | ((ulong)slot.ToSlot() << 16) | variant,
|
((ulong)setId << 32) | ((ulong)slot.ToSlot() << 16) | variant,
|
||||||
0xFFFFFFFFFFFF);
|
0xFFFFFFFFFFFF);
|
||||||
return begin >= 0 ? _equipment[begin].Item2 : null;
|
return begin >= 0 ? _equipment[begin].Item2 : Array.Empty<Item>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -68,12 +68,8 @@ internal class ObjectIdentification : IObjectIdentifier
|
||||||
|
|
||||||
private readonly IReadOnlyList<(ulong Key, IReadOnlyList<Item> Values)> _weapons;
|
private readonly IReadOnlyList<(ulong Key, IReadOnlyList<Item> Values)> _weapons;
|
||||||
private readonly IReadOnlyList<(ulong Key, IReadOnlyList<Item> Values)> _equipment;
|
private readonly IReadOnlyList<(ulong Key, IReadOnlyList<Item> Values)> _equipment;
|
||||||
private readonly IReadOnlyDictionary<string, IReadOnlyList<Action>> _actions;
|
private readonly IReadOnlyDictionary<string, IReadOnlyList<Action>> _actions;
|
||||||
|
private bool _disposed = false;
|
||||||
private readonly string _weaponsTag;
|
|
||||||
private readonly string _equipmentTag;
|
|
||||||
private readonly string _actionsTag;
|
|
||||||
private bool _disposed = false;
|
|
||||||
|
|
||||||
public ObjectIdentification(DalamudPluginInterface pluginInterface, DataManager dataManager, ClientLanguage language)
|
public ObjectIdentification(DalamudPluginInterface pluginInterface, DataManager dataManager, ClientLanguage language)
|
||||||
{
|
{
|
||||||
|
|
@ -81,39 +77,9 @@ internal class ObjectIdentification : IObjectIdentifier
|
||||||
_dataManager = dataManager;
|
_dataManager = dataManager;
|
||||||
_language = language;
|
_language = language;
|
||||||
|
|
||||||
_weaponsTag = $"Penumbra.Identification.Weapons.{_language}.V{Version}";
|
_weapons = TryCatchData("Weapons", CreateWeaponList);
|
||||||
_equipmentTag = $"Penumbra.Identification.Equipment.{_language}.V{Version}";
|
_equipment = TryCatchData("Equipment", CreateEquipmentList);
|
||||||
_actionsTag = $"Penumbra.Identification.Actions.{_language}.V{Version}";
|
_actions = TryCatchData("Actions", CreateActionList);
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_weapons = pluginInterface.GetOrCreateData(_weaponsTag, CreateWeaponList);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
PluginLog.Error($"Error creating shared identification data for weapons:\n{ex}");
|
|
||||||
_weapons = CreateWeaponList();
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_equipment = pluginInterface.GetOrCreateData(_equipmentTag, CreateEquipmentList);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
PluginLog.Error($"Error creating shared identification data for equipment:\n{ex}");
|
|
||||||
_equipment = CreateEquipmentList();
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_actions = pluginInterface.GetOrCreateData(_actionsTag, CreateActionList);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
PluginLog.Error($"Error creating shared identification data for actions:\n{ex}");
|
|
||||||
_actions = CreateActionList();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|
@ -122,15 +88,31 @@ internal class ObjectIdentification : IObjectIdentifier
|
||||||
return;
|
return;
|
||||||
|
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
_pluginInterface.RelinquishData(_weaponsTag);
|
_pluginInterface.RelinquishData(GetVersionedTag("Weapons"));
|
||||||
_pluginInterface.RelinquishData(_equipmentTag);
|
_pluginInterface.RelinquishData(GetVersionedTag("Equipment"));
|
||||||
_pluginInterface.RelinquishData(_actionsTag);
|
_pluginInterface.RelinquishData(GetVersionedTag("Actions"));
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
~ObjectIdentification()
|
~ObjectIdentification()
|
||||||
=> Dispose();
|
=> Dispose();
|
||||||
|
|
||||||
|
private string GetVersionedTag(string tag)
|
||||||
|
=> $"Penumbra.Identification.{tag}.{_language}.V{Version}";
|
||||||
|
|
||||||
|
private T TryCatchData<T>(string tag, Func<T> func) where T : class
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return _pluginInterface.GetOrCreateData(GetVersionedTag(tag), func);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
PluginLog.Error($"Error creating shared identification data for {tag}:\n{ex}");
|
||||||
|
return func();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static bool Add(IDictionary<ulong, HashSet<Item>> dict, ulong key, Item item)
|
private static bool Add(IDictionary<ulong, HashSet<Item>> dict, ulong key, Item item)
|
||||||
{
|
{
|
||||||
if (dict.TryGetValue(key, out var list))
|
if (dict.TryGetValue(key, out var list))
|
||||||
|
|
@ -181,7 +163,7 @@ internal class ObjectIdentification : IObjectIdentifier
|
||||||
var storage = new SortedList<ulong, HashSet<Item>>();
|
var storage = new SortedList<ulong, HashSet<Item>>();
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
{
|
{
|
||||||
switch (((EquipSlot)item.EquipSlotCategory.Row).ToSlot())
|
switch ((EquipSlot)item.EquipSlotCategory.Row)
|
||||||
{
|
{
|
||||||
// Accessories
|
// Accessories
|
||||||
case EquipSlot.RFinger:
|
case EquipSlot.RFinger:
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ public class Penumbra : IDalamudPlugin
|
||||||
ModFileSystem = ModFileSystem.Load();
|
ModFileSystem = ModFileSystem.Load();
|
||||||
ObjectReloader = new ObjectReloader();
|
ObjectReloader = new ObjectReloader();
|
||||||
PathResolver = new PathResolver( ResourceLoader );
|
PathResolver = new PathResolver( ResourceLoader );
|
||||||
Actors = new ActorManager( Dalamud.Objects, Dalamud.ClientState, Dalamud.GameData, u => ( short )PathResolver.CutsceneActor( u ) );
|
Actors = new ActorManager( Dalamud.PluginInterface, Dalamud.Objects, Dalamud.ClientState, Dalamud.GameData, u => ( short )PathResolver.CutsceneActor( u ) );
|
||||||
|
|
||||||
Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand )
|
Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand )
|
||||||
{
|
{
|
||||||
|
|
@ -290,7 +290,8 @@ public class Penumbra : IDalamudPlugin
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Dalamud.PluginInterface.RelinquishData( "test1" );
|
Actors?.Dispose();
|
||||||
|
Identifier?.Dispose();
|
||||||
Framework?.Dispose();
|
Framework?.Dispose();
|
||||||
ShutdownWebServer();
|
ShutdownWebServer();
|
||||||
DisposeInterface();
|
DisposeInterface();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue