mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Adding support for mod groups/options. Removed SwapFiles.
This commit is contained in:
parent
c472fdd8cf
commit
01215b5697
9 changed files with 284 additions and 80 deletions
|
|
@ -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('\\', '/'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
157
Penumbra/Models/GroupInformation.cs
Normal file
157
Penumbra/Models/GroupInformation.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,8 +6,11 @@ namespace Penumbra.Models
|
|||
public class ModInfo
|
||||
{
|
||||
public string FolderName { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public int Priority { 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; }
|
||||
|
|
|
|||
|
|
@ -12,9 +12,8 @@ namespace Penumbra.Models
|
|||
|
||||
public string Website { get; set; }
|
||||
|
||||
public Dictionary< string, string > FileSwaps { get; } = new();
|
||||
|
||||
public List<string> ChangedItems { get; set; } = new();
|
||||
|
||||
public GroupInformation Groups { get; set; } = new();
|
||||
}
|
||||
}
|
||||
|
|
@ -156,6 +156,9 @@ namespace Penumbra.Mods
|
|||
var entry = new ModInfo
|
||||
{
|
||||
Priority = ModSettings.Count,
|
||||
CurrentGroup = 0,
|
||||
CurrentTop = 0,
|
||||
CurrentBottom = 0,
|
||||
FolderName = mod.ModBasePath.Name,
|
||||
Enabled = true,
|
||||
Mod = mod
|
||||
|
|
@ -181,12 +184,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) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Windows.Forms.VisualStyles;
|
||||
using Penumbra.Models;
|
||||
using Swan.Logging;
|
||||
|
||||
namespace Penumbra.Mods
|
||||
{
|
||||
|
|
@ -9,7 +11,6 @@ namespace Penumbra.Mods
|
|||
{
|
||||
private readonly Plugin _plugin;
|
||||
public readonly Dictionary< string, FileInfo > ResolvedFiles = new();
|
||||
public readonly Dictionary< string, string > SwappedFiles = new();
|
||||
|
||||
public ModCollection Mods { get; set; }
|
||||
|
||||
|
|
@ -106,11 +107,10 @@ namespace Penumbra.Mods
|
|||
public void CalculateEffectiveFileList()
|
||||
{
|
||||
ResolvedFiles.Clear();
|
||||
SwappedFiles.Clear();
|
||||
|
||||
var registeredFiles = new Dictionary< string, string >();
|
||||
|
||||
foreach( var mod in Mods.GetOrderedAndEnabledModList() )
|
||||
foreach( var (mod, settings) in Mods.GetOrderedAndEnabledModListWithSettings() )
|
||||
{
|
||||
mod.FileConflicts?.Clear();
|
||||
|
||||
|
|
@ -119,31 +119,34 @@ 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( '\\' );
|
||||
|
||||
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;
|
||||
registeredFiles[ gamePath ] = mod.Meta.Name;
|
||||
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 if( registeredFiles.TryGetValue( gamePath, out var modName ) )
|
||||
{
|
||||
mod.AddConflict( modName, gamePath );
|
||||
}
|
||||
}
|
||||
else
|
||||
gamePath = relativeFilePath.Replace( '\\', '/' );
|
||||
|
||||
foreach( var swap in mod.Meta.FileSwaps )
|
||||
{
|
||||
// just assume people put not fucked paths in here lol
|
||||
if( !SwappedFiles.ContainsKey( swap.Value ) )
|
||||
if ( addFile )
|
||||
{
|
||||
SwappedFiles[ swap.Key.ToLowerInvariant() ] = swap.Value;
|
||||
registeredFiles[ swap.Key ] = mod.Meta.Name;
|
||||
}
|
||||
else if( registeredFiles.TryGetValue( swap.Key, out var modName ) )
|
||||
{
|
||||
mod.AddConflict( modName, swap.Key );
|
||||
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 +164,6 @@ namespace Penumbra.Mods
|
|||
DiscoverMods();
|
||||
}
|
||||
|
||||
|
||||
public FileInfo GetCandidateForGameFile( string gameResourcePath )
|
||||
{
|
||||
var val = ResolvedFiles.TryGetValue( gameResourcePath, out var candidate );
|
||||
|
|
@ -178,16 +180,11 @@ namespace Penumbra.Mods
|
|||
return candidate;
|
||||
}
|
||||
|
||||
public string GetSwappedFilePath( string gameResourcePath )
|
||||
{
|
||||
return SwappedFiles.TryGetValue( gameResourcePath, out var swappedPath ) ? swappedPath : null;
|
||||
}
|
||||
|
||||
public string ResolveSwappedOrReplacementFilePath( string gameResourcePath )
|
||||
public string ResolveReplacementFilePath( string gameResourcePath )
|
||||
{
|
||||
gameResourcePath = gameResourcePath.ToLowerInvariant();
|
||||
|
||||
return GetCandidateForGameFile( gameResourcePath )?.FullName ?? GetSwappedFilePath( gameResourcePath );
|
||||
return GetCandidateForGameFile( gameResourcePath )?.FullName;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
|
|
|||
|
|
@ -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 )
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
|
@ -129,7 +129,7 @@ namespace Penumbra
|
|||
PluginLog.Log( "[GetResourceHandler] {0}", gameFsPath );
|
||||
}
|
||||
|
||||
var replacementPath = Plugin.ModManager.ResolveSwappedOrReplacementFilePath( gameFsPath );
|
||||
var replacementPath = Plugin.ModManager.ResolveReplacementFilePath( gameFsPath );
|
||||
|
||||
// path must be < 260 because statically defined array length :(
|
||||
if( replacementPath == null || replacementPath.Length >= 260 )
|
||||
|
|
|
|||
|
|
@ -558,6 +558,39 @@ namespace Penumbra.UI
|
|||
}
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
|
|
@ -621,6 +654,7 @@ namespace Penumbra.UI
|
|||
|
||||
DrawEditButtons();
|
||||
|
||||
DrawGroupSelectors();
|
||||
|
||||
ImGui.TextWrapped( _selectedMod.Mod.Meta.Description ?? "" );
|
||||
|
||||
|
|
@ -649,25 +683,6 @@ namespace Penumbra.UI
|
|||
ImGui.EndTabItem();
|
||||
}
|
||||
|
||||
if( _selectedMod.Mod.Meta.FileSwaps.Any() )
|
||||
{
|
||||
if( ImGui.BeginTabItem( "File Swaps" ) )
|
||||
{
|
||||
ImGui.SetNextItemWidth( -1 );
|
||||
if( ImGui.ListBoxHeader( "##", AutoFillSize ) )
|
||||
{
|
||||
foreach( var file in _selectedMod.Mod.Meta.FileSwaps )
|
||||
{
|
||||
// todo: fucking gross alloc every frame * items
|
||||
ImGui.Selectable( $"{file.Key} -> {file.Value}" );
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.ListBoxFooter();
|
||||
ImGui.EndTabItem();
|
||||
}
|
||||
}
|
||||
|
||||
if( _selectedMod.Mod.FileConflicts.Any() )
|
||||
{
|
||||
if( ImGui.BeginTabItem( "File Conflicts" ) )
|
||||
|
|
@ -742,7 +757,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 );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue