fix #11, reshuffled some UI and added a button to the main menu/lobby

This commit is contained in:
Adam 2021-01-06 20:03:11 +11:00
parent 15eb435929
commit 8adeab1ba5
12 changed files with 399 additions and 134 deletions

View file

@ -12,6 +12,10 @@ namespace Penumbra
public bool IsEnabled { get; set; } = true;
public bool ShowAdvanced { get; set; } = false;
public bool DisableFileSystemNotifications { get; set; } = false;
public string CurrentCollection { get; set; } = @"D:/ffxiv/fs_mods/";
public List< string > ModCollections { get; set; } = new();

View file

@ -0,0 +1,50 @@
using System;
using System.Runtime.InteropServices;
using Dalamud.Plugin;
using Reloaded.Hooks.Definitions.X64;
namespace Penumbra.Game
{
public class GameUtils
{
private readonly DalamudPluginInterface _pluginInterface;
[Function( CallingConventions.Microsoft )]
public unsafe delegate void* LoadPlayerResourcesPrototype( IntPtr pResourceManager );
[Function( CallingConventions.Microsoft )]
public unsafe delegate void* UnloadPlayerResourcesPrototype( IntPtr pResourceManager );
public LoadPlayerResourcesPrototype LoadPlayerResources { get; private set; }
public UnloadPlayerResourcesPrototype UnloadPlayerResources { get; private set; }
// Object addresses
private IntPtr _playerResourceManagerAddress;
public IntPtr PlayerResourceManagerPtr => Marshal.ReadIntPtr( _playerResourceManagerAddress );
public GameUtils( DalamudPluginInterface pluginInterface )
{
_pluginInterface = pluginInterface;
var scanner = _pluginInterface.TargetModuleScanner;
var loadPlayerResourcesAddress =
scanner.ScanText(
"E8 ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? BA ?? ?? ?? ?? 41 B8 ?? ?? ?? ?? 48 8B 48 30 48 8B 01 FF 50 10 48 85 C0 74 0A " );
var unloadPlayerResourcesAddress =
scanner.ScanText( "41 55 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 4C 8B E9 48 83 C1 08" );
_playerResourceManagerAddress = scanner.GetStaticAddressFromSig( "0F 44 FE 48 8B 0D ?? ?? ?? ?? 48 85 C9 74 05" );
LoadPlayerResources = Marshal.GetDelegateForFunctionPointer< LoadPlayerResourcesPrototype >( loadPlayerResourcesAddress );
UnloadPlayerResources = Marshal.GetDelegateForFunctionPointer< UnloadPlayerResourcesPrototype >( unloadPlayerResourcesAddress );
}
public unsafe void ReloadPlayerResources()
{
UnloadPlayerResources( PlayerResourceManagerPtr );
LoadPlayerResources( PlayerResourceManagerPtr );
}
}
}

View file

@ -0,0 +1,10 @@
namespace Penumbra.Importer
{
public enum ImporterState
{
None,
WritingPackToDisk,
ExtractingModFiles,
Done
}
}

View file

@ -0,0 +1,26 @@
using System;
using System.IO;
using Lumina.Data;
namespace Penumbra.Importer
{
public class MagicTempFileStreamManagerAndDeleterFuckery : SqPackStream, IDisposable
{
private readonly FileStream _fileStream;
public MagicTempFileStreamManagerAndDeleterFuckery( FileStream stream ) : base( stream )
{
_fileStream = stream;
}
public new void Dispose()
{
var filePath = _fileStream.Name;
base.Dispose();
_fileStream.Dispose();
File.Delete( filePath );
}
}
}

View file

