Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
NotAdam committed Sep 2, 2020
0 parents commit 0e7650f
Showing 27 changed files with 1,596 additions and 0 deletions.
67 changes: 67 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@

[*]
charset=utf-8
end_of_line=crlf
trim_trailing_whitespace=false
insert_final_newline=false
indent_style=space
indent_size=4

# Microsoft .NET properties
csharp_new_line_before_members_in_object_initializers=false
csharp_preferred_modifier_order=public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion
csharp_space_after_cast=false
csharp_space_after_keywords_in_control_flow_statements=false
csharp_space_between_method_call_parameter_list_parentheses=true
csharp_space_between_method_declaration_parameter_list_parentheses=true
csharp_space_between_parentheses=control_flow_statements,expressions,type_casts
csharp_style_var_elsewhere=true:suggestion
csharp_style_var_for_built_in_types=true:suggestion
csharp_style_var_when_type_is_apparent=true:suggestion
dotnet_style_parentheses_in_arithmetic_binary_operators=never_if_unnecessary:none
dotnet_style_parentheses_in_other_binary_operators=never_if_unnecessary:none
dotnet_style_parentheses_in_relational_binary_operators=never_if_unnecessary:none
dotnet_style_predefined_type_for_locals_parameters_members=true:suggestion
dotnet_style_predefined_type_for_member_access=true:suggestion
dotnet_style_qualification_for_event=false:suggestion
dotnet_style_qualification_for_field=false:suggestion
dotnet_style_qualification_for_method=false:suggestion
dotnet_style_qualification_for_property=false:suggestion
dotnet_style_require_accessibility_modifiers=for_non_interface_members:suggestion

# ReSharper properties
resharper_autodetect_indent_settings=true
resharper_csharp_space_within_array_access_brackets=true
resharper_enforce_line_ending_style=true
resharper_place_attribute_on_same_line=false
resharper_space_after_cast=false
resharper_space_within_checked_parentheses=true
resharper_space_within_default_parentheses=true
resharper_space_within_nameof_parentheses=true
resharper_space_within_single_line_array_initializer_braces=true
resharper_space_within_sizeof_parentheses=true
resharper_space_within_typeof_parentheses=true
resharper_space_within_type_argument_angles=true
resharper_space_within_type_parameter_angles=true
resharper_use_indent_from_vs=false
resharper_wrap_lines=true

# ReSharper inspection severities
resharper_arrange_redundant_parentheses_highlighting=hint
resharper_arrange_this_qualifier_highlighting=hint
resharper_arrange_type_member_modifiers_highlighting=hint
resharper_arrange_type_modifiers_highlighting=hint
resharper_built_in_type_reference_style_for_member_access_highlighting=hint
resharper_built_in_type_reference_style_highlighting=hint
resharper_redundant_base_qualifier_highlighting=warning
resharper_suggest_var_or_type_built_in_types_highlighting=hint
resharper_suggest_var_or_type_elsewhere_highlighting=hint
resharper_suggest_var_or_type_simple_types_highlighting=hint
resharper_web_config_module_not_resolved_highlighting=warning
resharper_web_config_type_not_resolved_highlighting=warning
resharper_web_config_wrong_module_highlighting=warning

[*.{appxmanifest,asax,ascx,aspx,build,cg,cginc,compute,cs,cshtml,dtd,hlsl,hlsli,hlslinc,master,nuspec,razor,resw,resx,shader,skin,usf,ush,vb,xaml,xamlx,xoml,xsd}]
indent_style=space
indent_size=4
tab_width=4
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.vs/
obj/
bin/
*.user
13 changes: 13 additions & 0 deletions .idea/.idea.Penumbra/.idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/.idea.Penumbra/.idea/indexLayout.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/.idea.Penumbra/.idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/.idea.Penumbra/.idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions .idea/.idea.Penumbra/riderModule.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions Penumbra.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29709.97
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Penumbra", "Penumbra\Penumbra.csproj", "{13C812E9-0D42-4B95-8646-40EEBF30636F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F89C9EAE-25C8-43BE-8108-5921E5A93502}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B17E85B1-5F60-4440-9F9A-3DDE877E8CDF}
EndGlobalSection
EndGlobal
31 changes: 31 additions & 0 deletions Penumbra/Configuration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Dalamud.Configuration;
using Dalamud.Plugin;
using System;

namespace Penumbra
{
[Serializable]
public class Configuration : IPluginConfiguration
{
public int Version { get; set; } = 0;

public bool IsEnabled { get; set; } = true;

public string BaseFolder { get; set; } = @"D:/ffxiv/fs_mods/";

// the below exist just to make saving less cumbersome

[NonSerialized]
private DalamudPluginInterface _pluginInterface;

public void Initialize( DalamudPluginInterface pluginInterface )
{
_pluginInterface = pluginInterface;
}

public void Save()
{
_pluginInterface.SavePluginConfig( this );
}
}
}
87 changes: 87 additions & 0 deletions Penumbra/DialogExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Penumbra
{
public static class DialogExtensions
{
public static Task< DialogResult > ShowDialogAsync( this CommonDialog form )
{
using var process = Process.GetCurrentProcess();
return form.ShowDialogAsync( new DialogHandle( process.MainWindowHandle ) );
}

public static Task< DialogResult > ShowDialogAsync( this CommonDialog form, IWin32Window owner )
{
var taskSource = new TaskCompletionSource< DialogResult >();
var th = new Thread( () => DialogThread( form, owner, taskSource ) );
th.Start();
return taskSource.Task;
}

[STAThread]
private static void DialogThread( CommonDialog form, IWin32Window owner,
TaskCompletionSource< DialogResult > taskSource )
{
Application.SetCompatibleTextRenderingDefault( false );
Application.EnableVisualStyles();
using var hiddenForm = new HiddenForm( form, owner, taskSource );
Application.Run( hiddenForm );
Application.ExitThread();
}

public class DialogHandle : IWin32Window
{
public IntPtr Handle { get; set; }

public DialogHandle( IntPtr handle )
{
Handle = handle;
}
}

public class HiddenForm : Form
{
private readonly CommonDialog form;
private readonly IWin32Window owner;
private readonly TaskCompletionSource< DialogResult > taskSource;

public HiddenForm( CommonDialog form, IWin32Window owner, TaskCompletionSource< DialogResult > taskSource )
{
this.form = form;
this.owner = owner;
this.taskSource = taskSource;

Opacity = 0;
FormBorderStyle = FormBorderStyle.None;
ShowInTaskbar = false;
Size = new Size( 0, 0 );

Shown += HiddenForm_Shown;
}

private void HiddenForm_Shown( object sender, EventArgs _ )
{
Hide();
try
{
var result = form.ShowDialog( owner );
taskSource.SetResult( result );
}
catch( Exception e )
{
taskSource.SetException( e );
}

Close();
}
}
}
}
80 changes: 80 additions & 0 deletions Penumbra/Extensions/FuckedExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System;
using System.Reflection;
using System.Reflection.Emit;

namespace Penumbra.Extensions
{
public static class FuckedExtensions
{
private delegate ref TFieldType RefGet< in TObject, TFieldType >( TObject obj );

/// <summary>
/// Create a delegate which will return a zero-copy reference to a given field in a manner that's fucked tiers of quick and
/// fucked tiers of stupid, but hey, why not?
/// </summary>
/// <remarks>
/// The only thing that this can't do is inline, this always ends up as a call instruction because we're generating code at
/// runtime and need to jump to it. That said, this is still super quick and provides a convenient and type safe shim around
/// a primitive type
///
/// You can use the resultant <see cref="RefGet{TObject,TFieldType}"/> to access a ref to a field on an object without invoking any
/// unsafe code too.
/// </remarks>
/// <param name="fieldName">The name of the field to grab a reference to</param>
/// <typeparam name="TObject">The object that holds the field</typeparam>
/// <typeparam name="TField">The type of the underlying field</typeparam>
/// <returns>A delegate that will return a reference to a particular field - zero copy</returns>
/// <exception cref="MissingFieldException"></exception>
private static RefGet< TObject, TField > CreateRefGetter< TObject, TField >( string fieldName ) where TField : unmanaged
{
const BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;

var fieldInfo = typeof( TObject ).GetField( fieldName, flags );
if( fieldInfo == null )
{
throw new MissingFieldException( typeof( TObject ).Name, fieldName );
}

var dm = new DynamicMethod(
$"__refget_{typeof( TObject ).Name}_{fieldInfo.Name}",
typeof( TField ).MakeByRefType(),
new[] { typeof( TObject ) },
typeof( TObject ),
true
);

var il = dm.GetILGenerator();

il.Emit( OpCodes.Ldarg_0 );
il.Emit( OpCodes.Ldflda, fieldInfo );
il.Emit( OpCodes.Ret );

return ( RefGet< TObject, TField > )dm.CreateDelegate( typeof( RefGet< TObject, TField > ) );
}

private static readonly RefGet< string, byte > StringRefGet = CreateRefGetter< string, byte >( "_firstChar" );

public static unsafe IntPtr UnsafePtr( this string str )
{
// nb: you can do it without __makeref but the code becomes way shittier because the way of getting the ptr
// is more fucked up so it's easier to just abuse __makeref
// but you can just use the StringRefGet func to get a `ref byte` too, though you'll probs want a better delegate so it's
// actually usable, lol
var fieldRef = __makeref( StringRefGet( str ) );

return *( IntPtr* )&fieldRef;
}

public static unsafe int UnsafeLength( this string str )
{
var fieldRef = __makeref( StringRefGet( str ) );

// c# strings are utf16 so we just multiply len by 2 to get the total byte count + 2 for null terminator (:D)
// very simple and intuitive

// this also maps to a defined structure, so you can just move the pointer backwards to read from the native string struct
// see: https://github.com/dotnet/coreclr/blob/master/src/vm/object.h#L897-L909
return *( int* )( *( IntPtr* )&fieldRef - 4 ) * 2 + 2;
}
}
}
39 changes: 39 additions & 0 deletions Penumbra/Importer/Models/ExtendedModPack.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Collections.Generic;

namespace Penumbra.Importer.Models
{
internal class OptionList
{
public string Name { get; set; }
public object Description { get; set; }
public string ImagePath { get; set; }
public List< SimpleMod > ModsJsons { get; set; }
public string GroupName { get; set; }
public string SelectionType { get; set; }
public bool IsChecked { get; set; }
}

internal class ModGroup
{
public string GroupName { get; set; }
public string SelectionType { get; set; }
public List< OptionList > OptionList { get; set; }
}

internal class ModPackPage
{
public int PageIndex { get; set; }
public List< ModGroup > ModGroups { get; set; }
}

internal class ExtendedModPack
{
public string TTMPVersion { get; set; }
public string Name { get; set; }
public string Author { get; set; }
public string Version { get; set; }
public string Description { get; set; }
public List< ModPackPage > ModPackPages { get; set; }
public List< SimpleMod > SimpleModsList { get; set; }
}
}
25 changes: 25 additions & 0 deletions Penumbra/Importer/Models/SimpleModPack.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Collections.Generic;

namespace Penumbra.Importer.Models
{
internal class SimpleModPack
{
public string TTMPVersion { get; set; }
public string Name { get; set; }
public string Author { get; set; }
public string Version { get; set; }
public string Description { get; set; }
public List< SimpleMod > SimpleModsList { get; set; }
}

internal class SimpleMod
{
public string Name { get; set; }
public string Category { get; set; }
public string FullPath { get; set; }
public int ModOffset { get; set; }
public int ModSize { get; set; }
public string DatFile { get; set; }
public object ModPackEntry { get; set; }
}
}
223 changes: 223 additions & 0 deletions Penumbra/Importer/TexToolsImport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Dalamud.Plugin;
using Ionic.Zip;
using Lumina.Data;
using Newtonsoft.Json;
using Penumbra.Importer.Models;
using Penumbra.Models;

namespace Penumbra.Importer
{
internal class TexToolsImport
{
private readonly DirectoryInfo _outDirectory;

public TexToolsImport( DirectoryInfo outDirectory )
{
_outDirectory = outDirectory;
}

public void ImportModPack( FileInfo modPackFile )
{
switch( modPackFile.Extension )
{
case ".ttmp":
ImportV1ModPack( modPackFile );
return;

case ".ttmp2":
ImportV2ModPack( modPackFile );
return;
}
}

private void ImportV1ModPack( FileInfo modPackFile )
{
PluginLog.Log( " -> Importing V1 ModPack" );

using var extractedModPack = ZipFile.Read( modPackFile.OpenRead() );

var modListRaw = GetStringFromZipEntry( extractedModPack[ "TTMPL.mpl" ], Encoding.UTF8 ).Split(
new[] { "\r\n", "\r", "\n" },
StringSplitOptions.None
);

var modList = modListRaw.Select( JsonConvert.DeserializeObject< SimpleMod > );

// Create a new ModMeta from the TTMP modlist info
var modMeta = new ModMeta
{
Author = "Unknown",
Name = modPackFile.Name,
Description = "Mod imported from TexTools mod pack"
};

// Open the mod data file from the modpack as a SqPackStream
var modData = GetSqPackStreamFromZipEntry( extractedModPack[ "TTMPD.mpd" ] );

var newModFolder = new DirectoryInfo( Path.Combine( _outDirectory.FullName,
Path.GetFileNameWithoutExtension( modPackFile.Name ) ) );
newModFolder.Create();

File.WriteAllText( Path.Combine( newModFolder.FullName, "meta.json" ),
JsonConvert.SerializeObject( modMeta ) );

ExtractSimpleModList( newModFolder, modList, modData );
}

private void ImportV2ModPack( FileInfo modPackFile )
{
using var extractedModPack = ZipFile.Read( modPackFile.OpenRead() );

var modList =
JsonConvert.DeserializeObject< SimpleModPack >( GetStringFromZipEntry( extractedModPack[ "TTMPL.mpl" ],
Encoding.UTF8 ) );

if( modList.TTMPVersion.EndsWith( "s" ) )
{
ImportSimpleV2ModPack( extractedModPack );
return;
}

if( modList.TTMPVersion.EndsWith( "w" ) )
{
ImportExtendedV2ModPack( extractedModPack );
}
}

private void ImportSimpleV2ModPack( ZipFile extractedModPack )
{
PluginLog.Log( " -> Importing Simple V2 ModPack" );

var modList =
JsonConvert.DeserializeObject< SimpleModPack >( GetStringFromZipEntry( extractedModPack[ "TTMPL.mpl" ],
Encoding.UTF8 ) );

// Create a new ModMeta from the TTMP modlist info
var modMeta = new ModMeta
{
Author = modList.Author,
Name = modList.Name,
Description = string.IsNullOrEmpty( modList.Description )
? "Mod imported from TexTools mod pack"
: modList.Description
};

// Open the mod data file from the modpack as a SqPackStream
var modData = GetSqPackStreamFromZipEntry( extractedModPack[ "TTMPD.mpd" ] );

var newModFolder = new DirectoryInfo( Path.Combine( _outDirectory.FullName,
Path.GetFileNameWithoutExtension( modList.Name ) ) );
newModFolder.Create();

File.WriteAllText( Path.Combine( newModFolder.FullName, "meta.json" ),
JsonConvert.SerializeObject( modMeta ) );

ExtractSimpleModList( newModFolder, modList.SimpleModsList, modData );
}

private void ImportExtendedV2ModPack( ZipFile extractedModPack )
{
PluginLog.Log( " -> Importing Extended V2 ModPack" );

var modList =
JsonConvert.DeserializeObject< ExtendedModPack >( GetStringFromZipEntry( extractedModPack[ "TTMPL.mpl" ],
Encoding.UTF8 ) );

// Create a new ModMeta from the TTMP modlist info
var modMeta = new ModMeta
{
Author = modList.Author,
Name = modList.Name,
Description = string.IsNullOrEmpty( modList.Description )
? "Mod imported from TexTools mod pack"
: modList.Description
};

// Open the mod data file from the modpack as a SqPackStream
var modData = GetSqPackStreamFromZipEntry( extractedModPack[ "TTMPD.mpd" ] );

var newModFolder = new DirectoryInfo( Path.Combine( _outDirectory.FullName,
Path.GetFileNameWithoutExtension( modList.Name ) ) );
newModFolder.Create();

File.WriteAllText( Path.Combine( newModFolder.FullName, "meta.json" ),
JsonConvert.SerializeObject( modMeta ) );

if( modList.SimpleModsList != null )
ExtractSimpleModList( newModFolder, modList.SimpleModsList, modData );

if( modList.ModPackPages == null )
return;

// Iterate through all pages
// For now, we are just going to import the default selections
// TODO: implement such a system in resrep?
foreach( var option in from modPackPage in modList.ModPackPages
from modGroup in modPackPage.ModGroups
from option in modGroup.OptionList
where option.IsChecked
select option )
{
ExtractSimpleModList( newModFolder, option.ModsJsons, modData );
}
}

private void ImportMetaModPack( FileInfo file )
{
throw new NotImplementedException();
}

private void ExtractSimpleModList( DirectoryInfo outDirectory, IEnumerable< SimpleMod > mods, SqPackStream dataStream )
{
// Extract each SimpleMod into the new mod folder
foreach( var simpleMod in mods )
{
if( simpleMod == null )
continue;

ExtractMod( outDirectory, simpleMod, dataStream );
}
}

private void ExtractMod( DirectoryInfo outDirectory, SimpleMod mod, SqPackStream dataStream )
{
PluginLog.Log( " -> Extracting {0} at {1}", mod.FullPath, mod.ModOffset.ToString( "X" ) );

try
{
var data = dataStream.ReadFile< FileResource >( mod.ModOffset );

var extractedFile = new FileInfo( Path.Combine( outDirectory.FullName, mod.FullPath ) );
extractedFile.Directory?.Create();

File.WriteAllBytes( extractedFile.FullName, data.Data );
}
catch( Exception ex )
{
PluginLog.LogError( ex, "Could not export mod." );
}
}

private static MemoryStream GetStreamFromZipEntry( ZipEntry entry )
{
var stream = new MemoryStream();
entry.Extract( stream );
return stream;
}

private static string GetStringFromZipEntry( ZipEntry entry, Encoding encoding )
{
return encoding.GetString( GetStreamFromZipEntry( entry ).ToArray() );
}

private static SqPackStream GetSqPackStreamFromZipEntry( ZipEntry entry )
{
return new SqPackStream( GetStreamFromZipEntry( entry ) );
}
}
}
100 changes: 100 additions & 0 deletions Penumbra/ModManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Plugin;
using Penumbra.Models;
using Newtonsoft.Json;

namespace Penumbra
{
public class ModManager
{
public DirectoryInfo BasePath { get; set; }

public readonly Dictionary< string, ResourceMod > AvailableMods = new Dictionary< string, ResourceMod >();

public readonly Dictionary< string, FileInfo > ResolvedFiles = new Dictionary< string, FileInfo >();

public ModManager( DirectoryInfo basePath )
{
BasePath = basePath;
}

public ModManager()
{
}

public void DiscoverMods()
{
if( BasePath == null )
{
return;
}

if( !BasePath.Exists )
{
return;
}

AvailableMods.Clear();
ResolvedFiles.Clear();

// get all mod dirs
foreach( var modDir in BasePath.EnumerateDirectories() )
{
var metaFile = modDir.EnumerateFiles().FirstOrDefault( f => f.Name == "meta.json" );

if( metaFile == null )
{
PluginLog.LogError( "mod meta is missing for resource mod: {ResourceModLocation}", modDir );
continue;
}

var meta = JsonConvert.DeserializeObject< Models.ModMeta >( File.ReadAllText( metaFile.FullName ) );

var mod = new ResourceMod
{
Meta = meta,
ModBasePath = modDir
};

AvailableMods[ modDir.Name ] = mod;
mod.RefreshModFiles();
}

// todo: sort the mods by priority here so that the file discovery works correctly

foreach( var mod in AvailableMods.Select( m => m.Value ) )
{
// fixup path
var baseDir = mod.ModBasePath.FullName;

foreach( var file in mod.ModFiles )
{
var path = file.FullName.Substring( baseDir.Length ).ToLowerInvariant()
.TrimStart( '\\' ).Replace( '\\', '/' );

// todo: notify when collisions happen? or some extra state on the file? not sure yet
// this code is shit all the same

if( !ResolvedFiles.ContainsKey( path ) )
{
ResolvedFiles[ path ] = file;
}
else
{
PluginLog.LogError(
"a different mod already fucks this file: {FilePath}",
ResolvedFiles[ path ].FullName
);
}
}
}
}

public FileInfo GetCandidateForGameFile( string resourcePath )
{
return ResolvedFiles.TryGetValue( resourcePath.ToLowerInvariant(), out var fileInfo ) ? fileInfo : null;
}
}
}
9 changes: 9 additions & 0 deletions Penumbra/Models/ModMeta.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Penumbra.Models
{
public class ModMeta
{
public string Name { get; set; }
public string Author { get; set; }
public string Description { get; set; }
}
}
193 changes: 193 additions & 0 deletions Penumbra/Penumbra.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using Dalamud.Hooking;
using Dalamud.Plugin;
using Penumbra.Structs;
using Penumbra.Util;
using FileMode = Penumbra.Structs.FileMode;
using Penumbra.Extensions;

