Merge branch 'develop' into MetaData

This commit is contained in:
Ottermandias 2021-04-19 17:17:14 +02:00
commit ba7cf17d68
8 changed files with 329 additions and 167 deletions

View file

@ -65,7 +65,7 @@ namespace Penumbra.Importer
} }
// You can in no way rely on any file paths in TTMPs so we need to just do this, sorry // You can in no way rely on any file paths in TTMPs so we need to just do this, sorry
private ZipEntry FindZipEntry( ZipFile file, string fileName ) private static ZipEntry? FindZipEntry( ZipFile file, string fileName )
{ {
for( var i = 0; i < file.Count; i++ ) for( var i = 0; i < file.Count; i++ )
{ {
@ -84,7 +84,10 @@ namespace Penumbra.Importer
// write shitty zip garbage to disk // write shitty zip garbage to disk
var entry = FindZipEntry( file, entryName ); var entry = FindZipEntry( file, entryName );
Debug.Assert( entry != null, $"Could not find in mod zip: {entryName}" ); if( entry == null )
{
throw new FileNotFoundException( $"ZIP does not contain a file named {entryName}." );
}
using var s = file.GetInputStream( entry ); using var s = file.GetInputStream( entry );
@ -100,7 +103,10 @@ namespace Penumbra.Importer
using var extractedModPack = new ZipFile( zfs ); using var extractedModPack = new ZipFile( zfs );
var mpl = FindZipEntry( extractedModPack, "TTMPL.mpl" ); var mpl = FindZipEntry( extractedModPack, "TTMPL.mpl" );
Debug.Assert( mpl != null, "Could not find mod meta in ZIP." ); if( mpl == null )
{
throw new FileNotFoundException( "ZIP does not contain a TTMPL.mpl file." );
}
var modRaw = GetStringFromZipEntry( extractedModPack, mpl, Encoding.UTF8 ); var modRaw = GetStringFromZipEntry( extractedModPack, mpl, Encoding.UTF8 );

View file

@ -20,6 +20,8 @@ namespace Penumbra.Models
public List< string > ChangedItems { get; set; } = new(); public List< string > ChangedItems { get; set; } = new();
[JsonProperty( ItemConverterType = typeof( GamePathConverter ))]
public Dictionary< GamePath, GamePath > FileSwaps { get; } = new(); public Dictionary< GamePath, GamePath > FileSwaps { get; } = new();
public Dictionary< string, OptionGroup > Groups { get; set; } = new(); public Dictionary< string, OptionGroup > Groups { get; set; } = new();

View file

@ -53,14 +53,14 @@ namespace Penumbra.UI
return; return;
} }
if( ImGui.ListBoxHeader( "##effective_files", AutoFillSize ) ) if( ImGui.BeginListBox( "##effective_files", AutoFillSize ) )
{ {
foreach( var file in _mods.ResolvedFiles ) foreach( var file in _mods.ResolvedFiles )
{ {
DrawFileLine( file.Value, file.Key ); DrawFileLine( file.Value, file.Key );
} }
ImGui.ListBoxFooter(); ImGui.EndListBox();
} }
ImGui.EndTabItem(); ImGui.EndTabItem();

View file

@ -45,8 +45,8 @@ namespace Penumbra.UI
private const string LabelConfigurationTab = "Configuration"; private const string LabelConfigurationTab = "Configuration";
private const string TooltipFilesTab = private const string TooltipFilesTab =
"Green files replace their standard game path counterpart (not in any option) or are in all options of a Single-Select option.\n" + "Green files replace their standard game path counterpart (not in any option) or are in all options of a Single-Select option.\n"
"Yellow files are restricted to some options."; + "Yellow files are restricted to some options.";
private const float TextSizePadding = 5f; private const float TextSizePadding = 5f;
private const float OptionSelectionWidth = 140f; private const float OptionSelectionWidth = 140f;
@ -130,8 +130,11 @@ namespace Penumbra.UI
} }
// This is only drawn when we have a mod selected, so we can forgive nulls. // This is only drawn when we have a mod selected, so we can forgive nulls.
private ModInfo Mod => _selector.Mod()!; private ModInfo Mod
private ModMeta Meta => Mod.Mod.Meta; => _selector.Mod()!;
private ModMeta Meta
=> Mod.Mod.Meta;
private void Save() private void Save()
{ {
@ -193,10 +196,10 @@ namespace Penumbra.UI
if( ImGui.BeginTabItem( LabelChangedItemsTab ) ) if( ImGui.BeginTabItem( LabelChangedItemsTab ) )
{ {
ImGui.SetNextItemWidth( -1 ); ImGui.SetNextItemWidth( -1 );
if( ImGui.ListBoxHeader( LabelChangedItemsHeader, AutoFillSize ) ) if( ImGui.BeginListBox( LabelChangedItemsHeader, AutoFillSize ) )
{ {
_changedItemsList ??= Meta.ChangedItems _changedItemsList ??= Meta.ChangedItems
.Select( ( I, index ) => ( $"{LabelChangedItemIdx}{index}", I ) ).ToArray(); .Select( ( I, index ) => ( $"{LabelChangedItemIdx}{index}", I ) ).ToArray();
for( var i = 0; i < Meta.ChangedItems.Count; ++i ) for( var i = 0; i < Meta.ChangedItems.Count; ++i )
{ {
@ -212,15 +215,15 @@ namespace Penumbra.UI
if( _editMode ) if( _editMode )
{ {
ImGui.SetNextItemWidth( -1 ); ImGui.SetNextItemWidth( -1 );
if( ImGui.InputText( LabelChangedItemNew, ref newItem, 128, flags ) if( ImGui.InputTextWithHint( LabelChangedItemNew, "Enter new changed item...", ref newItem, 128, flags )
&& newItem.Length > 0 ) && newItem.Length > 0 )
{ {
Meta.ChangedItems.Add( newItem ); Meta.ChangedItems.Add( newItem );
_selector.SaveCurrentMod(); _selector.SaveCurrentMod();
} }
} }
ImGui.ListBoxFooter(); ImGui.EndListBox();
} }
ImGui.EndTabItem(); ImGui.EndTabItem();
@ -239,7 +242,7 @@ namespace Penumbra.UI
} }
ImGui.SetNextItemWidth( -1 ); ImGui.SetNextItemWidth( -1 );
if( ImGui.ListBoxHeader( LabelConflictsHeader, AutoFillSize ) ) if( ImGui.BeginListBox( LabelConflictsHeader, AutoFillSize ) )
{ {
foreach( var kv in Mod.Mod.FileConflicts ) foreach( var kv in Mod.Mod.FileConflicts )
{ {
@ -258,7 +261,7 @@ namespace Penumbra.UI
ImGui.Unindent( 15 ); ImGui.Unindent( 15 );
} }
ImGui.ListBoxFooter(); ImGui.EndListBox();
} }
ImGui.EndTabItem(); ImGui.EndTabItem();
@ -266,6 +269,12 @@ namespace Penumbra.UI
private void DrawFileSwapTab() private void DrawFileSwapTab()
{ {
if( _editMode )
{
DrawFileSwapTabEdit();
return;
}
if( !Meta.FileSwaps.Any() ) if( !Meta.FileSwaps.Any() )
{ {
return; return;
@ -274,10 +283,11 @@ namespace Penumbra.UI
if( ImGui.BeginTabItem( LabelFileSwapTab ) ) if( ImGui.BeginTabItem( LabelFileSwapTab ) )
{ {
_fileSwapOffset ??= Meta.FileSwaps _fileSwapOffset ??= Meta.FileSwaps
.Max( P => ImGui.CalcTextSize( P.Key ).X ) + TextSizePadding; .Max( P => ImGui.CalcTextSize( P.Key ).X )
+ TextSizePadding;
ImGui.SetNextItemWidth( -1 ); ImGui.SetNextItemWidth( -1 );
if( ImGui.ListBoxHeader( LabelFileSwapHeader, AutoFillSize ) ) if( ImGui.BeginListBox( LabelFileSwapHeader, AutoFillSize ) )
{ {
foreach( var file in Meta.FileSwaps ) foreach( var file in Meta.FileSwaps )
{ {
@ -288,7 +298,7 @@ namespace Penumbra.UI
ImGui.Selectable( file.Value ); ImGui.Selectable( file.Value );
} }
ImGui.ListBoxFooter(); ImGui.EndListBox();
} }
ImGui.EndTabItem(); ImGui.EndTabItem();
@ -308,7 +318,7 @@ namespace Penumbra.UI
var len = Mod.Mod.ModBasePath.FullName.Length; var len = Mod.Mod.ModBasePath.FullName.Length;
_fullFilenameList = Mod.Mod.ModFiles _fullFilenameList = Mod.Mod.ModFiles
.Select( F => ( F, false, ColorGreen, new RelPath( F, Mod.Mod.ModBasePath ) ) ).ToArray(); .Select( F => ( F, false, ColorGreen, new RelPath( F, Mod.Mod.ModBasePath ) ) ).ToArray();
if( Meta.Groups.Count == 0 ) if( Meta.Groups.Count == 0 )
{ {
@ -353,7 +363,7 @@ namespace Penumbra.UI
} }
ImGui.SetNextItemWidth( -1 ); ImGui.SetNextItemWidth( -1 );
if( ImGui.ListBoxHeader( LabelFileListHeader, AutoFillSize ) ) if( ImGui.BeginListBox( LabelFileListHeader, AutoFillSize ) )
{ {
UpdateFilenameList(); UpdateFilenameList();
foreach( var file in _fullFilenameList! ) foreach( var file in _fullFilenameList! )
@ -363,7 +373,7 @@ namespace Penumbra.UI
ImGui.PopStyleColor(); ImGui.PopStyleColor();
} }
ImGui.ListBoxFooter(); ImGui.EndListBox();
} }
else else
{ {
@ -389,7 +399,7 @@ namespace Penumbra.UI
} }
if( path[ TextDefaultGamePath.Length ] != '-' if( path[ TextDefaultGamePath.Length ] != '-'
|| !int.TryParse( path.Substring( TextDefaultGamePath.Length + 1 ), out removeFolders ) ) || !int.TryParse( path.Substring( TextDefaultGamePath.Length + 1 ), out removeFolders ) )
{ {
return -1; return -1;
} }
@ -442,7 +452,7 @@ namespace Penumbra.UI
else else
{ {
changed = gamePaths changed = gamePaths
.Aggregate( changed, ( current, gamePath ) => current | option.AddFile( relName, gamePath ) ); .Aggregate( changed, ( current, gamePath ) => current | option.AddFile( relName, gamePath ) );
} }
} }
@ -473,7 +483,7 @@ namespace Penumbra.UI
ImGui.TextUnformatted( LabelGamePathsEdit ); ImGui.TextUnformatted( LabelGamePathsEdit );
ImGui.SameLine(); ImGui.SameLine();
ImGui.SetNextItemWidth( -1 ); ImGui.SetNextItemWidth( -1 );
ImGui.InputText( LabelGamePathsEditBox, ref _currentGamePaths, 128 ); ImGui.InputTextWithHint( LabelGamePathsEditBox, "Hover for help...", ref _currentGamePaths, 128 );
if( ImGui.IsItemHovered() ) if( ImGui.IsItemHovered() )
{ {
ImGui.SetTooltip( TooltipGamePathsEdit ); ImGui.SetTooltip( TooltipGamePathsEdit );
@ -545,7 +555,7 @@ namespace Penumbra.UI
{ {
string tmp = gamePath; string tmp = gamePath;
if( ImGui.InputText( $"##{fileName}_{gamePath}", ref tmp, 128, ImGuiInputTextFlags.EnterReturnsTrue ) if( ImGui.InputText( $"##{fileName}_{gamePath}", ref tmp, 128, ImGuiInputTextFlags.EnterReturnsTrue )
&& tmp != gamePath ) && tmp != gamePath )
{ {
gamePaths.Remove( gamePath ); gamePaths.Remove( gamePath );
if( tmp.Length > 0 ) if( tmp.Length > 0 )

View file

@ -25,13 +25,13 @@ namespace Penumbra.UI
private const char GamePathsSeparator = ';'; private const char GamePathsSeparator = ';';
private static readonly string TooltipFilesTabEdit = private static readonly string TooltipFilesTabEdit =
$"{TooltipFilesTab}\n" + $"{TooltipFilesTab}\n"
$"Red Files are replaced in another group or a different option in this group, but not contained in the current option."; + $"Red Files are replaced in another group or a different option in this group, but not contained in the current option.";
private static readonly string TooltipGamePathsEdit = private static readonly string TooltipGamePathsEdit =
$"Enter all game paths to add or remove, separated by '{GamePathsSeparator}'.\n" + $"Enter all game paths to add or remove, separated by '{GamePathsSeparator}'.\n"
$"Use '{TextDefaultGamePath}' to add the original file path." + + $"Use '{TextDefaultGamePath}' to add the original file path."
$"Use '{TextDefaultGamePath}-#' to skip the first # relative directories."; + $"Use '{TextDefaultGamePath}-#' to skip the first # relative directories.";
private const float MultiEditBoxWidth = 300f; private const float MultiEditBoxWidth = 300f;
@ -86,7 +86,7 @@ namespace Penumbra.UI
} }
ImGui.SetNextItemWidth( -1 ); ImGui.SetNextItemWidth( -1 );
if( ImGui.ListBoxHeader( LabelFileListHeader, AutoFillSize - new Vector2( 0, 1.5f * ImGui.GetTextLineHeight() ) ) ) if( ImGui.BeginListBox( LabelFileListHeader, AutoFillSize - new Vector2( 0, 1.5f * ImGui.GetTextLineHeight() ) ) )
{ {
for( var i = 0; i < Mod!.Mod.ModFiles.Count; ++i ) for( var i = 0; i < Mod!.Mod.ModFiles.Count; ++i )
{ {
@ -94,7 +94,7 @@ namespace Penumbra.UI
} }
} }
ImGui.ListBoxFooter(); ImGui.EndListBox();
DrawGroupRow(); DrawGroupRow();
ImGui.EndTabItem(); ImGui.EndTabItem();
@ -109,7 +109,8 @@ namespace Penumbra.UI
{ {
var groupName = group.GroupName; var groupName = group.GroupName;
if( ImGuiCustom.BeginFramedGroupEdit( ref groupName ) if( ImGuiCustom.BeginFramedGroupEdit( ref groupName )
&& groupName != group.GroupName && !Meta!.Groups.ContainsKey( groupName ) ) && groupName != group.GroupName
&& !Meta!.Groups.ContainsKey( groupName ) )
{ {
var oldConf = Mod!.Settings[ group.GroupName ]; var oldConf = Mod!.Settings[ group.GroupName ];
Meta.Groups.Remove( group.GroupName ); Meta.Groups.Remove( group.GroupName );
@ -120,7 +121,7 @@ namespace Penumbra.UI
{ {
GroupName = groupName, GroupName = groupName,
SelectionType = SelectType.Multi, SelectionType = SelectType.Multi,
Options = group.Options Options = group.Options,
}; };
Mod.Settings[ groupName ] = oldConf; Mod.Settings[ groupName ] = oldConf;
} }
@ -136,8 +137,9 @@ namespace Penumbra.UI
var newOption = ""; var newOption = "";
ImGui.SetCursorPosX( nameBoxStart ); ImGui.SetCursorPosX( nameBoxStart );
ImGui.SetNextItemWidth( MultiEditBoxWidth ); ImGui.SetNextItemWidth( MultiEditBoxWidth );
if( ImGui.InputText( $"##new_{group.GroupName}_l", ref newOption, 64, ImGuiInputTextFlags.EnterReturnsTrue ) if( ImGui.InputTextWithHint( $"##new_{group.GroupName}_l", "Add new option...", ref newOption, 64,
&& newOption.Length != 0 ) ImGuiInputTextFlags.EnterReturnsTrue )
&& newOption.Length != 0 )
{ {
group.Options.Add( new Option() group.Options.Add( new Option()
{ OptionName = newOption, OptionDesc = "", OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >() } ); { OptionName = newOption, OptionDesc = "", OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >() } );
@ -200,7 +202,7 @@ namespace Penumbra.UI
{ {
var groupName = group.GroupName; var groupName = group.GroupName;
if( ImGui.InputText( $"##{groupName}_add", ref groupName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) if( ImGui.InputText( $"##{groupName}_add", ref groupName, 64, ImGuiInputTextFlags.EnterReturnsTrue )
&& !Meta!.Groups.ContainsKey( groupName ) ) && !Meta!.Groups.ContainsKey( groupName ) )
{ {
var oldConf = Mod!.Settings[ group.GroupName ]; var oldConf = Mod!.Settings[ group.GroupName ];
if( groupName != group.GroupName ) if( groupName != group.GroupName )
@ -215,7 +217,7 @@ namespace Penumbra.UI
{ {
GroupName = groupName, GroupName = groupName,
Options = group.Options, Options = group.Options,
SelectionType = SelectType.Single SelectionType = SelectType.Single,
} ); } );
Mod.Settings[ groupName ] = oldConf; Mod.Settings[ groupName ] = oldConf;
} }
@ -245,7 +247,7 @@ namespace Penumbra.UI
{ {
OptionName = newName, OptionName = newName,
OptionDesc = "", OptionDesc = "",
OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >() OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(),
} ); } );
} }
} }
@ -264,7 +266,7 @@ namespace Penumbra.UI
group.Options[ code ] = new Option() group.Options[ code ] = new Option()
{ {
OptionName = newName, OptionDesc = group.Options[ code ].OptionDesc, OptionName = newName, OptionDesc = group.Options[ code ].OptionDesc,
OptionFiles = group.Options[ code ].OptionFiles OptionFiles = group.Options[ code ].OptionFiles,
}; };
} }
@ -304,7 +306,7 @@ namespace Penumbra.UI
{ {
GroupName = newGroup, GroupName = newGroup,
SelectionType = selectType, SelectionType = selectType,
Options = new List< Option >() Options = new List< Option >(),
}; };
Mod.Settings[ newGroup ] = 0; Mod.Settings[ newGroup ] = 0;
@ -314,12 +316,13 @@ namespace Penumbra.UI
private void DrawAddSingleGroupField( float labelEditPos ) private void DrawAddSingleGroupField( float labelEditPos )
{ {
var newGroup = ""; const string hint = "Add new Single Group...";
var newGroup = "";
if( labelEditPos == CheckMarkSize ) if( labelEditPos == CheckMarkSize )
{ {
ImGui.SetCursorPosX( CheckMarkSize ); ImGui.SetCursorPosX( CheckMarkSize );
ImGui.SetNextItemWidth( MultiEditBoxWidth ); ImGui.SetNextItemWidth( MultiEditBoxWidth );
if( ImGui.InputText( LabelNewSingleGroup, ref newGroup, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) if( ImGui.InputTextWithHint( LabelNewSingleGroup, hint, ref newGroup, 64, ImGuiInputTextFlags.EnterReturnsTrue ) )
{ {
AddNewGroup( newGroup, SelectType.Single ); AddNewGroup( newGroup, SelectType.Single );
} }
@ -327,7 +330,7 @@ namespace Penumbra.UI
else else
{ {
ImGuiCustom.RightJustifiedLabel( labelEditPos, LabelNewSingleGroup ); ImGuiCustom.RightJustifiedLabel( labelEditPos, LabelNewSingleGroup );
if( ImGui.InputText( LabelNewSingleGroupEdit, ref newGroup, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) if( ImGui.InputTextWithHint( LabelNewSingleGroupEdit, hint, ref newGroup, 64, ImGuiInputTextFlags.EnterReturnsTrue ) )
{ {
AddNewGroup( newGroup, SelectType.Single ); AddNewGroup( newGroup, SelectType.Single );
} }
@ -339,7 +342,7 @@ namespace Penumbra.UI
var newGroup = ""; var newGroup = "";
ImGui.SetCursorPosX( CheckMarkSize ); ImGui.SetCursorPosX( CheckMarkSize );
ImGui.SetNextItemWidth( MultiEditBoxWidth ); ImGui.SetNextItemWidth( MultiEditBoxWidth );
if( ImGui.InputText( LabelNewMultiGroup, ref newGroup, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) if( ImGui.InputTextWithHint( LabelNewMultiGroup, "Add new Multi Group...", ref newGroup, 64, ImGuiInputTextFlags.EnterReturnsTrue ) )
{ {
AddNewGroup( newGroup, SelectType.Multi ); AddNewGroup( newGroup, SelectType.Multi );
} }
@ -362,6 +365,87 @@ namespace Penumbra.UI
DrawAddMultiGroupField(); DrawAddMultiGroupField();
} }
private void DrawFileSwapTabEdit()
{
const string arrow = " -> ";
if( ImGui.BeginTabItem( LabelFileSwapTab ) )
{
ImGui.SetNextItemWidth( -1 );
if( ImGui.BeginListBox( LabelFileSwapHeader, AutoFillSize ) )
{
var swaps = Meta.FileSwaps.Keys.ToArray();
var arrowWidth = ImGui.CalcTextSize( arrow ).X;
var width = ( ImGui.GetWindowWidth() - arrowWidth - 4 * ImGui.GetStyle().ItemSpacing.X ) / 2;
for( var idx = 0; idx < swaps.Length + 1; ++idx )
{
var key = idx == swaps.Length ? GamePath.GenerateUnchecked( "" ) : swaps[ idx ];
var value = idx == swaps.Length ? GamePath.GenerateUnchecked( "" ) : Meta.FileSwaps[ key ];
string keyString = key;
string valueString = value;
ImGui.SetNextItemWidth( width );
if( ImGui.InputTextWithHint( $"##swapLhs_{idx}", "Enter new file to be replaced...", ref keyString,
GamePath.MaxGamePathLength, ImGuiInputTextFlags.EnterReturnsTrue ) )
{
var newKey = new GamePath( keyString );
if( newKey.CompareTo( key ) != 0 )
{
if( idx < swaps.Length )
{
Meta.FileSwaps.Remove( key );
}
if( newKey != string.Empty )
{
Meta.FileSwaps[ newKey ] = value;
}
_selector.SaveCurrentMod();
if( Mod.Enabled )
{
_selector.ReloadCurrentMod();
}
}
}
if( idx < swaps.Length )
{
ImGui.SameLine();
ImGui.TextUnformatted( arrow );
ImGui.SameLine();
ImGui.SetNextItemWidth( width );
if( ImGui.InputTextWithHint( $"##swapRhs_{idx}", "Enter new replacement path...", ref valueString,
GamePath.MaxGamePathLength,
ImGuiInputTextFlags.EnterReturnsTrue ) )
{
var newValue = new GamePath( valueString );
if( newValue.CompareTo( value ) != 0 )
{
Meta.FileSwaps[ key ] = newValue;
_selector.SaveCurrentMod();
if( Mod.Enabled )
{
_selector.ReloadCurrentMod();
}
}
}
}
}
ImGui.EndListBox();
}
ImGui.EndTabItem();
}
else
{
_fileSwapOffset = null;
}
}
} }
} }
} }

View file

@ -121,7 +121,7 @@ namespace Penumbra.UI
{ {
ImGui.SetNextItemWidth( SelectorButtonSizes.X * 4 ); ImGui.SetNextItemWidth( SelectorButtonSizes.X * 4 );
var tmp = _modFilter; var tmp = _modFilter;
if( ImGui.InputText( LabelModFilter, ref tmp, 256 ) ) if( ImGui.InputTextWithHint( LabelModFilter, "Filter Mods...", ref tmp, 256 ) )
{ {
_modFilter = tmp.ToLowerInvariant(); _modFilter = tmp.ToLowerInvariant();
} }

View file

@ -1,6 +1,8 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Penumbra.Util namespace Penumbra.Util
{ {
@ -29,7 +31,7 @@ namespace Penumbra.Util
=> _path = CheckPre( file, baseDir ) ? Trim( Substring( file, baseDir ) ) : ""; => _path = CheckPre( file, baseDir ) ? Trim( Substring( file, baseDir ) ) : "";
public RelPath( GamePath gamePath ) public RelPath( GamePath gamePath )
=> _path = gamePath ? ReplaceSlash( gamePath ) : ""; => _path = ReplaceSlash( gamePath );
private static bool CheckPre( FileInfo file, DirectoryInfo baseDir ) private static bool CheckPre( FileInfo file, DirectoryInfo baseDir )
=> file.FullName.StartsWith( baseDir.FullName ) && file.FullName.Length < MaxRelPathLength; => file.FullName.StartsWith( baseDir.FullName ) && file.FullName.Length < MaxRelPathLength;
@ -43,22 +45,22 @@ namespace Penumbra.Util
private static string Trim( string path ) private static string Trim( string path )
=> path.TrimStart( '\\' ); => path.TrimStart( '\\' );
public static implicit operator bool( RelPath relPath )
=> relPath._path.Length > 0;
public static implicit operator string( RelPath relPath ) public static implicit operator string( RelPath relPath )
=> relPath._path; => relPath._path;
public static explicit operator RelPath( string relPath ) public static explicit operator RelPath( string relPath )
=> new( relPath ); => new( relPath );
public bool Empty
=> _path.Length == 0;
public int CompareTo( object rhs ) public int CompareTo( object rhs )
{ {
return rhs switch return rhs switch
{ {
string => string.Compare( _path, _path, StringComparison.InvariantCulture ), string path => string.Compare( _path, path, StringComparison.InvariantCulture ),
RelPath path => string.Compare( _path, path._path, StringComparison.InvariantCulture ), RelPath path => string.Compare( _path, path._path, StringComparison.InvariantCulture ),
_ => -1 _ => -1,
}; };
} }
@ -120,10 +122,7 @@ namespace Penumbra.Util
=> new( path, true ); => new( path, true );
public static GamePath GenerateUncheckedLower( string path ) public static GamePath GenerateUncheckedLower( string path )
=> new( Lower(path), true ); => new( Lower( path ), true );
public static implicit operator bool( GamePath gamePath )
=> gamePath._path.Length > 0;
public static implicit operator string( GamePath gamePath ) public static implicit operator string( GamePath gamePath )
=> gamePath._path; => gamePath._path;
@ -131,6 +130,9 @@ namespace Penumbra.Util
public static explicit operator GamePath( string gamePath ) public static explicit operator GamePath( string gamePath )
=> new( gamePath ); => new( gamePath );
public bool Empty
=> _path.Length == 0;
public string Filename() public string Filename()
{ {
var idx = _path.LastIndexOf( "/", StringComparison.Ordinal ); var idx = _path.LastIndexOf( "/", StringComparison.Ordinal );
@ -143,11 +145,35 @@ namespace Penumbra.Util
{ {
string path => string.Compare( _path, path, StringComparison.InvariantCulture ), string path => string.Compare( _path, path, StringComparison.InvariantCulture ),
GamePath path => string.Compare( _path, path._path, StringComparison.InvariantCulture ), GamePath path => string.Compare( _path, path._path, StringComparison.InvariantCulture ),
_ => -1 _ => -1,
}; };
} }
public override string ToString() public override string ToString()
=> _path; => _path;
} }
}
public class GamePathConverter : JsonConverter
{
public override bool CanConvert( Type objectType )
=> objectType == typeof( GamePath );
public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer )
{
var token = JToken.Load( reader );
return token.ToObject< GamePath >();
}
public override bool CanWrite
=> true;
public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer )
{
if( value != null )
{
var v = ( GamePath) value;
serializer.Serialize( writer, v.ToString() );
}
}
}
}

View file

@ -1,14 +1,10 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Threading.Tasks;
using Lumina; using Lumina;
using Lumina.Data;
using Lumina.Data.Structs; using Lumina.Data.Structs;
namespace Penumbra.Util namespace Penumbra.Util
@ -19,12 +15,14 @@ namespace Penumbra.Util
protected BinaryReader Reader { get; set; } protected BinaryReader Reader { get; set; }
public PenumbraSqPackStream( FileInfo file ) : this( file.OpenRead() ) {} public PenumbraSqPackStream( FileInfo file )
: this( file.OpenRead() )
{ }
public PenumbraSqPackStream( Stream stream ) public PenumbraSqPackStream( Stream stream )
{ {
BaseStream = stream; BaseStream = stream;
Reader = new BinaryReader( BaseStream ); Reader = new BinaryReader( BaseStream );
} }
public SqPackHeader GetSqPackHeader() public SqPackHeader GetSqPackHeader()
@ -48,7 +46,7 @@ namespace Penumbra.Util
BaseStream.Position = offset; BaseStream.Position = offset;
var fileInfo = Reader.ReadStructure< SqPackFileInfo >(); var fileInfo = Reader.ReadStructure< SqPackFileInfo >();
var file = Activator.CreateInstance< T >(); var file = Activator.CreateInstance< T >();
// check if we need to read the extended model header or just default to the standard file header // check if we need to read the extended model header or just default to the standard file header
if( fileInfo.Type == FileType.Model ) if( fileInfo.Type == FileType.Model )
@ -59,32 +57,31 @@ namespace Penumbra.Util
file.FileInfo = new PenumbraFileInfo file.FileInfo = new PenumbraFileInfo
{ {
HeaderSize = modelFileInfo.Size, HeaderSize = modelFileInfo.Size,
Type = modelFileInfo.Type, Type = modelFileInfo.Type,
BlockCount = modelFileInfo.UsedNumberOfBlocks, BlockCount = modelFileInfo.UsedNumberOfBlocks,
RawFileSize = modelFileInfo.RawFileSize, RawFileSize = modelFileInfo.RawFileSize,
Offset = offset, Offset = offset,
// todo: is this useful? // todo: is this useful?
ModelBlock = modelFileInfo ModelBlock = modelFileInfo,
}; };
} }
else else
{ {
file.FileInfo = new PenumbraFileInfo file.FileInfo = new PenumbraFileInfo
{ {
HeaderSize = fileInfo.Size, HeaderSize = fileInfo.Size,
Type = fileInfo.Type, Type = fileInfo.Type,
BlockCount = fileInfo.NumberOfBlocks, BlockCount = fileInfo.NumberOfBlocks,
RawFileSize = fileInfo.RawFileSize, RawFileSize = fileInfo.RawFileSize,
Offset = offset Offset = offset,
}; };
} }
switch( fileInfo.Type ) switch( fileInfo.Type )
{ {
case FileType.Empty: case FileType.Empty: throw new FileNotFoundException( $"The file located at 0x{offset:x} is empty." );
throw new FileNotFoundException( $"The file located at 0x{offset:x} is empty." );
case FileType.Standard: case FileType.Standard:
ReadStandardFile( file, ms ); ReadStandardFile( file, ms );
@ -98,16 +95,17 @@ namespace Penumbra.Util
ReadTextureFile( file, ms ); ReadTextureFile( file, ms );
break; break;
default: default: throw new NotImplementedException( $"File Type {( uint )fileInfo.Type} is not implemented." );
throw new NotImplementedException( $"File Type {(UInt32)fileInfo.Type} is not implemented." );
} }
file.Data = ms.ToArray(); file.Data = ms.ToArray();
if( file.Data.Length != file.FileInfo.RawFileSize ) if( file.Data.Length != file.FileInfo.RawFileSize )
{
Debug.WriteLine( "Read data size does not match file size." ); Debug.WriteLine( "Read data size does not match file size." );
}
file.FileStream = new MemoryStream( file.Data, false ); file.FileStream = new MemoryStream( file.Data, false );
file.Reader = new BinaryReader( file.FileStream ); file.Reader = new BinaryReader( file.FileStream );
file.FileStream.Position = 0; file.FileStream.Position = 0;
file.LoadFile(); file.LoadFile();
@ -117,7 +115,7 @@ namespace Penumbra.Util
private void ReadStandardFile( PenumbraFileResource resource, MemoryStream ms ) private void ReadStandardFile( PenumbraFileResource resource, MemoryStream ms )
{ {
var blocks = Reader.ReadStructures< DatStdFileBlockInfos >( (int)resource.FileInfo.BlockCount ); var blocks = Reader.ReadStructures< DatStdFileBlockInfos >( ( int )resource.FileInfo!.BlockCount );
foreach( var block in blocks ) foreach( var block in blocks )
{ {
@ -128,9 +126,10 @@ namespace Penumbra.Util
ms.Position = 0; ms.Position = 0;
} }
private unsafe void ReadModelFile( PenumbraFileResource resource, MemoryStream ms ) { private unsafe void ReadModelFile( PenumbraFileResource resource, MemoryStream ms )
var mdlBlock = resource.FileInfo.ModelBlock; {
long baseOffset = resource.FileInfo.Offset + resource.FileInfo.HeaderSize; var mdlBlock = resource.FileInfo!.ModelBlock;
var baseOffset = resource.FileInfo.Offset + resource.FileInfo.HeaderSize;
// 1/1/3/3/3 stack/runtime/vertex/egeo/index // 1/1/3/3/3 stack/runtime/vertex/egeo/index
// TODO: consider testing if this is more reliable than the Explorer method // TODO: consider testing if this is more reliable than the Explorer method
@ -139,89 +138,111 @@ namespace Penumbra.Util
// but it seems to work fine in explorer... // but it seems to work fine in explorer...
int totalBlocks = mdlBlock.StackBlockNum; int totalBlocks = mdlBlock.StackBlockNum;
totalBlocks += mdlBlock.RuntimeBlockNum; totalBlocks += mdlBlock.RuntimeBlockNum;
for( int i = 0; i < 3; i++ ) for( var i = 0; i < 3; i++ )
{
totalBlocks += mdlBlock.VertexBufferBlockNum[ i ]; totalBlocks += mdlBlock.VertexBufferBlockNum[ i ];
for( int i = 0; i < 3; i++ ) }
totalBlocks += mdlBlock.EdgeGeometryVertexBufferBlockNum[ i ];
for( int i = 0; i < 3; i++ )
totalBlocks += mdlBlock.IndexBufferBlockNum[ i ];
var compressedBlockSizes = Reader.ReadStructures< UInt16 >( totalBlocks ); for( var i = 0; i < 3; i++ )
int currentBlock = 0; {
int stackSize; totalBlocks += mdlBlock.EdgeGeometryVertexBufferBlockNum[ i ];
int runtimeSize; }
int[] vertexDataOffsets = new int[3];
int[] indexDataOffsets = new int[3]; for( var i = 0; i < 3; i++ )
int[] vertexBufferSizes = new int[3]; {
int[] indexBufferSizes = new int[3]; totalBlocks += mdlBlock.IndexBufferBlockNum[ i ];
}
var compressedBlockSizes = Reader.ReadStructures< ushort >( totalBlocks );
var currentBlock = 0;
var vertexDataOffsets = new int[3];
var indexDataOffsets = new int[3];
var vertexBufferSizes = new int[3];
var indexBufferSizes = new int[3];
ms.Seek( 0x44, SeekOrigin.Begin ); ms.Seek( 0x44, SeekOrigin.Begin );
Reader.Seek( baseOffset + mdlBlock.StackOffset ); Reader.Seek( baseOffset + mdlBlock.StackOffset );
long stackStart = ms.Position; var stackStart = ms.Position;
for( int i = 0; i < mdlBlock.StackBlockNum; i++ ) { for( var i = 0; i < mdlBlock.StackBlockNum; i++ )
long lastPos = Reader.BaseStream.Position; {
var lastPos = Reader.BaseStream.Position;
ReadFileBlock( ms ); ReadFileBlock( ms );
Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] ); Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] );
currentBlock++; currentBlock++;
} }
long stackEnd = ms.Position; var stackEnd = ms.Position;
stackSize = (int) ( stackEnd - stackStart ); var stackSize = ( int )( stackEnd - stackStart );
Reader.Seek( baseOffset + mdlBlock.RuntimeOffset ); Reader.Seek( baseOffset + mdlBlock.RuntimeOffset );
long runtimeStart = ms.Position; var runtimeStart = ms.Position;
for( int i = 0; i < mdlBlock.RuntimeBlockNum; i++ ) { for( var i = 0; i < mdlBlock.RuntimeBlockNum; i++ )
long lastPos = Reader.BaseStream.Position; {
var lastPos = Reader.BaseStream.Position;
ReadFileBlock( ms ); ReadFileBlock( ms );
Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] ); Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] );
currentBlock++; currentBlock++;
} }
long runtimeEnd = ms.Position; var runtimeEnd = ms.Position;
runtimeSize = (int) ( runtimeEnd - runtimeStart ); var runtimeSize = ( int )( runtimeEnd - runtimeStart );
for( int i = 0; i < 3; i++ ) { for( var i = 0; i < 3; i++ )
{
if( mdlBlock.VertexBufferBlockNum[ i ] != 0 ) { if( mdlBlock.VertexBufferBlockNum[ i ] != 0 )
int currentVertexOffset = (int) ms.Position; {
var currentVertexOffset = ( int )ms.Position;
if( i == 0 || currentVertexOffset != vertexDataOffsets[ i - 1 ] ) if( i == 0 || currentVertexOffset != vertexDataOffsets[ i - 1 ] )
{
vertexDataOffsets[ i ] = currentVertexOffset; vertexDataOffsets[ i ] = currentVertexOffset;
}
else else
{
vertexDataOffsets[ i ] = 0; vertexDataOffsets[ i ] = 0;
}
Reader.Seek( baseOffset + mdlBlock.VertexBufferOffset[ i ] ); Reader.Seek( baseOffset + mdlBlock.VertexBufferOffset[ i ] );
for( int j = 0; j < mdlBlock.VertexBufferBlockNum[ i ]; j++ ) { for( var j = 0; j < mdlBlock.VertexBufferBlockNum[ i ]; j++ )
long lastPos = Reader.BaseStream.Position; {
vertexBufferSizes[ i ] += (int) ReadFileBlock( ms ); var lastPos = Reader.BaseStream.Position;
vertexBufferSizes[ i ] += ( int )ReadFileBlock( ms );
Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] ); Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] );
currentBlock++; currentBlock++;
} }
} }
if( mdlBlock.EdgeGeometryVertexBufferBlockNum[ i ] != 0 ) { if( mdlBlock.EdgeGeometryVertexBufferBlockNum[ i ] != 0 )
for( int j = 0; j < mdlBlock.EdgeGeometryVertexBufferBlockNum[ i ]; j++ ) { {
long lastPos = Reader.BaseStream.Position; for( var j = 0; j < mdlBlock.EdgeGeometryVertexBufferBlockNum[ i ]; j++ )
{
var lastPos = Reader.BaseStream.Position;
ReadFileBlock( ms ); ReadFileBlock( ms );
Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] ); Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] );
currentBlock++; currentBlock++;
} }
} }
if( mdlBlock.IndexBufferBlockNum[ i ] != 0 ) { if( mdlBlock.IndexBufferBlockNum[ i ] != 0 )
int currentIndexOffset = (int) ms.Position; {
var currentIndexOffset = ( int )ms.Position;
if( i == 0 || currentIndexOffset != indexDataOffsets[ i - 1 ] ) if( i == 0 || currentIndexOffset != indexDataOffsets[ i - 1 ] )
{
indexDataOffsets[ i ] = currentIndexOffset; indexDataOffsets[ i ] = currentIndexOffset;
}
else else
{
indexDataOffsets[ i ] = 0; indexDataOffsets[ i ] = 0;
}
// i guess this is only needed in the vertex area, for i = 0 // i guess this is only needed in the vertex area, for i = 0
// Reader.Seek( baseOffset + mdlBlock.IndexBufferOffset[ i ] ); // Reader.Seek( baseOffset + mdlBlock.IndexBufferOffset[ i ] );
for( int j = 0; j < mdlBlock.IndexBufferBlockNum[ i ]; j++ ) { for( var j = 0; j < mdlBlock.IndexBufferBlockNum[ i ]; j++ )
long lastPos = Reader.BaseStream.Position; {
indexBufferSizes[ i ] += (int) ReadFileBlock( ms ); var lastPos = Reader.BaseStream.Position;
indexBufferSizes[ i ] += ( int )ReadFileBlock( ms );
Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] ); Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] );
currentBlock++; currentBlock++;
} }
@ -234,33 +255,45 @@ namespace Penumbra.Util
ms.Write( BitConverter.GetBytes( runtimeSize ) ); ms.Write( BitConverter.GetBytes( runtimeSize ) );
ms.Write( BitConverter.GetBytes( mdlBlock.VertexDeclarationNum ) ); ms.Write( BitConverter.GetBytes( mdlBlock.VertexDeclarationNum ) );
ms.Write( BitConverter.GetBytes( mdlBlock.MaterialNum ) ); ms.Write( BitConverter.GetBytes( mdlBlock.MaterialNum ) );
for( int i = 0; i < 3; i++ ) for( var i = 0; i < 3; i++ )
{
ms.Write( BitConverter.GetBytes( vertexDataOffsets[ i ] ) ); ms.Write( BitConverter.GetBytes( vertexDataOffsets[ i ] ) );
for( int i = 0; i < 3; i++ ) }
for( var i = 0; i < 3; i++ )
{
ms.Write( BitConverter.GetBytes( indexDataOffsets[ i ] ) ); ms.Write( BitConverter.GetBytes( indexDataOffsets[ i ] ) );
for( int i = 0; i < 3; i++ ) }
for( var i = 0; i < 3; i++ )
{
ms.Write( BitConverter.GetBytes( vertexBufferSizes[ i ] ) ); ms.Write( BitConverter.GetBytes( vertexBufferSizes[ i ] ) );
for( int i = 0; i < 3; i++ ) }
for( var i = 0; i < 3; i++ )
{
ms.Write( BitConverter.GetBytes( indexBufferSizes[ i ] ) ); ms.Write( BitConverter.GetBytes( indexBufferSizes[ i ] ) );
ms.Write( new [] {mdlBlock.NumLods} ); }
ms.Write( new[] { mdlBlock.NumLods } );
ms.Write( BitConverter.GetBytes( mdlBlock.IndexBufferStreamingEnabled ) ); ms.Write( BitConverter.GetBytes( mdlBlock.IndexBufferStreamingEnabled ) );
ms.Write( BitConverter.GetBytes( mdlBlock.EdgeGeometryEnabled ) ); ms.Write( BitConverter.GetBytes( mdlBlock.EdgeGeometryEnabled ) );
ms.Write( new byte[] {0} ); ms.Write( new byte[] { 0 } );
} }
private void ReadTextureFile( PenumbraFileResource resource, MemoryStream ms ) private void ReadTextureFile( PenumbraFileResource resource, MemoryStream ms )
{ {
var blocks = Reader.ReadStructures< LodBlock >( (int)resource.FileInfo.BlockCount ); var blocks = Reader.ReadStructures< LodBlock >( ( int )resource.FileInfo!.BlockCount );
// if there is a mipmap header, the comp_offset // if there is a mipmap header, the comp_offset
// will not be 0 // will not be 0
uint mipMapSize = blocks[ 0 ].CompressedOffset; var mipMapSize = blocks[ 0 ].CompressedOffset;
if( mipMapSize != 0 ) if( mipMapSize != 0 )
{ {
long originalPos = BaseStream.Position; var originalPos = BaseStream.Position;
BaseStream.Position = resource.FileInfo.Offset + resource.FileInfo.HeaderSize; BaseStream.Position = resource.FileInfo.Offset + resource.FileInfo.HeaderSize;
ms.Write( Reader.ReadBytes( (int)mipMapSize ) ); ms.Write( Reader.ReadBytes( ( int )mipMapSize ) );
BaseStream.Position = originalPos; BaseStream.Position = originalPos;
} }
@ -269,12 +302,12 @@ namespace Penumbra.Util
for( byte i = 0; i < blocks.Count; i++ ) for( byte i = 0; i < blocks.Count; i++ )
{ {
// start from comp_offset // start from comp_offset
long runningBlockTotal = blocks[ i ].CompressedOffset + resource.FileInfo.Offset + resource.FileInfo.HeaderSize; var runningBlockTotal = blocks[ i ].CompressedOffset + resource.FileInfo.Offset + resource.FileInfo.HeaderSize;
ReadFileBlock( runningBlockTotal, ms, true ); ReadFileBlock( runningBlockTotal, ms, true );
for( int j = 1; j < blocks[ i ].BlockCount; j++ ) for( var j = 1; j < blocks[ i ].BlockCount; j++ )
{ {
runningBlockTotal += (UInt32)Reader.ReadInt16(); runningBlockTotal += ( uint )Reader.ReadInt16();
ReadFileBlock( runningBlockTotal, ms, true ); ReadFileBlock( runningBlockTotal, ms, true );
} }
@ -283,25 +316,24 @@ namespace Penumbra.Util
} }
} }
protected uint ReadFileBlock( MemoryStream dest, bool resetPosition = false ) { protected uint ReadFileBlock( MemoryStream dest, bool resetPosition = false )
return ReadFileBlock( Reader.BaseStream.Position, dest, resetPosition ); => ReadFileBlock( Reader.BaseStream.Position, dest, resetPosition );
}
protected uint ReadFileBlock( long offset, MemoryStream dest, bool resetPosition = false ) protected uint ReadFileBlock( long offset, MemoryStream dest, bool resetPosition = false )
{ {
long originalPosition = BaseStream.Position; var originalPosition = BaseStream.Position;
BaseStream.Position = offset; BaseStream.Position = offset;
var blockHeader = Reader.ReadStructure< DatBlockHeader >(); var blockHeader = Reader.ReadStructure< DatBlockHeader >();
// uncompressed block // uncompressed block
if( blockHeader.CompressedSize == 32000 ) if( blockHeader.CompressedSize == 32000 )
{ {
dest.Write( Reader.ReadBytes( (int)blockHeader.UncompressedSize ) ); dest.Write( Reader.ReadBytes( ( int )blockHeader.UncompressedSize ) );
return blockHeader.UncompressedSize; return blockHeader.UncompressedSize;
} }
var data = Reader.ReadBytes( (int)blockHeader.UncompressedSize ); var data = Reader.ReadBytes( ( int )blockHeader.UncompressedSize );
using( var compressedStream = new MemoryStream( data ) ) using( var compressedStream = new MemoryStream( data ) )
{ {
@ -311,7 +343,9 @@ namespace Penumbra.Util
} }
if( resetPosition ) if( resetPosition )
{
BaseStream.Position = originalPosition; BaseStream.Position = originalPosition;
}
return blockHeader.UncompressedSize; return blockHeader.UncompressedSize;
} }
@ -323,10 +357,10 @@ namespace Penumbra.Util
public class PenumbraFileInfo public class PenumbraFileInfo
{ {
public UInt32 HeaderSize; public uint HeaderSize;
public FileType Type; public FileType Type;
public UInt32 RawFileSize; public uint RawFileSize;
public UInt32 BlockCount; public uint BlockCount;
public long Offset { get; internal set; } public long Offset { get; internal set; }
@ -336,20 +370,20 @@ namespace Penumbra.Util
public class PenumbraFileResource public class PenumbraFileResource
{ {
public PenumbraFileResource() public PenumbraFileResource()
{ { }
}
public PenumbraFileInfo FileInfo { get; internal set; } public PenumbraFileInfo? FileInfo { get; internal set; }
public byte[] Data { get; internal set; } public byte[] Data { get; internal set; } = new byte[0];
public Span< byte > DataSpan => Data.AsSpan(); public Span< byte > DataSpan
=> Data.AsSpan();
public MemoryStream FileStream { get; internal set; } public MemoryStream? FileStream { get; internal set; }
public BinaryReader Reader { get; internal set; } public BinaryReader? Reader { get; internal set; }
public ParsedFilePath FilePath { get; internal set; } public ParsedFilePath? FilePath { get; internal set; }
/// <summary> /// <summary>
/// Called once the files are read out from the dats. Used to further parse the file into usable data structures. /// Called once the files are read out from the dats. Used to further parse the file into usable data structures.
@ -380,22 +414,22 @@ namespace Penumbra.Util
} }
[StructLayout( LayoutKind.Sequential )] [StructLayout( LayoutKind.Sequential )]
struct DatBlockHeader private struct DatBlockHeader
{ {
public UInt32 Size; public uint Size;
public UInt32 unknown1; public uint unknown1;
public UInt32 CompressedSize; public uint CompressedSize;
public UInt32 UncompressedSize; public uint UncompressedSize;
}; };
[StructLayout( LayoutKind.Sequential )] [StructLayout( LayoutKind.Sequential )]
struct LodBlock private struct LodBlock
{ {
public UInt32 CompressedOffset; public uint CompressedOffset;
public UInt32 CompressedSize; public uint CompressedSize;
public UInt32 DecompressedSize; public uint DecompressedSize;
public UInt32 BlockOffset; public uint BlockOffset;
public UInt32 BlockCount; public uint BlockCount;
} }
} }
} }