@ -16,13 +16,40 @@ namespace Penumbra.Importer
{
private readonly DirectoryInfo _outDirectory;
private const string TempFileName = "textools-import";
private readonly string _resolvedTempFilePath = null;
public ImporterState State { get; private set; }
public long TotalProgress { get; private set; }
public long CurrentProgress { get; private set; }
public float Progress
{
get
{
if( CurrentProgress != 0 )
{
// ReSharper disable twice RedundantCast
return ( float )CurrentProgress / ( float )TotalProgress;
}
return 0;
}
}
public string CurrentModPack { get; private set; }
public TexToolsImport( DirectoryInfo outDirectory )
{
_outDirectory = outDirectory;
_resolvedTempFilePath = Path.Combine( _outDirectory.FullName, TempFileName );
}
public void ImportModPack( FileInfo modPackFile )
{
CurrentModPack = modPackFile.Name;
switch( modPackFile.Extension )
{
case ".ttmp":
@ -33,6 +60,29 @@ namespace Penumbra.Importer
ImportV2ModPack( modPackFile );
return;
}
State = ImporterState.Done;
}
private void WriteZipEntryToTempFile( Stream s )
{
var fs = new FileStream( _resolvedTempFilePath, FileMode.Create );
s.CopyTo( fs );
fs.Close();
}
private SqPackStream GetMagicSqPackDeleterStream( ZipFile file, string entryName )
{
State = ImporterState.WritingPackToDisk;
// write shitty zip garbage to disk
var entry = file.GetEntry( entryName );
using var s = file.GetInputStream( entry );
WriteZipEntryToTempFile( s );
var fs = new FileStream( _resolvedTempFilePath, FileMode.Open );
return new MagicTempFileStreamManagerAndDeleterFuckery( fs );
}
private void ImportV1ModPack( FileInfo modPackFile )
@ -59,8 +109,7 @@ namespace Penumbra.Importer
};
// Open the mod data file from the modpack as a SqPackStream
var mpd = extractedModPack.GetEntry( "TTMPD.mpd" );
var modData = GetSqPackStreamFromZipEntry( extractedModPack, mpd );
using var modData = GetMagicSqPackDeleterStream( extractedModPack, "TTMPD.mpd" );
var newModFolder = new DirectoryInfo(
Path.Combine( _outDirectory.FullName,
@ -115,8 +164,7 @@ namespace Penumbra.Importer
};
// Open the mod data file from the modpack as a SqPackStream
var mpd = extractedModPack.GetEntry( "TTMPD.mpd" );
var modData = GetSqPackStreamFromZipEntry( extractedModPack, mpd );
using var modData = GetMagicSqPackDeleterStream( extractedModPack, "TTMPD.mpd" );
var newModFolder = new DirectoryInfo( Path.Combine( _outDirectory.FullName,
Path.GetFileNameWithoutExtension( modList.Name ) ) );
@ -142,12 +190,12 @@ namespace Penumbra.Importer
Name = modList.Name,
Description = string.IsNullOrEmpty( modList.Description )
? "Mod imported from TexTools mod pack"
: modList.Description
: modList.Description,
Version = modList.Version
};
// Open the mod data file from the modpack as a SqPackStream
var mpd = extractedModPack.GetEntry( "TTMPD.mpd" );
var modData = GetSqPackStreamFromZipEntry( extractedModPack, mpd );
using var modData = GetMagicSqPackDeleterStream( extractedModPack, "TTMPD.mpd" );
var newModFolder = new DirectoryInfo(
Path.Combine( _outDirectory.FullName,
@ -187,13 +235,24 @@ namespace Penumbra.Importer
private void ExtractSimpleModList( DirectoryInfo outDirectory, IEnumerable< SimpleMod > mods, SqPackStream dataStream )
{
State = ImporterState.ExtractingModFiles;
// haha allocation go brr
var wtf = mods.ToList();
TotalProgress = wtf.LongCount();
// Extract each SimpleMod into the new mod folder
foreach( var simpleMod in mods )
foreach( var simpleMod in wtf )
{
if( simpleMod == null )
{
// do we increment here too???? can this even happen?????
continue;
}
ExtractMod( outDirectory, simpleMod, dataStream );
CurrentProgress++;
}
}
@ -228,10 +287,5 @@ namespace Penumbra.Importer
s.CopyTo( ms );
return encoding.GetString( ms.ToArray() );
}
private static SqPackStream GetSqPackStreamFromZipEntry( ZipFile file, ZipEntry entry )
{
return new( GetStreamFromZipEntry( file, entry ) );
}
}
}