namespace Penumbra
{
public class Penumbra : IDisposable
{
public Plugin Plugin { get; set; }

public bool IsEnabled { get; set; }

public Crc32 Crc32 { get; }


// Delegate prototypes
public unsafe delegate byte ReadFilePrototype( IntPtr pFileHandler, SeFileDescriptor* pFileDesc, int priority, bool isSync );

public unsafe delegate byte ReadSqpackPrototype( IntPtr pFileHandler, SeFileDescriptor* pFileDesc, int priority, bool isSync );

public unsafe delegate void* GetResourceSyncPrototype( IntPtr pFileManager, uint* pCategoryId, char* pResourceType,
uint* pResourceHash, char* pPath, void* pUnknown );

public unsafe delegate void* GetResourceAsyncPrototype( IntPtr pFileManager, uint* pCategoryId, char* pResourceType,
uint* pResourceHash, char* pPath, void* pUnknown, bool isUnknown );


// Hooks
public Hook< GetResourceSyncPrototype > GetResourceSyncHook { get; private set; }
public Hook< GetResourceAsyncPrototype > GetResourceAsyncHook { get; private set; }
public Hook< ReadSqpackPrototype > ReadSqpackHook { get; private set; }

// Unmanaged functions
public ReadFilePrototype ReadFile { get; private set; }


public Penumbra( Plugin plugin )
{
Plugin = plugin;
Crc32 = new Crc32();
}

public unsafe void Init()
{
var scanner = Plugin.PluginInterface.TargetModuleScanner;

var readFileAddress =
scanner.ScanText( "E8 ?? ?? ?? ?? 84 C0 0F 84 ?? 00 00 00 4C 8B C3 BA 05" );

var readSqpackAddress =
scanner.ScanText( "E8 ?? ?? ?? ?? EB 05 E8 ?? ?? ?? ?? 84 C0 0F 84 ?? 00 00 00 4C 8B C3" );

var getResourceSyncAddress =
scanner.ScanText( "E8 ?? ?? 00 00 48 8D 4F ?? 48 89 87 ?? ?? 00 00" );

var getResourceAsyncAddress =
scanner.ScanText( "E8 ?? ?? ?? 00 48 8B D8 EB ?? F0 FF 83 ?? ?? 00 00" );


ReadSqpackHook = new Hook< ReadSqpackPrototype >( readSqpackAddress, new ReadSqpackPrototype( ReadSqpackHandler ) );

GetResourceSyncHook = new Hook< GetResourceSyncPrototype >( getResourceSyncAddress,
new GetResourceSyncPrototype( GetResourceSyncHandler ) );

GetResourceAsyncHook = new Hook< GetResourceAsyncPrototype >( getResourceAsyncAddress,
new GetResourceAsyncPrototype( GetResourceAsyncHandler ) );

ReadFile = Marshal.GetDelegateForFunctionPointer< ReadFilePrototype >( readFileAddress );
}


public unsafe void* GetResourceSyncHandler( IntPtr pFileManager, uint* pCategoryId,
char* pResourceType, uint* pResourceHash, char* pPath, void* pUnknown )
{
return GetResourceHandler( true, pFileManager, pCategoryId, pResourceType,
pResourceHash, pPath, pUnknown, false );
}

public unsafe void* GetResourceAsyncHandler( IntPtr pFileManager, uint* pCategoryId,
char* pResourceType, uint* pResourceHash, char* pPath, void* pUnknown, bool isUnknown )
{
return GetResourceHandler( false, pFileManager, pCategoryId, pResourceType,
pResourceHash, pPath, pUnknown, isUnknown );
}

private unsafe void* GetResourceHandler( bool isSync, IntPtr pFileManager, uint* pCategoryId,
char* pResourceType, uint* pResourceHash, char* pPath, void* pUnknown, bool isUnknown )
{
var gameFsPath = Marshal.PtrToStringAnsi( new IntPtr( pPath ) );

var candidate = Plugin.ModManager.GetCandidateForGameFile( gameFsPath );

// path must be < 260 because statically defined array length :(
if( candidate == null || candidate.FullName.Length >= 260 || !candidate.Exists )
{
return isSync
? GetResourceSyncHook.Original( pFileManager, pCategoryId, pResourceType,
pResourceHash, pPath, pUnknown )
: GetResourceAsyncHook.Original( pFileManager, pCategoryId, pResourceType,
pResourceHash, pPath, pUnknown, isUnknown );
}

var cleanPath = candidate.FullName.Replace( '\\', '/' );
var asciiPath = Encoding.ASCII.GetBytes( cleanPath );

var bPath = stackalloc byte[asciiPath.Length + 1];
Marshal.Copy( asciiPath, 0, new IntPtr( bPath ), asciiPath.Length );
pPath = ( char* )bPath;

Crc32.Init();
Crc32.Update( asciiPath );
*pResourceHash = Crc32.Checksum;

return isSync
? GetResourceSyncHook.Original( pFileManager, pCategoryId, pResourceType,
pResourceHash, pPath, pUnknown )
: GetResourceAsyncHook.Original( pFileManager, pCategoryId, pResourceType,
pResourceHash, pPath, pUnknown, isUnknown );
}


public unsafe byte ReadSqpackHandler( IntPtr pFileHandler, SeFileDescriptor* pFileDesc, int priority, bool isSync )
{
var gameFsPath = Marshal.PtrToStringAnsi( new IntPtr( pFileDesc->ResourceHandle->FileName ) );

var isRooted = Path.IsPathRooted( gameFsPath );

if( gameFsPath == null || gameFsPath.Length >= 260 || !isRooted )
{
return ReadSqpackHook.Original( pFileHandler, pFileDesc, priority, isSync );
}

#if DEBUG
PluginLog.Log( "loading modded file: {GameFsPath}", gameFsPath );
#endif

pFileDesc->FileMode = FileMode.LoadUnpackedResource;

var utfPath = Encoding.Unicode.GetBytes( gameFsPath );

Marshal.Copy( utfPath, 0, new IntPtr( &pFileDesc->UtfFileName ), utfPath.Length );

var fd = stackalloc byte[0x20 + utfPath.Length + 0x16];
Marshal.Copy( utfPath, 0, new IntPtr( fd + 0x21 ), utfPath.Length );

pFileDesc->FileDescriptor = fd;


return ReadFile( pFileHandler, pFileDesc, priority, isSync );
}

public void Enable()
{
if( IsEnabled )
return;

ReadSqpackHook.Enable();
GetResourceSyncHook.Enable();
GetResourceAsyncHook.Enable();

IsEnabled = true;
}

public void Disable()
{
if( !IsEnabled )
return;

ReadSqpackHook.Disable();
GetResourceSyncHook.Disable();
GetResourceAsyncHook.Disable();

IsEnabled = false;
}

public void Dispose()
{
if( IsEnabled )
Disable();

ReadSqpackHook.Dispose();
GetResourceSyncHook.Dispose();
GetResourceAsyncHook.Dispose();
}
}
}
95 changes: 95 additions & 0 deletions Penumbra/Penumbra.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{13C812E9-0D42-4B95-8646-40EEBF30636F}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Penumbra</RootNamespace>
<AssemblyName>Penumbra</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<LangVersion>8</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<Reference Include="Dalamud">
<HintPath>..\libs\Dalamud.dll</HintPath>
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\Dalamud.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="ImGui.NET">
<HintPath>..\libs\ImGui.NET.dll</HintPath>
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\ImGui.NET.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="ImGuiScene">
<HintPath>..\libs\ImGuiScene.dll</HintPath>
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\ImGuiScene.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Lumina">
<HintPath>..\libs\Lumina.dll</HintPath>
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\Lumina.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.Numerics" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Configuration.cs" />
<Compile Include="DialogExtensions.cs" />
<Compile Include="Importer\Models\ExtendedModPack.cs" />
<Compile Include="Importer\Models\SimpleModPack.cs" />
<Compile Include="Importer\TexToolsImport.cs" />
<Compile Include="Extensions\FuckedExtensions.cs" />
<Compile Include="Models\ModMeta.cs" />
<Compile Include="ModManager.cs" />
<Compile Include="Plugin.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Structs\FileMode.cs" />
<Compile Include="Structs\SeFileDescriptor.cs" />
<Compile Include="ResourceMod.cs" />
<Compile Include="Penumbra.cs" />
<Compile Include="Structs\ResourceHandle.cs" />
<Compile Include="SettingsInterface.cs" />
<Compile Include="Util\Crc32.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="DotNetZip">
<Version>1.13.8</Version>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
68 changes: 68 additions & 0 deletions Penumbra/Plugin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System.IO;
using Dalamud.Game.Command;
using Dalamud.Plugin;

namespace Penumbra
{
public class Plugin : IDalamudPlugin
{
public string Name => "Penumbra";

private const string CommandName = "/penumbra";

public DalamudPluginInterface PluginInterface { get; set; }
public Configuration Configuration { get; set; }

public Penumbra Penumbra { get; set; }

public ModManager ModManager { get; set; }

public SettingsInterface SettingsInterface { get; set; }

public void Initialize( DalamudPluginInterface pluginInterface )
{
PluginInterface = pluginInterface;

Configuration = PluginInterface.GetPluginConfig() as Configuration ?? new Configuration();
Configuration.Initialize( PluginInterface );

SettingsInterface = new SettingsInterface( this );
PluginInterface.UiBuilder.OnBuildUi += SettingsInterface.Draw;

ModManager = new ModManager( new DirectoryInfo( Configuration.BaseFolder ) );
ModManager.DiscoverMods();

Penumbra = new Penumbra( this );


PluginInterface.CommandManager.AddHandler( CommandName, new CommandInfo( OnCommand )
{
HelpMessage = "/penumbra 0 will disable penumbra, /penumbra 1 will enable it."
} );

Penumbra.Init();
Penumbra.Enable();
}

public void Dispose()
{
PluginInterface.UiBuilder.OnBuildUi -= SettingsInterface.Draw;

PluginInterface.CommandManager.RemoveHandler( CommandName );
PluginInterface.Dispose();

Penumbra.Dispose();
}

private void OnCommand( string command, string args )
{
if( args.Length > 0 )
Configuration.IsEnabled = args[ 0 ] == '1';

if( Configuration.IsEnabled )
Penumbra.Enable();
else
Penumbra.Disable();
}
}
}
35 changes: 35 additions & 0 deletions Penumbra/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.Reflection;
using System.Runtime.InteropServices;

// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Penumbra")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("absolute gangstas")]
[assembly: AssemblyProduct("Penumbra")]
[assembly: AssemblyCopyright("Copyright © 2020")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]

// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("13c812e9-0d42-4b95-8646-40eebf30636f")]

// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
35 changes: 35 additions & 0 deletions Penumbra/ResourceMod.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.IO;
using Dalamud.Plugin;
using Penumbra.Models;

