mirror of
https://github.com/xivdev/Penumbra.git
synced 2026-02-21 23:37:47 +01:00
initial commit
This commit is contained in:
commit
0e7650f89b
27 changed files with 1596 additions and 0 deletions
31
Penumbra/Configuration.cs
Normal file
31
Penumbra/Configuration.cs
Normal 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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
87
Penumbra/DialogExtensions.cs
Normal file
87
Penumbra/DialogExtensions.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
80
Penumbra/Extensions/FuckedExtensions.cs
Normal file
80
Penumbra/Extensions/FuckedExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
39
Penumbra/Importer/Models/ExtendedModPack.cs
Normal file
39
Penumbra/Importer/Models/ExtendedModPack.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
25
Penumbra/Importer/Models/SimpleModPack.cs
Normal file
25
Penumbra/Importer/Models/SimpleModPack.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
223
Penumbra/Importer/TexToolsImport.cs
Normal file
223
Penumbra/Importer/TexToolsImport.cs
Normal 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
100
Penumbra/ModManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
9
Penumbra/Models/ModMeta.cs
Normal file
9
Penumbra/Models/ModMeta.cs
Normal 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
193
Penumbra/Penumbra.cs
Normal 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
95
Penumbra/Penumbra.csproj
Normal 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
68
Penumbra/Plugin.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
35
Penumbra/Properties/AssemblyInfo.cs
Normal file
35
Penumbra/Properties/AssemblyInfo.cs
Normal 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
35
Penumbra/ResourceMod.cs
Normal 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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
316
Penumbra/SettingsInterface.cs
Normal file
316
Penumbra/SettingsInterface.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
9
Penumbra/Structs/FileMode.cs
Normal file
9
Penumbra/Structs/FileMode.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
namespace Penumbra.Structs
|
||||
{
|
||||
public enum FileMode : uint
|
||||
{
|
||||
LoadUnpackedResource = 0,
|
||||
LoadFileResource = 1, // Shit in My Games uses this
|
||||
LoadSqpackResource = 0x0B
|
||||
}
|
||||
}
|
||||
11
Penumbra/Structs/ResourceHandle.cs
Normal file
11
Penumbra/Structs/ResourceHandle.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Penumbra.Structs
|
||||
{
|
||||
[StructLayout( LayoutKind.Explicit )]
|
||||
public unsafe struct ResourceHandle
|
||||
{
|
||||
[FieldOffset( 0x48 )]
|
||||
public byte* FileName;
|
||||
}
|
||||
}
|
||||
21
Penumbra/Structs/SeFileDescriptor.cs
Normal file
21
Penumbra/Structs/SeFileDescriptor.cs
Normal 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
55
Penumbra/Util/Crc32.cs
Normal 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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue