Extract Strings to separate submodule.

This commit is contained in:
Ottermandias 2022-10-29 15:53:45 +02:00
parent bc901f3ff6
commit 35baba18bf
75 changed files with 751 additions and 1657 deletions

View file

@ -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,
};
}
}

View file

@ -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 );
}
/// <summary> Checks SE naming rules. </summary>
private static bool VerifyPlayerName( ByteString name )
{
// Total no more than 20 characters + space.
if( name.Length is < 5 or > 21 )
{
return false;
}
var split = name.Split( ( byte )' ' );
// Forename and surname, no more spaces.
if( split.Count != 2 )
{
return false;
}
static bool CheckNamePart( ByteString part )
{
// Each name part at least 2 and at most 15 characters.
if( part.Length is < 2 or > 15 )
{
return false;
}
// Each part starting with capitalized letter.
if( part[ 0 ] is < ( byte )'A' or > ( byte )'Z' )
{
return false;
}
// Every other symbol needs to be lowercase letter, hyphen or apostrophe.
if( part.Skip( 1 ).Any( c => c != ( byte )'\'' && c != ( byte )'-' && c is < ( byte )'a' or > ( byte )'z' ) )
{
return false;
}
var hyphens = part.Split( ( byte )'-' );
// Apostrophes can not be used in succession, after or before apostrophes.
return !hyphens.Any( p => p.Length == 0 || p[ 0 ] == ( byte )'\'' || p.Last() == ( byte )'\'' );
}
return CheckNamePart( split[ 0 ] ) && CheckNamePart( split[ 1 ] );
}
/// <summary> Checks if the world is a valid public world or ushort.MaxValue (any world). </summary>
private bool VerifyWorld( ushort worldId )
=> Worlds.ContainsKey( worldId );
/// <summary> Verify that the enum value is a specific actor and return the name if it is. </summary>
private static bool VerifySpecial( SpecialActor actor )
=> actor is >= SpecialActor.CharacterScreen and <= SpecialActor.Portrait;
/// <summary> Verify that the object index is a valid index for an NPC. </summary>
private static bool VerifyIndex( ushort index )
{
return index switch
{
< 200 => index % 2 == 0,
> ( ushort )SpecialActor.Portrait => index < 426,
_ => false,
};
}
/// <summary> Verify that the object kind is a valid owned object, and the corresponding data Id. </summary>
private bool VerifyOwnedData( ObjectKind kind, uint dataId )
{
return kind switch
{
ObjectKind.MountType => Mounts.ContainsKey( dataId ),
ObjectKind.Companion => Companions.ContainsKey( dataId ),
ObjectKind.BattleNpc => BNpcs.ContainsKey( dataId ),
_ => false,
};
}
private bool VerifyNpcData( ObjectKind kind, uint dataId )
=> kind switch
{
ObjectKind.BattleNpc => BNpcs.ContainsKey( dataId ),
ObjectKind.EventNpc => ENpcs.ContainsKey( dataId ),
_ => false,
};
}

View file

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

View file

@ -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,
}

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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" );
}
}

View file

@ -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;
}
}

View file

@ -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() );
}
}
}
}

View file

@ -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() );
}

View file

@ -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() );
}
}
}
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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,
};
}

View file

@ -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,
};
}
}

View file

@ -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
{

View file

@ -36,6 +36,7 @@
<ItemGroup>
<ProjectReference Include="..\Penumbra.Api\Penumbra.Api.csproj" />
<ProjectReference Include="..\Penumbra.String\Penumbra.String.csproj" />
</ItemGroup>
<ItemGroup>
@ -51,6 +52,10 @@
<HintPath>$(DalamudLibPath)Lumina.Excel.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="FFXIVClientStructs">
<HintPath>$(DalamudLibPath)FFXIVClientStructs.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>$(DalamudLibPath)Newtonsoft.Json.dll</HintPath>
<Private>False</Private>

View file

@ -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;
}

View file

@ -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 );

View file

@ -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 );
}