View file

@ -7,6 +7,10 @@ namespace Penumbra.Models
public string Name { get; set; }
public string Author { get; set; }
public string Description { get; set; }
public string Version { get; set; }
public string Website { get; set; }
public Dictionary< string, string > FileSwaps { get; } = new();
}

View file

@ -15,6 +15,7 @@ namespace Penumbra.Mods
public List< ModInfo > ModSettings { get; set; }
public ResourceMod[] EnabledMods { get; set; }
public ModCollection( DirectoryInfo basePath )
{
_basePath = basePath;
@ -117,7 +118,7 @@ namespace Penumbra.Mods
public void ReorderMod( ModInfo info, bool up )
{
// todo: certified fucked tier
var prio = info.Priority;
var swapPrio = up ? prio + 1 : prio - 1;
var swapMeta = ModSettings.FirstOrDefault( x => x.Priority == swapPrio );
@ -126,10 +127,10 @@ namespace Penumbra.Mods
{
return;
}
info.Priority = swapPrio;
swapMeta.Priority = prio;
// reorder mods list
ModSettings = ModSettings.OrderBy( x => x.Priority ).ToList();
EnabledMods = GetOrderedAndEnabledModList().ToArray();
@ -137,8 +138,7 @@ namespace Penumbra.Mods
// save new prios
Save();
}
public ModInfo FindModSettings( string name )
{

View file

@ -1,10 +1,11 @@
using System;
using System.Collections.Generic;
using System.IO;
using Penumbra.Models;
namespace Penumbra.Mods
{
public class ModManager
public class ModManager : IDisposable
{
public readonly Dictionary< string, FileInfo > ResolvedFiles = new();
public readonly Dictionary< string, string > SwappedFiles = new();
@ -23,6 +24,33 @@ namespace Penumbra.Mods
DiscoverMods( _basePath );
}
// private void FileSystemWatcherOnChanged( object sender, FileSystemEventArgs e )
// {
// #if DEBUG
// PluginLog.Verbose( "file changed: {FullPath}", e.FullPath );
// #endif
//
// if( _plugin.ImportInProgress )
// {
// return;
// }
//
// if( _plugin.Configuration.DisableFileSystemNotifications )
// {
// return;
// }
//
// var file = e.FullPath;
//
// if( !ResolvedFiles.Any( x => x.Value.FullName == file ) )
// {
// return;
// }
//
// PluginLog.Log( "a loaded file has been modified - file: {FullPath}", file );
// _plugin.GameUtils.ReloadPlayerResources();
// }
public void DiscoverMods( string basePath )
{
DiscoverMods( new DirectoryInfo( basePath ) );
@ -43,7 +71,21 @@ namespace Penumbra.Mods
_basePath = basePath;
ResolvedFiles.Clear();
// haha spaghet
// _fileSystemWatcher?.Dispose();
// _fileSystemWatcher = new FileSystemWatcher( _basePath.FullName )
// {
// NotifyFilter = NotifyFilters.LastWrite |
// NotifyFilters.FileName |
// NotifyFilters.DirectoryName,
// IncludeSubdirectories = true,
// EnableRaisingEvents = true
// };
//
// _fileSystemWatcher.Changed += FileSystemWatcherOnChanged;
// _fileSystemWatcher.Created += FileSystemWatcherOnChanged;
// _fileSystemWatcher.Deleted += FileSystemWatcherOnChanged;
// _fileSystemWatcher.Renamed += FileSystemWatcherOnChanged;
Mods = new ModCollection( basePath );
Mods.Load();
@ -62,7 +104,7 @@ namespace Penumbra.Mods
foreach( var mod in Mods.GetOrderedAndEnabledModList() )
{
mod.FileConflicts?.Clear();
// fixup path
var baseDir = mod.ModBasePath.FullName;
@ -138,5 +180,10 @@ namespace Penumbra.Mods
return GetCandidateForGameFile( gameResourcePath )?.FullName ?? GetSwappedFilePath( gameResourcePath );
}
public void Dispose()
{
// _fileSystemWatcher?.Dispose();
}
}
}

View file

@ -7,5 +7,6 @@
"RepoUrl": "https://github.com/xivdev/Penumbra",
"ApplicableVersion": "any",
"Tags": [ "modding" ],
"DalamudApiLevel": 69420
"DalamudApiLevel": 69420,
"LoadPriority": 69420
}

View file

@ -1,11 +1,6 @@
using System;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Runtime.InteropServices;
using Dalamud.Game.Command;
using Dalamud.Plugin;
using Penumbra.Extensions;
using Penumbra.Game;
using Penumbra.Mods;
using Penumbra.UI;
@ -18,6 +13,7 @@ namespace Penumbra
private const string CommandName = "/penumbra";
public DalamudPluginInterface PluginInterface { get; set; }
public Configuration Configuration { get; set; }
public ResourceLoader ResourceLoader { get; set; }
@ -26,21 +22,26 @@ namespace Penumbra
public SettingsInterface SettingsInterface { get; set; }
public GameUtils GameUtils { get; set; }
public string PluginDebugTitleStr { get; private set; }
public bool ImportInProgress => SettingsInterface?.IsImportRunning ?? true;
public void Initialize( DalamudPluginInterface pluginInterface )
{
PluginInterface = pluginInterface;
Configuration = PluginInterface.GetPluginConfig() as Configuration ?? new Configuration();
Configuration.Initialize( PluginInterface );
GameUtils = new GameUtils( PluginInterface );
ModManager = new ModManager();
ModManager.DiscoverMods( Configuration.CurrentCollection );
ResourceLoader = new ResourceLoader( this );
PluginInterface.CommandManager.AddHandler( CommandName, new CommandInfo( OnCommand )
{
HelpMessage = "/penumbra - toggle ui\n/penumbra reload - reload mod file lists & discover any new mods"
@ -48,7 +49,7 @@ namespace Penumbra
ResourceLoader.Init();
ResourceLoader.Enable();
SettingsInterface = new SettingsInterface( this );
PluginInterface.UiBuilder.OnBuildUi += SettingsInterface.Draw;
@ -57,6 +58,8 @@ namespace Penumbra
public void Dispose()
{
ModManager?.Dispose();
PluginInterface.UiBuilder.OnBuildUi -= SettingsInterface.Draw;
PluginInterface.CommandManager.RemoveHandler( CommandName );

View file

@ -36,13 +36,6 @@ namespace Penumbra
public unsafe delegate void* GetResourceAsyncPrototype( IntPtr pFileManager, uint* pCategoryId, char* pResourceType,
uint* pResourceHash, char* pPath, void* pUnknown, bool isUnknown );
[Function( CallingConventions.Microsoft )]
public unsafe delegate void* LoadPlayerResourcesPrototype( IntPtr pResourceManager );
[Function( CallingConventions.Microsoft )]
public unsafe delegate void* UnloadPlayerResourcesPrototype( IntPtr pResourceManager );
// Hooks
public IHook< GetResourceSyncPrototype > GetResourceSyncHook { get; private set; }
public IHook< GetResourceAsyncPrototype > GetResourceAsyncHook { get; private set; }
@ -52,14 +45,6 @@ namespace Penumbra
public ReadFilePrototype ReadFile { get; private set; }
public LoadPlayerResourcesPrototype LoadPlayerResources { get; private set; }
public UnloadPlayerResourcesPrototype UnloadPlayerResources { get; private set; }
// Object addresses
private IntPtr _playerResourceManagerAddress;
public IntPtr PlayerResourceManagerPtr => Marshal.ReadIntPtr( _playerResourceManagerAddress );
public bool LogAllFiles = false;
@ -91,20 +76,6 @@ namespace Penumbra
GetResourceAsyncHook = new Hook< GetResourceAsyncPrototype >( GetResourceAsyncHandler, ( long )getResourceAsyncAddress );
ReadFile = Marshal.GetDelegateForFunctionPointer< ReadFilePrototype >( readFileAddress );
/////
var loadPlayerResourcesAddress =
scanner.ScanText(
"E8 ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? BA ?? ?? ?? ?? 41 B8 ?? ?? ?? ?? 48 8B 48 30 48 8B 01 FF 50 10 48 85 C0 74 0A " );
var unloadPlayerResourcesAddress =
scanner.ScanText( "41 55 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 4C 8B E9 48 83 C1 08" );
_playerResourceManagerAddress = scanner.GetStaticAddressFromSig( "0F 44 FE 48 8B 0D ?? ?? ?? ?? 48 85 C9 74 05" );
LoadPlayerResources = Marshal.GetDelegateForFunctionPointer< LoadPlayerResourcesPrototype >( loadPlayerResourcesAddress );
UnloadPlayerResources = Marshal.GetDelegateForFunctionPointer< UnloadPlayerResourcesPrototype >( unloadPlayerResourcesAddress );
ReadFile = Marshal.GetDelegateForFunctionPointer< ReadFilePrototype >( readFileAddress );
}
@ -215,12 +186,6 @@ namespace Penumbra
return ReadFile( pFileHandler, pFileDesc, priority, isSync );
}
public unsafe void ReloadPlayerResource()
{
UnloadPlayerResources( PlayerResourceManagerPtr );
LoadPlayerResources( PlayerResourceManagerPtr );
}
public void Enable()
{
if( IsEnabled )

View file

@ -32,7 +32,8 @@ namespace Penumbra.UI
private int? _selectedModDeleteIndex;
private ModInfo _selectedMod;
private bool _isImportRunning = false;
public bool IsImportRunning = false;
private TexToolsImport _texToolsImport = null!;
public SettingsInterface( Plugin plugin )
{
@ -72,6 +73,39 @@ namespace Penumbra.UI
ImGui.EndMainMenuBar();
}
if( !_plugin.PluginInterface.ClientState.Condition.Any() && !Visible )
{
// draw mods button on da menu :DDD
var ss = ImGui.GetIO().DisplaySize;
var padding = 50;
var width = 200;
var height = 45;
// magic numbers
ImGui.SetNextWindowPos( new Vector2( ss.X - padding - width, ss.Y - padding - height ), ImGuiCond.Always );
if(
ImGui.Begin(
"Penumbra Menu Buttons",
ImGuiWindowFlags.AlwaysAutoResize |
ImGuiWindowFlags.NoBackground |
ImGuiWindowFlags.NoDecoration |
ImGuiWindowFlags.NoMove |
ImGuiWindowFlags.NoScrollbar |
ImGuiWindowFlags.NoResize |
ImGuiWindowFlags.NoSavedSettings
)
)
{
if( ImGui.Button( "Manage Mods", new Vector2( width, height ) ) )
{
Visible = !Visible;
}
ImGui.End();
}
}
if( !Visible )
{
return;
@ -91,11 +125,19 @@ namespace Penumbra.UI
ImGui.BeginTabBar( "PenumbraSettings" );
DrawSettingsTab();
DrawImportTab();
if( !_isImportRunning )
if( !IsImportRunning )
{
DrawResourceMods();
DrawEffectiveFileList();
DrawModBrowser();
DrawInstalledMods();
if( _plugin.Configuration.ShowAdvanced )
{
DrawEffectiveFileList();
}
DrawDeleteModal();
}
@ -105,6 +147,105 @@ namespace Penumbra.UI
ImGui.End();
}
void DrawImportTab()
{
var ret = ImGui.BeginTabItem( "Import Mods" );
if( !ret )
{
return;
}
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 )
{
foreach( var fileName in picker.FileNames )
{
PluginLog.Log( "-> {0} START", fileName );
try
{
_texToolsImport = new TexToolsImport( new DirectoryInfo( _plugin.Configuration.CurrentCollection ) );
_texToolsImport.ImportModPack( new FileInfo( fileName ) );
}
catch( Exception ex )
{
PluginLog.LogError( ex, "Could not import one or more modpacks." );
}
PluginLog.Log( "-> {0} OK!", fileName );
}
_texToolsImport = null;
ReloadMods();
}
IsImportRunning = false;
} );
}
}
else
{
ImGui.Button( "Import in progress..." );
if( _texToolsImport != null )
{
switch( _texToolsImport.State )
{
case ImporterState.None:
break;
case ImporterState.WritingPackToDisk:
ImGui.Text( "Writing modpack to disk before extracting..." );
break;
case ImporterState.ExtractingModFiles:
{
var str =
$"{_texToolsImport.CurrentModPack} - {_texToolsImport.CurrentProgress} of {_texToolsImport.TotalProgress} files";
ImGui.ProgressBar( _texToolsImport.Progress, new Vector2( -1, 0 ), str );
break;
}
case ImporterState.Done:
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
ImGui.EndTabItem();
}
[Conditional( "DEBUG" )]
void DrawModBrowser()
{
var ret = ImGui.BeginTabItem( "Available Mods" );
if( !ret )
{
return;
}
ImGui.Text( "woah" );
ImGui.EndTabItem();
}
void DrawSettingsTab()
{
var ret = ImGui.BeginTabItem( "Settings" );
@ -113,11 +254,14 @@ namespace Penumbra.UI
return;
}
bool dirty = false;
// FUCKKKKK
var basePath = _plugin.Configuration.CurrentCollection;
if( ImGui.InputText( "Root Folder", ref basePath, 255 ) )
{
_plugin.Configuration.CurrentCollection = basePath;
dirty = true;
}
if( ImGui.Button( "Rediscover Mods" ) )
@ -134,77 +278,34 @@ namespace Penumbra.UI
ImGui.SetCursorPosY( ImGui.GetCursorPosY() + 15 );
#if DEBUG
ImGui.Text( "debug shit" );
if( ImGui.Button( "Reload Player Resource" ) )
var showAdvanced = _plugin.Configuration.ShowAdvanced;
if( ImGui.Checkbox( "Show Advanced Settings", ref showAdvanced ) )
{
_plugin.ResourceLoader.ReloadPlayerResource();
_plugin.Configuration.ShowAdvanced = showAdvanced;
dirty = true;
}
if( _plugin.ResourceLoader != null )
if( _plugin.Configuration.ShowAdvanced )
{
ImGui.Checkbox( "DEBUG Log all loaded files", ref _plugin.ResourceLoader.LogAllFiles );
}
ImGui.SetCursorPosY( ImGui.GetCursorPosY() + 15 );
#endif
if( !_isImportRunning )
{
if( ImGui.Button( "Import TexTools Modpacks" ) )
if( _plugin.ResourceLoader != null )
{
_isImportRunning = true;
ImGui.Checkbox( "Log all loaded files", ref _plugin.ResourceLoader.LogAllFiles );
}
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 fswatch = _plugin.Configuration.DisableFileSystemNotifications;
if( ImGui.Checkbox( "Disable filesystem change notifications", ref fswatch ) )
{
_plugin.Configuration.DisableFileSystemNotifications = fswatch;
dirty = true;
}
var result = await picker.ShowDialogAsync();
if( result == DialogResult.OK )
{
try
{
var importer =
new TexToolsImport( new DirectoryInfo( _plugin.Configuration.CurrentCollection ) );
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;
} );
if( ImGui.Button( "Reload Player Resource" ) )
{
_plugin.GameUtils.ReloadPlayerResources();
}
}
else
{
ImGui.Button( "Import in progress..." );
}
ImGui.SetCursorPosY( ImGui.GetCursorPosY() + 15 );
if( ImGui.Button( "Save Settings" ) )
if( dirty )
{
_plugin.Configuration.Save();
}
@ -385,9 +486,9 @@ namespace Penumbra.UI
ImGui.EndPopup();
}
void DrawResourceMods()
void DrawInstalledMods()
{
var ret = ImGui.BeginTabItem( "Mods" );
var ret = ImGui.BeginTabItem( "Installed Mods" );
if( !ret )
{
return;