namespace Penumbra
{
public class ResourceMod
{
public ModMeta Meta { get; set; }

public DirectoryInfo ModBasePath { get; set; }

public List< FileInfo > ModFiles { get; } = new List< FileInfo >();

public void RefreshModFiles()
{
if( ModBasePath == null )
{
PluginLog.LogError( "no basepath has been set on {ResourceModName}", Meta.Name );
return;
}

// we don't care about any _files_ in the root dir, but any folders should be a game folder/file combo
foreach( var dir in ModBasePath.EnumerateDirectories() )
{
foreach( var file in dir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
{
ModFiles.Add( file );
}
}
}
}
}
316 changes: 316 additions & 0 deletions Penumbra/SettingsInterface.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,316 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Runtime.Remoting.Messaging;
using System.Threading.Tasks;
using System.Windows.Forms;
using Dalamud.Interface;
using Dalamud.Plugin;
using ImGuiNET;
using Penumbra.Importer;

namespace Penumbra
{
public class SettingsInterface
{
private readonly Plugin _plugin;

public bool Visible { get; set; } = true;

private static readonly Vector2 AutoFillSize = new Vector2( -1, -1 );
private static readonly Vector2 ModListSize = new Vector2( 200, -1 );

private static readonly Vector2 MinSettingsSize = new Vector2( 650, 450 );
private static readonly Vector2 MaxSettingsSize = new Vector2( 69420, 42069 );

private int _selectedModIndex;
private ResourceMod _selectedMod;

private bool _isImportRunning = false;

public SettingsInterface( Plugin plugin )
{
_plugin = plugin;
}

public void Draw()
{
ImGui.SetNextWindowSizeConstraints( MinSettingsSize, MaxSettingsSize );
var ret = ImGui.Begin( _plugin.Name );
if( !ret )
{
return;
}

ImGui.BeginTabBar( "PenumbraSettings" );

DrawSettingsTab();
DrawResourceMods();
DrawEffectiveFileList();

ImGui.EndTabBar();

ImGui.End();
}

void DrawSettingsTab()
{
var ret = ImGui.BeginTabItem( "Settings" );
if( !ret )
{
return;
}

// FUCKKKKK
var basePath = _plugin.Configuration.BaseFolder;
if( ImGui.InputText( "Root Folder", ref basePath, 255 ) )
{
_plugin.Configuration.BaseFolder = basePath;
}

if( ImGui.Button( "Rediscover Mods" ) )
{
ReloadMods();
}

if( !_isImportRunning )
{
if( ImGui.Button( "Import TexTools Modpacks" ) )
{
_isImportRunning = true;

Task.Run( async () =>
{
var picker = new OpenFileDialog
{
Multiselect = true,
Filter = "TexTools TTMP Modpack (*.ttmp2)|*.ttmp*|All files (*.*)|*.*",
CheckFileExists = true,
Title = "Pick one or more modpacks."
};

var result = await picker.ShowDialogAsync();

if( result == DialogResult.OK )
{
try
{
var importer =
new TexToolsImport( new DirectoryInfo( _plugin.Configuration.BaseFolder ) );

foreach( var fileName in picker.FileNames )
{
PluginLog.Log( "-> {0} START", fileName );

importer.ImportModPack( new FileInfo( fileName ) );

PluginLog.Log( "-> {0} OK!", fileName );
}

ReloadMods();
}
catch( Exception ex )
{
PluginLog.LogError( ex, "Could not import one or more modpacks." );
}
}

_isImportRunning = false;
} );
}
}
else
{
ImGui.Button( "Import in progress..." );
}

if( ImGui.Button( "Save Settings" ) )
_plugin.Configuration.Save();

ImGui.EndTabItem();
}

void DrawModsSelector()
{
// Selector pane
ImGui.BeginGroup();
ImGui.PushStyleVar( ImGuiStyleVar.ItemSpacing, new Vector2( 0, 0 ) );

// Inlay selector list
ImGui.BeginChild( "availableModList", new Vector2( 180, -ImGui.GetFrameHeightWithSpacing() ), true );

for( var modIndex = 0; modIndex < _plugin.ModManager.AvailableMods.Count; modIndex++ )
{
var mod = _plugin.ModManager.AvailableMods.ElementAt( modIndex );

if( ImGui.Selectable( mod.Value.Meta.Name, modIndex == _selectedModIndex ) )
{
_selectedModIndex = modIndex;
_selectedMod = mod.Value;
}
}

ImGui.EndChild();

// Selector controls
ImGui.PushStyleVar( ImGuiStyleVar.WindowPadding, new Vector2( 0, 0 ) );
ImGui.PushStyleVar( ImGuiStyleVar.FrameRounding, 0 );
ImGui.PushFont( UiBuilder.IconFont );
if( _selectedModIndex != 0 )
{
if( ImGui.Button( FontAwesomeIcon.ArrowUp.ToIconString(), new Vector2( 45, 0 ) ) )
{
}
}
else
{
ImGui.PushStyleVar( ImGuiStyleVar.Alpha, 0.5f );
ImGui.Button( FontAwesomeIcon.ArrowUp.ToIconString(), new Vector2( 45, 0 ) );
ImGui.PopStyleVar();
}

ImGui.PopFont();

if( ImGui.IsItemHovered() )
ImGui.SetTooltip( "Move the selected mod up in priority" );

ImGui.PushFont( UiBuilder.IconFont );

ImGui.SameLine();

if( _selectedModIndex != _plugin.ModManager.AvailableMods.Count - 1 )
{
if( ImGui.Button( FontAwesomeIcon.ArrowDown.ToIconString(), new Vector2( 45, 0 ) ) )
{
}
}
else
{
ImGui.PushStyleVar( ImGuiStyleVar.Alpha, 0.5f );
ImGui.Button( FontAwesomeIcon.ArrowDown.ToIconString(), new Vector2( 45, 0 ) );
ImGui.PopStyleVar();
}


ImGui.PopFont();

if( ImGui.IsItemHovered() )
ImGui.SetTooltip( "Move the selected mod down in priority" );

ImGui.PushFont( UiBuilder.IconFont );

ImGui.SameLine();

if( ImGui.Button( FontAwesomeIcon.Trash.ToIconString(), new Vector2( 45, 0 ) ) )
{
}

ImGui.PopFont();

if( ImGui.IsItemHovered() )
ImGui.SetTooltip( "Delete the selected mod" );

ImGui.PushFont( UiBuilder.IconFont );

ImGui.SameLine();

if( ImGui.Button( FontAwesomeIcon.Plus.ToIconString(), new Vector2( 45, 0 ) ) )
{
}

ImGui.PopFont();

if( ImGui.IsItemHovered() )
ImGui.SetTooltip( "Add an empty mod" );

ImGui.PopStyleVar( 3 );

ImGui.EndGroup();
}

void DrawResourceMods()
{
var ret = ImGui.BeginTabItem( "Resource Mods" );
if( !ret )
{
return;
}

DrawModsSelector();

ImGui.SameLine();

if( _selectedMod != null )
{
try
{
ImGui.BeginChild( "selectedModInfo", AutoFillSize, true );

ImGui.Text( _selectedMod.Meta.Name );
ImGui.SameLine();
ImGui.TextColored( new Vector4( 1f, 1f, 1f, 0.66f ), "by" );
ImGui.SameLine();
ImGui.Text( _selectedMod.Meta.Author );

ImGui.TextWrapped( _selectedMod.Meta.Description ?? "" );

ImGui.SetCursorPosY( ImGui.GetCursorPosY() + 12 );

// list files
ImGui.Text( "Files:" );
ImGui.SetNextItemWidth( -1 );
if( ImGui.ListBoxHeader( "##", AutoFillSize ) )
{
foreach( var file in _selectedMod.ModFiles )
{
ImGui.Selectable( file.FullName );
}
}

ImGui.ListBoxFooter();

ImGui.EndChild();
}
catch( Exception ex )
{
PluginLog.LogError( ex, "fuck" );
}
}

ImGui.EndTabItem();
}

void DrawEffectiveFileList()
{
var ret = ImGui.BeginTabItem( "Effective File List" );
if( !ret )
{
return;
}

if( ImGui.ListBoxHeader( "##", AutoFillSize ) )
{
// todo: virtualise this
foreach( var file in _plugin.ModManager.ResolvedFiles )
{
ImGui.Selectable( file.Value.FullName );
}
}

ImGui.ListBoxFooter();

ImGui.EndTabItem();
}

private void ReloadMods()
{
_selectedMod = null;

// haha yikes
_plugin.ModManager = new ModManager( new DirectoryInfo( _plugin.Configuration.BaseFolder ) );
_plugin.ModManager.DiscoverMods();
}
}
}
9 changes: 9 additions & 0 deletions Penumbra/Structs/FileMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Penumbra.Structs
{
public enum FileMode : uint
{
LoadUnpackedResource = 0,
LoadFileResource = 1, // Shit in My Games uses this
LoadSqpackResource = 0x0B
}
}
11 changes: 11 additions & 0 deletions Penumbra/Structs/ResourceHandle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Runtime.InteropServices;

namespace Penumbra.Structs
{
[StructLayout( LayoutKind.Explicit )]
public unsafe struct ResourceHandle
{
[FieldOffset( 0x48 )]
public byte* FileName;
}
}
21 changes: 21 additions & 0 deletions Penumbra/Structs/SeFileDescriptor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Runtime.InteropServices;

namespace Penumbra.Structs
{
[StructLayout( LayoutKind.Explicit )]
public unsafe struct SeFileDescriptor
{
[FieldOffset( 0x00 )]
public FileMode FileMode;

[FieldOffset( 0x30 )]
public void* FileDescriptor; //

[FieldOffset( 0x50 )]
public ResourceHandle* ResourceHandle; //


[FieldOffset( 0x68 )]
public byte UtfFileName; //
}
}
55 changes: 55 additions & 0 deletions Penumbra/Util/Crc32.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System;
using System.Linq;
using System.Runtime.CompilerServices;
using Penumbra.Extensions;

namespace Penumbra.Util
{
/// <summary>
/// Performs the 32-bit reversed variant of the cyclic redundancy check algorithm
/// </summary>
public class Crc32
{
private const uint POLY = 0xedb88320;

private static readonly uint[] CrcArray =
Enumerable.Range( 0, 256 ).Select( i =>
{
var k = ( uint )i;
for( var j = 0; j < 8; j++ )
k = ( k & 1 ) != 0 ? ( k >> 1 ) ^ POLY : k >> 1;

return k;
} ).ToArray();

public uint Checksum => ~_crc32;

private uint _crc32 = 0xFFFFFFFF;

/// <summary>
/// Initializes Crc32's state
/// </summary>
public void Init()
{
_crc32 = 0xFFFFFFFF;
}

/// <summary>
/// Updates Crc32's state with new data
/// </summary>
/// <param name="data">Data to calculate the new CRC from</param>
[MethodImpl( MethodImplOptions.AggressiveInlining )]
public void Update( byte[] data )
{
foreach( var b in data )
Update( b );
}

[MethodImpl( MethodImplOptions.AggressiveInlining )]
public void Update( byte b )
{
_crc32 = CrcArray[ ( _crc32 ^ b ) & 0xFF ] ^
( ( _crc32 >> 8 ) & 0x00FFFFFF );
}
}
}
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Penumbra

Penumbra is a runtime mod loader for FINAL FANTASY XIV, with a bunch of other useful features baked in:

* No need to back up your install - mods don't touch game files
* Disable and enable mods without restarting the game
* Resolve conflicts between mods by changing mod order
* Files can be edited and are often replicated in-game after a map change or closing and reopening a window

## Current Status
Penumbra, in its current state, is not intended for widespread use. It is mainly aimed at developers and people who don't need their hands held (for now).

We're working towards a 1.0 release, and you can follow it's progress [here](https://github.com/xivdev/Penumbra/projects/1).

## Contributing
Contributions are welcome, but please make an issue first before writing any code. It's possible what you want to implement is out of scope for this project, or could be reworked so that it would provide greater benefit.

## TexTools Mods
Penumbra has support for TexTools modpacks, however no support will be given. This is aimed as an interim measure while better tooling is developed, but note that there is no ETA for this.

### Why not support TexTools?

Because it sucks. It's slow to use, the workflow is awful and the codebase is so far gone any notable improvements or support for new features would mean throwing out significant portions of the existing codebase. If you're interested in helping build new tooling, or are just curious about what's available, you can check out Umbra [here](https://github.com/NotAdam/Lumina/tree/master/src/Umbra).

0 comments on commit 0e7650f

Please sign in to comment.