initial commit

This commit is contained in:
Adam 2020-09-02 22:19:09 +10:00
commit 0e7650f89b
27 changed files with 1596 additions and 0 deletions

31
Penumbra/Configuration.cs Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
Penumbra/ModManager.cs Normal file
View file

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

View file

@ -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
Penumbra/Penumbra.cs Normal file
View file

@ -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
Penumbra/Penumbra.csproj Normal file
View file

@ -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
Penumbra/Plugin.cs Normal file
View file

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

View file

@ -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
Penumbra/ResourceMod.cs Normal file
View file

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

View file

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

View file

@ -0,0 +1,9 @@
namespace Penumbra.Structs
{
public enum FileMode : uint
{
LoadUnpackedResource = 0,
LoadFileResource = 1, // Shit in My Games uses this
LoadSqpackResource = 0x0B
}
}

View file

@ -0,0 +1,11 @@
using System.Runtime.InteropServices;
namespace Penumbra.Structs
{
[StructLayout( LayoutKind.Explicit )]
public unsafe struct ResourceHandle
{
[FieldOffset( 0x48 )]
public byte* FileName;
}
}

View file

@ -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
Penumbra/Util/Crc32.cs Normal file
View file

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