Merge pull request #13 from Ottermandias/modinfo_improved

Modinfo improved
This commit is contained in:
Adam 2021-01-15 23:47:52 +11:00 committed by GitHub
commit fa9e4d7dcc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 399 additions and 65 deletions

View file

@ -204,11 +204,6 @@ namespace Penumbra.Importer
); );
newModFolder.Create(); newModFolder.Create();
File.WriteAllText(
Path.Combine( newModFolder.FullName, "meta.json" ),
JsonConvert.SerializeObject( modMeta )
);
if( modList.SimpleModsList != null ) if( modList.SimpleModsList != null )
ExtractSimpleModList( newModFolder, modList.SimpleModsList, modData ); ExtractSimpleModList( newModFolder, modList.SimpleModsList, modData );
@ -216,15 +211,34 @@ namespace Penumbra.Importer
return; return;
// Iterate through all pages // 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 foreach( var option in from modPackPage in modList.ModPackPages
from modGroup in modPackPage.ModGroups from modGroup in modPackPage.ModGroups
from option in modGroup.OptionList from option in modGroup.OptionList
where option.IsChecked
select option ) select option )
{ {
ExtractSimpleModList( newModFolder, option.ModsJsons, modData ); var OptionFolder = new DirectoryInfo(Path.Combine(newModFolder.FullName, option.Name));
ExtractSimpleModList(OptionFolder, option.ModsJsons, modData );
AddMeta(OptionFolder, newModFolder, modMeta, option.Name);
}
File.WriteAllText(
Path.Combine( newModFolder.FullName, "meta.json" ),
JsonConvert.SerializeObject( modMeta, Formatting.Indented )
);
}
void AddMeta(DirectoryInfo optionFolder, DirectoryInfo baseFolder, ModMeta meta, string optionName)
{
var optionFolderLength = optionFolder.FullName.Length;
var baseFolderLength = baseFolder.FullName.Length;
foreach( var dir in optionFolder.EnumerateDirectories() )
{
foreach( var file in dir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
{
meta.Groups.AddFileToOtherGroups(optionName
, file.FullName.Substring(baseFolderLength).TrimStart('\\')
, file.FullName.Substring(optionFolderLength).TrimStart('\\').Replace('\\', '/'));
}
} }
} }

View file

@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Linq;
namespace Penumbra.Models
{
[Serializable]
public class GroupInformation : ISerializable
{
// This class is just used as a temp class while (de)-serializing.
// It converts the flags into lists and back.
[Serializable]
private class GroupDescription : ISerializable
{
public GroupDescription(GroupInformation info, (string, uint, uint, ulong) vars)
{
GamePath = vars.Item1;
static List<string> AddGroupTypes(ulong flags, ulong bound, List<string> groupType)
{
List<string> ret = null;
if (flags != uint.MaxValue)
{
ret = new();
for (var i = 0; i < groupType.Count; ++i)
{
var flag = 1u << i;
if ((flags & flag) == flag)
ret.Add(groupType[i]);
}
}
return ret;
}
// Tops and Bottoms are uint.
TopTypes = AddGroupTypes(vars.Item2, uint.MaxValue, info.TopTypes);
BottomTypes = AddGroupTypes(vars.Item3, uint.MaxValue, info.BottomTypes);
// Exclusions are the other way around and ulong.
GroupExclusions = AddGroupTypes(~vars.Item4, 0, info.OtherGroups);
}
public (string, uint, uint, ulong) ToTuple(GroupInformation info)
{
static ulong TypesToFlags(List<string> ownTypes, List<string> globalTypes)
{
if (ownTypes == null)
return ulong.MaxValue;
ulong flags = 0;
foreach (var x in ownTypes)
{
var index = globalTypes.IndexOf(x);
if (index >= 0)
flags |= (1u << index);
}
return flags;
}
var tops = (uint) TypesToFlags(TopTypes, info.TopTypes);
var bottoms = (uint) TypesToFlags(BottomTypes, info.BottomTypes);
// Exclusions are the other way around.
var groupEx = (GroupExclusions == null) ? ulong.MaxValue : ~TypesToFlags(GroupExclusions, info.OtherGroups);
return (GamePath, tops, bottoms, groupEx);
}
public string GamePath { get; set; }
public List<string> TopTypes { get; set; } = null;
public List<string> BottomTypes { get; set; } = null;
public List<string> GroupExclusions { get; set; } = null;
// Customize (De)-Serialization to ignore nulls.
public GroupDescription(SerializationInfo info, StreamingContext context)
{
List<string> readListOrNull(string name)
{
try
{
var ret = (List<string>) info.GetValue(name, typeof(List<string>));
if (ret == null || ret.Count == 0)
return null;
return ret;
}
catch (Exception) { return null; }
}
GamePath = info.GetString("GamePath");
TopTypes = readListOrNull("TopTypes");
BottomTypes = readListOrNull("BottomTypes");
GroupExclusions = readListOrNull("GroupExclusions");
}
public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue( "GamePath", GamePath );
if (TopTypes != null) info.AddValue("TopTypes", TopTypes);
if (BottomTypes != null) info.AddValue("BottomTypes", BottomTypes);
if (GroupExclusions != null) info.AddValue("GroupExclusions", GroupExclusions);
}
}
public List<string> TopTypes { get; set; } = new();
public List<string> BottomTypes { get; set; } = new();
public List<string> OtherGroups { get; set; } = new();
public void AddFileToOtherGroups(string optionName, string fileName, string gamePath)
{
var idx = OtherGroups.IndexOf(optionName);
if (idx < 0)
{
idx = OtherGroups.Count;
OtherGroups.Add(optionName);
}
(string, uint, uint, ulong) tuple = (gamePath, uint.MaxValue, uint.MaxValue, (1ul << idx));
if (!FileToGameAndGroup.TryGetValue(fileName, out var tuple2))
{
FileToGameAndGroup.Add(fileName, tuple);
}
else
{
tuple2.Item1 = tuple.Item1;
tuple2.Item4 |= tuple.Item4;
}
}
public Dictionary<string, (string, uint, uint, ulong)> FileToGameAndGroup { get; set; } = new();
public GroupInformation(){ }
public GroupInformation(SerializationInfo info, StreamingContext context)
{
try { TopTypes = (List<string>) info.GetValue( "TopTypes", TopTypes.GetType() ); } catch(Exception){ }
try { BottomTypes = (List<string>) info.GetValue( "BottomTypes", BottomTypes.GetType() ); } catch(Exception){ }
try { OtherGroups = (List<string>) info.GetValue( "Groups", OtherGroups.GetType() ); } catch(Exception){ }
try
{
Dictionary<string, GroupDescription> dict = new();
dict = (Dictionary<string, GroupDescription>) info.GetValue( "FileToGameAndGroups", dict.GetType());
foreach (var pair in dict)
FileToGameAndGroup.Add(pair.Key, pair.Value.ToTuple(this));
} catch (Exception){ }
}
public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
{
if ((TopTypes?.Count ?? 0) > 0) info.AddValue("TopTypes", TopTypes);
if ((BottomTypes?.Count ?? 0) > 0) info.AddValue("BottomTypes", BottomTypes);
if ((OtherGroups?.Count ?? 0) > 0) info.AddValue("Groups", OtherGroups);
if ((FileToGameAndGroup?.Count ?? 0) > 0)
{
var dict = FileToGameAndGroup.ToDictionary( pair => pair.Key, pair => new GroupDescription( this, pair.Value ) );
info.AddValue("FileToGameAndGroups", dict);
}
}
}
}

View file

@ -6,8 +6,11 @@ namespace Penumbra.Models
public class ModInfo public class ModInfo
{ {
public string FolderName { get; set; } public string FolderName { get; set; }
public bool Enabled { get; set; } public bool Enabled { get; set; }
public int Priority { get; set; } public int Priority { get; set; }
public int CurrentTop { get; set; } = 0;
public int CurrentBottom { get; set; } = 0;
public int CurrentGroup { get; set; } = 0;
[JsonIgnore] [JsonIgnore]
public ResourceMod Mod { get; set; } public ResourceMod Mod { get; set; }

View file

@ -12,6 +12,10 @@ namespace Penumbra.Models
public string Website { get; set; } public string Website { get; set; }
public List<string> ChangedItems { get; set; } = new();
public Dictionary< string, string > FileSwaps { get; } = new(); public Dictionary< string, string > FileSwaps { get; } = new();
public GroupInformation Groups { get; set; } = new();
} }
} }

View file

