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();
File.WriteAllText(
Path.Combine( newModFolder.FullName, "meta.json" ),
JsonConvert.SerializeObject( modMeta )
);
if( modList.SimpleModsList != null )
ExtractSimpleModList( newModFolder, modList.SimpleModsList, modData );
@ -216,15 +211,34 @@ namespace Penumbra.Importer
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 );
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

@ -8,6 +8,9 @@ namespace Penumbra.Models
public string FolderName { get; set; }
public bool Enabled { 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]
public ResourceMod Mod { get; set; }

View file

@ -12,6 +12,10 @@ namespace Penumbra.Models
public string Website { get; set; }
public List<string> ChangedItems { get; set; } = 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 );
}
public IEnumerable< ResourceMod > GetOrderedAndEnabledModList()
public IEnumerable<ModInfo> GetOrderedAndEnabledModSettings()
{
return ModSettings
.Where( x => x.Enabled )
.OrderBy( x => x.Priority )
.OrderBy( x => x.Priority );
}
public IEnumerable<ResourceMod> GetOrderedAndEnabledModList()
{
return GetOrderedAndEnabledModSettings()
.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 >();
foreach( var mod in Mods.GetOrderedAndEnabledModList() )
foreach( var (mod, settings) in Mods.GetOrderedAndEnabledModListWithSettings() )
{
mod.FileConflicts?.Clear();
@ -119,9 +119,25 @@ namespace Penumbra.Mods
foreach( var file in mod.ModFiles )
{
var gamePath = file.FullName.Substring( baseDir.Length )
.TrimStart( '\\' ).Replace( '\\', '/' );
var relativeFilePath = file.FullName.Substring( baseDir.Length ).TrimStart( '\\' );
string gamePath;
bool addFile = true;
(string, uint, uint, ulong) tuple;
if (mod.Meta.Groups.FileToGameAndGroup.TryGetValue(relativeFilePath, out tuple))
{
gamePath = tuple.Item1;
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
gamePath = relativeFilePath.Replace( '\\', '/' );
if ( addFile )
{
if( !ResolvedFiles.ContainsKey( gamePath ) )
{
ResolvedFiles[ gamePath.ToLowerInvariant() ] = file;
@ -132,6 +148,7 @@ namespace Penumbra.Mods
mod.AddConflict( modName, gamePath );
}
}
}
foreach( var swap in mod.Meta.FileSwaps )
{
@ -161,7 +178,6 @@ namespace Penumbra.Mods
DiscoverMods();
}
public FileInfo GetCandidateForGameFile( string gameResourcePath )
{
var val = ResolvedFiles.TryGetValue( gameResourcePath, out var candidate );
@ -190,6 +206,7 @@ namespace Penumbra.Mods
return GetCandidateForGameFile( gameResourcePath )?.FullName ?? GetSwappedFilePath( gameResourcePath );
}
public void Dispose()
{
// _fileSystemWatcher?.Dispose();

View file

@ -31,6 +31,11 @@ namespace Penumbra.Mods
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 )

View file

@ -8,6 +8,7 @@ using System.Windows.Forms;
using Dalamud.Interface;
using Dalamud.Plugin;
using ImGuiNET;
using Newtonsoft.Json;
using Penumbra.Importer;
using Penumbra.Models;
@ -267,6 +268,8 @@ namespace Penumbra.UI
if( ImGui.Button( "Rediscover Mods" ) )
{
ReloadMods();
_selectedModIndex = 0;
_selectedMod = null;
}
ImGui.SameLine();
@ -502,6 +505,105 @@ namespace Penumbra.UI
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()
{
var ret = ImGui.BeginTabItem( "Installed Mods" );
@ -533,12 +635,24 @@ namespace Penumbra.UI
ImGui.BeginChild( "selectedModInfo", AutoFillSize, true );
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.TextColored( new Vector4( 1f, 1f, 1f, 0.66f ), "by" );
ImGui.SameLine();
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 );
@ -550,22 +664,32 @@ namespace Penumbra.UI
_plugin.ModManager.CalculateEffectiveFileList();
}
if( ImGui.Button( "Open Mod Folder" ) )
{
Process.Start( _selectedMod.Mod.ModBasePath.FullName );
}
DrawEditButtons();
DrawGroupSelectors();
ImGui.TextWrapped( _selectedMod.Mod.Meta.Description ?? "" );
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" ) )
{
ImGui.SetNextItemWidth( -1 );
if( ImGui.ListBoxHeader( "##", AutoFillSize ) )
{
foreach( var file in _selectedMod.Mod.ModFiles )
{
ImGui.Selectable( file.FullName );
}
}
ImGui.ListBoxFooter();
ImGui.EndTabItem();
@ -589,7 +713,6 @@ namespace Penumbra.UI
ImGui.EndTabItem();
}
}
if( _selectedMod.Mod.FileConflicts.Any() )
{
if( ImGui.BeginTabItem( "File Conflicts" ) )
@ -664,7 +787,7 @@ namespace Penumbra.UI
// todo: virtualise this
foreach( var file in _plugin.ModManager.ResolvedFiles )
{
ImGui.Selectable( file.Value.FullName );
ImGui.Selectable( file.Value.FullName + " -> " + file.Key );
}
}