diff --git a/.gitmodules b/.gitmodules index b5eb77bb..94049366 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,3 +6,7 @@ path = Penumbra.Api url = git@github.com:Ottermandias/Penumbra.Api.git branch = main +[submodule "Penumbra.String"] + path = Penumbra.String + url = git@github.com:Ottermandias/Penumbra.String.git + branch = main diff --git a/Penumbra.GameData/Actors/ActorIdentifier.cs b/Penumbra.GameData/Actors/ActorIdentifier.cs new file mode 100644 index 00000000..5651382e --- /dev/null +++ b/Penumbra.GameData/Actors/ActorIdentifier.cs @@ -0,0 +1,152 @@ +using System; +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); + + // @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 SpecialActor 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.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.Owned => HomeWorld == other.HomeWorld && PlayerName.EqualsCi( other.PlayerName ) && Manager.DataIdEquals( this, other ), + IdentifierType.Special => Special == other.Special, + IdentifierType.Npc => Index == other.Index && DataId == other.DataId && Manager.DataIdEquals( this, other ), + _ => false, + }; + } + + public override bool Equals( object? obj ) + => obj is ActorIdentifier other && Equals( other ); + + public bool IsValid + => Type != IdentifierType.Invalid; + + public override string ToString() + => Manager?.ToString( this ) + ?? Type switch + { + IdentifierType.Player => $"{PlayerName} ({HomeWorld})", + IdentifierType.Owned => $"{PlayerName}s {Kind} {DataId} ({HomeWorld})", + IdentifierType.Special => ActorManager.ToName( Special ), + IdentifierType.Npc => + Index == ushort.MaxValue + ? $"{Kind} #{DataId}" + : $"{Kind} #{DataId} at {Index}", + _ => "Invalid", + }; + + public override int GetHashCode() + => Type switch + { + IdentifierType.Player => HashCode.Combine( IdentifierType.Player, PlayerName, HomeWorld ), + IdentifierType.Owned => HashCode.Combine( IdentifierType.Owned, Kind, PlayerName, HomeWorld, DataId ), + IdentifierType.Special => HashCode.Combine( IdentifierType.Special, Special ), + IdentifierType.Npc => HashCode.Combine( IdentifierType.Npc, Kind, Index, DataId ), + _ => 0, + }; + + internal ActorIdentifier( IdentifierType type, ObjectKind kind, ushort index, uint data, ByteString playerName ) + { + Type = type; + Kind = kind; + Special = ( SpecialActor )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.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() ); + ret.Add( nameof( Index ), Index ); + ret.Add( nameof( DataId ), DataId ); + 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; + } + + return lhs.Kind switch + { + ObjectKind.MountType => manager.Mounts.TryGetValue( lhs.DataId, out var lhsName ) + && manager.Mounts.TryGetValue( rhs.DataId, out var rhsName ) + && lhsName.Equals( rhsName, StringComparison.OrdinalIgnoreCase ), + ObjectKind.Companion => manager.Companions.TryGetValue( lhs.DataId, out var lhsName ) + && manager.Companions.TryGetValue( rhs.DataId, out var rhsName ) + && lhsName.Equals( rhsName, StringComparison.OrdinalIgnoreCase ), + ObjectKind.BattleNpc => manager.BNpcs.TryGetValue( lhs.DataId, out var lhsName ) + && manager.BNpcs.TryGetValue( rhs.DataId, out var rhsName ) + && lhsName.Equals( rhsName, StringComparison.OrdinalIgnoreCase ), + ObjectKind.EventNpc => manager.ENpcs.TryGetValue( lhs.DataId, out var lhsName ) + && manager.ENpcs.TryGetValue( rhs.DataId, out var rhsName ) + && lhsName.Equals( rhsName, StringComparison.OrdinalIgnoreCase ), + _ => false, + }; + } +} \ No newline at end of file diff --git a/Penumbra.GameData/Actors/ActorManager.cs b/Penumbra.GameData/Actors/ActorManager.cs new file mode 100644 index 00000000..f3a1936e --- /dev/null +++ b/Penumbra.GameData/Actors/ActorManager.cs @@ -0,0 +1,352 @@ +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 ); + } + + + /// Checks SE naming rules. + 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 ] ); + } + + /// Checks if the world is a valid public world or ushort.MaxValue (any world). + private bool VerifyWorld( ushort worldId ) + => Worlds.ContainsKey( worldId ); + + /// Verify that the enum value is a specific actor and return the name if it is. + private static bool VerifySpecial( SpecialActor actor ) + => actor is >= SpecialActor.CharacterScreen and <= SpecialActor.Portrait; + + /// Verify that the object index is a valid index for an NPC. + private static bool VerifyIndex( ushort index ) + { + return index switch + { + < 200 => index % 2 == 0, + > ( ushort )SpecialActor.Portrait => index < 426, + _ => false, + }; + } + + /// Verify that the object kind is a valid owned object, and the corresponding data Id. + 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, + }; +} \ No newline at end of file diff --git a/Penumbra.GameData/Actors/IdentifierType.cs b/Penumbra.GameData/Actors/IdentifierType.cs new file mode 100644 index 00000000..a582aa14 --- /dev/null +++ b/Penumbra.GameData/Actors/IdentifierType.cs @@ -0,0 +1,10 @@ +namespace Penumbra.GameData.Actors; + +public enum IdentifierType : byte +{ + Invalid, + Player, + Owned, + Special, + Npc, +}; \ No newline at end of file diff --git a/Penumbra.GameData/Actors/SpecialActor.cs b/Penumbra.GameData/Actors/SpecialActor.cs new file mode 100644 index 00000000..5319c747 --- /dev/null +++ b/Penumbra.GameData/Actors/SpecialActor.cs @@ -0,0 +1,12 @@ +namespace Penumbra.GameData.Actors; + +public enum SpecialActor : ushort +{ + CutsceneStart = 200, + CutsceneEnd = 240, + CharacterScreen = 240, + ExamineScreen = 241, + FittingRoom = 242, + DyePreview = 243, + Portrait = 244, +} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/ByteStringFunctions.Case.cs b/Penumbra.GameData/ByteString/ByteStringFunctions.Case.cs deleted file mode 100644 index aeda4bfd..00000000 --- a/Penumbra.GameData/ByteString/ByteStringFunctions.Case.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System.Linq; -using Penumbra.GameData.Util; - -namespace Penumbra.GameData.ByteString; - -public static unsafe partial class ByteStringFunctions -{ - private static readonly byte[] AsciiLowerCaseBytes = Enumerable.Range( 0, 256 ) - .Select( i => ( byte )char.ToLowerInvariant( ( char )i ) ) - .ToArray(); - - private static readonly byte[] AsciiUpperCaseBytes = Enumerable.Range( 0, 256 ) - .Select( i => ( byte )char.ToUpperInvariant( ( char )i ) ) - .ToArray(); - - // Convert a byte to its ASCII-lowercase version. - public static byte AsciiToLower( byte b ) - => AsciiLowerCaseBytes[ b ]; - - // Check if a byte is ASCII-lowercase. - public static bool AsciiIsLower( byte b ) - => AsciiToLower( b ) == b; - - // Convert a byte to its ASCII-uppercase version. - public static byte AsciiToUpper( byte b ) - => AsciiUpperCaseBytes[ b ]; - - // Check if a byte is ASCII-uppercase. - public static bool AsciiIsUpper( byte b ) - => AsciiToUpper( b ) == b; - - // Check if a byte array of given length is ASCII-lowercase. - public static bool IsAsciiLowerCase( byte* path, int length ) - { - var end = path + length; - for( ; path < end; ++path ) - { - if( *path != AsciiLowerCaseBytes[*path] ) - { - return false; - } - } - - return true; - } - - // Compare two byte arrays of given lengths ASCII-case-insensitive. - public static int AsciiCaselessCompare( byte* lhs, int lhsLength, byte* rhs, int rhsLength ) - { - if( lhsLength == rhsLength ) - { - return lhs == rhs ? 0 : Functions.MemCmpCaseInsensitiveUnchecked( lhs, rhs, rhsLength ); - } - - if( lhsLength < rhsLength ) - { - var cmp = Functions.MemCmpCaseInsensitiveUnchecked( lhs, rhs, lhsLength ); - return cmp != 0 ? cmp : -1; - } - - var cmp2 = Functions.MemCmpCaseInsensitiveUnchecked( lhs, rhs, rhsLength ); - return cmp2 != 0 ? cmp2 : 1; - } - - // Check two byte arrays of given lengths for ASCII-case-insensitive equality. - public static bool AsciiCaselessEquals( byte* lhs, int lhsLength, byte* rhs, int rhsLength ) - { - if( lhsLength != rhsLength ) - { - return false; - } - - if( lhs == rhs || lhsLength == 0 ) - { - return true; - } - - return Functions.MemCmpCaseInsensitiveUnchecked( lhs, rhs, lhsLength ) == 0; - } - - // Check if a byte array of given length consists purely of ASCII characters. - public static bool IsAscii( byte* path, int length ) - { - var length8 = length / 8; - var end8 = ( ulong* )path + length8; - for( var ptr8 = ( ulong* )path; ptr8 < end8; ++ptr8 ) - { - if( ( *ptr8 & 0x8080808080808080ul ) != 0 ) - { - return false; - } - } - - var end = path + length; - for( path += length8 * 8; path < end; ++path ) - { - if( *path > 127 ) - { - return false; - } - } - - return true; - } -} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/ByteStringFunctions.Comparison.cs b/Penumbra.GameData/ByteString/ByteStringFunctions.Comparison.cs deleted file mode 100644 index 3e7382c9..00000000 --- a/Penumbra.GameData/ByteString/ByteStringFunctions.Comparison.cs +++ /dev/null @@ -1,95 +0,0 @@ -using Penumbra.GameData.Util; - -namespace Penumbra.GameData.ByteString; - -public static unsafe partial class ByteStringFunctions -{ - // Lexicographically compare two byte arrays of given length. - public static int Compare( byte* lhs, int lhsLength, byte* rhs, int rhsLength ) - { - if( lhsLength == rhsLength ) - { - return lhs == rhs ? 0 : Functions.MemCmpUnchecked( lhs, rhs, rhsLength ); - } - - if( lhsLength < rhsLength ) - { - var cmp = Functions.MemCmpUnchecked( lhs, rhs, lhsLength ); - return cmp != 0 ? cmp : -1; - } - - var cmp2 = Functions.MemCmpUnchecked( lhs, rhs, rhsLength ); - return cmp2 != 0 ? cmp2 : 1; - } - - // Lexicographically compare one byte array of given length with a null-terminated byte array of unknown length. - public static int Compare( byte* lhs, int lhsLength, byte* rhs ) - { - var end = lhs + lhsLength; - for( var tmp = lhs; tmp < end; ++tmp, ++rhs ) - { - if( *rhs == 0 ) - { - return 1; - } - - var diff = *tmp - *rhs; - if( diff != 0 ) - { - return diff; - } - } - - return 0; - } - - // Lexicographically compare two null-terminated byte arrays of unknown length not larger than maxLength. - public static int Compare( byte* lhs, byte* rhs, int maxLength = int.MaxValue ) - { - var end = lhs + maxLength; - for( var tmp = lhs; tmp < end; ++tmp, ++rhs ) - { - if( *lhs == 0 ) - { - return *rhs == 0 ? 0 : -1; - } - - if( *rhs == 0 ) - { - return 1; - } - - var diff = *tmp - *rhs; - if( diff != 0 ) - { - return diff; - } - } - - return 0; - } - - // Check two byte arrays of given length for equality. - public static bool Equals( byte* lhs, int lhsLength, byte* rhs, int rhsLength ) - { - if( lhsLength != rhsLength ) - { - return false; - } - - if( lhs == rhs || lhsLength == 0 ) - { - return true; - } - - return Functions.MemCmpUnchecked( lhs, rhs, lhsLength ) == 0; - } - - // Check one byte array of given length for equality against a null-terminated byte array of unknown length. - private static bool Equal( byte* lhs, int lhsLength, byte* rhs ) - => Compare( lhs, lhsLength, rhs ) == 0; - - // Check two null-terminated byte arrays of unknown length not larger than maxLength for equality. - private static bool Equal( byte* lhs, byte* rhs, int maxLength = int.MaxValue ) - => Compare( lhs, rhs, maxLength ) == 0; -} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/ByteStringFunctions.Construction.cs b/Penumbra.GameData/ByteString/ByteStringFunctions.Construction.cs deleted file mode 100644 index 18cc3a81..00000000 --- a/Penumbra.GameData/ByteString/ByteStringFunctions.Construction.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using System.Text; -using Penumbra.GameData.Util; - -namespace Penumbra.GameData.ByteString; - -public static unsafe partial class ByteStringFunctions -{ - // Used for static null-terminators. - public class NullTerminator - { - public readonly byte* NullBytePtr; - - public NullTerminator() - { - NullBytePtr = ( byte* )Marshal.AllocHGlobal( 1 ); - *NullBytePtr = 0; - } - - ~NullTerminator() - => Marshal.FreeHGlobal( ( IntPtr )NullBytePtr ); - } - - // Convert a C# unicode-string to an unmanaged UTF8-byte array and return the pointer. - // If the length would exceed the given maxLength, return a nullpointer instead. - public static byte* Utf8FromString( string s, out int length, int maxLength = int.MaxValue ) - { - length = Encoding.UTF8.GetByteCount( s ); - if( length >= maxLength ) - { - return null; - } - - var path = ( byte* )Marshal.AllocHGlobal( length + 1 ); - fixed( char* ptr = s ) - { - Encoding.UTF8.GetBytes( ptr, s.Length, path, length + 1 ); - } - - path[ length ] = 0; - return path; - } - - // Create a copy of a given string and return the pointer. - public static byte* CopyString( byte* path, int length ) - { - var ret = ( byte* )Marshal.AllocHGlobal( length + 1 ); - Functions.MemCpyUnchecked( ret, path, length ); - ret[ length ] = 0; - return ret; - } - - // Check the length of a null-terminated byte array no longer than the given maxLength. - public static int CheckLength( byte* path, int maxLength = int.MaxValue ) - { - var end = path + maxLength; - for( var it = path; it < end; ++it ) - { - if( *it == 0 ) - { - return ( int )( it - path ); - } - } - - throw new ArgumentOutOfRangeException( "Null-terminated path too long" ); - } -} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/ByteStringFunctions.Manipulation.cs b/Penumbra.GameData/ByteString/ByteStringFunctions.Manipulation.cs deleted file mode 100644 index 7d21593a..00000000 --- a/Penumbra.GameData/ByteString/ByteStringFunctions.Manipulation.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Penumbra.GameData.ByteString; - -public static unsafe partial class ByteStringFunctions -{ - // Replace all occurrences of from in a byte array of known length with to. - public static int Replace( byte* ptr, int length, byte from, byte to ) - { - var end = ptr + length; - var numReplaced = 0; - for( ; ptr < end; ++ptr ) - { - if( *ptr == from ) - { - *ptr = to; - ++numReplaced; - } - } - - return numReplaced; - } - - // Convert a byte array of given length to ASCII-lowercase. - public static void AsciiToLowerInPlace( byte* path, int length ) - { - for( var i = 0; i < length; ++i ) - { - path[ i ] = AsciiLowerCaseBytes[ path[ i ] ]; - } - } - - // Copy a byte array and convert the copy to ASCII-lowercase. - public static byte* AsciiToLower( byte* path, int length ) - { - var ptr = ( byte* )Marshal.AllocHGlobal( length + 1 ); - ptr[ length ] = 0; - for( var i = 0; i < length; ++i ) - { - ptr[ i ] = AsciiLowerCaseBytes[ path[ i ] ]; - } - - return ptr; - } -} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/FullPath.cs b/Penumbra.GameData/ByteString/FullPath.cs deleted file mode 100644 index 6d3cc0bd..00000000 --- a/Penumbra.GameData/ByteString/FullPath.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System; -using System.IO; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Penumbra.GameData.Util; - -namespace Penumbra.GameData.ByteString; - -[JsonConverter( typeof( FullPathConverter ) )] -public readonly struct FullPath : IComparable, IEquatable< FullPath > -{ - public readonly string FullName; - public readonly Utf8String InternalName; - public readonly ulong Crc64; - - public static readonly FullPath Empty = new(string.Empty); - - public FullPath( DirectoryInfo baseDir, Utf8RelPath relPath ) - : this( Path.Combine( baseDir.FullName, relPath.ToString() ) ) - { } - - public FullPath( FileInfo file ) - : this( file.FullName ) - { } - - - public FullPath( string s ) - { - FullName = s; - InternalName = Utf8String.FromString( FullName.Replace( '\\', '/' ), out var name, true ) ? name : Utf8String.Empty; - Crc64 = Functions.ComputeCrc64( InternalName.Span ); - } - - public FullPath( Utf8GamePath path ) - { - FullName = path.ToString().Replace( '/', '\\' ); - InternalName = path.Path; - Crc64 = Functions.ComputeCrc64( InternalName.Span ); - } - - public bool Exists - => File.Exists( FullName ); - - public string Extension - => Path.GetExtension( FullName ); - - public string Name - => Path.GetFileName( FullName ); - - public bool ToGamePath( DirectoryInfo dir, out Utf8GamePath path ) - { - path = Utf8GamePath.Empty; - if( !InternalName.IsAscii || !FullName.StartsWith( dir.FullName ) ) - { - return false; - } - - var substring = InternalName.Substring( dir.FullName.Length + 1 ); - - path = new Utf8GamePath( substring ); - return true; - } - - public bool ToRelPath( DirectoryInfo dir, out Utf8RelPath path ) - { - path = Utf8RelPath.Empty; - if( !FullName.StartsWith( dir.FullName ) ) - { - return false; - } - - var substring = InternalName.Substring( dir.FullName.Length + 1 ); - - path = new Utf8RelPath( substring.Replace( ( byte )'/', ( byte )'\\' ) ); - return true; - } - - public int CompareTo( object? obj ) - => obj switch - { - FullPath p => InternalName?.CompareTo( p.InternalName ) ?? -1, - FileInfo f => string.Compare( FullName, f.FullName, StringComparison.OrdinalIgnoreCase ), - Utf8String u => InternalName?.CompareTo( u ) ?? -1, - string s => string.Compare( FullName, s, StringComparison.OrdinalIgnoreCase ), - _ => -1, - }; - - public bool Equals( FullPath other ) - { - if( Crc64 != other.Crc64 ) - { - return false; - } - - if( FullName.Length == 0 || other.FullName.Length == 0 ) - { - return true; - } - - return InternalName.Equals( other.InternalName ); - } - - public bool IsRooted - => new Utf8GamePath( InternalName ).IsRooted(); - - public override int GetHashCode() - => InternalName.Crc32; - - public override string ToString() - => FullName; - - public class FullPathConverter : JsonConverter - { - public override bool CanConvert( Type objectType ) - => objectType == typeof( FullPath ); - - public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) - { - var token = JToken.Load( reader ).ToString(); - return new FullPath( token ); - } - - public override bool CanWrite - => true; - - public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) - { - if( value is FullPath p ) - { - serializer.Serialize( writer, p.ToString() ); - } - } - } -} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/Utf8GamePath.cs b/Penumbra.GameData/ByteString/Utf8GamePath.cs deleted file mode 100644 index 79386002..00000000 --- a/Penumbra.GameData/ByteString/Utf8GamePath.cs +++ /dev/null @@ -1,168 +0,0 @@ -using System; -using System.IO; -using Dalamud.Utility; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Penumbra.GameData.Util; - -namespace Penumbra.GameData.ByteString; - -// NewGamePath wrap some additional validity checking around Utf8String, -// provide some filesystem helpers, and conversion to Json. -[JsonConverter( typeof( Utf8GamePathConverter ) )] -public readonly struct Utf8GamePath : IEquatable< Utf8GamePath >, IComparable< Utf8GamePath >, IDisposable -{ - public const int MaxGamePathLength = 256; - - public readonly Utf8String Path; - public static readonly Utf8GamePath Empty = new(Utf8String.Empty); - - internal Utf8GamePath( Utf8String s ) - => Path = s; - - public int Length - => Path.Length; - - public bool IsEmpty - => Path.IsEmpty; - - public Utf8GamePath ToLower() - => new(Path.AsciiToLower()); - - public static unsafe bool FromPointer( byte* ptr, out Utf8GamePath path, bool lower = false ) - { - var utf = new Utf8String( ptr ); - return ReturnChecked( utf, out path, lower ); - } - - public static bool FromSpan( ReadOnlySpan< byte > data, out Utf8GamePath path, bool lower = false ) - { - var utf = Utf8String.FromSpanUnsafe( data, false, null, null ); - return ReturnChecked( utf, out path, lower ); - } - - // Does not check for Forward/Backslashes due to assuming that SE-strings use the correct one. - // Does not check for initial slashes either, since they are assumed to be by choice. - // Checks for maxlength, ASCII and lowercase. - private static bool ReturnChecked( Utf8String utf, out Utf8GamePath path, bool lower = false ) - { - path = Empty; - if( !utf.IsAscii || utf.Length > MaxGamePathLength ) - { - return false; - } - - path = new Utf8GamePath( lower ? utf.AsciiToLower() : utf ); - return true; - } - - public Utf8GamePath Clone() - => new(Path.Clone()); - - public static explicit operator Utf8GamePath( string s ) - => FromString( s, out var p, true ) ? p : Empty; - - public static bool FromString( string? s, out Utf8GamePath path, bool toLower = false ) - { - path = Empty; - if( s.IsNullOrEmpty() ) - { - return true; - } - - var substring = s!.Replace( '\\', '/' ).TrimStart( '/' ); - if( substring.Length > MaxGamePathLength ) - { - return false; - } - - if( substring.Length == 0 ) - { - return true; - } - - if( !Utf8String.FromString( substring, out var ascii, toLower ) || !ascii.IsAscii ) - { - return false; - } - - path = new Utf8GamePath( ascii ); - return true; - } - - public static bool FromFile( FileInfo file, DirectoryInfo baseDir, out Utf8GamePath path, bool toLower = false ) - { - path = Empty; - if( !file.FullName.StartsWith( baseDir.FullName ) ) - { - return false; - } - - var substring = file.FullName[ ( baseDir.FullName.Length + 1 ).. ]; - return FromString( substring, out path, toLower ); - } - - public Utf8String Filename() - { - var idx = Path.LastIndexOf( ( byte )'/' ); - return idx == -1 ? Path : Path.Substring( idx + 1 ); - } - - public Utf8String Extension() - { - var idx = Path.LastIndexOf( ( byte )'.' ); - return idx == -1 ? Utf8String.Empty : Path.Substring( idx ); - } - - public bool Equals( Utf8GamePath other ) - => Path.Equals( other.Path ); - - public override int GetHashCode() - => Path.GetHashCode(); - - public int CompareTo( Utf8GamePath other ) - => Path.CompareTo( other.Path ); - - public override string ToString() - => Path.ToString(); - - public void Dispose() - => Path.Dispose(); - - public bool IsRooted() - => IsRooted( Path ); - - public static bool IsRooted( Utf8String path ) - => path.Length >= 1 && ( path[ 0 ] == '/' || path[ 0 ] == '\\' ) - || path.Length >= 2 - && ( path[ 0 ] >= 'A' && path[ 0 ] <= 'Z' || path[ 0 ] >= 'a' && path[ 0 ] <= 'z' ) - && path[ 1 ] == ':'; - - public class Utf8GamePathConverter : JsonConverter - { - public override bool CanConvert( Type objectType ) - => objectType == typeof( Utf8GamePath ); - - public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) - { - var token = JToken.Load( reader ).ToString(); - return FromString( token, out var p, true ) - ? p - : throw new JsonException( $"Could not convert \"{token}\" to {nameof( Utf8GamePath )}." ); - } - - public override bool CanWrite - => true; - - public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) - { - if( value is Utf8GamePath p ) - { - serializer.Serialize( writer, p.ToString() ); - } - } - } - - public GamePath ToGamePath() - => GamePath.GenerateUnchecked( ToString() ); -} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/Utf8RelPath.cs b/Penumbra.GameData/ByteString/Utf8RelPath.cs deleted file mode 100644 index cef27b6f..00000000 --- a/Penumbra.GameData/ByteString/Utf8RelPath.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System; -using System.IO; -using Dalamud.Utility; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace Penumbra.GameData.ByteString; - -[JsonConverter( typeof( Utf8RelPathConverter ) )] -public readonly struct Utf8RelPath : IEquatable< Utf8RelPath >, IComparable< Utf8RelPath >, IDisposable -{ - public const int MaxRelPathLength = 250; - - public readonly Utf8String Path; - public static readonly Utf8RelPath Empty = new(Utf8String.Empty); - - internal Utf8RelPath( Utf8String path ) - => Path = path; - - - public static explicit operator Utf8RelPath( string s ) - { - if( !FromString( s, out var p ) ) - { - return Empty; - } - - return new Utf8RelPath( p.Path.AsciiToLower() ); - } - - public static bool FromString( string? s, out Utf8RelPath path ) - { - path = Empty; - if( s.IsNullOrEmpty() ) - { - return true; - } - - var substring = s.Replace( '/', '\\' ).TrimStart('\\'); - if( substring.Length > MaxRelPathLength ) - { - return false; - } - - if( substring.Length == 0 ) - { - return true; - } - - if( !Utf8String.FromString( substring, out var ascii, true ) || !ascii.IsAscii ) - { - return false; - } - - path = new Utf8RelPath( ascii ); - return true; - } - - public static bool FromFile( FileInfo file, DirectoryInfo baseDir, out Utf8RelPath path ) - { - path = Empty; - if( !file.FullName.StartsWith( baseDir.FullName ) ) - { - return false; - } - - var substring = file.FullName[ (baseDir.FullName.Length + 1).. ]; - return FromString( substring, out path ); - } - - public static bool FromFile( FullPath file, DirectoryInfo baseDir, out Utf8RelPath path ) - { - path = Empty; - if( !file.FullName.StartsWith( baseDir.FullName ) ) - { - return false; - } - - var substring = file.FullName[ (baseDir.FullName.Length + 1).. ]; - return FromString( substring, out path ); - } - - public Utf8RelPath( Utf8GamePath gamePath ) - => Path = gamePath.Path.Replace( ( byte )'/', ( byte )'\\' ); - - public unsafe Utf8GamePath ToGamePath( int skipFolders = 0 ) - { - var idx = 0; - while( skipFolders > 0 ) - { - idx = Path.IndexOf( ( byte )'\\', idx ) + 1; - --skipFolders; - if( idx <= 0 ) - { - return Utf8GamePath.Empty; - } - } - - var length = Path.Length - idx; - var ptr = ByteStringFunctions.CopyString( Path.Path + idx, length ); - ByteStringFunctions.Replace( ptr, length, ( byte )'\\', ( byte )'/' ); - ByteStringFunctions.AsciiToLowerInPlace( ptr, length ); - var utf = new Utf8String().Setup( ptr, length, null, true, true, true, true ); - return new Utf8GamePath( utf ); - } - - public int CompareTo( Utf8RelPath rhs ) - => Path.CompareTo( rhs.Path ); - - public bool Equals( Utf8RelPath other ) - => Path.Equals( other.Path ); - - public override string ToString() - => Path.ToString(); - - public void Dispose() - => Path.Dispose(); - - public class Utf8RelPathConverter : JsonConverter - { - public override bool CanConvert( Type objectType ) - => objectType == typeof( Utf8RelPath ); - - public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) - { - var token = JToken.Load( reader ).ToString(); - return FromString( token, out var p ) - ? p - : throw new JsonException( $"Could not convert \"{token}\" to {nameof( Utf8RelPath )}." ); - } - - public override bool CanWrite - => true; - - public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) - { - if( value is Utf8RelPath p ) - { - serializer.Serialize( writer, p.ToString() ); - } - } - } -} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/Utf8String.Access.cs b/Penumbra.GameData/ByteString/Utf8String.Access.cs deleted file mode 100644 index a74dd672..00000000 --- a/Penumbra.GameData/ByteString/Utf8String.Access.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; - -namespace Penumbra.GameData.ByteString; - -// Utf8String is a wrapper around unsafe byte strings. -// It may be used to store owned strings in unmanaged space, -// as well as refer to unowned strings. -// Unowned strings may change their value and thus become corrupt, -// so they should never be stored, just used locally or with great care. -// The string keeps track of whether it is owned or not, it also can keep track -// of some other information, like the string being pure ASCII, ASCII-lowercase or null-terminated. -// Owned strings are always null-terminated. -// Any constructed string will compute its own CRC32-value (as long as the string itself is not changed). -public sealed unsafe partial class Utf8String : IEnumerable< byte > -{ - // We keep information on some of the state of the Utf8String in specific bits. - // This costs some potential max size, but that is not relevant for our case. - // Except for destruction/dispose, or if the non-owned pointer changes values, - // the CheckedFlag, AsciiLowerCaseFlag and AsciiFlag are the only things that are mutable. - private const uint NullTerminatedFlag = 0x80000000; - private const uint OwnedFlag = 0x40000000; - private const uint AsciiCheckedFlag = 0x04000000; - private const uint AsciiFlag = 0x08000000; - private const uint AsciiLowerCheckedFlag = 0x10000000; - private const uint AsciiLowerFlag = 0x20000000; - private const uint FlagMask = 0x03FFFFFF; - - public bool IsNullTerminated - => ( _length & NullTerminatedFlag ) != 0; - - public bool IsOwned - => ( _length & OwnedFlag ) != 0; - - public bool IsAscii - => CheckAscii(); - - public bool IsAsciiLowerCase - => CheckAsciiLower(); - - public byte* Path - => _path; - - public int Crc32 - => _crc32; - - public int Length - => ( int )( _length & FlagMask ); - - public bool IsEmpty - => Length == 0; - - public ReadOnlySpan< byte > Span - => new(_path, Length); - - public byte this[ int idx ] - => ( uint )idx < Length ? _path[ idx ] : throw new IndexOutOfRangeException(); - - public IEnumerator< byte > GetEnumerator() - { - for( var i = 0; i < Length; ++i ) - { - yield return Span[ i ]; - } - } - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - // Only not readonly due to dispose. - // ReSharper disable once NonReadonlyMemberInGetHashCode - public override int GetHashCode() - => _crc32; -} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/Utf8String.Comparison.cs b/Penumbra.GameData/ByteString/Utf8String.Comparison.cs deleted file mode 100644 index 6d96dce5..00000000 --- a/Penumbra.GameData/ByteString/Utf8String.Comparison.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System; -using System.Linq; - -namespace Penumbra.GameData.ByteString; - -public sealed unsafe partial class Utf8String : IEquatable< Utf8String >, IComparable< Utf8String > -{ - public bool Equals( Utf8String? other ) - { - if( ReferenceEquals( null, other ) ) - { - return false; - } - - if( ReferenceEquals( this, other ) ) - { - return true; - } - - return _crc32 == other._crc32 && ByteStringFunctions.Equals( _path, Length, other._path, other.Length ); - } - - public bool EqualsCi( Utf8String? other ) - { - if( ReferenceEquals( null, other ) ) - { - return false; - } - - if( ReferenceEquals( this, other ) ) - { - return true; - } - - if( ( IsAsciiLowerInternal ?? false ) && ( other.IsAsciiLowerInternal ?? false ) ) - { - return _crc32 == other._crc32 && ByteStringFunctions.Equals( _path, Length, other._path, other.Length ); - } - - return ByteStringFunctions.AsciiCaselessEquals( _path, Length, other._path, other.Length ); - } - - public int CompareTo( Utf8String? other ) - { - if( ReferenceEquals( this, other ) ) - { - return 0; - } - - if( ReferenceEquals( null, other ) ) - { - return 1; - } - - return ByteStringFunctions.Compare( _path, Length, other._path, other.Length ); - } - - public int CompareToCi( Utf8String? other ) - { - if( ReferenceEquals( null, other ) ) - { - return 0; - } - - if( ReferenceEquals( this, other ) ) - { - return 1; - } - - if( ( IsAsciiLowerInternal ?? false ) && ( other.IsAsciiLowerInternal ?? false ) ) - { - return ByteStringFunctions.Compare( _path, Length, other._path, other.Length ); - } - - return ByteStringFunctions.AsciiCaselessCompare( _path, Length, other._path, other.Length ); - } - - public bool StartsWith( Utf8String other ) - { - var otherLength = other.Length; - return otherLength <= Length && ByteStringFunctions.Equals( other.Path, otherLength, Path, otherLength ); - } - - public bool EndsWith( Utf8String other ) - { - var otherLength = other.Length; - var offset = Length - otherLength; - return offset >= 0 && ByteStringFunctions.Equals( other.Path, otherLength, Path + offset, otherLength ); - } - - public bool StartsWith( params char[] chars ) - { - if( chars.Length > Length ) - { - return false; - } - - var ptr = _path; - return chars.All( t => *ptr++ == ( byte )t ); - } - - public bool EndsWith( params char[] chars ) - { - if( chars.Length > Length ) - { - return false; - } - - var ptr = _path + Length - chars.Length; - return chars.All( c => *ptr++ == ( byte )c ); - } - - public int IndexOf( byte b, int from = 0 ) - { - var end = _path + Length; - for( var tmp = _path + from; tmp < end; ++tmp ) - { - if( *tmp == b ) - { - return ( int )( tmp - _path ); - } - } - - return -1; - } - - public int LastIndexOf( byte b, int to = 0 ) - { - var end = _path + to; - for( var tmp = _path + Length - 1; tmp >= end; --tmp ) - { - if( *tmp == b ) - { - return ( int )( tmp - _path ); - } - } - - return -1; - } -} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/Utf8String.Construction.cs b/Penumbra.GameData/ByteString/Utf8String.Construction.cs deleted file mode 100644 index 1e6ab325..00000000 --- a/Penumbra.GameData/ByteString/Utf8String.Construction.cs +++ /dev/null @@ -1,215 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using Penumbra.GameData.Util; - -namespace Penumbra.GameData.ByteString; - -public sealed unsafe partial class Utf8String : IDisposable -{ - // statically allocated null-terminator for empty strings to point to. - private static readonly ByteStringFunctions.NullTerminator Null = new(); - - public static readonly Utf8String Empty = new(); - - // actual data members. - private byte* _path; - private uint _length; - private int _crc32; - - // Create an empty string. - public Utf8String() - { - _path = Null.NullBytePtr; - _length |= AsciiCheckedFlag | AsciiFlag | AsciiLowerCheckedFlag | AsciiLowerFlag | NullTerminatedFlag | AsciiFlag; - _crc32 = 0; - } - - // Create a temporary Utf8String from a byte pointer. - // This computes CRC, checks for ASCII and AsciiLower and assumes Null-Termination. - public Utf8String( byte* path ) - { - var length = Functions.ComputeCrc32AsciiLowerAndSize( path, out var crc32, out var lower, out var ascii ); - Setup( path, length, crc32, true, false, lower, ascii ); - } - - // Construct a temporary Utf8String from a given byte string of known size. - // Other known attributes can also be provided and are not computed. - // Can throw ArgumentOutOfRange if length is higher than max length. - // The Crc32 will be computed. - public static Utf8String FromByteStringUnsafe( byte* path, int length, bool isNullTerminated, bool? isLower = null, bool? isAscii = false ) - => new Utf8String().Setup( path, length, null, isNullTerminated, false, isLower, isAscii ); - - // Same as above, just with a span. - public static Utf8String FromSpanUnsafe( ReadOnlySpan< byte > path, bool isNullTerminated, bool? isLower = null, bool? isAscii = false ) - { - fixed( byte* ptr = path ) - { - return FromByteStringUnsafe( ptr, path.Length, isNullTerminated, isLower, isAscii ); - } - } - - // Construct a Utf8String from a given unicode string, possibly converted to ascii lowercase. - // Only returns false if the length exceeds the max length. - public static bool FromString( string? path, out Utf8String ret, bool toAsciiLower = false ) - { - if( string.IsNullOrEmpty( path ) ) - { - ret = Empty; - return true; - } - - var p = ByteStringFunctions.Utf8FromString( path, out var l, ( int )FlagMask ); - if( p == null ) - { - ret = Empty; - return false; - } - - if( toAsciiLower ) - { - ByteStringFunctions.AsciiToLowerInPlace( p, l ); - } - - ret = new Utf8String().Setup( p, l, null, true, true, toAsciiLower ? true : null, l == path.Length ); - return true; - } - - // Does not check for length and just assumes the isLower state from the second argument. - public static Utf8String FromStringUnsafe( string? path, bool? isLower ) - { - if( string.IsNullOrEmpty( path ) ) - { - return Empty; - } - - var p = ByteStringFunctions.Utf8FromString( path, out var l ); - var ret = new Utf8String().Setup( p, l, null, true, true, isLower, l == path.Length ); - return ret; - } - - // Free memory if the string is owned. - private void ReleaseUnmanagedResources() - { - if( !IsOwned ) - { - return; - } - - Marshal.FreeHGlobal( ( IntPtr )_path ); - GC.RemoveMemoryPressure( Length + 1 ); - _length = AsciiCheckedFlag | AsciiFlag | AsciiLowerCheckedFlag | AsciiLowerFlag | NullTerminatedFlag; - _path = Null.NullBytePtr; - _crc32 = 0; - } - - // Manually free memory. Sets the string to an empty string. - public void Dispose() - { - ReleaseUnmanagedResources(); - GC.SuppressFinalize( this ); - } - - ~Utf8String() - { - ReleaseUnmanagedResources(); - } - - // Setup from all given values. - // Only called from constructors or factory functions in this library. - [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] - internal Utf8String Setup( byte* path, int length, int? crc32, bool isNullTerminated, bool isOwned, - bool? isLower = null, bool? isAscii = null ) - { - if( length > FlagMask ) - { - throw new ArgumentOutOfRangeException( nameof( length ) ); - } - - _path = path; - _length = ( uint )length; - _crc32 = crc32 ?? ( int )~Lumina.Misc.Crc32.Get( new ReadOnlySpan< byte >( path, length ) ); - if( isNullTerminated ) - { - _length |= NullTerminatedFlag; - } - - if( isOwned ) - { - GC.AddMemoryPressure( length + 1 ); - _length |= OwnedFlag; - } - - if( isLower != null ) - { - _length |= AsciiLowerCheckedFlag; - if( isLower.Value ) - { - _length |= AsciiLowerFlag; - } - } - - if( isAscii != null ) - { - _length |= AsciiCheckedFlag; - if( isAscii.Value ) - { - _length |= AsciiFlag; - } - } - - return this; - } - - private bool CheckAscii() - { - switch( _length & ( AsciiCheckedFlag | AsciiFlag ) ) - { - case AsciiCheckedFlag: return false; - case AsciiCheckedFlag | AsciiFlag: return true; - default: - _length |= AsciiCheckedFlag; - var isAscii = ByteStringFunctions.IsAscii( _path, Length ); - if( isAscii ) - { - _length |= AsciiFlag; - } - - return isAscii; - } - } - - private bool CheckAsciiLower() - { - switch( _length & ( AsciiLowerCheckedFlag | AsciiLowerFlag ) ) - { - case AsciiLowerCheckedFlag: return false; - case AsciiLowerCheckedFlag | AsciiLowerFlag: return true; - default: - _length |= AsciiLowerCheckedFlag; - var isAsciiLower = ByteStringFunctions.IsAsciiLowerCase( _path, Length ); - if( isAsciiLower ) - { - _length |= AsciiLowerFlag; - } - - return isAsciiLower; - } - } - - private bool? IsAsciiInternal - => ( _length & ( AsciiCheckedFlag | AsciiFlag ) ) switch - { - AsciiCheckedFlag => false, - AsciiFlag => true, - _ => null, - }; - - private bool? IsAsciiLowerInternal - => ( _length & ( AsciiLowerCheckedFlag | AsciiLowerFlag ) ) switch - { - AsciiLowerCheckedFlag => false, - AsciiLowerFlag => true, - _ => null, - }; -} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/Utf8String.Manipulation.cs b/Penumbra.GameData/ByteString/Utf8String.Manipulation.cs deleted file mode 100644 index c4332a1d..00000000 --- a/Penumbra.GameData/ByteString/Utf8String.Manipulation.cs +++ /dev/null @@ -1,168 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using Penumbra.GameData.Util; - -namespace Penumbra.GameData.ByteString; - -public sealed unsafe partial class Utf8String -{ - // Create a C# Unicode string from this string. - // If the string is known to be pure ASCII, use that encoding, otherwise UTF8. - public override string ToString() - => Length == 0 - ? string.Empty - : ( _length & AsciiFlag ) != 0 - ? Encoding.ASCII.GetString( _path, Length ) - : Encoding.UTF8.GetString( _path, Length ); - - - // Convert the ascii portion of the string to lowercase. - // Only creates a new string and copy if the string is not already known to be lowercase. - public Utf8String AsciiToLower() - => ( _length & AsciiLowerFlag ) == 0 - ? new Utf8String().Setup( ByteStringFunctions.AsciiToLower( _path, Length ), Length, null, true, true, true, IsAsciiInternal ) - : this; - - // Convert the ascii portion of the string to mixed case (i.e. capitalize every first letter in a word) - // Clones the string. - public Utf8String AsciiToMixed() - { - var length = Length; - if( length == 0 ) - { - return Empty; - } - - var ret = Clone(); - var previousWhitespace = true; - var end = ret.Path + length; - for( var ptr = ret.Path; ptr < end; ++ptr ) - { - if( previousWhitespace ) - { - *ptr = ByteStringFunctions.AsciiToUpper( *ptr ); - } - - previousWhitespace = char.IsWhiteSpace( ( char )*ptr ); - } - - return ret; - } - - // Convert the ascii portion of the string to lowercase. - // Guaranteed to create an owned copy. - public Utf8String AsciiToLowerClone() - => ( _length & AsciiLowerFlag ) == 0 - ? new Utf8String().Setup( ByteStringFunctions.AsciiToLower( _path, Length ), Length, null, true, true, true, IsAsciiInternal ) - : Clone(); - - // Create an owned copy of the given string. - public Utf8String Clone() - { - var ret = new Utf8String(); - ret._length = _length | OwnedFlag | NullTerminatedFlag; - ret._path = ByteStringFunctions.CopyString( Path, Length ); - ret._crc32 = Crc32; - return ret; - } - - // Create a non-owning substring from the given position. - // If from is negative or too large, the returned string will be the empty string. - public Utf8String Substring( int from ) - => ( uint )from < Length - ? FromByteStringUnsafe( _path + from, Length - from, IsNullTerminated, IsAsciiLowerInternal, IsAsciiInternal ) - : Empty; - - // Create a non-owning substring from the given position of the given length. - // If from is negative or too large, the returned string will be the empty string. - // If from + length is too large, it will be the same as if length was not specified. - public Utf8String Substring( int from, int length ) - { - var maxLength = Length - ( uint )from; - if( maxLength <= 0 ) - { - return Empty; - } - - return length < maxLength - ? FromByteStringUnsafe( _path + from, length, false, IsAsciiLowerInternal, IsAsciiInternal ) - : Substring( from ); - } - - // Create a owned copy of the string and replace all occurences of from with to in it. - public Utf8String Replace( byte from, byte to ) - { - var length = Length; - var newPtr = ByteStringFunctions.CopyString( _path, length ); - var numReplaced = ByteStringFunctions.Replace( newPtr, length, from, to ); - return new Utf8String().Setup( newPtr, length, numReplaced == 0 ? _crc32 : null, true, true, IsAsciiLowerInternal, IsAsciiInternal ); - } - - // Join a number of strings with a given byte between them. - public static Utf8String Join( byte splitter, params Utf8String[] strings ) - { - var length = strings.Sum( s => s.Length ) + strings.Length; - var data = ( byte* )Marshal.AllocHGlobal( length ); - - var ptr = data; - bool? isLower = ByteStringFunctions.AsciiIsLower( splitter ); - bool? isAscii = splitter < 128; - foreach( var s in strings ) - { - Functions.MemCpyUnchecked( ptr, s.Path, s.Length ); - ptr += s.Length; - *ptr++ = splitter; - isLower = Combine( isLower, s.IsAsciiLowerInternal ); - isAscii &= s.IsAscii; - } - - --length; - data[ length ] = 0; - var ret = FromByteStringUnsafe( data, length, true, isLower, isAscii ); - ret._length |= OwnedFlag; - return ret; - } - - // Split a string and return a list of the substrings delimited by b. - // You can specify the maximum number of splits (if the maximum is reached, the last substring may contain delimiters). - // You can also specify to ignore empty substrings inside delimiters. Those are also not counted for max splits. - public List< Utf8String > Split( byte b, int maxSplits = int.MaxValue, bool removeEmpty = true ) - { - var ret = new List< Utf8String >(); - var start = 0; - for( var idx = IndexOf( b, start ); idx >= 0; idx = IndexOf( b, start ) ) - { - if( start != idx || !removeEmpty ) - { - ret.Add( Substring( start, idx - start ) ); - } - - start = idx + 1; - if( ret.Count == maxSplits - 1 ) - { - break; - } - } - - ret.Add( Substring( start ) ); - return ret; - } - - private static bool? Combine( bool? val1, bool? val2 ) - { - return ( val1, val2 ) switch - { - (null, null) => null, - (null, true) => null, - (null, false) => false, - (true, null) => null, - (true, true) => true, - (true, false) => false, - (false, null) => false, - (false, true) => false, - (false, false) => false, - }; - } -} \ No newline at end of file diff --git a/Penumbra.GameData/Enums/ResourceType.cs b/Penumbra.GameData/Enums/ResourceType.cs index dedaf3c2..42783c97 100644 --- a/Penumbra.GameData/Enums/ResourceType.cs +++ b/Penumbra.GameData/Enums/ResourceType.cs @@ -1,6 +1,7 @@ using System; using System.IO; -using Penumbra.GameData.ByteString; +using Penumbra.String; +using Penumbra.String.Functions; namespace Penumbra.GameData.Enums; @@ -93,10 +94,10 @@ public static class ResourceTypeExtensions }; } - public static ResourceType FromString( Utf8String path ) + public static ResourceType FromString( ByteString path ) { var extIdx = path.LastIndexOf( ( byte )'.' ); - var ext = extIdx == -1 ? path : extIdx == path.Length - 1 ? Utf8String.Empty : path.Substring( extIdx + 1 ); + var ext = extIdx == -1 ? path : extIdx == path.Length - 1 ? ByteString.Empty : path.Substring( extIdx + 1 ); return ext.Length switch { diff --git a/Penumbra.GameData/Penumbra.GameData.csproj b/Penumbra.GameData/Penumbra.GameData.csproj index cb51d6dc..0aaf9c89 100644 --- a/Penumbra.GameData/Penumbra.GameData.csproj +++ b/Penumbra.GameData/Penumbra.GameData.csproj @@ -36,6 +36,7 @@ + @@ -51,6 +52,10 @@ $(DalamudLibPath)Lumina.Excel.dll False + + $(DalamudLibPath)FFXIVClientStructs.dll + False + $(DalamudLibPath)Newtonsoft.Json.dll False diff --git a/Penumbra.GameData/Structs/CharacterEquip.cs b/Penumbra.GameData/Structs/CharacterEquip.cs index a8744fff..58a3b317 100644 --- a/Penumbra.GameData/Structs/CharacterEquip.cs +++ b/Penumbra.GameData/Structs/CharacterEquip.cs @@ -1,6 +1,7 @@ using System; using Penumbra.GameData.Enums; using Penumbra.GameData.Util; +using Penumbra.String.Functions; namespace Penumbra.GameData.Structs; @@ -107,9 +108,9 @@ public readonly unsafe struct CharacterEquip public void Load( CharacterEquip source ) { - Functions.MemCpyUnchecked( _armor, source._armor, sizeof( CharacterArmor ) * 10 ); + MemoryUtility.MemCpyUnchecked( _armor, source._armor, sizeof( CharacterArmor ) * 10 ); } public bool Equals( CharacterEquip other ) - => Functions.MemCmpUnchecked( ( void* )_armor, ( void* )other._armor, sizeof( CharacterArmor ) * 10 ) == 0; + => MemoryUtility.MemCmpUnchecked( ( void* )_armor, ( void* )other._armor, sizeof( CharacterArmor ) * 10 ) == 0; } \ No newline at end of file diff --git a/Penumbra.GameData/Structs/CustomizeData.cs b/Penumbra.GameData/Structs/CustomizeData.cs index 3f184bb5..1524ae11 100644 --- a/Penumbra.GameData/Structs/CustomizeData.cs +++ b/Penumbra.GameData/Structs/CustomizeData.cs @@ -1,5 +1,5 @@ using System; -using Penumbra.GameData.Util; +using Penumbra.String.Functions; namespace Penumbra.GameData.Structs; @@ -13,7 +13,7 @@ public unsafe struct CustomizeData : IEquatable< CustomizeData > { fixed( byte* ptr = Data ) { - Functions.MemCpyUnchecked( ptr, source, Size ); + MemoryUtility.MemCpyUnchecked( ptr, source, Size ); } } @@ -21,7 +21,7 @@ public unsafe struct CustomizeData : IEquatable< CustomizeData > { fixed( byte* ptr = Data ) { - Functions.MemCpyUnchecked( target, ptr, Size ); + MemoryUtility.MemCpyUnchecked( target, ptr, Size ); } } @@ -36,12 +36,12 @@ public unsafe struct CustomizeData : IEquatable< CustomizeData > { fixed( byte* ptr = Data ) { - return Functions.MemCmpUnchecked( ptr, other.Data, Size ) == 0; + return MemoryUtility.MemCmpUnchecked( ptr, other.Data, Size ) == 0; } } public static bool Equals( CustomizeData* lhs, CustomizeData* rhs ) - => Functions.MemCmpUnchecked( lhs, rhs, Size ) == 0; + => MemoryUtility.MemCmpUnchecked( lhs, rhs, Size ) == 0; public override bool Equals( object? obj ) => obj is CustomizeData other && Equals( other ); diff --git a/Penumbra.GameData/Util/Functions.cs b/Penumbra.GameData/Util/Functions.cs deleted file mode 100644 index 81e3dc28..00000000 --- a/Penumbra.GameData/Util/Functions.cs +++ /dev/null @@ -1,157 +0,0 @@ -using System; -using System.Reflection; -using System.Runtime.InteropServices; -using ByteStringFunctions = Penumbra.GameData.ByteString.ByteStringFunctions; - -namespace Penumbra.GameData.Util; - -public static class Functions -{ - public static ulong ComputeCrc64( string name ) - { - if( name.Length == 0 ) - { - return 0; - } - - var lastSlash = name.LastIndexOf( '/' ); - if( lastSlash == -1 ) - { - return Lumina.Misc.Crc32.Get( name ); - } - - var folder = name[ ..lastSlash ]; - var file = name[ ( lastSlash + 1 ).. ]; - return ( ( ulong )Lumina.Misc.Crc32.Get( folder ) << 32 ) | Lumina.Misc.Crc32.Get( file ); - } - - public static ulong ComputeCrc64( ReadOnlySpan< byte > name ) - { - if( name.Length == 0 ) - { - return 0; - } - - var lastSlash = name.LastIndexOf( ( byte )'/' ); - if( lastSlash == -1 ) - { - return Lumina.Misc.Crc32.Get( name ); - } - - var folder = name[ ..lastSlash ]; - var file = name[ ( lastSlash + 1 ).. ]; - return ( ( ulong )Lumina.Misc.Crc32.Get( folder ) << 32 ) | Lumina.Misc.Crc32.Get( file ); - } - - private static readonly uint[] CrcTable = - typeof( Lumina.Misc.Crc32 ).GetField( "CrcTable", BindingFlags.Static | BindingFlags.NonPublic )?.GetValue( null ) as uint[] - ?? throw new Exception( "Could not fetch CrcTable from Lumina." ); - - - public static unsafe int ComputeCrc64LowerAndSize( byte* ptr, out ulong crc64, out int crc32Ret, out bool isLower, out bool isAscii ) - { - var tmp = ptr; - uint crcFolder = 0; - uint crcFile = 0; - var crc32 = uint.MaxValue; - crc64 = 0; - isLower = true; - isAscii = true; - while( true ) - { - var value = *tmp; - if( value == 0 ) - { - break; - } - - if( ByteStringFunctions.AsciiToLower( *tmp ) != *tmp ) - { - isLower = false; - } - - if( value > 0x80 ) - { - isAscii = false; - } - - if( value == ( byte )'/' ) - { - crcFolder = crc32; - crcFile = uint.MaxValue; - crc32 = CrcTable[ ( byte )( crc32 ^ value ) ] ^ ( crc32 >> 8 ); - } - else - { - crcFile = CrcTable[ ( byte )( crcFolder ^ value ) ] ^ ( crcFolder >> 8 ); - crc32 = CrcTable[ ( byte )( crc32 ^ value ) ] ^ ( crc32 >> 8 ); - } - - ++tmp; - } - - var size = ( int )( tmp - ptr ); - crc64 = ~( ( ulong )crcFolder << 32 ) | crcFile; - crc32Ret = ( int )~crc32; - return size; - } - - public static unsafe int ComputeCrc32AsciiLowerAndSize( byte* ptr, out int crc32Ret, out bool isLower, out bool isAscii ) - { - var tmp = ptr; - var crc32 = uint.MaxValue; - isLower = true; - isAscii = true; - while( true ) - { - var value = *tmp; - if( value == 0 ) - { - break; - } - - if( ByteStringFunctions.AsciiToLower( *tmp ) != *tmp ) - { - isLower = false; - } - - if( value > 0x80 ) - { - isAscii = false; - } - - crc32 = CrcTable[ ( byte )( crc32 ^ value ) ] ^ ( crc32 >> 8 ); - ++tmp; - } - - var size = ( int )( tmp - ptr ); - crc32Ret = ( int )~crc32; - return size; - } - - [DllImport( "msvcrt.dll", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl, SetLastError = false )] - private static extern unsafe IntPtr memcpy( void* dest, void* src, int count ); - - public static unsafe void MemCpyUnchecked( void* dest, void* src, int count ) - => memcpy( dest, src, count ); - - - [DllImport( "msvcrt.dll", EntryPoint = "memcmp", CallingConvention = CallingConvention.Cdecl, SetLastError = false )] - private static extern unsafe int memcmp( void* b1, void* b2, int count ); - - public static unsafe int MemCmpUnchecked( void* ptr1, void* ptr2, int count ) - => memcmp( ptr1, ptr2, count ); - - - [DllImport( "msvcrt.dll", EntryPoint = "_memicmp", CallingConvention = CallingConvention.Cdecl, SetLastError = false )] - private static extern unsafe int memicmp( void* b1, void* b2, int count ); - - public static unsafe int MemCmpCaseInsensitiveUnchecked( void* ptr1, void* ptr2, int count ) - => memicmp( ptr1, ptr2, count ); - - [DllImport( "msvcrt.dll", EntryPoint = "memset", CallingConvention = CallingConvention.Cdecl, SetLastError = false )] - private static extern unsafe void* memset( void* dest, int c, int count ); - - public static unsafe void* MemSet( void* dest, byte value, int count ) - => memset( dest, value, count ); -} \ No newline at end of file diff --git a/Penumbra.String b/Penumbra.String new file mode 160000 index 00000000..f41af0fb --- /dev/null +++ b/Penumbra.String @@ -0,0 +1 @@ +Subproject commit f41af0fb88626f1579d3c4370b32b901f3c4d3c2 diff --git a/Penumbra.sln b/Penumbra.sln index 33e5a03d..5c11aaea 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -16,6 +16,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OtterGui", "OtterGui\OtterG EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.Api", "Penumbra.Api\Penumbra.Api.csproj", "{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.String", "Penumbra.String\Penumbra.String.csproj", "{5549BAFD-6357-4B1A-800C-75AC36E5B76D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -38,6 +40,10 @@ Global {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|Any CPU.Build.0 = Debug|Any CPU {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|Any CPU.ActiveCfg = Release|Any CPU {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|Any CPU.Build.0 = Release|Any CPU + {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 7ccad1ce..d6b0b51c 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -3,7 +3,6 @@ using Dalamud.Plugin; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using Penumbra.GameData.ByteString; using Penumbra.Mods; using System; using System.Collections.Generic; @@ -12,6 +11,8 @@ using System.Linq; using System.Numerics; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; +using Penumbra.String; +using Penumbra.String.Classes; namespace Penumbra.Api; @@ -536,7 +537,7 @@ public class IpcTester : IDisposable private unsafe void UpdateLastCreated( IntPtr gameObject, string _, IntPtr _2, IntPtr _3, IntPtr _4 ) { var obj = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )gameObject; - _lastCreatedGameObjectName = new Utf8String( obj->GetName() ).ToString(); + _lastCreatedGameObjectName = new ByteString( obj->GetName() ).ToString(); _lastCreatedGameObjectTime = DateTimeOffset.Now; _lastCreatedDrawObject = IntPtr.Zero; } @@ -544,7 +545,7 @@ public class IpcTester : IDisposable private unsafe void UpdateLastCreated2( IntPtr gameObject, string _, IntPtr drawObject ) { var obj = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )gameObject; - _lastCreatedGameObjectName = new Utf8String( obj->GetName() ).ToString(); + _lastCreatedGameObjectName = new ByteString( obj->GetName() ).ToString(); _lastCreatedGameObjectTime = DateTimeOffset.Now; _lastCreatedDrawObject = drawObject; } @@ -552,7 +553,7 @@ public class IpcTester : IDisposable private unsafe void UpdateGameObjectResourcePath( IntPtr gameObject, string gamePath, string fullPath ) { var obj = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )gameObject; - _lastResolvedObject = obj != null ? new Utf8String( obj->GetName() ).ToString() : "Unknown"; + _lastResolvedObject = obj != null ? new ByteString( obj->GetName() ).ToString() : "Unknown"; _lastResolvedGamePath = gamePath; _lastResolvedFullPath = fullPath; _lastResolvedGamePathTime = DateTimeOffset.Now; diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 0484f986..8cc799f1 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -3,7 +3,6 @@ using Lumina.Data; using Newtonsoft.Json; using OtterGui; using Penumbra.Collections; -using Penumbra.GameData.ByteString; using Penumbra.Interop.Resolver; using Penumbra.Interop.Structs; using Penumbra.Meta.Manipulations; @@ -15,6 +14,7 @@ using System.IO; using System.Linq; using System.Reflection; using Penumbra.Api.Enums; +using Penumbra.String.Classes; namespace Penumbra.Api; diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index 263b892f..bb41f6f1 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -1,11 +1,11 @@ using OtterGui; using Penumbra.Collections; -using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; using Penumbra.Mods; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using Penumbra.String.Classes; namespace Penumbra.Api; diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 75bfcef5..1e211076 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -143,8 +143,8 @@ public partial class ModCollection return false; } - _specialCollections[ ( int )collectionType ] = Empty; - CollectionChanged.Invoke( collectionType, null, Empty, null ); + _specialCollections[ ( int )collectionType ] = Default; + CollectionChanged.Invoke( collectionType, null, Default, null ); return true; } @@ -172,8 +172,8 @@ public partial class ModCollection return false; } - _characters[ characterName ] = Empty; - CollectionChanged.Invoke( CollectionType.Character, null, Empty, characterName ); + _characters[ characterName ] = Default; + CollectionChanged.Invoke( CollectionType.Character, null, Default, characterName ); return true; } diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 5b636966..a4d1619c 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -1,5 +1,4 @@ using OtterGui.Classes; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Meta.Manager; using Penumbra.Mods; @@ -8,8 +7,8 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; using Penumbra.Interop; -using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; +using Penumbra.String.Classes; namespace Penumbra.Collections; diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 2d2262cc..acf3b9b6 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -1,6 +1,5 @@ using OtterGui; using OtterGui.Classes; -using Penumbra.GameData.ByteString; using Penumbra.Meta.Manager; using Penumbra.Meta.Manipulations; using Penumbra.Mods; @@ -8,6 +7,8 @@ using System; using System.Collections.Generic; using System.Linq; using Penumbra.Api.Enums; +using Penumbra.GameData.Util; +using Penumbra.String.Classes; namespace Penumbra.Collections; @@ -240,7 +241,7 @@ public partial class ModCollection if( addMetaChanges ) { ++_collection.ChangeCounter; - if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) + if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) { Penumbra.ResidentResources.Reload(); MetaManipulations.SetFiles(); @@ -413,7 +414,7 @@ public partial class ModCollection // Add the same conflict list to both conflict directions. var conflictList = new List< object > { data }; _conflicts[ addedMod ] = addedConflicts.Append( new ModConflicts( existingMod, conflictList, existingPriority < addedPriority, - existingPriority != addedPriority ) ); + existingPriority != addedPriority ) ); _conflicts[ existingMod ] = existingConflicts.Append( new ModConflicts( addedMod, conflictList, existingPriority >= addedPriority, existingPriority != addedPriority ) ); @@ -474,9 +475,9 @@ public partial class ModCollection // Skip IMCs because they would result in far too many false-positive items, // since they are per set instead of per item-slot/item/variant. var identifier = GameData.GameData.GetIdentifier(); - foreach( var (resolved, modPath) in ResolvedFiles.Where( file => !file.Key.Path.EndsWith( 'i', 'm', 'c' ) ) ) + foreach( var (resolved, modPath) in ResolvedFiles.Where( file => !file.Key.Path.EndsWith( "imc"u8 ) ) ) { - foreach( var (name, obj) in identifier.Identify( resolved.ToGamePath() ) ) + foreach( var (name, obj) in identifier.Identify( new GamePath( resolved.ToString() ) ) ) { if( !_changedItems.TryGetValue( name, out var data ) ) { diff --git a/Penumbra/Import/Textures/Texture.cs b/Penumbra/Import/Textures/Texture.cs index b3df03f0..c54218d5 100644 --- a/Penumbra/Import/Textures/Texture.cs +++ b/Penumbra/Import/Textures/Texture.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; @@ -11,7 +10,7 @@ using Lumina.Data.Files; using OtterGui; using OtterGui.Raii; using OtterTex; -using Penumbra.GameData.ByteString; +using Penumbra.String.Classes; using Penumbra.UI.Classes; using SixLabors.ImageSharp.PixelFormats; using Image = SixLabors.ImageSharp.Image; diff --git a/Penumbra/Interop/CharacterUtility.DecalReverter.cs b/Penumbra/Interop/CharacterUtility.DecalReverter.cs index a439ac48..5aee657a 100644 --- a/Penumbra/Interop/CharacterUtility.DecalReverter.cs +++ b/Penumbra/Interop/CharacterUtility.DecalReverter.cs @@ -1,8 +1,8 @@ using System; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Collections; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; +using Penumbra.String.Classes; namespace Penumbra.Interop; diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index f04f2930..ba2f2962 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -7,8 +7,9 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.STD; using Penumbra.Collections; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; +using Penumbra.String; +using Penumbra.String.Classes; namespace Penumbra.Interop.Loader; @@ -19,7 +20,7 @@ public unsafe partial class ResourceLoader private readonly Hook< ResourceHandleDecRef > _decRefHook; public delegate IntPtr ResourceHandleDestructor( ResourceHandle* handle ); - + [Signature( "48 89 5C 24 ?? 57 48 83 EC ?? 48 8D 05 ?? ?? ?? ?? 48 8B D9 48 89 01 B8", DetourName = nameof( ResourceHandleDestructorDetour ) )] public static Hook< ResourceHandleDestructor >? ResourceHandleDestructorHook; @@ -28,7 +29,7 @@ public unsafe partial class ResourceLoader { if( handle != null ) { - Penumbra.Log.Information( $"[ResourceLoader] Destructing Resource Handle {handle->FileName} at 0x{( ulong )handle:X} (Refcount {handle->RefCount})."); + Penumbra.Log.Information( $"[ResourceLoader] Destructing Resource Handle {handle->FileName} at 0x{( ulong )handle:X} (Refcount {handle->RefCount})." ); } return ResourceHandleDestructorHook!.Original( handle ); @@ -248,7 +249,7 @@ public unsafe partial class ResourceLoader Penumbra.Log.Information( $"[ResourceLoader] Loaded {pathString} to 0x{( ulong )handle:X}. (Refcount {handle->RefCount})" ); } - private static void LogLoadedFile( Utf8String path, bool success, bool custom ) + private static void LogLoadedFile( ByteString path, bool success, bool custom ) => Penumbra.Log.Information( success ? $"[ResourceLoader] Loaded {path} from {( custom ? "local files" : "SqPack" )}" : $"[ResourceLoader] Failed to load {path} from {( custom ? "local files" : "SqPack" )}." ); diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index a0ca377f..8e28d2ce 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -7,9 +7,10 @@ using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Collections; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; +using Penumbra.String; +using Penumbra.String.Classes; using FileMode = Penumbra.Interop.Structs.FileMode; using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; @@ -71,7 +72,7 @@ public unsafe partial class ResourceLoader private event Action< Utf8GamePath, ResourceType, FullPath?, object? >? PathResolved; - public ResourceHandle* ResolvePathSync( ResourceCategory category, ResourceType type, Utf8String path ) + public ResourceHandle* ResolvePathSync( ResourceCategory category, ResourceType type, ByteString path ) { var hash = path.Crc32; return GetResourceHandler( true, *ResourceManager, &category, &type, &hash, path.Path, null, false ); @@ -209,7 +210,7 @@ public unsafe partial class ResourceLoader } // Load the resource from an SqPack and trigger the FileLoaded event. - private byte DefaultResourceLoad( Utf8String path, ResourceManager* resourceManager, + private byte DefaultResourceLoad( ByteString path, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync ) { var ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); @@ -218,7 +219,7 @@ public unsafe partial class ResourceLoader } // Load the resource from a path on the users hard drives. - private byte DefaultRootedResourceLoad( Utf8String gamePath, ResourceManager* resourceManager, + private byte DefaultRootedResourceLoad( ByteString gamePath, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync ) { // Specify that we are loading unpacked files from the drive. @@ -246,7 +247,7 @@ public unsafe partial class ResourceLoader } // Load a resource by its path. If it is rooted, it will be loaded from the drive, otherwise from the SqPack. - internal byte DefaultLoadResource( Utf8String gamePath, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, + internal byte DefaultLoadResource( ByteString gamePath, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync ) => Utf8GamePath.IsRooted( gamePath ) ? DefaultRootedResourceLoad( gamePath, resourceManager, fileDescriptor, priority, isSync ) @@ -262,7 +263,7 @@ public unsafe partial class ResourceLoader _incRefHook.Dispose(); } - private static int ComputeHash( Utf8String path, GetResourceParameters* pGetResParams ) + private static int ComputeHash( ByteString path, GetResourceParameters* pGetResParams ) { if( pGetResParams == null || !pGetResParams->IsPartialRead ) { @@ -272,11 +273,11 @@ public unsafe partial class ResourceLoader // When the game requests file only partially, crc32 includes that information, in format of: // path/to/file.ext.hex_offset.hex_size // ex) music/ex4/BGM_EX4_System_Title.scd.381adc.30000 - return Utf8String.Join( + return ByteString.Join( ( byte )'.', path, - Utf8String.FromStringUnsafe( pGetResParams->SegmentOffset.ToString( "x" ), true ), - Utf8String.FromStringUnsafe( pGetResParams->SegmentLength.ToString( "x" ), true ) + ByteString.FromStringUnsafe( pGetResParams->SegmentOffset.ToString( "x" ), true ), + ByteString.FromStringUnsafe( pGetResParams->SegmentLength.ToString( "x" ), true ) ).Crc32; } diff --git a/Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs b/Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs index 67454300..415b18f7 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; +using Penumbra.String.Classes; namespace Penumbra.Interop.Loader; @@ -26,7 +26,7 @@ public unsafe partial class ResourceLoader // We use it to check against our stored CRC64s and if it corresponds, we return the custom flag. public delegate IntPtr CheckFileStatePrototype( IntPtr unk1, ulong crc64 ); - [Signature( "E8 ?? ?? ?? ?? 48 85 c0 74 ?? 45 0f b6 ce 48 89 44 24", DetourName = nameof(CheckFileStateDetour) )] + [Signature( "E8 ?? ?? ?? ?? 48 85 c0 74 ?? 45 0f b6 ce 48 89 44 24", DetourName = nameof( CheckFileStateDetour ) )] public Hook< CheckFileStatePrototype > CheckFileStateHook = null!; private IntPtr CheckFileStateDetour( IntPtr ptr, ulong crc64 ) @@ -48,7 +48,7 @@ public unsafe partial class ResourceLoader // We hook the extern functions to just return the local one if given the custom flag as last argument. public delegate byte LoadTexFileExternPrototype( ResourceHandle* handle, int unk1, IntPtr unk2, bool unk3, IntPtr unk4 ); - [Signature( "E8 ?? ?? ?? ?? 0F B6 E8 48 8B CB E8", DetourName = nameof(LoadTexFileExternDetour) )] + [Signature( "E8 ?? ?? ?? ?? 0F B6 E8 48 8B CB E8", DetourName = nameof( LoadTexFileExternDetour ) )] public Hook< LoadTexFileExternPrototype > LoadTexFileExternHook = null!; private byte LoadTexFileExternDetour( ResourceHandle* resourceHandle, int unk1, IntPtr unk2, bool unk3, IntPtr ptr ) @@ -59,7 +59,7 @@ public unsafe partial class ResourceLoader public delegate byte LoadMdlFileExternPrototype( ResourceHandle* handle, IntPtr unk1, bool unk2, IntPtr unk3 ); - [Signature( "E8 ?? ?? ?? ?? EB 02 B0 F1", DetourName = nameof(LoadMdlFileExternDetour) )] + [Signature( "E8 ?? ?? ?? ?? EB 02 B0 F1", DetourName = nameof( LoadMdlFileExternDetour ) )] public Hook< LoadMdlFileExternPrototype > LoadMdlFileExternHook = null!; private byte LoadMdlFileExternDetour( ResourceHandle* resourceHandle, IntPtr unk1, bool unk2, IntPtr ptr ) diff --git a/Penumbra/Interop/Loader/ResourceLoader.cs b/Penumbra/Interop/Loader/ResourceLoader.cs index 44d55214..de91fb2a 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.cs @@ -3,9 +3,10 @@ using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Collections; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; +using Penumbra.String; +using Penumbra.String.Classes; namespace Penumbra.Interop.Loader; @@ -127,7 +128,7 @@ public unsafe partial class ResourceLoader : IDisposable // Event fired whenever a resource is newly loaded. // Success indicates the return value of the loading function (which does not imply that the resource was actually successfully loaded) // custom is true if the file was loaded from local files instead of the default SqPacks. - public delegate void FileLoadedDelegate( Utf8String path, bool success, bool custom ); + public delegate void FileLoadedDelegate( ByteString path, bool success, bool custom ); public event FileLoadedDelegate? FileLoaded; // Customization point to control how path resolving is handled. @@ -140,7 +141,7 @@ public unsafe partial class ResourceLoader : IDisposable // Customize file loading for any GamePaths that start with "|". // Same procedure as above. - public delegate bool ResourceLoadCustomizationDelegate( Utf8String split, Utf8String path, ResourceManager* resourceManager, + public delegate bool ResourceLoadCustomizationDelegate( ByteString split, ByteString path, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte retValue ); public event ResourceLoadCustomizationDelegate? ResourceLoadCustomization; diff --git a/Penumbra/Interop/Loader/ResourceLogger.cs b/Penumbra/Interop/Loader/ResourceLogger.cs index 25d8ea59..dceaad36 100644 --- a/Penumbra/Interop/Loader/ResourceLogger.cs +++ b/Penumbra/Interop/Loader/ResourceLogger.cs @@ -1,6 +1,7 @@ using System; using System.Text.RegularExpressions; -using Penumbra.GameData.ByteString; +using Penumbra.String; +using Penumbra.String.Classes; namespace Penumbra.Interop.Loader; @@ -84,7 +85,7 @@ public class ResourceLogger : IDisposable // Returns the converted string if the filter matches, and null otherwise. // The filter matches if it is empty, if it is a valid and matching regex or if the given string contains it. - private string? Match( Utf8String data ) + private string? Match( ByteString data ) { var s = data.ToString(); return Filter.Length == 0 || ( _filterRegex?.IsMatch( s ) ?? s.Contains( Filter, StringComparison.OrdinalIgnoreCase ) ) diff --git a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs index 9a38bf56..3744bb67 100644 --- a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs @@ -2,8 +2,8 @@ using System; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using Penumbra.Collections; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; +using Penumbra.String.Classes; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; namespace Penumbra.Interop.Resolver; diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs index aa6e7ed4..3742a51e 100644 --- a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs @@ -8,8 +8,8 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Api; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Classes; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; +using Penumbra.String.Classes; namespace Penumbra.Interop.Resolver; @@ -57,7 +57,7 @@ public unsafe partial class PathResolver { if( type == ResourceType.Tex && LastCreatedCollection.Valid - && gamePath.Path.Substring( "chara/common/texture/".Length ).StartsWith( 'd', 'e', 'c', 'a', 'l' ) ) + && gamePath.Path.Substring( "chara/common/texture/".Length ).StartsWith( "decal"u8 ) ) { resolveData = LastCreatedCollection; return true; diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index ef09c616..ec975dfa 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -7,8 +7,8 @@ using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Component.GUI; using Lumina.Excel.GeneratedSheets; using Penumbra.Collections; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; +using Penumbra.String; using CustomizeData = Penumbra.GameData.Structs.CustomizeData; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; @@ -66,7 +66,7 @@ public unsafe partial class PathResolver } var block = data + 0x7A; - return new Utf8String( block ).ToString(); + return new ByteString( block ).ToString(); } // Obtain the name of the player character if the glamour plate edit window is open. @@ -130,7 +130,7 @@ public unsafe partial class PathResolver if( owner != null ) { - return new Utf8String( owner->Name ).ToString(); + return new ByteString( owner->Name ).ToString(); } return null; @@ -169,7 +169,7 @@ public unsafe partial class PathResolver if( Penumbra.Config.PreferNamedCollectionsOverOwners ) { // Early return if we prefer the actors own name over its owner. - actorName = new Utf8String( gameObject->Name ).ToString(); + actorName = new ByteString( gameObject->Name ).ToString(); if( actorName.Length > 0 && CollectionByActorName( actorName, out var actorCollection ) ) { @@ -189,7 +189,7 @@ public unsafe partial class PathResolver >= CutsceneCharacters.CutsceneStartIdx and < CutsceneCharacters.CutsceneEndIdx => GetCutsceneName( gameObject ), _ => null, } - ?? GetOwnerName( gameObject ) ?? actorName ?? new Utf8String( gameObject->Name ).ToString(); + ?? GetOwnerName( gameObject ) ?? actorName ?? new ByteString( gameObject->Name ).ToString(); // First check temporary character collections, then the own configuration, then special collections. var collection = CollectionByActorName( actualName, out var c ) diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs index baf333f2..90893c20 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -4,9 +4,10 @@ using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Collections; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; +using Penumbra.String; +using Penumbra.String.Classes; namespace Penumbra.Interop.Resolver; @@ -78,7 +79,7 @@ public unsafe partial class PathResolver // We need to set the correct collection for the actual material path that is loaded // before actually loading the file. - public bool MtrlLoadHandler( Utf8String split, Utf8String path, ResourceManager* resourceManager, + public bool MtrlLoadHandler( ByteString split, ByteString path, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte ret ) { ret = 0; @@ -149,7 +150,7 @@ public unsafe partial class PathResolver } var mtrl = ( MtrlResource* )mtrlResourceHandle; - var mtrlPath = Utf8String.FromSpanUnsafe( mtrl->Handle.FileNameSpan(), true, null, true ); + var mtrlPath = ByteString.FromSpanUnsafe( mtrl->Handle.FileNameSpan(), true, null, true ); _mtrlData = _paths.TryGetValue( mtrlPath, out var c ) ? c : ResolveData.Invalid; } } diff --git a/Penumbra/Interop/Resolver/PathResolver.PathState.cs b/Penumbra/Interop/Resolver/PathResolver.PathState.cs index a9bd1ee0..42c458c3 100644 --- a/Penumbra/Interop/Resolver/PathResolver.PathState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.PathState.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using Dalamud.Utility.Signatures; using Penumbra.Collections; -using Penumbra.GameData.ByteString; +using Penumbra.String; namespace Penumbra.Interop.Resolver; @@ -31,7 +31,7 @@ public unsafe partial class PathResolver private readonly ResolverHooks _monster; // This map links files to their corresponding collection, if it is non-default. - private readonly ConcurrentDictionary< Utf8String, ResolveData > _pathCollections = new(); + private readonly ConcurrentDictionary< ByteString, ResolveData > _pathCollections = new(); public PathState( PathResolver parent ) { @@ -69,13 +69,13 @@ public unsafe partial class PathResolver public int Count => _pathCollections.Count; - public IEnumerable< KeyValuePair< Utf8String, ResolveData > > Paths + public IEnumerable< KeyValuePair< ByteString, ResolveData > > Paths => _pathCollections; - public bool TryGetValue( Utf8String path, out ResolveData collection ) + public bool TryGetValue( ByteString path, out ResolveData collection ) => _pathCollections.TryGetValue( path, out collection ); - public bool Consume( Utf8String path, out ResolveData collection ) + public bool Consume( ByteString path, out ResolveData collection ) => _pathCollections.TryRemove( path, out collection ); // Just add or remove the resolved path. @@ -87,13 +87,13 @@ public unsafe partial class PathResolver return path; } - var gamePath = new Utf8String( ( byte* )path ); + var gamePath = new ByteString( ( byte* )path ); SetCollection( gameObject, gamePath, collection ); return path; } // Special handling for paths so that we do not store non-owned temporary strings in the dictionary. - public void SetCollection( IntPtr gameObject, Utf8String path, ModCollection collection ) + public void SetCollection( IntPtr gameObject, ByteString path, ModCollection collection ) { if( _pathCollections.ContainsKey( path ) || path.IsOwned ) { diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 17311ed3..c2f4f3ef 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -5,9 +5,10 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Collections; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Loader; +using Penumbra.String; +using Penumbra.String.Classes; namespace Penumbra.Interop.Resolver; @@ -151,7 +152,7 @@ public partial class PathResolver : IDisposable return resolveData; } - internal IEnumerable< KeyValuePair< Utf8String, ResolveData > > PathCollections + internal IEnumerable< KeyValuePair< ByteString, ResolveData > > PathCollections => _paths.Paths; internal IEnumerable< KeyValuePair< IntPtr, (ResolveData, int) > > DrawObjectMap diff --git a/Penumbra/Meta/Files/CmpFile.cs b/Penumbra/Meta/Files/CmpFile.cs index 0f308bd6..5a4919bf 100644 --- a/Penumbra/Meta/Files/CmpFile.cs +++ b/Penumbra/Meta/Files/CmpFile.cs @@ -4,6 +4,7 @@ using Penumbra.GameData.Structs; using Penumbra.GameData.Util; using Penumbra.Interop.Structs; using System.Collections.Generic; +using Penumbra.String.Functions; namespace Penumbra.Meta.Files; @@ -23,7 +24,7 @@ public sealed unsafe class CmpFile : MetaBaseFile } public override void Reset() - => Functions.MemCpyUnchecked( Data, ( byte* )DefaultData.Data, DefaultData.Length ); + => MemoryUtility.MemCpyUnchecked( Data, ( byte* )DefaultData.Data, DefaultData.Length ); public void Reset( IEnumerable< (SubRace, RspAttribute) > entries ) { diff --git a/Penumbra/Meta/Files/EqdpFile.cs b/Penumbra/Meta/Files/EqdpFile.cs index d9a22c41..5290fb04 100644 --- a/Penumbra/Meta/Files/EqdpFile.cs +++ b/Penumbra/Meta/Files/EqdpFile.cs @@ -4,6 +4,7 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.GameData.Util; using Penumbra.Interop.Structs; +using Penumbra.String.Functions; namespace Penumbra.Meta.Files; @@ -63,7 +64,7 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile public override void Reset() { var def = ( byte* )DefaultData.Data; - Functions.MemCpyUnchecked( Data, def, IdentifierSize + PreambleSize ); + MemoryUtility.MemCpyUnchecked( Data, def, IdentifierSize + PreambleSize ); var controlPtr = ( ushort* )( def + IdentifierSize + PreambleSize ); var dataBasePtr = controlPtr + BlockCount; @@ -73,18 +74,18 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile { if( controlPtr[ i ] == CollapsedBlock ) { - Functions.MemSet( myDataPtr, 0, BlockSize * EqdpEntrySize ); + MemoryUtility.MemSet( myDataPtr, 0, BlockSize * EqdpEntrySize ); } else { - Functions.MemCpyUnchecked( myDataPtr, dataBasePtr + controlPtr[ i ], BlockSize * EqdpEntrySize ); + MemoryUtility.MemCpyUnchecked( myDataPtr, dataBasePtr + controlPtr[ i ], BlockSize * EqdpEntrySize ); } myControlPtr[ i ] = ( ushort )( i * BlockSize ); myDataPtr += BlockSize; } - Functions.MemSet( myDataPtr, 0, Length - ( int )( ( byte* )myDataPtr - Data ) ); + MemoryUtility.MemSet( myDataPtr, 0, Length - ( int )( ( byte* )myDataPtr - Data ) ); } public void Reset( IEnumerable< int > entries ) diff --git a/Penumbra/Meta/Files/EqpGmpFile.cs b/Penumbra/Meta/Files/EqpGmpFile.cs index 60649952..5d15031b 100644 --- a/Penumbra/Meta/Files/EqpGmpFile.cs +++ b/Penumbra/Meta/Files/EqpGmpFile.cs @@ -5,6 +5,7 @@ using System.Numerics; using Penumbra.GameData.Structs; using Penumbra.GameData.Util; using Penumbra.Interop.Structs; +using Penumbra.String.Functions; namespace Penumbra.Meta.Files; @@ -49,7 +50,7 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile protected virtual void SetEmptyBlock( int idx ) { - Functions.MemSet( Data + idx * BlockSize * EntrySize, 0, BlockSize * EntrySize ); + MemoryUtility.MemSet( Data + idx * BlockSize * EntrySize, 0, BlockSize * EntrySize ); } public sealed override void Reset() @@ -62,7 +63,7 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile var collapsed = ( ( controlBlock >> i ) & 1 ) == 0; if( !collapsed ) { - Functions.MemCpyUnchecked( Data + i * BlockSize * EntrySize, ptr + expandedBlocks * BlockSize * EntrySize, BlockSize * EntrySize ); + MemoryUtility.MemCpyUnchecked( Data + i * BlockSize * EntrySize, ptr + expandedBlocks * BlockSize * EntrySize, BlockSize * EntrySize ); expandedBlocks++; } else diff --git a/Penumbra/Meta/Files/EstFile.cs b/Penumbra/Meta/Files/EstFile.cs index 7c6b3591..5c76ff71 100644 --- a/Penumbra/Meta/Files/EstFile.cs +++ b/Penumbra/Meta/Files/EstFile.cs @@ -4,6 +4,7 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Util; using Penumbra.Interop.Structs; using Penumbra.Meta.Manipulations; +using Penumbra.String.Functions; namespace Penumbra.Meta.Files; @@ -170,8 +171,8 @@ public sealed unsafe class EstFile : MetaBaseFile { var (d, length) = DefaultData; var data = ( byte* )d; - Functions.MemCpyUnchecked( Data, data, length ); - Functions.MemSet( Data + length, 0, Length - length ); + MemoryUtility.MemCpyUnchecked( Data, data, length ); + MemoryUtility.MemSet( Data + length, 0, Length - length ); } public EstFile( EstManipulation.EstType estType ) diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index f1309e64..8f36507e 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -1,11 +1,12 @@ using System; using System.Numerics; using Newtonsoft.Json; -using Penumbra.GameData.ByteString; +using OtterGui; using Penumbra.GameData.Enums; -using Penumbra.GameData.Util; using Penumbra.Interop.Structs; using Penumbra.Meta.Manipulations; +using Penumbra.String.Classes; +using Penumbra.String.Functions; namespace Penumbra.Meta.Files; @@ -150,7 +151,7 @@ public unsafe class ImcFile : MetaBaseFile var defaultPtr = ( ImcEntry* )( Data + PreambleSize ); for( var i = oldCount + 1; i < numVariants + 1; ++i ) { - Functions.MemCpyUnchecked( defaultPtr + i * NumParts, defaultPtr, NumParts * sizeof( ImcEntry ) ); + MemoryUtility.MemCpyUnchecked( defaultPtr + i * NumParts, defaultPtr, NumParts * sizeof( ImcEntry ) ); } Penumbra.Log.Verbose( $"Expanded IMC {Path} from {oldCount} to {numVariants} variants." ); @@ -188,8 +189,8 @@ public unsafe class ImcFile : MetaBaseFile var file = Dalamud.GameData.GetFile( Path.ToString() ); fixed( byte* ptr = file!.Data ) { - Functions.MemCpyUnchecked( Data, ptr, file.Data.Length ); - Functions.MemSet( Data + file.Data.Length, 0, Length - file.Data.Length ); + MemoryUtility.MemCpyUnchecked( Data, ptr, file.Data.Length ); + MemoryUtility.MemSet( Data + file.Data.Length, 0, Length - file.Data.Length ); } } @@ -207,7 +208,7 @@ public unsafe class ImcFile : MetaBaseFile { NumParts = BitOperations.PopCount( *( ushort* )( ptr + 2 ) ); AllocateData( file.Data.Length ); - Functions.MemCpyUnchecked( Data, ptr, file.Data.Length ); + MemoryUtility.MemCpyUnchecked( Data, ptr, file.Data.Length ); } } @@ -243,7 +244,7 @@ public unsafe class ImcFile : MetaBaseFile return; } - Functions.MemCpyUnchecked( newData, Data, ActualLength ); + MemoryUtility.MemCpyUnchecked( newData, Data, ActualLength ); Penumbra.MetaFileManager.Free( data, length ); resource->SetData( ( IntPtr )newData, ActualLength ); diff --git a/Penumbra/Meta/Files/MetaBaseFile.cs b/Penumbra/Meta/Files/MetaBaseFile.cs index 8a167216..dbe8a35b 100644 --- a/Penumbra/Meta/Files/MetaBaseFile.cs +++ b/Penumbra/Meta/Files/MetaBaseFile.cs @@ -1,6 +1,7 @@ using System; using Dalamud.Memory; using Penumbra.GameData.Util; +using Penumbra.String.Functions; using CharacterUtility = Penumbra.Interop.CharacterUtility; namespace Penumbra.Meta.Files; @@ -57,12 +58,12 @@ public unsafe class MetaBaseFile : IDisposable var data = ( byte* )Penumbra.MetaFileManager.AllocateFileMemory( ( ulong )newLength ); if( newLength > Length ) { - Functions.MemCpyUnchecked( data, Data, Length ); - Functions.MemSet( data + Length, 0, newLength - Length ); + MemoryUtility.MemCpyUnchecked( data, Data, Length ); + MemoryUtility.MemSet( data + Length, 0, newLength - Length ); } else { - Functions.MemCpyUnchecked( data, Data, newLength ); + MemoryUtility.MemCpyUnchecked( data, Data, newLength ); } ReleaseUnmanagedResources(); diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index 5887aba2..d61f158f 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -2,11 +2,12 @@ using System; using System.Collections.Generic; using FFXIVClientStructs.FFXIV.Client.System.Resource; using OtterGui.Filesystem; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; +using Penumbra.String; +using Penumbra.String.Classes; namespace Penumbra.Meta.Manager; @@ -149,7 +150,7 @@ public partial class MetaManager => new($"|{_collection.Name}_{_collection.ChangeCounter}|{path}"); - private static unsafe bool ImcLoadHandler( Utf8String split, Utf8String path, ResourceManager* resourceManager, + private static unsafe bool ImcLoadHandler( ByteString split, ByteString path, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte ret ) { ret = 0; diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs index 5bd505df..682fe3af 100644 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -2,10 +2,10 @@ using System; using System.Runtime.InteropServices; using Newtonsoft.Json; using Newtonsoft.Json.Converters; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; +using Penumbra.String.Classes; namespace Penumbra.Meta.Manipulations; diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs index 4380e0a0..9029800a 100644 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -4,6 +4,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Penumbra.GameData.Util; using Penumbra.Interop.Structs; +using Penumbra.String.Functions; namespace Penumbra.Meta.Manipulations; @@ -217,7 +218,7 @@ public readonly struct MetaManipulation : IEquatable< MetaManipulation >, ICompa { fixed( MetaManipulation* lhs = &this ) { - return Functions.MemCmpUnchecked( lhs, &other, sizeof( MetaManipulation ) ); + return MemoryUtility.MemCmpUnchecked( lhs, &other, sizeof( MetaManipulation ) ); } } diff --git a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs index b0a136af..d60356ac 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs @@ -5,7 +5,7 @@ using System.IO; using System.Linq; using System.Security.Cryptography; using System.Threading.Tasks; -using Penumbra.GameData.ByteString; +using Penumbra.String.Classes; namespace Penumbra.Mods; @@ -124,7 +124,7 @@ public partial class Mod foreach( var file in files ) { // Skip any UI Files because deduplication causes weird crashes for those. - if( file.SubModUsage.Any( f => f.Item2.Path.StartsWith( 'u', 'i', '/' ) ) ) + if( file.SubModUsage.Any( f => f.Item2.Path.StartsWith( "ui/"u8 ) ) ) { continue; } diff --git a/Penumbra/Mods/Editor/Mod.Editor.Edit.cs b/Penumbra/Mods/Editor/Mod.Editor.Edit.cs index 29f5389b..c2221ecc 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Edit.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Edit.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Linq; -using Penumbra.GameData.ByteString; +using Penumbra.String.Classes; using Penumbra.Util; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Editor/Mod.Editor.Files.cs b/Penumbra/Mods/Editor/Mod.Editor.Files.cs index 114359db..583442fd 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Files.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Files.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using Penumbra.GameData.ByteString; +using Penumbra.String.Classes; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs b/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs index 3061d390..45c6707c 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs @@ -5,9 +5,9 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; using OtterGui; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; +using Penumbra.String.Classes; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index 704d1285..82e171eb 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -4,8 +4,8 @@ using System.Linq; using OtterGui; using OtterGui.Filesystem; using Penumbra.Api.Enums; -using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; +using Penumbra.String.Classes; using Penumbra.Util; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Mod.ChangedItems.cs b/Penumbra/Mods/Mod.ChangedItems.cs index d2996b71..1057bf0d 100644 --- a/Penumbra/Mods/Mod.ChangedItems.cs +++ b/Penumbra/Mods/Mod.ChangedItems.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Penumbra.GameData.Util; namespace Penumbra.Mods; @@ -14,7 +15,7 @@ public sealed partial class Mod ChangedItems.Clear(); foreach( var gamePath in AllRedirects ) { - identifier.Identify( ChangedItems, gamePath.ToGamePath() ); + identifier.Identify( ChangedItems, new GamePath(gamePath.ToString()) ); } // TODO: manipulations diff --git a/Penumbra/Mods/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs index 699fceaa..a566b795 100644 --- a/Penumbra/Mods/Mod.Creation.cs +++ b/Penumbra/Mods/Mod.Creation.cs @@ -6,8 +6,8 @@ using Dalamud.Utility; using OtterGui.Classes; using OtterGui.Filesystem; using Penumbra.Api.Enums; -using Penumbra.GameData.ByteString; using Penumbra.Import; +using Penumbra.String.Classes; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs index b09f680c..a12b29fc 100644 --- a/Penumbra/Mods/Mod.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -5,8 +5,8 @@ using System.Linq; using Newtonsoft.Json.Linq; using OtterGui; using Penumbra.Api.Enums; -using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; +using Penumbra.String.Classes; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Mod.Meta.Migration.cs b/Penumbra/Mods/Mod.Meta.Migration.cs index 8d907be9..fa16f4c2 100644 --- a/Penumbra/Mods/Mod.Meta.Migration.cs +++ b/Penumbra/Mods/Mod.Meta.Migration.cs @@ -7,7 +7,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using Penumbra.Api.Enums; -using Penumbra.GameData.ByteString; +using Penumbra.String.Classes; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Mod.TemporaryMod.cs b/Penumbra/Mods/Mod.TemporaryMod.cs index c41dcf57..802852e9 100644 --- a/Penumbra/Mods/Mod.TemporaryMod.cs +++ b/Penumbra/Mods/Mod.TemporaryMod.cs @@ -4,8 +4,8 @@ using System.IO; using System.Linq; using OtterGui.Classes; using Penumbra.Collections; -using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; +using Penumbra.String.Classes; namespace Penumbra.Mods; @@ -59,7 +59,7 @@ public sealed partial class Mod var defaultMod = mod._default; foreach( var (gamePath, fullPath) in collection.ResolvedFiles ) { - if( gamePath.Path.EndsWith( '.', 'i', 'm', 'c' ) ) + if( gamePath.Path.EndsWith( ".imc"u8 ) ) { continue; } diff --git a/Penumbra/Mods/Subclasses/ISubMod.cs b/Penumbra/Mods/Subclasses/ISubMod.cs index 01bfefc6..89bf2053 100644 --- a/Penumbra/Mods/Subclasses/ISubMod.cs +++ b/Penumbra/Mods/Subclasses/ISubMod.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using System.IO; using Newtonsoft.Json; -using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; +using Penumbra.String.Classes; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index 47c777cd..883f74e6 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -4,9 +4,9 @@ using System.IO; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Penumbra.GameData.ByteString; using Penumbra.Import; using Penumbra.Meta.Manipulations; +using Penumbra.String.Classes; namespace Penumbra.Mods; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 791bac47..e0ed235b 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using System.Text; using Dalamud.Game.Command; using Dalamud.Interface.Windowing; @@ -21,6 +22,8 @@ using Penumbra.Interop; using Penumbra.UI; using Penumbra.Util; using Penumbra.Collections; +using Penumbra.GameData; +using Penumbra.GameData.Actors; using Penumbra.Interop.Loader; using Penumbra.Interop.Resolver; using Penumbra.Mods; @@ -55,6 +58,8 @@ public class Penumbra : IDalamudPlugin public static TempModManager TempMods { get; private set; } = null!; public static ResourceLoader ResourceLoader { get; private set; } = null!; public static FrameworkManager Framework { get; private set; } = null!; + public static ActorManager Actors { get; private set; } = null!; + public static readonly List< Exception > ImcExceptions = new(); public readonly ResourceLogger ResourceLogger; @@ -98,6 +103,7 @@ public class Penumbra : IDalamudPlugin ModFileSystem = ModFileSystem.Load(); ObjectReloader = new ObjectReloader(); PathResolver = new PathResolver( ResourceLoader ); + Actors = new ActorManager( Dalamud.Objects, Dalamud.ClientState, Dalamud.GameData, u => ( short )PathResolver.CutsceneActor( u ) ); Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand ) { @@ -144,7 +150,7 @@ public class Penumbra : IDalamudPlugin { Log.Information( $"Penumbra Version {Version}, Commit #{CommitHash} successfully Loaded." ); } - + Dalamud.PluginInterface.UiBuilder.Draw += _windowSystem.Draw; OtterTex.NativeDll.Initialize( Dalamud.PluginInterface.AssemblyLocation.DirectoryName ); @@ -283,6 +289,8 @@ public class Penumbra : IDalamudPlugin public void Dispose() { + Dalamud.PluginInterface.RelinquishData( "test1" ); + Framework?.Dispose(); ShutdownWebServer(); DisposeInterface(); IpcProviders?.Dispose(); @@ -290,7 +298,6 @@ public class Penumbra : IDalamudPlugin ObjectReloader?.Dispose(); ModFileSystem?.Dispose(); CollectionManager?.Dispose(); - Framework?.Dispose(); Dalamud.Commands.RemoveHandler( CommandName ); diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs index 42727936..d8c13c81 100644 --- a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs +++ b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs @@ -9,10 +9,10 @@ using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Files; using Penumbra.Mods; -using Functions = Penumbra.GameData.Util.Functions; +using Penumbra.String.Classes; +using Penumbra.String.Functions; namespace Penumbra.UI.Classes; @@ -459,14 +459,14 @@ public partial class ModEditWindow ref var rows = ref file.ColorSets[ colorSetIdx ].Rows; fixed( void* ptr = data, output = &rows ) { - Functions.MemCpyUnchecked( output, ptr, Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() ); + MemoryUtility.MemCpyUnchecked( output, ptr, Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() ); if( data.Length >= Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() + Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >() && file.ColorDyeSets.Length > colorSetIdx ) { ref var dyeRows = ref file.ColorDyeSets[ colorSetIdx ].Rows; fixed( void* output2 = &dyeRows ) { - Functions.MemCpyUnchecked( output2, ( byte* )ptr + Marshal.SizeOf< MtrlFile.ColorSet.RowArray >(), Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >() ); + MemoryUtility.MemCpyUnchecked( output2, ( byte* )ptr + Marshal.SizeOf< MtrlFile.ColorSet.RowArray >(), Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >() ); } } } @@ -489,8 +489,8 @@ public partial class ModEditWindow var data = new byte[MtrlFile.ColorSet.Row.Size + 2]; fixed( byte* ptr = data ) { - Functions.MemCpyUnchecked( ptr, &row, MtrlFile.ColorSet.Row.Size ); - Functions.MemCpyUnchecked( ptr + MtrlFile.ColorSet.Row.Size, &dye, 2 ); + MemoryUtility.MemCpyUnchecked( ptr, &row, MtrlFile.ColorSet.Row.Size ); + MemoryUtility.MemCpyUnchecked( ptr + MtrlFile.ColorSet.Row.Size, &dye, 2 ); } var text = Convert.ToBase64String( data ); diff --git a/Penumbra/UI/Classes/ModEditWindow.Files.cs b/Penumbra/UI/Classes/ModEditWindow.Files.cs index dff8eaa6..9b9c16d2 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Files.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Files.cs @@ -7,9 +7,8 @@ using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; -using Penumbra.GameData.ByteString; -using Penumbra.GameData.Util; using Penumbra.Mods; +using Penumbra.String.Classes; namespace Penumbra.UI.Classes; diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 36aae29d..8a00234a 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -8,11 +8,11 @@ using Dalamud.Interface.Windowing; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.Import.Textures; using Penumbra.Mods; +using Penumbra.String.Classes; using Penumbra.Util; using static Penumbra.Mods.Mod; diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index f5ffdcf9..10311595 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -32,6 +32,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod SubscribeRightClickFolder( InheritDescendants, 15 ); SubscribeRightClickFolder( OwnDescendants, 15 ); SubscribeRightClickFolder( SetDefaultImportFolder, 100 ); + SubscribeRightClickLeaf( ToggleLeafFavorite, 0 ); SubscribeRightClickMain( ClearDefaultImportFolder, 100 ); AddButton( AddNewModButton, 0 ); AddButton( AddImportModButton, 1 ); @@ -117,9 +118,10 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod protected override void DrawLeafName( FileSystem< Mod >.Leaf leaf, in ModState state, bool selected ) { - var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags; - using var c = ImRaii.PushColor( ImGuiCol.Text, state.Color.Value() ); - using var id = ImRaii.PushId( leaf.Value.Index ); + var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags; + using var c = ImRaii.PushColor( ImGuiCol.Text, state.Color.Value() ) + .Push( ImGuiCol.HeaderHovered, 0x4000FFFF, leaf.Value.Favorite ); + using var id = ImRaii.PushId( leaf.Value.Index ); ImRaii.TreeNode( leaf.Value.Name, flags ).Dispose(); } @@ -157,6 +159,14 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod } } + private static void ToggleLeafFavorite( FileSystem< Mod >.Leaf mod ) + { + if( ImGui.MenuItem( mod.Value.Favorite ? "Remove Favorite" : "Mark as Favorite" ) ) + { + Penumbra.ModManager.ChangeModFavorite( mod.Value.Index, !mod.Value.Favorite ); + } + } + private static void SetDefaultImportFolder( ModFileSystem.Folder folder ) { if( ImGui.MenuItem( "Set As Default Import Folder" ) ) diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index c9cf6284..0d64c1b8 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -9,9 +9,9 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using Penumbra.GameData.ByteString; using Penumbra.Interop.Loader; using Penumbra.Interop.Structs; +using Penumbra.String; using CharacterUtility = Penumbra.Interop.CharacterUtility; namespace Penumbra.UI; @@ -56,6 +56,8 @@ public partial class ConfigWindow ImGui.NewLine(); DrawPathResolverDebug(); ImGui.NewLine(); + DrawActorsDebug(); + ImGui.NewLine(); DrawDebugCharacterUtility(); ImGui.NewLine(); DrawDebugTabMetaLists(); @@ -148,6 +150,31 @@ public partial class ConfigWindow } } + private static unsafe void DrawActorsDebug() + { + if( !ImGui.CollapsingHeader( "Actors" ) ) + { + return; + } + + using var table = ImRaii.Table( "##actors", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + -Vector2.UnitX ); + if( !table ) + { + return; + } + + foreach( var obj in Dalamud.Objects ) + { + ImGuiUtil.DrawTableColumn( $"{( ( GameObject* )obj.Address )->ObjectIndex}" ); + ImGuiUtil.DrawTableColumn( $"0x{obj.Address:X}" ); + var identifier = Penumbra.Actors.FromObject( obj ); + ImGuiUtil.DrawTableColumn( Penumbra.Actors.ToString( identifier ) ); + ImGuiUtil.DrawTableColumn( identifier.DataId.ToString() ); + + } + } + // Draw information about which draw objects correspond to which game objects // and which paths are due to be loaded by which collection. private unsafe void DrawPathResolverDebug() @@ -173,7 +200,7 @@ public partial class ConfigWindow ImGui.TableNextColumn(); var obj = ( GameObject* )Dalamud.Objects.GetObjectAddress( idx ); var (address, name) = - obj != null ? ( $"0x{( ulong )obj:X}", new Utf8String( obj->Name ).ToString() ) : ( "NULL", "NULL" ); + obj != null ? ( $"0x{( ulong )obj:X}", new ByteString( obj->Name ).ToString() ) : ( "NULL", "NULL" ); ImGui.TextUnformatted( address ); ImGui.TableNextColumn(); ImGui.TextUnformatted( name ); diff --git a/Penumbra/UI/ConfigWindow.EffectiveTab.cs b/Penumbra/UI/ConfigWindow.EffectiveTab.cs index d096d3ee..a27307d9 100644 --- a/Penumbra/UI/ConfigWindow.EffectiveTab.cs +++ b/Penumbra/UI/ConfigWindow.EffectiveTab.cs @@ -7,9 +7,9 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using Penumbra.Collections; -using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; using Penumbra.Mods; +using Penumbra.String.Classes; namespace Penumbra.UI; diff --git a/Penumbra/UI/ConfigWindow.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs index e767f755..bf4f44b6 100644 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -9,17 +9,16 @@ using OtterGui; using OtterGui.Raii; using Penumbra.Api.Enums; using Penumbra.Collections; -using Penumbra.GameData.ByteString; -using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; +using Penumbra.String; using Penumbra.UI.Classes; namespace Penumbra.UI; public partial class ConfigWindow { - // Draw text given by a Utf8String. - internal static unsafe void Text( Utf8String s ) + // Draw text given by a ByteString. + internal static unsafe void Text( ByteString s ) => ImGuiNative.igTextUnformatted( s.Path, s.Path + s.Length ); // Draw text given by a byte pointer. @@ -30,8 +29,8 @@ public partial class ConfigWindow private static unsafe void Text( ResourceHandle* resource ) => Text( resource->FileName(), resource->FileNameLength ); - // Draw a Utf8String as a selectable. - internal static unsafe bool Selectable( Utf8String s, bool selected ) + // Draw a ByteString as a selectable. + internal static unsafe bool Selectable( ByteString s, bool selected ) { var tmp = ( byte )( selected ? 1 : 0 ); return ImGuiNative.igSelectable_Bool( s.Path, tmp, ImGuiSelectableFlags.None, Vector2.Zero ) != 0; @@ -77,8 +76,8 @@ public partial class ConfigWindow } // A selectable that copies its text to clipboard on selection and provides a on-hover tooltip about that, - // using an Utf8String. - private static unsafe void CopyOnClickSelectable( Utf8String text ) + // using an ByteString. + private static unsafe void CopyOnClickSelectable( ByteString text ) { if( ImGuiNative.igSelectable_Bool( text.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero ) != 0 ) { diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs index 5fc8dcea..28015b79 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs @@ -6,9 +6,10 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Widgets; -using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; using Penumbra.Mods; +using Penumbra.String; +using Penumbra.String.Classes; using Penumbra.UI.Classes; namespace Penumbra.UI; @@ -32,11 +33,11 @@ public partial class ConfigWindow private Tabs _availableTabs = 0; // Required to use tabs that can not be closed but have a flag to set them open. - private static readonly Utf8String ConflictTabHeader = Utf8String.FromStringUnsafe( "Conflicts", false ); - private static readonly Utf8String DescriptionTabHeader = Utf8String.FromStringUnsafe( "Description", false ); - private static readonly Utf8String SettingsTabHeader = Utf8String.FromStringUnsafe( "Settings", false ); - private static readonly Utf8String ChangedItemsTabHeader = Utf8String.FromStringUnsafe( "Changed Items", false ); - private static readonly Utf8String EditModTabHeader = Utf8String.FromStringUnsafe( "Edit Mod", false ); + private static readonly ByteString ConflictTabHeader = ByteString.FromSpanUnsafe( "Conflicts"u8, true, false, true ); + private static readonly ByteString DescriptionTabHeader = ByteString.FromSpanUnsafe( "Description"u8, true, false, true ); + private static readonly ByteString SettingsTabHeader = ByteString.FromSpanUnsafe( "Settings"u8, true, false, true ); + private static readonly ByteString ChangedItemsTabHeader = ByteString.FromSpanUnsafe( "Changed Items"u8, true, false, true ); + private static readonly ByteString EditModTabHeader = ByteString.FromSpanUnsafe( "Edit Mod"u8, true, false, true ); private readonly TagButtons _modTags = new(); @@ -147,6 +148,7 @@ public partial class ConfigWindow { Penumbra.ModManager.ChangeLocalTag( _mod.Index, tagIdx, editedTag ); } + if( _mod.ModTags.Count > 0 ) { _modTags.Draw( "Mod Tags: ", "Tags assigned by the mod creator and saved with the mod data. To edit these, look at Edit Mod.", _mod.ModTags, out var _, false, @@ -226,7 +228,7 @@ public partial class ConfigWindow // Draw a tab by given name if it is available, and deal with changing the preferred tab. - private ImRaii.IEndObject DrawTab( Utf8String name, Tabs flag ) + private ImRaii.IEndObject DrawTab( ByteString name, Tabs flag ) { if( !_availableTabs.HasFlag( flag ) ) { diff --git a/Penumbra/UI/ConfigWindow.ResourceTab.cs b/Penumbra/UI/ConfigWindow.ResourceTab.cs index 801878cf..ad128ee9 100644 --- a/Penumbra/UI/ConfigWindow.ResourceTab.cs +++ b/Penumbra/UI/ConfigWindow.ResourceTab.cs @@ -8,8 +8,8 @@ using FFXIVClientStructs.STD; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using Penumbra.GameData.ByteString; using Penumbra.Interop.Loader; +using Penumbra.String.Classes; namespace Penumbra.UI; diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs index bf8c0de9..2d47e290 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs @@ -3,8 +3,8 @@ using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using Penumbra.GameData.ByteString; using Penumbra.Interop; +using Penumbra.String.Classes; using Penumbra.UI.Classes; namespace Penumbra.UI; diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index b68a4adc..38fcd25f 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -67,8 +67,12 @@ "penumbra.gamedata": { "type": "Project", "dependencies": { - "Penumbra.Api": "[1.0.0, )" + "Penumbra.Api": "[1.0.0, )", + "Penumbra.String": "[1.0.0, )" } + }, + "penumbra.string": { + "type": "Project" } } }