@ -181,12 +181,23 @@ namespace Penumbra.Mods
return AddModSettings( mod ); return AddModSettings( mod );
} }
public IEnumerable< ResourceMod > GetOrderedAndEnabledModList() public IEnumerable<ModInfo> GetOrderedAndEnabledModSettings()
{ {
return ModSettings return ModSettings
.Where( x => x.Enabled ) .Where( x => x.Enabled )
.OrderBy( x => x.Priority ) .OrderBy( x => x.Priority );
}
public IEnumerable<ResourceMod> GetOrderedAndEnabledModList()
{
return GetOrderedAndEnabledModSettings()
.Select( x => x.Mod ); .Select( x => x.Mod );
} }
public IEnumerable<(ResourceMod, ModInfo)> GetOrderedAndEnabledModListWithSettings()
{
return GetOrderedAndEnabledModSettings()
.Select( x => (x.Mod, x) );
}
} }
} }

View file

@ -110,7 +110,7 @@ namespace Penumbra.Mods
var registeredFiles = new Dictionary< string, string >(); var registeredFiles = new Dictionary< string, string >();
foreach( var mod in Mods.GetOrderedAndEnabledModList() ) foreach( var (mod, settings) in Mods.GetOrderedAndEnabledModListWithSettings() )
{ {
mod.FileConflicts?.Clear(); mod.FileConflicts?.Clear();
@ -119,17 +119,34 @@ namespace Penumbra.Mods
foreach( var file in mod.ModFiles ) foreach( var file in mod.ModFiles )
{ {
var gamePath = file.FullName.Substring( baseDir.Length ) var relativeFilePath = file.FullName.Substring( baseDir.Length ).TrimStart( '\\' );
.TrimStart( '\\' ).Replace( '\\', '/' );
if( !ResolvedFiles.ContainsKey( gamePath ) ) string gamePath;
bool addFile = true;
(string, uint, uint, ulong) tuple;
if (mod.Meta.Groups.FileToGameAndGroup.TryGetValue(relativeFilePath, out tuple))
{ {
ResolvedFiles[ gamePath.ToLowerInvariant() ] = file; gamePath = tuple.Item1;
registeredFiles[ gamePath ] = mod.Meta.Name; var (_, tops, bottoms, excludes) = tuple;
var validTop = ((1u << settings.CurrentTop) & tops ) != 0;
var validBottom = ((1u << settings.CurrentBottom) & bottoms ) != 0;
var validGroup = ((1ul << settings.CurrentGroup) & excludes) != 0;
addFile = validTop && validBottom && validGroup;
} }
else if( registeredFiles.TryGetValue( gamePath, out var modName ) ) else
gamePath = relativeFilePath.Replace( '\\', '/' );
if ( addFile )
{ {
mod.AddConflict( modName, gamePath ); if( !ResolvedFiles.ContainsKey( gamePath ) )
{
ResolvedFiles[ gamePath.ToLowerInvariant() ] = file;
registeredFiles[ gamePath ] = mod.Meta.Name;
}
else if( registeredFiles.TryGetValue( gamePath, out var modName ) )
{
mod.AddConflict( modName, gamePath );
}
} }
} }
@ -161,7 +178,6 @@ namespace Penumbra.Mods
DiscoverMods(); DiscoverMods();
} }
public FileInfo GetCandidateForGameFile( string gameResourcePath ) public FileInfo GetCandidateForGameFile( string gameResourcePath )
{ {
var val = ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ); var val = ResolvedFiles.TryGetValue( gameResourcePath, out var candidate );
@ -190,6 +206,7 @@ namespace Penumbra.Mods
return GetCandidateForGameFile( gameResourcePath )?.FullName ?? GetSwappedFilePath( gameResourcePath ); return GetCandidateForGameFile( gameResourcePath )?.FullName ?? GetSwappedFilePath( gameResourcePath );
} }
public void Dispose() public void Dispose()
{ {
// _fileSystemWatcher?.Dispose(); // _fileSystemWatcher?.Dispose();

View file

@ -31,6 +31,11 @@ namespace Penumbra.Mods
ModFiles.Add( file ); ModFiles.Add( file );
} }
} }
// Only add if not in a sub-folder, otherwise it was already added.
foreach( var pair in Meta.Groups.FileToGameAndGroup )
if (pair.Key.IndexOfAny(new char[]{'/', '\\'}) < 0)
ModFiles.Add( new FileInfo(Path.Combine(ModBasePath.FullName, pair.Key)) );
} }
public void AddConflict( string modName, string path ) public void AddConflict( string modName, string path )

View file

@ -8,6 +8,7 @@ using System.Windows.Forms;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Plugin; using Dalamud.Plugin;
using ImGuiNET; using ImGuiNET;
using Newtonsoft.Json;
using Penumbra.Importer; using Penumbra.Importer;
using Penumbra.Models; using Penumbra.Models;
@ -267,6 +268,8 @@ namespace Penumbra.UI
if( ImGui.Button( "Rediscover Mods" ) ) if( ImGui.Button( "Rediscover Mods" ) )
{ {
ReloadMods(); ReloadMods();
_selectedModIndex = 0;
_selectedMod = null;
} }
ImGui.SameLine(); ImGui.SameLine();
@ -502,6 +505,105 @@ namespace Penumbra.UI
ImGui.EndPopup(); ImGui.EndPopup();
} }
// Website button with On-Hover address if valid http(s), otherwise text.
private void DrawWebsiteText()
{
if ((_selectedMod.Mod.Meta.Website?.Length ?? 0) > 0)
{
var validUrl = Uri.TryCreate(_selectedMod.Mod.Meta.Website, UriKind.Absolute, out Uri uriResult)
&& (uriResult.Scheme == Uri.UriSchemeHttps ||uriResult.Scheme == Uri.UriSchemeHttp);
ImGui.SameLine();
if (validUrl)
{
if (ImGui.SmallButton("Open Website"))
{
Process.Start( _selectedMod.Mod.Meta.Website );
}
if (ImGui.IsItemHovered())
{
ImGui.BeginTooltip();
ImGui.Text( _selectedMod.Mod.Meta.Website );
ImGui.EndTooltip();
}
}
else
{
ImGui.TextColored( new Vector4( 1f, 1f, 1f, 0.66f ), "from" );
ImGui.SameLine();
ImGui.Text(_selectedMod.Mod.Meta.Website);
}
}
}
// Create Mod-Handling buttons.
private void DrawEditButtons()
{
ImGui.SameLine();
if( ImGui.Button( "Open Mod Folder" ) )
{
Process.Start( _selectedMod.Mod.ModBasePath.FullName );
}
ImGui.SameLine();
if( ImGui.Button( "Edit JSON" ) )
{
var metaPath = Path.Combine( _selectedMod.Mod.ModBasePath.FullName, "meta.json");
File.WriteAllText( metaPath, JsonConvert.SerializeObject( _selectedMod.Mod.Meta, Formatting.Indented ) );
Process.Start( metaPath );
}
ImGui.SameLine();
if( ImGui.Button( "Reload JSON" ) )
{
ReloadMods();
// May select a different mod than before if mods were added or deleted, but will not crash.
if (_selectedModIndex < _plugin.ModManager.Mods.ModSettings.Count)
{
_selectedMod = _plugin.ModManager.Mods.ModSettings[_selectedModIndex];
}
else
{
_selectedModIndex = 0;
_selectedMod = null;
}
}
}
private void DrawGroupSelectors()
{
var hasTopTypes = (_selectedMod.Mod.Meta.Groups.TopTypes?.Count ?? 0) > 1;
var hasBottomTypes = (_selectedMod.Mod.Meta.Groups.BottomTypes?.Count ?? 0) > 1;
var hasGroups = (_selectedMod.Mod.Meta.Groups.OtherGroups?.Count ?? 0) > 1;
var numSelectors = (hasTopTypes ? 1 : 0) + (hasBottomTypes ? 1 : 0) + (hasGroups ? 1 : 0);
var selectorWidth = (ImGui.GetWindowWidth()
- (hasTopTypes ? ImGui.CalcTextSize("Top ").X : 0)
- (hasBottomTypes ? ImGui.CalcTextSize("Bottom ").X : 0)
- (hasGroups ? ImGui.CalcTextSize("Group ").X : 0)) / numSelectors;
void DrawSelector(string label, string propertyName, System.Collections.Generic.List<string> list, bool sameLine)
{
var current = (int) _selectedMod.GetType().GetProperty(propertyName).GetValue(_selectedMod);
ImGui.SetNextItemWidth( selectorWidth );
if (sameLine) ImGui.SameLine();
if ( ImGui.Combo(label, ref current, list.ToArray(), list.Count()) )
{
_selectedMod.GetType().GetProperty(propertyName).SetValue(_selectedMod, current);
_plugin.ModManager.Mods.Save();
_plugin.ModManager.CalculateEffectiveFileList();
}
}
if ( hasTopTypes )
DrawSelector("Top", "CurrentTop", _selectedMod.Mod.Meta.Groups.TopTypes, false);
if ( hasBottomTypes )
DrawSelector("Bottom", "CurrentBottom", _selectedMod.Mod.Meta.Groups.BottomTypes, hasTopTypes);
if ( hasGroups )
DrawSelector("Group", "CurrentGroup", _selectedMod.Mod.Meta.Groups.OtherGroups, numSelectors > 1);
}
void DrawInstalledMods() void DrawInstalledMods()
{ {
var ret = ImGui.BeginTabItem( "Installed Mods" ); var ret = ImGui.BeginTabItem( "Installed Mods" );
@ -533,12 +635,24 @@ namespace Penumbra.UI
ImGui.BeginChild( "selectedModInfo", AutoFillSize, true ); ImGui.BeginChild( "selectedModInfo", AutoFillSize, true );
ImGui.Text( _selectedMod.Mod.Meta.Name ); ImGui.Text( _selectedMod.Mod.Meta.Name );
// (Version ...) or nothing.
if ((_selectedMod.Mod.Meta.Version?.Length ?? 0) > 0)
{
ImGui.SameLine();
ImGui.Text($"(Version {_selectedMod.Mod.Meta.Version})" );
}
// by Author or Unknown.
ImGui.SameLine(); ImGui.SameLine();
ImGui.TextColored( new Vector4( 1f, 1f, 1f, 0.66f ), "by" ); ImGui.TextColored( new Vector4( 1f, 1f, 1f, 0.66f ), "by" );
ImGui.SameLine(); ImGui.SameLine();
ImGui.Text( _selectedMod.Mod.Meta.Author ); if ((_selectedMod.Mod.Meta.Author?.Length ?? 0) > 0 )
ImGui.Text( _selectedMod.Mod.Meta.Author );
else
ImGui.Text( "Unknown" );
ImGui.TextWrapped( _selectedMod.Mod.Meta.Description ?? "" ); DrawWebsiteText();
ImGui.SetCursorPosY( ImGui.GetCursorPosY() + 10 ); ImGui.SetCursorPosY( ImGui.GetCursorPosY() + 10 );
@ -550,22 +664,32 @@ namespace Penumbra.UI
_plugin.ModManager.CalculateEffectiveFileList(); _plugin.ModManager.CalculateEffectiveFileList();
} }
if( ImGui.Button( "Open Mod Folder" ) ) DrawEditButtons();
{
Process.Start( _selectedMod.Mod.ModBasePath.FullName ); DrawGroupSelectors();
}
ImGui.TextWrapped( _selectedMod.Mod.Meta.Description ?? "" );
ImGui.BeginTabBar( "PenumbraPluginDetails" ); ImGui.BeginTabBar( "PenumbraPluginDetails" );
if ( (_selectedMod.Mod.Meta.ChangedItems?.Count ?? 0 ) > 0)
{
if( ImGui.BeginTabItem( "Changed Items" ) )
{
ImGui.SetNextItemWidth( -1 );
if( ImGui.ListBoxHeader( "###", AutoFillSize ) )
foreach(var item in _selectedMod.Mod.Meta.ChangedItems)
ImGui.Selectable( item );
ImGui.ListBoxFooter();
ImGui.EndTabItem();
}
}
if( ImGui.BeginTabItem( "Files" ) ) if( ImGui.BeginTabItem( "Files" ) )
{ {
ImGui.SetNextItemWidth( -1 ); ImGui.SetNextItemWidth( -1 );
if( ImGui.ListBoxHeader( "##", AutoFillSize ) ) if( ImGui.ListBoxHeader( "##", AutoFillSize ) )
{
foreach( var file in _selectedMod.Mod.ModFiles ) foreach( var file in _selectedMod.Mod.ModFiles )
{
ImGui.Selectable( file.FullName ); ImGui.Selectable( file.FullName );
}
}
ImGui.ListBoxFooter(); ImGui.ListBoxFooter();
ImGui.EndTabItem(); ImGui.EndTabItem();
@ -589,7 +713,6 @@ namespace Penumbra.UI
ImGui.EndTabItem(); ImGui.EndTabItem();
} }
} }
if( _selectedMod.Mod.FileConflicts.Any() ) if( _selectedMod.Mod.FileConflicts.Any() )
{ {
if( ImGui.BeginTabItem( "File Conflicts" ) ) if( ImGui.BeginTabItem( "File Conflicts" ) )
@ -664,7 +787,7 @@ namespace Penumbra.UI
// todo: virtualise this // todo: virtualise this
foreach( var file in _plugin.ModManager.ResolvedFiles ) foreach( var file in _plugin.ModManager.ResolvedFiles )
{ {
ImGui.Selectable( file.Value.FullName ); ImGui.Selectable( file.Value.FullName + " -> " + file.Key );
} }
} }