Complete refactoring of most code, indiscriminate application of .editorconfig and general cleanup.

This commit is contained in:
Ottermandias 2021-06-19 11:53:54 +02:00
parent 5332119a63
commit a19ec226c5
84 changed files with 3168 additions and 1709 deletions

View file

@ -1,8 +1,10 @@
using System.Collections.Generic;
using System.Linq; using System.Linq;
using EmbedIO; using EmbedIO;
using EmbedIO.Routing; using EmbedIO.Routing;
using EmbedIO.WebApi; using EmbedIO.WebApi;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra.API namespace Penumbra.API
{ {
@ -10,37 +12,38 @@ namespace Penumbra.API
{ {
private readonly Plugin _plugin; private readonly Plugin _plugin;
public ModsController( Plugin plugin ) => _plugin = plugin; public ModsController( Plugin plugin )
=> _plugin = plugin;
[Route( HttpVerbs.Get, "/mods" )] [Route( HttpVerbs.Get, "/mods" )]
public object? GetMods() public object? GetMods()
{ {
var modManager = Service< ModManager >.Get(); var modManager = Service< ModManager >.Get();
return modManager.Mods?.ModSettings.Select( x => new return modManager.CurrentCollection.Cache?.AvailableMods.Select( x => new
{ {
x.Enabled, x.Settings.Enabled,
x.Priority, x.Settings.Priority,
x.FolderName, x.Data.BasePath.Name,
x.Mod.Meta, x.Data.Meta,
BasePath = x.Mod.ModBasePath.FullName, BasePath = x.Data.BasePath.FullName,
Files = x.Mod.ModFiles.Select( fi => fi.FullName ) Files = x.Data.Resources.ModFiles.Select( fi => fi.FullName ),
} ); } )
?? null;
} }
[Route( HttpVerbs.Post, "/mods" )] [Route( HttpVerbs.Post, "/mods" )]
public object CreateMod() public object CreateMod()
{ => new { };
return new { };
}
[Route( HttpVerbs.Get, "/files" )] [Route( HttpVerbs.Get, "/files" )]
public object GetFiles() public object GetFiles()
{ {
var modManager = Service< ModManager >.Get(); var modManager = Service< ModManager >.Get();
return modManager.ResolvedFiles.ToDictionary( return modManager.CurrentCollection.Cache?.ResolvedFiles.ToDictionary(
o => o.Key, o => ( string )o.Key,
o => o.Value.FullName o => o.Value.FullName
); )
?? new Dictionary< string, string >();
} }
} }
} }

View file

@ -1,14 +1,16 @@
using System; using System;
using System.Collections.Generic;
using Dalamud.Configuration; using Dalamud.Configuration;
using Dalamud.Plugin; using Dalamud.Plugin;
using Penumbra.Util;
namespace Penumbra namespace Penumbra
{ {
[Serializable] [Serializable]
public class Configuration : IPluginConfiguration public class Configuration : IPluginConfiguration
{ {
public int Version { get; set; } = 0; private const int CurrentVersion = 1;
public int Version { get; set; } = CurrentVersion;
public bool IsEnabled { get; set; } = true; public bool IsEnabled { get; set; } = true;
@ -18,25 +20,39 @@ namespace Penumbra
public bool EnableHttpApi { get; set; } public bool EnableHttpApi { get; set; }
public string CurrentCollection { get; set; } = @"D:/ffxiv/fs_mods/"; public string ModDirectory { get; set; } = @"D:/ffxiv/fs_mods/";
public List< string > ModCollections { get; set; } = new(); public string CurrentCollection { get; set; } = "Default";
public bool InvertModListOrder { get; set; } public bool InvertModListOrder { internal get; set; }
// the below exist just to make saving less cumbersome public static Configuration Load( DalamudPluginInterface pi )
[NonSerialized]
private DalamudPluginInterface? _pluginInterface;
public void Initialize( DalamudPluginInterface pluginInterface )
{ {
_pluginInterface = pluginInterface; var configuration = pi.GetPluginConfig() as Configuration ?? new Configuration();
if( configuration.Version == CurrentVersion )
{
return configuration;
}
MigrateConfiguration.Version0To1( configuration );
configuration.Save( pi );
return configuration;
}
public void Save( DalamudPluginInterface pi )
{
try
{
pi.SavePluginConfig( this );
}
catch( Exception e )
{
PluginLog.Error( $"Could not save plugin configuration:\n{e}" );
}
} }
public void Save() public void Save()
{ => Save( Service< DalamudPluginInterface >.Get() );
_pluginInterface?.SavePluginConfig( this );
}
} }
} }

View file

@ -1,7 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
namespace Penumbra.Game namespace Penumbra.Game.Enums
{ {
public enum BodySlot : byte public enum BodySlot : byte
{ {
@ -10,7 +10,7 @@ namespace Penumbra.Game
Face, Face,
Tail, Tail,
Body, Body,
Zear Zear,
} }
public static class BodySlotEnumExtension public static class BodySlotEnumExtension
@ -24,7 +24,7 @@ namespace Penumbra.Game
BodySlot.Hair => "hair", BodySlot.Hair => "hair",
BodySlot.Body => "body", BodySlot.Body => "body",
BodySlot.Tail => "tail", BodySlot.Tail => "tail",
_ => throw new InvalidEnumArgumentException() _ => throw new InvalidEnumArgumentException(),
}; };
} }
} }
@ -37,7 +37,7 @@ namespace Penumbra.Game
{ BodySlot.Face.ToSuffix(), BodySlot.Face }, { BodySlot.Face.ToSuffix(), BodySlot.Face },
{ BodySlot.Hair.ToSuffix(), BodySlot.Hair }, { BodySlot.Hair.ToSuffix(), BodySlot.Hair },
{ BodySlot.Body.ToSuffix(), BodySlot.Body }, { BodySlot.Body.ToSuffix(), BodySlot.Body },
{ BodySlot.Tail.ToSuffix(), BodySlot.Tail } { BodySlot.Tail.ToSuffix(), BodySlot.Tail },
}; };
} }
} }

View file

@ -1,7 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
namespace Penumbra.Game namespace Penumbra.Game.Enums
{ {
public enum CustomizationType : byte public enum CustomizationType : byte
{ {
@ -15,7 +15,7 @@ namespace Penumbra.Game
DecalFace, DecalFace,
DecalEquip, DecalEquip,
Skin, Skin,
Etc Etc,
} }
public static class CustomizationTypeEnumExtension public static class CustomizationTypeEnumExtension
@ -30,7 +30,7 @@ namespace Penumbra.Game
CustomizationType.Hair => "hir", CustomizationType.Hair => "hir",
CustomizationType.Tail => "til", CustomizationType.Tail => "til",
CustomizationType.Etc => "etc", CustomizationType.Etc => "etc",
_ => throw new InvalidEnumArgumentException() _ => throw new InvalidEnumArgumentException(),
}; };
} }
} }
@ -44,7 +44,7 @@ namespace Penumbra.Game
{ CustomizationType.Accessory.ToSuffix(), CustomizationType.Accessory }, { CustomizationType.Accessory.ToSuffix(), CustomizationType.Accessory },
{ CustomizationType.Hair.ToSuffix(), CustomizationType.Hair }, { CustomizationType.Hair.ToSuffix(), CustomizationType.Hair },
{ CustomizationType.Tail.ToSuffix(), CustomizationType.Tail }, { CustomizationType.Tail.ToSuffix(), CustomizationType.Tail },
{ CustomizationType.Etc.ToSuffix(), CustomizationType.Etc } { CustomizationType.Etc.ToSuffix(), CustomizationType.Etc },
}; };
} }
} }

View file

@ -1,7 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
namespace Penumbra.Game namespace Penumbra.Game.Enums
{ {
public enum EquipSlot : byte public enum EquipSlot : byte
{ {
@ -27,7 +27,7 @@ namespace Penumbra.Game
FullBody = 19, FullBody = 19,
BodyHands = 20, BodyHands = 20,
BodyLegsFeet = 21, BodyLegsFeet = 21,
All = 22 All = 22,
} }
public static class EquipSlotEnumExtension public static class EquipSlotEnumExtension
@ -46,7 +46,7 @@ namespace Penumbra.Game
EquipSlot.RingR => "rir", EquipSlot.RingR => "rir",
EquipSlot.RingL => "ril", EquipSlot.RingL => "ril",
EquipSlot.Wrists => "wrs", EquipSlot.Wrists => "wrs",
_ => throw new InvalidEnumArgumentException() _ => throw new InvalidEnumArgumentException(),
}; };
} }
@ -59,7 +59,7 @@ namespace Penumbra.Game
EquipSlot.Legs => true, EquipSlot.Legs => true,
EquipSlot.Feet => true, EquipSlot.Feet => true,
EquipSlot.Body => true, EquipSlot.Body => true,
_ => false _ => false,
}; };
} }
@ -72,7 +72,7 @@ namespace Penumbra.Game
EquipSlot.RingR => true, EquipSlot.RingR => true,
EquipSlot.RingL => true, EquipSlot.RingL => true,
EquipSlot.Wrists => true, EquipSlot.Wrists => true,
_ => false _ => false,
}; };
} }
} }
@ -90,7 +90,7 @@ namespace Penumbra.Game
{ EquipSlot.Neck.ToSuffix(), EquipSlot.Neck }, { EquipSlot.Neck.ToSuffix(), EquipSlot.Neck },
{ EquipSlot.RingR.ToSuffix(), EquipSlot.RingR }, { EquipSlot.RingR.ToSuffix(), EquipSlot.RingR },
{ EquipSlot.RingL.ToSuffix(), EquipSlot.RingL }, { EquipSlot.RingL.ToSuffix(), EquipSlot.RingL },
{ EquipSlot.Wrists.ToSuffix(), EquipSlot.Wrists } { EquipSlot.Wrists.ToSuffix(), EquipSlot.Wrists },
}; };
} }
} }

View file

@ -1,6 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace Penumbra.Game namespace Penumbra.Game.Enums
{ {
public enum FileType : byte public enum FileType : byte
{ {
@ -16,7 +16,7 @@ namespace Penumbra.Game
Model, Model,
Shader, Shader,
Font, Font,
Environment Environment,
} }
public static partial class GameData public static partial class GameData
@ -39,7 +39,7 @@ namespace Penumbra.Game
{ ".shpk", FileType.Shader }, { ".shpk", FileType.Shader },
{ ".shcd", FileType.Shader }, { ".shcd", FileType.Shader },
{ ".fdt", FileType.Font }, { ".fdt", FileType.Font },
{ ".envb", FileType.Environment } { ".envb", FileType.Environment },
}; };
} }
} }

View file

@ -1,4 +1,4 @@
namespace Penumbra.Game namespace Penumbra.Game.Enums
{ {
public enum ObjectType : byte public enum ObjectType : byte
{ {
@ -16,6 +16,6 @@ namespace Penumbra.Game
Equipment, Equipment,
Character, Character,
Weapon, Weapon,
Font Font,
} }
} }

View file

@ -2,7 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
namespace Penumbra.Game namespace Penumbra.Game.Enums
{ {
public enum Gender : byte public enum Gender : byte
{ {
@ -10,7 +10,7 @@ namespace Penumbra.Game
Male, Male,
Female, Female,
MaleNpc, MaleNpc,
FemaleNpc FemaleNpc,
} }
public enum Race : byte public enum Race : byte
@ -24,7 +24,7 @@ namespace Penumbra.Game
Roegadyn, Roegadyn,
AuRa, AuRa,
Hrothgar, Hrothgar,
Viera Viera,
} }
public enum GenderRace : ushort public enum GenderRace : ushort
@ -63,7 +63,7 @@ namespace Penumbra.Game
VieraFemale = 1801, VieraFemale = 1801,
VieraFemaleNpc = 1804, VieraFemaleNpc = 1804,
UnknownMaleNpc = 9104, UnknownMaleNpc = 9104,
UnknownFemaleNpc = 9204 UnknownFemaleNpc = 9204,
} }
public static class RaceEnumExtensions public static class RaceEnumExtensions
@ -118,7 +118,7 @@ namespace Penumbra.Game
GenderRace.VieraFemaleNpc => ( Gender.FemaleNpc, Race.Viera ), GenderRace.VieraFemaleNpc => ( Gender.FemaleNpc, Race.Viera ),
GenderRace.UnknownMaleNpc => ( Gender.MaleNpc, Race.Unknown ), GenderRace.UnknownMaleNpc => ( Gender.MaleNpc, Race.Unknown ),
GenderRace.UnknownFemaleNpc => ( Gender.FemaleNpc, Race.Unknown ), GenderRace.UnknownFemaleNpc => ( Gender.FemaleNpc, Race.Unknown ),
_ => throw new InvalidEnumArgumentException() _ => throw new InvalidEnumArgumentException(),
}; };
} }
@ -163,7 +163,7 @@ namespace Penumbra.Game
GenderRace.VieraFemaleNpc => "1804", GenderRace.VieraFemaleNpc => "1804",
GenderRace.UnknownMaleNpc => "9104", GenderRace.UnknownMaleNpc => "9104",
GenderRace.UnknownFemaleNpc => "9204", GenderRace.UnknownFemaleNpc => "9204",
_ => throw new InvalidEnumArgumentException() _ => throw new InvalidEnumArgumentException(),
}; };
} }
} }
@ -208,7 +208,7 @@ namespace Penumbra.Game
"1804" => GenderRace.VieraFemaleNpc, "1804" => GenderRace.VieraFemaleNpc,
"9104" => GenderRace.UnknownMaleNpc, "9104" => GenderRace.UnknownMaleNpc,
"9204" => GenderRace.UnknownFemaleNpc, "9204" => GenderRace.UnknownFemaleNpc,
_ => throw new KeyNotFoundException() _ => throw new KeyNotFoundException(),
}; };
} }
@ -233,7 +233,7 @@ namespace Penumbra.Game
Race.Roegadyn => GenderRace.RoegadynMale, Race.Roegadyn => GenderRace.RoegadynMale,
Race.AuRa => GenderRace.AuRaMale, Race.AuRa => GenderRace.AuRaMale,
Race.Hrothgar => GenderRace.HrothgarMale, Race.Hrothgar => GenderRace.HrothgarMale,
_ => GenderRace.Unknown _ => GenderRace.Unknown,
}, },
Gender.MaleNpc => race switch Gender.MaleNpc => race switch
{ {
@ -245,7 +245,7 @@ namespace Penumbra.Game
Race.Roegadyn => GenderRace.RoegadynMaleNpc, Race.Roegadyn => GenderRace.RoegadynMaleNpc,
Race.AuRa => GenderRace.AuRaMaleNpc, Race.AuRa => GenderRace.AuRaMaleNpc,
Race.Hrothgar => GenderRace.HrothgarMaleNpc, Race.Hrothgar => GenderRace.HrothgarMaleNpc,
_ => GenderRace.Unknown _ => GenderRace.Unknown,
}, },
Gender.Female => race switch Gender.Female => race switch
{ {
@ -257,7 +257,7 @@ namespace Penumbra.Game
Race.Roegadyn => GenderRace.RoegadynFemale, Race.Roegadyn => GenderRace.RoegadynFemale,
Race.AuRa => GenderRace.AuRaFemale, Race.AuRa => GenderRace.AuRaFemale,
Race.Viera => GenderRace.VieraFemale, Race.Viera => GenderRace.VieraFemale,
_ => GenderRace.Unknown _ => GenderRace.Unknown,
}, },
Gender.FemaleNpc => race switch Gender.FemaleNpc => race switch
{ {
@ -269,9 +269,9 @@ namespace Penumbra.Game
Race.Roegadyn => GenderRace.RoegadynFemaleNpc, Race.Roegadyn => GenderRace.RoegadynFemaleNpc,
Race.AuRa => GenderRace.AuRaFemaleNpc, Race.AuRa => GenderRace.AuRaFemaleNpc,
Race.Viera => GenderRace.VieraFemaleNpc, Race.Viera => GenderRace.VieraFemaleNpc,
_ => GenderRace.Unknown _ => GenderRace.Unknown,
}, },
_ => GenderRace.Unknown _ => GenderRace.Unknown,
}; };
} }
} }

View file

@ -1,6 +1,7 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using Penumbra.Mods; using Penumbra.Game.Enums;
using Penumbra.Meta;
namespace Penumbra.Game namespace Penumbra.Game
{ {
@ -46,7 +47,7 @@ namespace Penumbra.Game
RingL1 = 0b0100000000, RingL1 = 0b0100000000,
RingL2 = 0b1000000000, RingL2 = 0b1000000000,
RingLMask = 0b1100000000 RingLMask = 0b1100000000,
} }
public static class Eqdp public static class Eqdp
@ -65,7 +66,7 @@ namespace Penumbra.Game
EquipSlot.Wrists => 4, EquipSlot.Wrists => 4,
EquipSlot.RingR => 6, EquipSlot.RingR => 6,
EquipSlot.RingL => 8, EquipSlot.RingL => 8,
_ => throw new InvalidEnumArgumentException() _ => throw new InvalidEnumArgumentException(),
}; };
} }
@ -100,7 +101,7 @@ namespace Penumbra.Game
EquipSlot.Wrists => EqdpEntry.WristsMask, EquipSlot.Wrists => EqdpEntry.WristsMask,
EquipSlot.RingR => EqdpEntry.RingRMask, EquipSlot.RingR => EqdpEntry.RingRMask,
EquipSlot.RingL => EqdpEntry.RingLMask, EquipSlot.RingL => EqdpEntry.RingLMask,
_ => 0 _ => 0,
}; };
} }
} }

View file

@ -1,6 +1,7 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using Penumbra.Mods; using Penumbra.Game.Enums;
using Penumbra.Meta;
namespace Penumbra.Game namespace Penumbra.Game
{ {
@ -79,7 +80,7 @@ namespace Penumbra.Game
_61 = 0x20_00_00ul << 40, _61 = 0x20_00_00ul << 40,
_62 = 0x40_00_00ul << 40, _62 = 0x40_00_00ul << 40,
_63 = 0x80_00_00ul << 40, _63 = 0x80_00_00ul << 40,
HeadMask = 0xFF_FF_FFul << 40 HeadMask = 0xFF_FF_FFul << 40,
} }
public static class Eqp public static class Eqp
@ -93,7 +94,7 @@ namespace Penumbra.Game
EquipSlot.Hands => ( 1, 3 ), EquipSlot.Hands => ( 1, 3 ),
EquipSlot.Feet => ( 1, 4 ), EquipSlot.Feet => ( 1, 4 ),
EquipSlot.Head => ( 3, 5 ), EquipSlot.Head => ( 3, 5 ),
_ => throw new InvalidEnumArgumentException() _ => throw new InvalidEnumArgumentException(),
}; };
} }
@ -123,7 +124,7 @@ namespace Penumbra.Game
EquipSlot.Legs => EqpEntry.LegsMask, EquipSlot.Legs => EqpEntry.LegsMask,
EquipSlot.Feet => EqpEntry.FeetMask, EquipSlot.Feet => EqpEntry.FeetMask,
EquipSlot.Hands => EqpEntry.HandsMask, EquipSlot.Hands => EqpEntry.HandsMask,
_ => 0 _ => 0,
}; };
} }
} }

View file

@ -1,6 +1,7 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Dalamud; using Dalamud;
using Penumbra.Game.Enums;
namespace Penumbra.Game namespace Penumbra.Game
{ {
@ -16,7 +17,7 @@ namespace Penumbra.Game
PrimaryId = setId, PrimaryId = setId,
GenderRace = gr, GenderRace = gr,
Variant = variant, Variant = variant,
EquipSlot = slot EquipSlot = slot,
}; };
public static GameObjectInfo Weapon( FileType type, ushort setId, ushort weaponId, byte variant = 0 ) public static GameObjectInfo Weapon( FileType type, ushort setId, ushort weaponId, byte variant = 0 )
@ -26,7 +27,7 @@ namespace Penumbra.Game
ObjectType = ObjectType.Weapon, ObjectType = ObjectType.Weapon,
PrimaryId = setId, PrimaryId = setId,
SecondaryId = weaponId, SecondaryId = weaponId,
Variant = variant Variant = variant,
}; };
public static GameObjectInfo Customization( FileType type, CustomizationType customizationType, ushort id = 0 public static GameObjectInfo Customization( FileType type, CustomizationType customizationType, ushort id = 0
@ -39,7 +40,7 @@ namespace Penumbra.Game
GenderRace = gr, GenderRace = gr,
BodySlot = bodySlot, BodySlot = bodySlot,
Variant = variant, Variant = variant,
CustomizationType = customizationType CustomizationType = customizationType,
}; };
public static GameObjectInfo Monster( FileType type, ushort monsterId, ushort bodyId, byte variant = 0 ) public static GameObjectInfo Monster( FileType type, ushort monsterId, ushort bodyId, byte variant = 0 )
@ -49,7 +50,7 @@ namespace Penumbra.Game
ObjectType = ObjectType.Monster, ObjectType = ObjectType.Monster,
PrimaryId = monsterId, PrimaryId = monsterId,
SecondaryId = bodyId, SecondaryId = bodyId,
Variant = variant Variant = variant,
}; };
public static GameObjectInfo DemiHuman( FileType type, ushort demiHumanId, ushort bodyId, byte variant = 0, public static GameObjectInfo DemiHuman( FileType type, ushort demiHumanId, ushort bodyId, byte variant = 0,
@ -61,7 +62,7 @@ namespace Penumbra.Game
PrimaryId = demiHumanId, PrimaryId = demiHumanId,
SecondaryId = bodyId, SecondaryId = bodyId,
Variant = variant, Variant = variant,
EquipSlot = slot EquipSlot = slot,
}; };
public static GameObjectInfo Map( FileType type, byte c1, byte c2, byte c3, byte c4, byte variant, byte suffix = 0 ) public static GameObjectInfo Map( FileType type, byte c1, byte c2, byte c3, byte c4, byte variant, byte suffix = 0 )
@ -74,7 +75,7 @@ namespace Penumbra.Game
MapC3 = c3, MapC3 = c3,
MapC4 = c4, MapC4 = c4,
MapSuffix = suffix, MapSuffix = suffix,
Variant = variant Variant = variant,
}; };
public static GameObjectInfo Icon( FileType type, uint iconId, bool hq, ClientLanguage lang = ClientLanguage.English ) public static GameObjectInfo Icon( FileType type, uint iconId, bool hq, ClientLanguage lang = ClientLanguage.English )
@ -84,7 +85,7 @@ namespace Penumbra.Game
ObjectType = ObjectType.Map, ObjectType = ObjectType.Map,
IconId = iconId, IconId = iconId,
IconHq = hq, IconHq = hq,
Language = lang Language = lang,
}; };
[FieldOffset( 0 )] [FieldOffset( 0 )]

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Dalamud.Plugin; using Dalamud.Plugin;
using Penumbra.Game.Enums;
using Penumbra.Util; using Penumbra.Util;
namespace Penumbra.Game namespace Penumbra.Game
@ -61,7 +62,7 @@ namespace Penumbra.Game
, { ObjectType.Monster, new Regex[]{ new(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/b\k'id'\.imc") } } , { ObjectType.Monster, new Regex[]{ new(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/b\k'id'\.imc") } }
, { ObjectType.Equipment, new Regex[]{ new(@"chara/equipment/e(?'id'\d{4})/e\k'id'\.imc") } } , { ObjectType.Equipment, new Regex[]{ new(@"chara/equipment/e(?'id'\d{4})/e\k'id'\.imc") } }
, { ObjectType.DemiHuman, new Regex[]{ new(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/e\k'equip'\.imc") } } , { ObjectType.DemiHuman, new Regex[]{ new(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/e\k'equip'\.imc") } }
, { ObjectType.Accessory, new Regex[]{ new(@"chara/accessory/a(?'id'\d{4})/a\k'id'\.imc") } } } } , { ObjectType.Accessory, new Regex[]{ new(@"chara/accessory/a(?'id'\d{4})/a\k'id'\.imc") } } } },
}; };
// @formatter:on // @formatter:on
@ -90,7 +91,7 @@ namespace Penumbra.Game
DemiHumanFolder => ObjectType.DemiHuman, DemiHumanFolder => ObjectType.DemiHuman,
MonsterFolder => ObjectType.Monster, MonsterFolder => ObjectType.Monster,
CommonFolder => ObjectType.Character, CommonFolder => ObjectType.Character,
_ => ObjectType.Unknown _ => ObjectType.Unknown,
}, },
UiFolder => folders[ 1 ] switch UiFolder => folders[ 1 ] switch
{ {
@ -98,22 +99,22 @@ namespace Penumbra.Game
LoadingFolder => ObjectType.LoadingScreen, LoadingFolder => ObjectType.LoadingScreen,
MapFolder => ObjectType.Map, MapFolder => ObjectType.Map,
InterfaceFolder => ObjectType.Interface, InterfaceFolder => ObjectType.Interface,
_ => ObjectType.Unknown _ => ObjectType.Unknown,
}, },
CommonFolder => folders[ 1 ] switch CommonFolder => folders[ 1 ] switch
{ {
FontFolder => ObjectType.Font, FontFolder => ObjectType.Font,
_ => ObjectType.Unknown _ => ObjectType.Unknown,
}, },
HousingFolder => ObjectType.Housing, HousingFolder => ObjectType.Housing,
WorldFolder1 => folders[ 1 ] switch WorldFolder1 => folders[ 1 ] switch
{ {
HousingFolder => ObjectType.Housing, HousingFolder => ObjectType.Housing,
_ => ObjectType.World _ => ObjectType.World,
}, },
WorldFolder2 => ObjectType.World, WorldFolder2 => ObjectType.World,
VfxFolder => ObjectType.Vfx, VfxFolder => ObjectType.Vfx,
_ => ObjectType.Unknown _ => ObjectType.Unknown,
}; };
} }
@ -297,7 +298,7 @@ namespace Penumbra.Game
"ja" => Dalamud.ClientLanguage.Japanese, "ja" => Dalamud.ClientLanguage.Japanese,
"de" => Dalamud.ClientLanguage.German, "de" => Dalamud.ClientLanguage.German,
"fr" => Dalamud.ClientLanguage.French, "fr" => Dalamud.ClientLanguage.French,
_ => Dalamud.ClientLanguage.English _ => Dalamud.ClientLanguage.English,
}; };
return GameObjectInfo.Icon( fileType, id, hq, language ); return GameObjectInfo.Icon( fileType, id, hq, language );
} }

View file

@ -1,5 +1,5 @@
using System.IO; using System.IO;
using Penumbra.Mods; using Penumbra.Meta;
namespace Penumbra.Game namespace Penumbra.Game
{ {

View file

@ -44,7 +44,7 @@ namespace Penumbra.Game
RedrawAll( actors ); RedrawAll( actors );
} }
foreach( var actor in actors.Where( A => A.Name == name ) ) foreach( var actor in actors.Where( a => a.Name == name ) )
{ {
Redraw( actor ); Redraw( actor );
} }

View file

@ -30,11 +30,14 @@ namespace Penumbra.Hooks
// Object addresses // Object addresses
private readonly IntPtr _playerResourceManagerAddress; private readonly IntPtr _playerResourceManagerAddress;
public IntPtr PlayerResourceManagerPtr => Marshal.ReadIntPtr( _playerResourceManagerAddress );
public IntPtr PlayerResourceManagerPtr
=> Marshal.ReadIntPtr( _playerResourceManagerAddress );
private readonly IntPtr _characterResourceManagerAddress; private readonly IntPtr _characterResourceManagerAddress;
public unsafe CharacterResourceManager* CharacterResourceManagerPtr => public unsafe CharacterResourceManager* CharacterResourceManagerPtr
( CharacterResourceManager* )Marshal.ReadIntPtr( _characterResourceManagerAddress ).ToPointer(); => ( CharacterResourceManager* )Marshal.ReadIntPtr( _characterResourceManagerAddress ).ToPointer();
public GameResourceManagement( DalamudPluginInterface pluginInterface ) public GameResourceManagement( DalamudPluginInterface pluginInterface )
{ {
@ -70,7 +73,7 @@ namespace Penumbra.Hooks
public unsafe string ResourceToPath( byte* resource ) public unsafe string ResourceToPath( byte* resource )
=> Marshal.PtrToStringAnsi( new IntPtr( *( char** )( resource + 9 * 8 ) ) )!; => Marshal.PtrToStringAnsi( new IntPtr( *( char** )( resource + 9 * 8 ) ) )!;
public unsafe void ReloadCharacterResources() private unsafe void ReloadCharacterResources()
{ {
var oldResources = new IntPtr[NumResources]; var oldResources = new IntPtr[NumResources];
var resources = new IntPtr( &CharacterResourceManagerPtr->Resources ); var resources = new IntPtr( &CharacterResourceManagerPtr->Resources );
@ -88,9 +91,9 @@ namespace Penumbra.Hooks
continue; continue;
} }
PluginLog.Debug( "Freeing " + PluginLog.Debug( "Freeing "
$"{ResourceToPath( ( byte* )oldResources[ i ].ToPointer() )}, replaced with " + + $"{ResourceToPath( ( byte* )oldResources[ i ].ToPointer() )}, replaced with "
$"{ResourceToPath( ( byte* )pResources[ i ] )}" ); + $"{ResourceToPath( ( byte* )pResources[ i ] )}" );
UnloadCharacterResource( oldResources[ i ] ); UnloadCharacterResource( oldResources[ i ] );
} }

View file

@ -0,0 +1,45 @@
using System;
using Dalamud.Plugin;
namespace Penumbra.Hooks
{
public unsafe class MusicManager
{
private readonly IntPtr _musicManager;
public MusicManager( Plugin plugin )
{
var scanner = plugin!.PluginInterface!.TargetModuleScanner;
var framework = plugin.PluginInterface.Framework.Address.BaseAddress;
// the wildcard is basically the framework offset we want (lol)
// .text:000000000009051A 48 8B 8E 18 2A 00 00 mov rcx, [rsi+2A18h]
// .text:0000000000090521 39 78 20 cmp [rax+20h], edi
// .text:0000000000090524 0F 94 C2 setz dl
// .text:0000000000090527 45 33 C0 xor r8d, r8d
// .text:000000000009052A E8 41 1C 15 00 call musicInit
var musicInitCallLocation = scanner.ScanText( "48 8B 8E ?? ?? ?? ?? 39 78 20 0F 94 C2 45 33 C0" );
var musicManagerOffset = *( int* )( musicInitCallLocation + 3 );
PluginLog.Debug( "Found MusicInitCall location at 0x{Location:X16}. Framework offset for MusicManager is 0x{Offset:X8}",
musicInitCallLocation.ToInt64(), musicManagerOffset );
_musicManager = *( IntPtr* )( framework + musicManagerOffset );
PluginLog.Debug( "MusicManager found at 0x{Location:X16}", _musicManager );
}
public bool StreamingEnabled
{
get => *( bool* )( _musicManager + 50 );
private set
{
PluginLog.Debug( value ? "Music streaming enabled." : "Music streaming disabled." );
*( bool* )( _musicManager + 50 ) = value;
}
}
public void EnableStreaming()
=> StreamingEnabled = true;
public void DisableStreaming()
=> StreamingEnabled = false;
}
}

View file

@ -2,6 +2,7 @@ using System;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using Dalamud.Plugin; using Dalamud.Plugin;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Structs; using Penumbra.Structs;
@ -30,12 +31,12 @@ namespace Penumbra.Hooks
public unsafe delegate byte ReadSqpackPrototype( IntPtr pFileHandler, SeFileDescriptor* pFileDesc, int priority, bool isSync ); public unsafe delegate byte ReadSqpackPrototype( IntPtr pFileHandler, SeFileDescriptor* pFileDesc, int priority, bool isSync );
[Function( CallingConventions.Microsoft )] [Function( CallingConventions.Microsoft )]
public unsafe delegate void* GetResourceSyncPrototype( IntPtr pFileManager, uint* pCategoryId, char* pResourceType, public unsafe delegate void* GetResourceSyncPrototype( IntPtr pFileManager, uint* pCategoryId, char* pResourceType
uint* pResourceHash, char* pPath, void* pUnknown ); , uint* pResourceHash, char* pPath, void* pUnknown );
[Function( CallingConventions.Microsoft )] [Function( CallingConventions.Microsoft )]
public unsafe delegate void* GetResourceAsyncPrototype( IntPtr pFileManager, uint* pCategoryId, char* pResourceType, public unsafe delegate void* GetResourceAsyncPrototype( IntPtr pFileManager, uint* pCategoryId, char* pResourceType
uint* pResourceHash, char* pPath, void* pUnknown, bool isUnknown ); , uint* pResourceHash, char* pPath, void* pUnknown, bool isUnknown );
// Hooks // Hooks
public IHook< GetResourceSyncPrototype >? GetResourceSyncHook { get; private set; } public IHook< GetResourceSyncPrototype >? GetResourceSyncHook { get; private set; }
@ -47,6 +48,7 @@ namespace Penumbra.Hooks
public bool LogAllFiles = false; public bool LogAllFiles = false;
public Regex? LogFileFilter = null;
public ResourceLoader( Plugin plugin ) public ResourceLoader( Plugin plugin )
@ -87,7 +89,8 @@ namespace Penumbra.Hooks
uint* pResourceHash, uint* pResourceHash,
char* pPath, char* pPath,
void* pUnknown void* pUnknown
) => GetResourceHandler( true, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, false ); )
=> GetResourceHandler( true, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, false );
private unsafe void* GetResourceAsyncHandler( private unsafe void* GetResourceAsyncHandler(
IntPtr pFileManager, IntPtr pFileManager,
@ -97,7 +100,8 @@ namespace Penumbra.Hooks
char* pPath, char* pPath,
void* pUnknown, void* pUnknown,
bool isUnknown bool isUnknown
) => GetResourceHandler( false, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown ); )
=> GetResourceHandler( false, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown );
private unsafe void* CallOriginalHandler( private unsafe void* CallOriginalHandler(
bool isSync, bool isSync,
@ -141,24 +145,29 @@ namespace Penumbra.Hooks
bool isUnknown bool isUnknown
) )
{ {
string file;
var modManager = Service< ModManager >.Get(); var modManager = Service< ModManager >.Get();
if( !Plugin!.Configuration!.IsEnabled || modManager == null ) if( !Plugin!.Configuration!.IsEnabled || modManager == null )
{ {
if( LogAllFiles ) if( LogAllFiles )
{ {
PluginLog.Log( "[GetResourceHandler] {0}", file = Marshal.PtrToStringAnsi( new IntPtr( pPath ) )!;
GamePath.GenerateUncheckedLower( Marshal.PtrToStringAnsi( new IntPtr( pPath ) )! ) ); if( LogFileFilter == null || LogFileFilter.IsMatch( file ) )
{
PluginLog.Log( "[GetResourceHandler] {0}", file );
}
} }
return CallOriginalHandler( isSync, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown ); return CallOriginalHandler( isSync, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown );
} }
var gameFsPath = GamePath.GenerateUncheckedLower( Marshal.PtrToStringAnsi( new IntPtr( pPath ) )! ); file = Marshal.PtrToStringAnsi( new IntPtr( pPath ) )!;
var replacementPath = modManager.ResolveSwappedOrReplacementFilePath( gameFsPath ); var gameFsPath = GamePath.GenerateUncheckedLower( file );
if( LogAllFiles ) var replacementPath = modManager.CurrentCollection.ResolveSwappedOrReplacementPath( gameFsPath );
if( LogAllFiles && ( LogFileFilter == null || LogFileFilter.IsMatch( file ) ) )
{ {
PluginLog.Log( "[GetResourceHandler] {0}", gameFsPath ); PluginLog.Log( "[GetResourceHandler] {0}", file );
} }
// path must be < 260 because statically defined array length :( // path must be < 260 because statically defined array length :(

View file

@ -1,40 +0,0 @@
using System;
using System.Runtime.InteropServices;
using Dalamud.Plugin;
using Reloaded.Hooks;
using Reloaded.Hooks.Definitions;
using Reloaded.Hooks.Definitions.X64;
namespace Penumbra.Hooks
{
public unsafe class SoundShit
{
private readonly IntPtr _musicManager;
public SoundShit( Plugin plugin )
{
var scanner = plugin!.PluginInterface!.TargetModuleScanner;
var fw = plugin.PluginInterface.Framework.Address.BaseAddress;
// the wildcard is basically the framework offset we want (lol)
// .text:000000000009051A 48 8B 8E 18 2A 00 00 mov rcx, [rsi+2A18h]
// .text:0000000000090521 39 78 20 cmp [rax+20h], edi
// .text:0000000000090524 0F 94 C2 setz dl
// .text:0000000000090527 45 33 C0 xor r8d, r8d
// .text:000000000009052A E8 41 1C 15 00 call musicInit
var shit = scanner.ScanText( "48 8B 8E ?? ?? ?? ?? 39 78 20 0F 94 C2 45 33 C0" );
var fuckkk = *( int* )( shit + 3 );
_musicManager = *( IntPtr* )( fw + fuckkk );
StreamingEnabled = false;
// PluginLog.Information("disabled streaming: {addr}", _musicManager);
}
public bool StreamingEnabled
{
get => *( bool* )( _musicManager + 50 );
set => *( bool* )( _musicManager + 50 ) = value;
}
}
}

View file

@ -5,6 +5,6 @@ namespace Penumbra.Importer
None, None,
WritingPackToDisk, WritingPackToDisk,
ExtractingModFiles, ExtractingModFiles,
Done Done,
} }
} }

View file

@ -1,15 +1,16 @@
using System; using System;
using System.IO; using System.IO;
using Lumina.Data;
using Penumbra.Util; using Penumbra.Util;
namespace Penumbra.Importer namespace Penumbra.Importer
{ {
public class MagicTempFileStreamManagerAndDeleterFuckery : PenumbraSqPackStream, IDisposable public class MagicTempFileStreamManagerAndDeleter : PenumbraSqPackStream, IDisposable
{ {
private readonly FileStream _fileStream; private readonly FileStream _fileStream;
public MagicTempFileStreamManagerAndDeleterFuckery( FileStream stream ) : base( stream ) => _fileStream = stream; public MagicTempFileStreamManagerAndDeleter( FileStream stream )
: base( stream )
=> _fileStream = stream;
public new void Dispose() public new void Dispose()
{ {

View file

@ -1,5 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using Penumbra.Models; using Penumbra.Structs;
namespace Penumbra.Importer.Models namespace Penumbra.Importer.Models
{ {

View file

@ -7,8 +7,10 @@ using Dalamud.Plugin;
using ICSharpCode.SharpZipLib.Zip; using ICSharpCode.SharpZipLib.Zip;
using Newtonsoft.Json; using Newtonsoft.Json;
using Penumbra.Importer.Models; using Penumbra.Importer.Models;
using Penumbra.Models; using Penumbra.Mod;
using Penumbra.Structs;
using Penumbra.Util; using Penumbra.Util;
using FileMode = System.IO.FileMode;
namespace Penumbra.Importer namespace Penumbra.Importer
{ {
@ -46,6 +48,12 @@ namespace Penumbra.Importer
_resolvedTempFilePath = Path.Combine( _outDirectory.FullName, TempFileName ); _resolvedTempFilePath = Path.Combine( _outDirectory.FullName, TempFileName );
} }
private static string ReplaceBadXivSymbols( string source )
=> source.ReplaceInvalidPathSymbols().RemoveNonAsciiSymbols();
private static DirectoryInfo NewOptionDirectory( DirectoryInfo baseDir, string optionName )
=> new( Path.Combine( baseDir.FullName, ReplaceBadXivSymbols( optionName ) ) );
public void ImportModPack( FileInfo modPackFile ) public void ImportModPack( FileInfo modPackFile )
{ {
CurrentModPack = modPackFile.Name; CurrentModPack = modPackFile.Name;
@ -94,7 +102,7 @@ namespace Penumbra.Importer
WriteZipEntryToTempFile( s ); WriteZipEntryToTempFile( s );
var fs = new FileStream( _resolvedTempFilePath, FileMode.Open ); var fs = new FileStream( _resolvedTempFilePath, FileMode.Open );
return new MagicTempFileStreamManagerAndDeleterFuckery( fs ); return new MagicTempFileStreamManagerAndDeleter( fs );
} }
private void VerifyVersionAndImport( FileInfo modPackFile ) private void VerifyVersionAndImport( FileInfo modPackFile )
@ -187,13 +195,11 @@ namespace Penumbra.Importer
public static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName ) public static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName )
{ {
var correctedPath = Path.Combine( outDirectory.FullName, var newModFolder = NewOptionDirectory( outDirectory, Path.GetFileName( modListName ) );
Path.GetFileName( modListName ).RemoveInvalidPathSymbols().RemoveNonAsciiSymbols() );
var newModFolder = new DirectoryInfo( correctedPath );
var i = 2; var i = 2;
while( newModFolder.Exists && i < 12 ) while( newModFolder.Exists && i < 12 )
{ {
newModFolder = new DirectoryInfo( correctedPath + $" ({i++})" ); newModFolder = new DirectoryInfo( newModFolder.FullName + $" ({i++})" );
} }
if( newModFolder.Exists ) if( newModFolder.Exists )
@ -272,7 +278,7 @@ namespace Penumbra.Importer
foreach( var group in page.ModGroups.Where( group => group.GroupName != null && group.OptionList != null ) ) foreach( var group in page.ModGroups.Where( group => group.GroupName != null && group.OptionList != null ) )
{ {
var groupFolder = new DirectoryInfo( Path.Combine( newModFolder.FullName, group.GroupName!.ReplaceInvalidPathSymbols().RemoveNonAsciiSymbols( ) ) ); var groupFolder = NewOptionDirectory( newModFolder, group.GroupName! );
if( groupFolder.Exists ) if( groupFolder.Exists )
{ {
groupFolder = new DirectoryInfo( groupFolder.FullName + $" ({page.PageIndex})" ); groupFolder = new DirectoryInfo( groupFolder.FullName + $" ({page.PageIndex})" );
@ -281,7 +287,7 @@ namespace Penumbra.Importer
foreach( var option in group.OptionList!.Where( option => option.Name != null && option.ModsJsons != null ) ) foreach( var option in group.OptionList!.Where( option => option.Name != null && option.ModsJsons != null ) )
{ {
var optionFolder = new DirectoryInfo( Path.Combine( groupFolder.FullName, option.Name!.ReplaceInvalidPathSymbols().RemoveNonAsciiSymbols() ) ); var optionFolder = NewOptionDirectory( groupFolder, option.Name! );
ExtractSimpleModList( optionFolder, option.ModsJsons!, modData ); ExtractSimpleModList( optionFolder, option.ModsJsons!, modData );
} }
@ -311,7 +317,7 @@ namespace Penumbra.Importer
OptionDesc = string.IsNullOrEmpty( opt.Description ) ? "" : opt.Description!, OptionDesc = string.IsNullOrEmpty( opt.Description ) ? "" : opt.Description!,
OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(), OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(),
}; };
var optDir = new DirectoryInfo( Path.Combine( groupFolder.FullName, opt.Name!.ReplaceInvalidPathSymbols().RemoveNonAsciiSymbols() ) ); var optDir = NewOptionDirectory( groupFolder, opt.Name! );
if( optDir.Exists ) if( optDir.Exists )
{ {
foreach( var file in optDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) foreach( var file in optDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )

View file

@ -5,9 +5,11 @@ using System.Text.RegularExpressions;
using Dalamud.Plugin; using Dalamud.Plugin;
using Lumina.Data.Files; using Lumina.Data.Files;
using Penumbra.Game; using Penumbra.Game;
using Penumbra.MetaData; using Penumbra.Game.Enums;
using Penumbra.Mods; using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Util; using Penumbra.Util;
using GameData = Penumbra.Game.Enums.GameData;
namespace Penumbra.Importer namespace Penumbra.Importer
{ {

View file

@ -4,7 +4,7 @@ using System.Linq;
using Lumina.Data; using Lumina.Data;
using Penumbra.Game; using Penumbra.Game;
namespace Penumbra.MetaData namespace Penumbra.Meta.Files
{ {
// EQDP file structure: // EQDP file structure:
// [Identifier][BlockSize:ushort][BlockCount:ushort] // [Identifier][BlockSize:ushort][BlockCount:ushort]
@ -37,7 +37,8 @@ namespace Penumbra.MetaData
} }
} }
public ref EqdpEntry this[ ushort setId ] => ref GetTrueEntry( setId ); public ref EqdpEntry this[ ushort setId ]
=> ref GetTrueEntry( setId );
public EqdpFile Clone() public EqdpFile Clone()
@ -49,8 +50,11 @@ namespace Penumbra.MetaData
private ushort ExpandedBlockCount { get; set; } private ushort ExpandedBlockCount { get; set; }
private EqdpEntry[]?[] Blocks { get; } private EqdpEntry[]?[] Blocks { get; }
private int BlockIdx( ushort id ) => ( ushort )( id / BlockSize ); private int BlockIdx( ushort id )
private int SubIdx( ushort id ) => ( ushort )( id % BlockSize ); => ( ushort )( id / BlockSize );
private int SubIdx( ushort id )
=> ( ushort )( id % BlockSize );
private bool ExpandBlock( int idx ) private bool ExpandBlock( int idx )
{ {

View file

@ -4,7 +4,7 @@ using System.Linq;
using Lumina.Data; using Lumina.Data;
using Penumbra.Game; using Penumbra.Game;
namespace Penumbra.MetaData namespace Penumbra.Meta.Files
{ {
// EQP Structure: // EQP Structure:
// 64 x [Block collapsed or not bit] // 64 x [Block collapsed or not bit]
@ -28,7 +28,7 @@ namespace Penumbra.MetaData
} }
public byte[] WriteBytes() public byte[] WriteBytes()
=> WriteBytes( _entries, E => ( ulong )E ); => WriteBytes( _entries, e => ( ulong )e );
public EqpFile Clone() public EqpFile Clone()
=> new( this ); => new( this );
@ -40,7 +40,7 @@ namespace Penumbra.MetaData
=> GetEntry( _entries, setId, ( EqpEntry )0 ); => GetEntry( _entries, setId, ( EqpEntry )0 );
public bool SetEntry( ushort setId, EqpEntry entry ) public bool SetEntry( ushort setId, EqpEntry entry )
=> SetEntry( _entries, setId, entry, E => E == 0, ( E1, E2 ) => E1 == E2 ); => SetEntry( _entries, setId, entry, e => e == 0, ( e1, e2 ) => e1 == e2 );
public ref EqpEntry this[ ushort setId ] public ref EqpEntry this[ ushort setId ]
=> ref GetTrueEntry( _entries, setId ); => ref GetTrueEntry( _entries, setId );

View file

@ -2,9 +2,9 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Lumina.Data; using Lumina.Data;
using Penumbra.Game; using Penumbra.Game.Enums;
namespace Penumbra.MetaData namespace Penumbra.Meta.Files
{ {
// EST Structure: // EST Structure:
// 1x [NumEntries : UInt32] // 1x [NumEntries : UInt32]
@ -101,7 +101,7 @@ namespace Penumbra.MetaData
return 0; return 0;
} }
return !setDict.TryGetValue( setId, out var entry ) ? (ushort) 0 : entry; return !setDict.TryGetValue( setId, out var entry ) ? ( ushort )0 : entry;
} }
public byte[] WriteBytes() public byte[] WriteBytes()

View file

@ -1,7 +1,7 @@
using Lumina.Data; using Lumina.Data;
using Penumbra.Game; using Penumbra.Game;
namespace Penumbra.MetaData namespace Penumbra.Meta.Files
{ {
// GmpFiles use the same structure as Eqp Files. // GmpFiles use the same structure as Eqp Files.
// Entries are also one ulong. // Entries are also one ulong.
@ -22,19 +22,19 @@ namespace Penumbra.MetaData
} }
public byte[] WriteBytes() public byte[] WriteBytes()
=> WriteBytes( _entries, E => ( ulong )E ); => WriteBytes( _entries, e => ( ulong )e );
public GmpFile Clone() public GmpFile Clone()
=> new( this ); => new( this );
public GmpFile( FileResource file ) public GmpFile( FileResource file )
=> ReadFile( _entries, file, I => ( GmpEntry )I ); => ReadFile( _entries, file, i => ( GmpEntry )i );
public GmpEntry GetEntry( ushort setId ) public GmpEntry GetEntry( ushort setId )
=> GetEntry( _entries, setId, ( GmpEntry )0 ); => GetEntry( _entries, setId, ( GmpEntry )0 );
public bool SetEntry( ushort setId, GmpEntry entry ) public bool SetEntry( ushort setId, GmpEntry entry )
=> SetEntry( _entries, setId, entry, E => E == 0, ( E1, E2 ) => E1 == E2 ); => SetEntry( _entries, setId, entry, e => e == 0, ( e1, e2 ) => e1 == e2 );
public ref GmpEntry this[ ushort setId ] public ref GmpEntry this[ ushort setId ]
=> ref GetTrueEntry( _entries, setId ); => ref GetTrueEntry( _entries, setId );

View file

@ -2,22 +2,33 @@ using System;
using System.ComponentModel; using System.ComponentModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Dalamud.Plugin;
using Lumina.Data.Files; using Lumina.Data.Files;
using Penumbra.Game; using Penumbra.Game.Enums;
using Penumbra.Mods;
namespace Penumbra.MetaData namespace Penumbra.Meta.Files
{ {
public class InvalidImcVariantException : ArgumentOutOfRangeException public class InvalidImcVariantException : ArgumentOutOfRangeException
{ {
public InvalidImcVariantException() public InvalidImcVariantException()
: base("Trying to manipulate invalid variant.") : base( "Trying to manipulate invalid variant." )
{ } { }
} }
public static class ImcExtensions public static class ImcExtensions
{ {
public static ulong ToInteger( this ImcFile.ImageChangeData imc )
{
ulong ret = imc.MaterialId;
ret |= ( ulong )imc.DecalId << 8;
ret |= ( ulong )imc.AttributeMask << 16;
ret |= ( ulong )imc.SoundId << 16;
ret |= ( ulong )imc.VfxId << 32;
var tmp = imc.GetType().GetField( "_MaterialAnimationIdMask",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance );
ret |= ( ulong )( byte )tmp!.GetValue( imc ) << 40;
return ret;
}
public static bool Equal( this ImcFile.ImageChangeData lhs, ImcFile.ImageChangeData rhs ) public static bool Equal( this ImcFile.ImageChangeData lhs, ImcFile.ImageChangeData rhs )
=> lhs.MaterialId == rhs.MaterialId => lhs.MaterialId == rhs.MaterialId
&& lhs.DecalId == rhs.DecalId && lhs.DecalId == rhs.DecalId
@ -35,7 +46,6 @@ namespace Penumbra.MetaData
bw.Write( variant.MaterialAnimationId ); bw.Write( variant.MaterialAnimationId );
} }
public static byte[] WriteBytes( this ImcFile file ) public static byte[] WriteBytes( this ImcFile file )
{ {
var parts = file.PartMask == 31 ? 5 : 1; var parts = file.PartMask == 31 ? 5 : 1;
@ -104,10 +114,10 @@ namespace Penumbra.MetaData
Count = file.Count, Count = file.Count,
PartMask = file.PartMask, PartMask = file.PartMask,
}; };
var parts = file.GetParts().Select( P => new ImcFile.ImageChangeParts() var parts = file.GetParts().Select( p => new ImcFile.ImageChangeParts()
{ {
DefaultVariant = P.DefaultVariant, DefaultVariant = p.DefaultVariant,
Variants = ( ImcFile.ImageChangeData[] )P.Variants.Clone(), Variants = ( ImcFile.ImageChangeData[] )p.Variants.Clone(),
} ).ToArray(); } ).ToArray();
var prop = ret.GetType().GetField( "Parts", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance ); var prop = ret.GetType().GetField( "Parts", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance );
prop!.SetValue( ret, parts ); prop!.SetValue( ret, parts );

View file

@ -4,10 +4,10 @@ using Dalamud.Plugin;
using Lumina.Data; using Lumina.Data;
using Lumina.Data.Files; using Lumina.Data.Files;
using Penumbra.Game; using Penumbra.Game;
using Penumbra.Mods; using Penumbra.Game.Enums;
using Penumbra.Util; using Penumbra.Util;
namespace Penumbra.MetaData namespace Penumbra.Meta.Files
{ {
public class MetaDefaults public class MetaDefaults
{ {
@ -112,14 +112,20 @@ namespace Penumbra.MetaData
return m.Type switch return m.Type switch
{ {
MetaType.Imc => GetDefaultImcFile( m.ImcIdentifier.ObjectType, m.ImcIdentifier.PrimaryId, m.ImcIdentifier.SecondaryId ) MetaType.Imc => GetDefaultImcFile( m.ImcIdentifier.ObjectType, m.ImcIdentifier.PrimaryId, m.ImcIdentifier.SecondaryId )
?.GetValue( m ).Equal( m.ImcValue ) ?? false, ?.GetValue( m ).Equal( m.ImcValue )
MetaType.Gmp => GetDefaultGmpFile()?.GetEntry( m.GmpIdentifier.SetId ) == m.GmpValue, ?? true,
MetaType.Eqp => GetDefaultEqpFile()?.GetEntry( m.EqpIdentifier.SetId ).Reduce( m.EqpIdentifier.Slot ) == m.EqpValue, MetaType.Gmp => GetDefaultGmpFile()?.GetEntry( m.GmpIdentifier.SetId )
== m.GmpValue,
MetaType.Eqp => GetDefaultEqpFile()?.GetEntry( m.EqpIdentifier.SetId )
.Reduce( m.EqpIdentifier.Slot )
== m.EqpValue,
MetaType.Eqdp => GetDefaultEqdpFile( m.EqdpIdentifier.Slot, m.EqdpIdentifier.GenderRace )?.GetEntry( m.EqdpIdentifier.SetId ) MetaType.Eqdp => GetDefaultEqdpFile( m.EqdpIdentifier.Slot, m.EqdpIdentifier.GenderRace )?.GetEntry( m.EqdpIdentifier.SetId )
.Reduce( m.EqdpIdentifier.Slot ) == m.EqdpValue, .Reduce( m.EqdpIdentifier.Slot )
== m.EqdpValue,
MetaType.Est => GetDefaultEstFile( m.EstIdentifier.ObjectType, m.EstIdentifier.EquipSlot, m.EstIdentifier.BodySlot ) MetaType.Est => GetDefaultEstFile( m.EstIdentifier.ObjectType, m.EstIdentifier.EquipSlot, m.EstIdentifier.BodySlot )
?.GetEntry( m.EstIdentifier.GenderRace, m.EstIdentifier.PrimaryId ) == m.EstValue, ?.GetEntry( m.EstIdentifier.GenderRace, m.EstIdentifier.PrimaryId )
_ => throw new NotImplementedException() == m.EstValue,
_ => throw new NotImplementedException(),
}; };
} }
@ -132,7 +138,7 @@ namespace Penumbra.MetaData
MetaType.Eqp => GetNewEqpFile(), MetaType.Eqp => GetNewEqpFile(),
MetaType.Eqdp => GetNewEqdpFile( m.EqdpIdentifier.Slot, m.EqdpIdentifier.GenderRace ), MetaType.Eqdp => GetNewEqdpFile( m.EqdpIdentifier.Slot, m.EqdpIdentifier.GenderRace ),
MetaType.Est => GetNewEstFile( m.EstIdentifier.ObjectType, m.EstIdentifier.EquipSlot, m.EstIdentifier.BodySlot ), MetaType.Est => GetNewEstFile( m.EstIdentifier.ObjectType, m.EstIdentifier.EquipSlot, m.EstIdentifier.BodySlot ),
_ => throw new NotImplementedException() _ => throw new NotImplementedException(),
}; };
} }
} }

View file

@ -1,9 +1,8 @@
using System; using System;
using Penumbra.Game; using Penumbra.Game.Enums;
using Penumbra.Mods;
using Penumbra.Util; using Penumbra.Util;
namespace Penumbra.MetaData namespace Penumbra.Meta.Files
{ {
public static class MetaFileNames public static class MetaFileNames
{ {

149
Penumbra/Meta/Identifier.cs Normal file
View file

@ -0,0 +1,149 @@
using System.Runtime.InteropServices;
using Penumbra.Game.Enums;
namespace Penumbra.Meta
{
public enum MetaType : byte
{
Unknown = 0,
Imc = 1,
Eqdp = 2,
Eqp = 3,
Est = 4,
Gmp = 5,
};
[StructLayout( LayoutKind.Explicit )]
public struct EqpIdentifier
{
[FieldOffset( 0 )]
public ulong Value;
[FieldOffset( 0 )]
public MetaType Type;
[FieldOffset( 1 )]
public EquipSlot Slot;
[FieldOffset( 2 )]
public ushort SetId;
public override string ToString()
=> $"Eqp - {SetId} - {Slot}";
}
[StructLayout( LayoutKind.Explicit )]
public struct EqdpIdentifier
{
[FieldOffset( 0 )]
public ulong Value;
[FieldOffset( 0 )]
public MetaType Type;
[FieldOffset( 1 )]
public EquipSlot Slot;
[FieldOffset( 2 )]
public GenderRace GenderRace;
[FieldOffset( 4 )]
public ushort SetId;
public override string ToString()
=> $"Eqdp - {SetId} - {Slot} - {GenderRace.Split().Item2} {GenderRace.Split().Item1}";
}
[StructLayout( LayoutKind.Explicit )]
public struct GmpIdentifier
{
[FieldOffset( 0 )]
public ulong Value;
[FieldOffset( 0 )]
public MetaType Type;
[FieldOffset( 1 )]
public ushort SetId;
public override string ToString()
=> $"Gmp - {SetId}";
}
[StructLayout( LayoutKind.Explicit )]
public struct EstIdentifier
{
[FieldOffset( 0 )]
public ulong Value;
[FieldOffset( 0 )]
public MetaType Type;
[FieldOffset( 1 )]
public ObjectType ObjectType;
[FieldOffset( 2 )]
public EquipSlot EquipSlot;
[FieldOffset( 3 )]
public BodySlot BodySlot;
[FieldOffset( 4 )]
public GenderRace GenderRace;
[FieldOffset( 6 )]
public ushort PrimaryId;
public override string ToString()
=> ObjectType == ObjectType.Equipment
? $"Est - {PrimaryId} - {EquipSlot} - {GenderRace.Split().Item2} {GenderRace.Split().Item1}"
: $"Est - {PrimaryId} - {BodySlot} - {GenderRace.Split().Item2} {GenderRace.Split().Item1}";
}
[StructLayout( LayoutKind.Explicit )]
public struct ImcIdentifier
{
[FieldOffset( 0 )]
public ulong Value;
[FieldOffset( 0 )]
public MetaType Type;
[FieldOffset( 1 )]
public byte _objectAndBody;
public ObjectType ObjectType
{
get => ( ObjectType )( _objectAndBody & 0b00011111 );
set => _objectAndBody = ( byte )( ( _objectAndBody & 0b11100000 ) | ( byte )value );
}
public BodySlot BodySlot
{
get => ( BodySlot )( _objectAndBody >> 5 );
set => _objectAndBody = ( byte )( ( _objectAndBody & 0b00011111 ) | ( ( byte )value << 5 ) );
}
[FieldOffset( 2 )]
public ushort PrimaryId;
[FieldOffset( 4 )]
public ushort Variant;
[FieldOffset( 6 )]
public ushort SecondaryId;
[FieldOffset( 6 )]
public EquipSlot EquipSlot;
public override string ToString()
{
return ObjectType switch
{
ObjectType.Accessory => $"Imc - {PrimaryId} - {EquipSlot} - {Variant}",
ObjectType.Equipment => $"Imc - {PrimaryId} - {EquipSlot} - {Variant}",
_ => $"Imc - {PrimaryId} - {ObjectType} - {SecondaryId} - {BodySlot} - {Variant}",
};
}
}
}

View file

@ -0,0 +1,221 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Plugin;
using Newtonsoft.Json;
using Penumbra.Importer;
using Penumbra.Meta.Files;
using Penumbra.Mod;
using Penumbra.Structs;
using Penumbra.Util;
namespace Penumbra.Meta
{
// Corresponds meta manipulations of any kind with the settings for a mod.
// DefaultData contains all manipulations that are active regardless of option groups.
// GroupData contains a mapping of Group -> { Options -> {Manipulations} }.
public class MetaCollection
{
public List< MetaManipulation > DefaultData = new();
public Dictionary< string, Dictionary< string, List< MetaManipulation > > > GroupData = new();
// Store total number of manipulations for some ease of access.
[JsonProperty]
public int Count { get; private set; } = 0;
// Return an enumeration of all active meta manipulations for a given mod with given settings.
public IEnumerable< MetaManipulation > GetManipulationsForConfig( ModSettings settings, ModMeta modMeta )
{
if( Count == DefaultData.Count )
{
return DefaultData;
}
IEnumerable< MetaManipulation > ret = DefaultData;
foreach( var group in modMeta.Groups )
{
if( !GroupData.TryGetValue( group.Key, out var metas ) || !settings.Settings.TryGetValue( group.Key, out var setting ) )
{
continue;
}
if( group.Value.SelectionType == SelectType.Single )
{
var settingName = group.Value.Options[ setting ].OptionName;
if( metas.TryGetValue( settingName, out var meta ) )
{
ret = ret.Concat( meta );
}
}
else
{
for( var i = 0; i < group.Value.Options.Count; ++i )
{
var flag = 1 << i;
if( ( setting & flag ) == 0 )
{
continue;
}
var settingName = group.Value.Options[ i ].OptionName;
if( metas.TryGetValue( settingName, out var meta ) )
{
ret = ret.Concat( meta );
}
}
}
}
return ret;
}
// Check that the collection is still basically valid,
// i.e. keep it sorted, and verify that the options stored by name are all still part of the mod,
// and that the contained manipulations are still valid and non-default manipulations.
public bool Validate( ModMeta modMeta )
{
var defaultFiles = Service< MetaDefaults >.Get();
SortLists();
foreach( var group in GroupData )
{
if( !modMeta.Groups.TryGetValue( group.Key, out var options ) )
{
return false;
}
foreach( var option in group.Value )
{
if( options.Options.All( o => o.OptionName != option.Key ) )
{
return false;
}
if( option.Value.Any( manip => defaultFiles.CheckAgainstDefault( manip ) ) )
{
return false;
}
}
}
return DefaultData.All( manip => !defaultFiles.CheckAgainstDefault( manip ) );
}
// Re-sort all manipulations.
private void SortLists()
{
DefaultData.Sort();
foreach( var list in GroupData.Values.SelectMany( g => g.Values ) )
{
list.Sort();
}
}
// Add a parsed TexTools .meta file to a given option group and option. If group is the empty string, add it to default.
// Creates the option group and the option if necessary.
private void AddMeta( string group, string option, TexToolsMeta meta )
{
if( meta.Manipulations.Count == 0 )
{
return;
}
if( group.Length == 0 )
{
DefaultData.AddRange( meta.Manipulations );
}
else if( option.Length == 0 )
{ }
else if( !GroupData.TryGetValue( group, out var options ) )
{
GroupData.Add( group, new Dictionary< string, List< MetaManipulation > >() { { option, meta.Manipulations.ToList() } } );
}
else if( !options.TryGetValue( option, out var list ) )
{
options.Add( option, meta.Manipulations.ToList() );
}
else
{
list.AddRange( meta.Manipulations );
}
Count += meta.Manipulations.Count;
}
// Update the whole meta collection by reading all TexTools .meta files in a mod directory anew,
// combining them with the given ModMeta.
public void Update( IEnumerable< FileInfo > files, DirectoryInfo basePath, ModMeta modMeta )
{
DefaultData.Clear();
GroupData.Clear();
foreach( var file in files.Where( f => f.Extension == ".meta" ) )
{
var metaData = new TexToolsMeta( File.ReadAllBytes( file.FullName ) );
if( metaData.FilePath == string.Empty || metaData.Manipulations.Count == 0 )
{
continue;
}
var path = new RelPath( file, basePath );
var foundAny = false;
foreach( var group in modMeta.Groups )
{
foreach( var option in group.Value.Options.Where( o => o.OptionFiles.ContainsKey( path ) ) )
{
foundAny = true;
AddMeta( group.Key, option.OptionName, metaData );
}
}
if( !foundAny )
{
AddMeta( string.Empty, string.Empty, metaData );
}
}
SortLists();
}
public static FileInfo FileName( DirectoryInfo basePath )
=> new( Path.Combine( basePath.FullName, "metadata_manipulations.json" ) );
public void SaveToFile( FileInfo file )
{
try
{
var text = JsonConvert.SerializeObject( this, Formatting.Indented );
File.WriteAllText( file.FullName, text );
}
catch( Exception e )
{
PluginLog.Error( $"Could not write metadata manipulations file to {file.FullName}:\n{e}" );
}
}
public static MetaCollection? LoadFromFile( FileInfo file )
{
if( !file.Exists )
{
return null;
}
try
{
var text = File.ReadAllText( file.FullName );
var collection = JsonConvert.DeserializeObject< MetaCollection >( text,
new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } );
return collection;
}
catch( Exception e )
{
PluginLog.Error( $"Could not load mod metadata manipulations from {file.FullName}:\n{e}" );
return null;
}
}
}
}

View file

@ -5,10 +5,10 @@ using System.Linq;
using Dalamud.Plugin; using Dalamud.Plugin;
using Lumina.Data.Files; using Lumina.Data.Files;
using Penumbra.Hooks; using Penumbra.Hooks;
using Penumbra.Meta.Files;
using Penumbra.Util; using Penumbra.Util;
using Penumbra.MetaData;
namespace Penumbra.Mods namespace Penumbra.Meta
{ {
public class MetaManager : IDisposable public class MetaManager : IDisposable
{ {
@ -45,9 +45,15 @@ namespace Penumbra.Mods
private readonly GameResourceManagement _resourceManagement; private readonly GameResourceManagement _resourceManagement;
private readonly Dictionary< GamePath, FileInfo > _resolvedFiles; private readonly Dictionary< GamePath, FileInfo > _resolvedFiles;
private readonly HashSet< MetaManipulation > _currentManipulations = new(); private readonly Dictionary< MetaManipulation, Mod.Mod > _currentManipulations = new();
private readonly Dictionary< GamePath, FileInformation > _currentFiles = new(); private readonly Dictionary< GamePath, FileInformation > _currentFiles = new();
public IEnumerable< (MetaManipulation, Mod.Mod) > Manipulations
=> _currentManipulations.Select( kvp => ( kvp.Key, kvp.Value ) );
public bool TryGetValue( MetaManipulation manip, out Mod.Mod mod )
=> _currentManipulations.TryGetValue( manip, out mod );
private static void DisposeFile( FileInfo? file ) private static void DisposeFile( FileInfo? file )
{ {
if( !( file?.Exists ?? false ) ) if( !( file?.Exists ?? false ) )
@ -65,7 +71,7 @@ namespace Penumbra.Mods
} }
} }
public void Dispose() private void Reset( bool reload )
{ {
foreach( var file in _currentFiles ) foreach( var file in _currentFiles )
{ {
@ -76,11 +82,26 @@ namespace Penumbra.Mods
_currentManipulations.Clear(); _currentManipulations.Clear();
_currentFiles.Clear(); _currentFiles.Clear();
ClearDirectory(); ClearDirectory();
if( reload )
{
_resourceManagement.ReloadPlayerResources(); _resourceManagement.ReloadPlayerResources();
} }
}
public void Reset()
=> Reset( true );
public void Dispose()
=> Reset();
~MetaManager()
{
Reset( false );
}
private void ClearDirectory() private void ClearDirectory()
{ {
_dir.Refresh();
if( _dir.Exists ) if( _dir.Exists )
{ {
try try
@ -94,18 +115,18 @@ namespace Penumbra.Mods
} }
} }
public MetaManager( Dictionary< GamePath, FileInfo > resolvedFiles, DirectoryInfo modDir ) public MetaManager( string name, Dictionary< GamePath, FileInfo > resolvedFiles, DirectoryInfo modDir )
{ {
_resolvedFiles = resolvedFiles; _resolvedFiles = resolvedFiles;
_default = Service< MetaDefaults >.Get(); _default = Service< MetaDefaults >.Get();
_resourceManagement = Service< GameResourceManagement >.Get(); _resourceManagement = Service< GameResourceManagement >.Get();
_dir = new DirectoryInfo( Path.Combine( modDir.FullName, TmpDirectory ) ); _dir = new DirectoryInfo( Path.Combine( modDir.FullName, TmpDirectory, name.ReplaceInvalidPathSymbols().RemoveNonAsciiSymbols() ) );
ClearDirectory(); ClearDirectory();
Directory.CreateDirectory( _dir.FullName );
} }
public void WriteNewFiles() public void WriteNewFiles()
{ {
Directory.CreateDirectory( _dir.FullName );
foreach( var kvp in _currentFiles.Where( kvp => kvp.Value.Changed ) ) foreach( var kvp in _currentFiles.Where( kvp => kvp.Value.Changed ) )
{ {
kvp.Value.Write( _dir ); kvp.Value.Write( _dir );
@ -115,13 +136,14 @@ namespace Penumbra.Mods
_resourceManagement.ReloadPlayerResources(); _resourceManagement.ReloadPlayerResources();
} }
public bool ApplyMod( MetaManipulation m ) public bool ApplyMod( MetaManipulation m, Mod.Mod mod )
{ {
if( !_currentManipulations.Add( m ) ) if( _currentManipulations.ContainsKey( m ) )
{ {
return false; return false;
} }
_currentManipulations.Add( m, mod );
var gamePath = m.CorrespondingFilename(); var gamePath = m.CorrespondingFilename();
try try
{ {

View file

@ -1,134 +1,45 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Newtonsoft.Json;
using Penumbra.Game; using Penumbra.Game;
using Penumbra.MetaData; using Penumbra.Game.Enums;
using Penumbra.Meta.Files;
using Penumbra.Util; using Penumbra.Util;
using Swan;
using ImcFile = Lumina.Data.Files.ImcFile; using ImcFile = Lumina.Data.Files.ImcFile;
namespace Penumbra.Mods namespace Penumbra.Meta
{ {
public enum MetaType : byte public class MetaManipulationConverter : JsonConverter< MetaManipulation >
{ {
Unknown = 0, public override void WriteJson( JsonWriter writer, MetaManipulation manip, JsonSerializer serializer )
Imc = 1,
Eqdp = 2,
Eqp = 3,
Est = 4,
Gmp = 5
};
[StructLayout( LayoutKind.Explicit )]
public struct EqpIdentifier
{ {
[FieldOffset( 0 )] var s = Convert.ToBase64String( manip.ToBytes() );
public ulong Value; writer.WriteValue( s );
}
[FieldOffset( 0 )] public override MetaManipulation ReadJson( JsonReader reader, Type objectType, MetaManipulation existingValue, bool hasExistingValue,
public MetaType Type; JsonSerializer serializer )
[FieldOffset( 1 )] {
public EquipSlot Slot; if( reader.TokenType != JsonToken.String )
{
throw new JsonReaderException();
}
[FieldOffset( 2 )] var bytes = Convert.FromBase64String( ( string )reader.Value! );
public ushort SetId; using MemoryStream m = new( bytes );
} using BinaryReader br = new( m );
var i = br.ReadUInt64();
[StructLayout( LayoutKind.Explicit )] var v = br.ReadUInt64();
public struct EqdpIdentifier return new MetaManipulation( i, v );
{ }
[FieldOffset( 0 )]
public ulong Value;
[FieldOffset( 0 )]
public MetaType Type;
[FieldOffset( 1 )]
public EquipSlot Slot;
[FieldOffset( 2 )]
public GenderRace GenderRace;
[FieldOffset( 4 )]
public ushort SetId;
}
[StructLayout( LayoutKind.Explicit )]
public struct GmpIdentifier
{
[FieldOffset( 0 )]
public ulong Value;
[FieldOffset( 0 )]
public MetaType Type;
[FieldOffset( 1 )]
public ushort SetId;
}
[StructLayout( LayoutKind.Explicit )]
public struct EstIdentifier
{
[FieldOffset( 0 )]
public ulong Value;
[FieldOffset( 0 )]
public MetaType Type;
[FieldOffset( 1 )]
public ObjectType ObjectType;
[FieldOffset( 2 )]
public EquipSlot EquipSlot;
[FieldOffset( 3 )]
public BodySlot BodySlot;
[FieldOffset( 4 )]
public GenderRace GenderRace;
[FieldOffset( 6 )]
public ushort PrimaryId;
}
[StructLayout( LayoutKind.Explicit )]
public struct ImcIdentifier
{
[FieldOffset( 0 )]
public ulong Value;
[FieldOffset( 0 )]
public MetaType Type;
[FieldOffset( 1 )]
public byte _objectAndBody;
public ObjectType ObjectType
{
get => ( ObjectType )( _objectAndBody & 0b00011111 );
set => _objectAndBody = ( byte )( ( _objectAndBody & 0b11100000 ) | ( byte )value );
}
public BodySlot BodySlot
{
get => ( BodySlot )( _objectAndBody & 0b11100000 );
set => _objectAndBody = ( byte )( ( _objectAndBody & 0b00011111 ) | ( byte )value );
}
[FieldOffset( 2 )]
public ushort PrimaryId;
[FieldOffset( 4 )]
public ushort Variant;
[FieldOffset( 6 )]
public ushort SecondaryId;
[FieldOffset( 6 )]
public EquipSlot EquipSlot;
} }
[StructLayout( LayoutKind.Explicit )] [StructLayout( LayoutKind.Explicit )]
[JsonConverter( typeof( MetaManipulationConverter ) )]
public struct MetaManipulation : IComparable public struct MetaManipulation : IComparable
{ {
public static MetaManipulation Eqp( EquipSlot equipSlot, ushort setId, EqpEntry value ) public static MetaManipulation Eqp( EquipSlot equipSlot, ushort setId, EqpEntry value )
@ -138,9 +49,9 @@ namespace Penumbra.Mods
{ {
Type = MetaType.Eqp, Type = MetaType.Eqp,
Slot = equipSlot, Slot = equipSlot,
SetId = setId SetId = setId,
}, },
EqpValue = value EqpValue = value,
}; };
public static MetaManipulation Eqdp( EquipSlot equipSlot, GenderRace gr, ushort setId, EqdpEntry value ) public static MetaManipulation Eqdp( EquipSlot equipSlot, GenderRace gr, ushort setId, EqdpEntry value )
@ -151,9 +62,9 @@ namespace Penumbra.Mods
Type = MetaType.Eqdp, Type = MetaType.Eqdp,
Slot = equipSlot, Slot = equipSlot,
GenderRace = gr, GenderRace = gr,
SetId = setId SetId = setId,
}, },
EqdpValue = value EqdpValue = value,
}; };
public static MetaManipulation Gmp( ushort setId, GmpEntry value ) public static MetaManipulation Gmp( ushort setId, GmpEntry value )
@ -162,9 +73,9 @@ namespace Penumbra.Mods
GmpIdentifier = new GmpIdentifier() GmpIdentifier = new GmpIdentifier()
{ {
Type = MetaType.Gmp, Type = MetaType.Gmp,
SetId = setId SetId = setId,
}, },
GmpValue = value GmpValue = value,
}; };
public static MetaManipulation Est( ObjectType type, EquipSlot equipSlot, GenderRace gr, BodySlot bodySlot, ushort setId, public static MetaManipulation Est( ObjectType type, EquipSlot equipSlot, GenderRace gr, BodySlot bodySlot, ushort setId,
@ -178,9 +89,9 @@ namespace Penumbra.Mods
GenderRace = gr, GenderRace = gr,
EquipSlot = equipSlot, EquipSlot = equipSlot,
BodySlot = bodySlot, BodySlot = bodySlot,
PrimaryId = setId PrimaryId = setId,
}, },
EstValue = value EstValue = value,
}; };
public static MetaManipulation Imc( ObjectType type, BodySlot secondaryType, ushort primaryId, ushort secondaryId public static MetaManipulation Imc( ObjectType type, BodySlot secondaryType, ushort primaryId, ushort secondaryId
@ -194,9 +105,9 @@ namespace Penumbra.Mods
BodySlot = secondaryType, BodySlot = secondaryType,
PrimaryId = primaryId, PrimaryId = primaryId,
SecondaryId = secondaryId, SecondaryId = secondaryId,
Variant = idx Variant = idx,
}, },
ImcValue = value ImcValue = value,
}; };
public static MetaManipulation Imc( EquipSlot slot, ushort primaryId, ushort idx, ImcFile.ImageChangeData value ) public static MetaManipulation Imc( EquipSlot slot, ushort primaryId, ushort idx, ImcFile.ImageChangeData value )
@ -208,11 +119,18 @@ namespace Penumbra.Mods
ObjectType = slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, ObjectType = slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment,
EquipSlot = slot, EquipSlot = slot,
PrimaryId = primaryId, PrimaryId = primaryId,
Variant = idx Variant = idx,
}, },
ImcValue = value ImcValue = value,
}; };
internal MetaManipulation( ulong identifier, ulong value )
: this()
{
Identifier = identifier;
Value = value;
}
[FieldOffset( 0 )] [FieldOffset( 0 )]
public readonly ulong Identifier; public readonly ulong Identifier;
@ -257,7 +175,7 @@ namespace Penumbra.Mods
=> Identifier.GetHashCode(); => Identifier.GetHashCode();
public int CompareTo( object? rhs ) public int CompareTo( object? rhs )
=> Identifier.CompareTo( rhs ); => Identifier.CompareTo( rhs is MetaManipulation m ? m.Identifier : null );
public GamePath CorrespondingFilename() public GamePath CorrespondingFilename()
{ {
@ -268,7 +186,7 @@ namespace Penumbra.Mods
MetaType.Est => MetaFileNames.Est( EstIdentifier.ObjectType, EstIdentifier.EquipSlot, EstIdentifier.BodySlot ), MetaType.Est => MetaFileNames.Est( EstIdentifier.ObjectType, EstIdentifier.EquipSlot, EstIdentifier.BodySlot ),
MetaType.Gmp => MetaFileNames.Gmp(), MetaType.Gmp => MetaFileNames.Gmp(),
MetaType.Imc => MetaFileNames.Imc( ImcIdentifier.ObjectType, ImcIdentifier.PrimaryId, ImcIdentifier.SecondaryId ), MetaType.Imc => MetaFileNames.Imc( ImcIdentifier.ObjectType, ImcIdentifier.PrimaryId, ImcIdentifier.SecondaryId ),
_ => throw new InvalidEnumArgumentException() _ => throw new InvalidEnumArgumentException(),
}; };
} }
@ -296,5 +214,18 @@ namespace Penumbra.Mods
value = ImcValue; value = ImcValue;
return true; return true;
} }
public string IdentifierString()
{
return Type switch
{
MetaType.Eqp => $"EQP - {EqpIdentifier}",
MetaType.Eqdp => $"EQDP - {EqdpIdentifier}",
MetaType.Est => $"EST - {EstIdentifier}",
MetaType.Gmp => $"GMP - {GmpIdentifier}",
MetaType.Imc => $"IMC - {ImcIdentifier}",
_ => throw new InvalidEnumArgumentException(),
};
}
} }
} }

View file

@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Plugin;
using Newtonsoft.Json.Linq;
using Penumbra.Mod;
using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra
{
public static class MigrateConfiguration
{
public static void Version0To1( Configuration config )
{
if( config.Version != 0 )
{
return;
}
config.ModDirectory = config.CurrentCollection;
config.CurrentCollection = "Default";
config.Version = 1;
ResettleCollectionJson( config );
}
private static void ResettleCollectionJson( Configuration config )
{
var collectionJson = new FileInfo( Path.Combine( config.ModDirectory, "collection.json" ) );
if( !collectionJson.Exists )
{
return;
}
var defaultCollection = new ModCollection();
var defaultCollectionFile = defaultCollection.FileName();
if( defaultCollectionFile.Exists )
{
return;
}
try
{
var text = File.ReadAllText( collectionJson.FullName );
var data = JArray.Parse( text );
var maxPriority = 0;
foreach( var setting in data.Cast< JObject >() )
{
var modName = ( string )setting[ "FolderName" ]!;
var enabled = ( bool )setting[ "Enabled" ]!;
var priority = ( int )setting[ "Priority" ]!;
var settings = setting[ "Settings" ]!.ToObject< Dictionary< string, int > >()
?? setting[ "Conf" ]!.ToObject< Dictionary< string, int > >();
var save = new ModSettings()
{
Enabled = enabled,
Priority = priority,
Settings = settings!,
};
defaultCollection.Settings.Add( modName, save );
maxPriority = Math.Max( maxPriority, priority );
}
if( config.InvertModListOrder )
{
foreach( var setting in defaultCollection.Settings.Values )
{
setting.Priority = maxPriority - setting.Priority;
}
}
defaultCollection.Save( Service< DalamudPluginInterface >.Get() );
}
catch( Exception e )
{
PluginLog.Error( $"Could not migrate the old collection file to new collection files:\n{e}" );
throw;
}
}
}
}

32
Penumbra/Mod/Mod.cs Normal file
View file

@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.IO;
using Penumbra.Util;
namespace Penumbra.Mod
{
public class Mod
{
public ModSettings Settings { get; }
public ModData Data { get; }
public ModCache Cache { get; }
public Mod( ModSettings settings, ModData data )
{
Settings = settings;
Data = data;
Cache = new ModCache();
}
public bool FixSettings()
=> Settings.FixInvalidSettings( Data.Meta );
public HashSet< GamePath > GetFiles( FileInfo file )
{
var relPath = new RelPath( file, Data.BasePath );
return ModFunctions.GetFilesForConfig( relPath, Settings, Data.Meta );
}
public override string ToString()
=> Data.Meta.Name;
}
}

57
Penumbra/Mod/ModCache.cs Normal file
View file

@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.Linq;
using Penumbra.Meta;
using Penumbra.Util;
namespace Penumbra.Mod
{
public class ModCache
{
public Dictionary< Mod, (List< GamePath > Files, List< MetaManipulation > Manipulations) > Conflicts { get; private set; } = new();
public void AddConflict( Mod precedingMod, GamePath gamePath )
{
if( Conflicts.TryGetValue( precedingMod, out var conflicts ) && !conflicts.Files.Contains( gamePath ) )
{
conflicts.Files.Add( gamePath );
}
else
{
Conflicts[ precedingMod ] = ( new List< GamePath > { gamePath }, new List< MetaManipulation >() );
}
}
public void AddConflict( Mod precedingMod, MetaManipulation manipulation )
{
if( Conflicts.TryGetValue( precedingMod, out var conflicts ) && !conflicts.Manipulations.Contains( manipulation ) )
{
conflicts.Manipulations.Add( manipulation );
}
else
{
Conflicts[ precedingMod ] = ( new List< GamePath >(), new List< MetaManipulation > { manipulation } );
}
}
public void ClearConflicts()
=> Conflicts.Clear();
public void ClearFileConflicts()
{
Conflicts = Conflicts.Where( kvp => kvp.Value.Manipulations.Count > 0 ).ToDictionary( kvp => kvp.Key, kvp =>
{
kvp.Value.Files.Clear();
return kvp.Value;
} );
}
public void ClearMetaConflicts()
{
Conflicts = Conflicts.Where( kvp => kvp.Value.Files.Count > 0 ).ToDictionary( kvp => kvp.Key, kvp =>
{
kvp.Value.Manipulations.Clear();
return kvp.Value;
} );
}
}
}

View file

@ -6,9 +6,10 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Security.Cryptography; using System.Security.Cryptography;
using Dalamud.Plugin; using Dalamud.Plugin;
using Penumbra.Structs;
using Penumbra.Util; using Penumbra.Util;
namespace Penumbra.Models namespace Penumbra.Mod
{ {
public class ModCleanup public class ModCleanup
{ {
@ -202,7 +203,7 @@ namespace Penumbra.Models
private static bool FileIsInAnyGroup( ModMeta meta, RelPath relPath, bool exceptDuplicates = false ) private static bool FileIsInAnyGroup( ModMeta meta, RelPath relPath, bool exceptDuplicates = false )
{ {
var groupEnumerator = exceptDuplicates var groupEnumerator = exceptDuplicates
? meta.Groups.Values.Where( G => G.GroupName != Duplicates ) ? meta.Groups.Values.Where( g => g.GroupName != Duplicates )
: meta.Groups.Values; : meta.Groups.Values;
return groupEnumerator.SelectMany( group => group.Options ) return groupEnumerator.SelectMany( group => group.Options )
.Any( option => option.OptionFiles.ContainsKey( relPath ) ); .Any( option => option.OptionFiles.ContainsKey( relPath ) );
@ -315,7 +316,8 @@ namespace Penumbra.Models
private static void RemoveUselessGroups( ModMeta meta ) private static void RemoveUselessGroups( ModMeta meta )
{ {
meta.Groups = meta.Groups.Where( kvp => kvp.Value.Options.Any( o => o.OptionFiles.Count > 0 ) ).ToDictionary( kvp => kvp.Key, kvp => kvp.Value ); meta.Groups = meta.Groups.Where( kvp => kvp.Value.Options.Any( o => o.OptionFiles.Count > 0 ) )
.ToDictionary( kvp => kvp.Key, kvp => kvp.Value );
} }
// Goes through all Single-Select options and checks if file links are in each of them. // Goes through all Single-Select options and checks if file links are in each of them.
@ -356,7 +358,7 @@ namespace Penumbra.Models
var usedRelPath = new RelPath( usedGamePath ); var usedRelPath = new RelPath( usedGamePath );
required.AddFile( usedRelPath, gamePath ); required.AddFile( usedRelPath, gamePath );
required.AddFile( usedRelPath, usedGamePath ); required.AddFile( usedRelPath, usedGamePath );
RemoveFromGroups( meta, relPath, gamePath, GroupType.Single, true ); RemoveFromGroups( meta, relPath, gamePath, GroupType.Single );
} }
else if( MoveFile( meta, baseDir.FullName, path, relPath ) ) else if( MoveFile( meta, baseDir.FullName, path, relPath ) )
{ {
@ -366,7 +368,7 @@ namespace Penumbra.Models
FindOrCreateDuplicates( meta ).AddFile( relPath, gamePath ); FindOrCreateDuplicates( meta ).AddFile( relPath, gamePath );
} }
RemoveFromGroups( meta, relPath, gamePath, GroupType.Single, true ); RemoveFromGroups( meta, relPath, gamePath, GroupType.Single );
} }
} }
} }

59
Penumbra/Mod/ModData.cs Normal file
View file

@ -0,0 +1,59 @@
using System.IO;
using Dalamud.Plugin;
namespace Penumbra.Mod
{
public class ModData
{
public DirectoryInfo BasePath;
public ModMeta Meta;
public ModResources Resources;
public FileInfo MetaFile { get; set; }
private ModData( DirectoryInfo basePath, ModMeta meta, ModResources resources )
{
BasePath = basePath;
Meta = meta;
Resources = resources;
MetaFile = MetaFileInfo( basePath );
}
public static FileInfo MetaFileInfo( DirectoryInfo basePath )
=> new( Path.Combine( basePath.FullName, "meta.json" ) );
public static ModData? LoadMod( DirectoryInfo basePath )
{
basePath.Refresh();
if( !basePath.Exists )
{
PluginLog.Error( $"Supplied mod directory {basePath} does not exist." );
return null;
}
var metaFile = MetaFileInfo( basePath );
if( !metaFile.Exists )
{
PluginLog.Debug( "No mod meta found for {ModLocation}.", basePath.Name );
return null;
}
var meta = ModMeta.LoadFromFile( metaFile );
if( meta == null )
{
return null;
}
var data = new ModResources();
if( data.RefreshModFiles( basePath ).HasFlag( ResourceChange.Meta ) )
{
data.SetManipulations( meta, basePath );
}
return new ModData( basePath, meta, data );
}
public void SaveMeta()
=> Meta.SaveToFile( MetaFile );
}
}

View file

@ -0,0 +1,82 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Penumbra.Structs;
using Penumbra.Util;
namespace Penumbra.Mod
{
public static class ModFunctions
{
public static bool CleanUpCollection( Dictionary< string, ModSettings > settings, IEnumerable< DirectoryInfo > modPaths )
{
var hashes = modPaths.Select( p => p.Name ).ToHashSet();
var missingMods = settings.Keys.Where( k => !hashes.Contains( k ) ).ToArray();
var anyChanges = false;
foreach( var toRemove in missingMods )
{
anyChanges |= settings.Remove( toRemove );
}
return anyChanges;
}
public static HashSet< GamePath > GetFilesForConfig( RelPath relPath, ModSettings settings, ModMeta meta )
{
var doNotAdd = false;
var files = new HashSet< GamePath >();
foreach( var group in meta.Groups.Values.Where( g => g.Options.Count > 0 ) )
{
doNotAdd |= group.ApplyGroupFiles( relPath, settings.Settings[ group.GroupName ], files );
}
if( !doNotAdd )
{
files.Add( new GamePath( relPath ) );
}
return files;
}
public static ModSettings ConvertNamedSettings( NamedModSettings namedSettings, ModMeta meta )
{
ModSettings ret = new()
{
Priority = namedSettings.Priority,
Settings = namedSettings.Settings.Keys.ToDictionary( k => k, _ => 0 ),
};
foreach( var kvp in namedSettings.Settings )
{
if( !meta.Groups.TryGetValue( kvp.Key, out var info ) )
{
continue;
}
if( info.SelectionType == SelectType.Single )
{
if( namedSettings.Settings[ kvp.Key ].Count == 0 )
{
ret.Settings[ kvp.Key ] = 0;
}
else
{
var idx = info.Options.FindIndex( o => o.OptionName == namedSettings.Settings[ kvp.Key ].Last() );
ret.Settings[ kvp.Key ] = idx < 0 ? 0 : idx;
}
}
else
{
foreach( var idx in namedSettings.Settings[ kvp.Key ]
.Select( option => info.Options.FindIndex( o => o.OptionName == option ) )
.Where( idx => idx >= 0 ) )
{
ret.Settings[ kvp.Key ] |= 1 << idx;
}
}
}
return ret;
}
}
}

103
Penumbra/Mod/ModMeta.cs Normal file
View file

@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Plugin;
using Newtonsoft.Json;
using Penumbra.Structs;
using Penumbra.Util;
namespace Penumbra.Mod
{
// Contains descriptive data about the mod as well as possible settings and fileswaps.
public class ModMeta
{
public uint FileVersion { get; set; }
public string Name { get; set; } = "Mod";
public string Author { get; set; } = "";
public string Description { get; set; } = "";
public string Version { get; set; } = "";
public string Website { get; set; } = "";
public List< string > ChangedItems { get; set; } = new();
[JsonProperty( ItemConverterType = typeof( GamePathConverter ) )]
public Dictionary< GamePath, GamePath > FileSwaps { get; set; } = new();
public Dictionary< string, OptionGroup > Groups { get; set; } = new();
[JsonIgnore]
private int FileHash { get; set; }
[JsonIgnore]
public bool HasGroupsWithConfig { get; private set; }
public bool RefreshFromFile( FileInfo filePath )
{
var newMeta = LoadFromFile( filePath );
if( newMeta == null )
{
return true;
}
if( newMeta.FileHash == FileHash )
{
return false;
}
FileVersion = newMeta.FileVersion;
Name = newMeta.Name;
Author = newMeta.Author;
Description = newMeta.Description;
Version = newMeta.Version;
Website = newMeta.Website;
ChangedItems = newMeta.ChangedItems;
FileSwaps = newMeta.FileSwaps;
Groups = newMeta.Groups;
FileHash = newMeta.FileHash;
HasGroupsWithConfig = newMeta.HasGroupsWithConfig;
return true;
}
public static ModMeta? LoadFromFile( FileInfo filePath )
{
try
{
var text = File.ReadAllText( filePath.FullName );
var meta = JsonConvert.DeserializeObject< ModMeta >( text,
new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } );
if( meta != null )
{
meta.FileHash = text.GetHashCode();
meta.HasGroupsWithConfig = meta.Groups.Values.Any( g => g.SelectionType == SelectType.Multi || g.Options.Count > 1 );
}
return meta;
}
catch( Exception e )
{
PluginLog.Error( $"Could not load mod meta:\n{e}" );
return null;
}
}
public void SaveToFile( FileInfo filePath )
{
try
{
var text = JsonConvert.SerializeObject( this, Formatting.Indented );
var newHash = text.GetHashCode();
if( newHash != FileHash )
{
File.WriteAllText( filePath.FullName, text );
FileHash = newHash;
}
}
catch( Exception e )
{
PluginLog.Error( $"Could not write meta file for mod {Name} to {filePath.FullName}:\n{e}" );
}
}
}
}

View file

@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Penumbra.Meta;
namespace Penumbra.Mod
{
[Flags]
public enum ResourceChange
{
Files = 1,
Meta = 2,
}
// Contains static mod data that should only change on filesystem changes.
public class ModResources
{
public List< FileInfo > ModFiles { get; private set; } = new();
public List< FileInfo > MetaFiles { get; private set; } = new();
public MetaCollection MetaManipulations { get; private set; } = new();
private void ForceManipulationsUpdate( ModMeta meta, DirectoryInfo basePath )
{
MetaManipulations.Update( MetaFiles, basePath, meta );
MetaManipulations.SaveToFile( MetaCollection.FileName( basePath ) );
}
public void SetManipulations( ModMeta meta, DirectoryInfo basePath )
{
var newManipulations = MetaCollection.LoadFromFile( MetaCollection.FileName( basePath ) );
if( newManipulations == null )
{
ForceManipulationsUpdate( meta, basePath );
}
else
{
MetaManipulations = newManipulations;
if( !MetaManipulations.Validate( meta ) )
{
ForceManipulationsUpdate( meta, basePath );
}
}
}
// Update the current set of files used by the mod,
// returns true if anything changed.
public ResourceChange RefreshModFiles( DirectoryInfo basePath )
{
List< FileInfo > tmpFiles = new( ModFiles.Count );
List< FileInfo > tmpMetas = new( MetaFiles.Count );
// we don't care about any _files_ in the root dir, but any folders should be a game folder/file combo
foreach( var file in basePath.EnumerateDirectories()
.SelectMany( dir => dir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
.OrderBy( f => f.FullName ) )
{
if( file.Extension != ".meta" )
{
tmpFiles.Add( file );
}
else
{
tmpMetas.Add( file );
}
}
ResourceChange changes = 0;
if( !tmpFiles.SequenceEqual( ModFiles ) )
{
ModFiles = tmpFiles;
changes |= ResourceChange.Files;
}
if( !tmpMetas.SequenceEqual( MetaFiles ) )
{
MetaFiles = tmpMetas;
changes |= ResourceChange.Meta;
}
return changes;
}
}
}

View file

@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Penumbra.Structs;
namespace Penumbra.Mod
{
public class ModSettings
{
public bool Enabled { get; set; }
public int Priority { get; set; }
public Dictionary< string, int > Settings { get; set; } = new();
// For backwards compatibility
private Dictionary< string, int > Conf
{
set => Settings = value;
}
public ModSettings DeepCopy()
{
var settings = new ModSettings
{
Enabled = Enabled,
Priority = Priority,
Settings = Settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ),
};
return settings;
}
public static ModSettings DefaultSettings( ModMeta meta )
{
return new()
{
Enabled = false,
Priority = 0,
Settings = meta.Groups.ToDictionary( kvp => kvp.Key, _ => 0 ),
};
}
public bool FixSpecificSetting( string name, ModMeta meta )
{
if( !meta.Groups.TryGetValue( name, out var group ) )
{
return Settings.Remove( name );
}
if( Settings.TryGetValue( name, out var oldSetting ) )
{
Settings[ name ] = group.SelectionType switch
{
SelectType.Single => Math.Min( Math.Max( oldSetting, 0 ), group.Options.Count - 1 ),
SelectType.Multi => Math.Min( Math.Max( oldSetting, 0 ), ( 1 << group.Options.Count ) - 1 ),
_ => Settings[ group.GroupName ],
};
return oldSetting != Settings[ group.GroupName ];
}
Settings[ name ] = 0;
return true;
}
public bool FixInvalidSettings( ModMeta meta )
{
if( meta.Groups.Count == 0 )
{
return false;
}
return Settings.Keys.ToArray().Union( meta.Groups.Keys )
.Aggregate( false, ( current, name ) => current | FixSpecificSetting( name, meta ) );
}
}
}

View file

@ -1,7 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Penumbra.Structs;
namespace Penumbra.Models namespace Penumbra.Mod
{ {
public class NamedModSettings public class NamedModSettings
{ {
@ -11,7 +12,7 @@ namespace Penumbra.Models
public void AddFromModSetting( ModSettings s, ModMeta meta ) public void AddFromModSetting( ModSettings s, ModMeta meta )
{ {
Priority = s.Priority; Priority = s.Priority;
Settings = s.Settings.Keys.ToDictionary( K => K, K => new HashSet< string >() ); Settings = s.Settings.Keys.ToDictionary( k => k, _ => new HashSet< string >() );
foreach( var kvp in Settings ) foreach( var kvp in Settings )
{ {

View file

@ -1,42 +0,0 @@
using System.Collections.Generic;
using Newtonsoft.Json;
using Penumbra.Util;
namespace Penumbra.Models
{
public enum SelectType
{
Single,
Multi
}
public struct Option
{
public string OptionName;
public string OptionDesc;
[JsonProperty( ItemConverterType = typeof( SingleOrArrayConverter< GamePath > ) )]
public Dictionary< RelPath, HashSet< GamePath > > OptionFiles;
public bool AddFile( RelPath filePath, GamePath gamePath )
{
if( OptionFiles.TryGetValue( filePath, out var set ) )
{
return set.Add( gamePath );
}
OptionFiles[ filePath ] = new HashSet< GamePath >() { gamePath };
return true;
}
}
public struct OptionGroup
{
public string GroupName;
[JsonConverter( typeof( Newtonsoft.Json.Converters.StringEnumConverter ) )]
public SelectType SelectionType;
public List< Option > Options;
}
}

View file

@ -1,23 +0,0 @@
using Newtonsoft.Json;
using Penumbra.Mods;
namespace Penumbra.Models
{
public class ModInfo : ModSettings
{
public ModInfo( ResourceMod mod )
=> Mod = mod;
public string FolderName { get; set; } = "";
public bool Enabled { get; set; }
[JsonIgnore]
public ResourceMod Mod { get; set; }
public bool FixSpecificSetting( string name )
=> FixSpecificSetting( Mod.Meta, name );
public bool FixInvalidSettings()
=> FixInvalidSettings( Mod.Meta );
}
}

View file

@ -1,134 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
using Penumbra.Util;
namespace Penumbra.Models
{
public class ModMeta
{
public uint FileVersion { get; set; }
public string Name { get; set; } = "Mod";
public string Author { get; set; } = "";
public string Description { get; set; } = "";
public string Version { get; set; } = "";
public string Website { get; set; } = "";
public List< string > ChangedItems { get; set; } = new();
[JsonProperty( ItemConverterType = typeof( GamePathConverter ))]
public Dictionary< GamePath, GamePath > FileSwaps { get; } = new();
public Dictionary< string, OptionGroup > Groups { get; set; } = new();
[JsonIgnore]
public bool HasGroupWithConfig { get; set; } = false;
private static readonly JsonSerializerSettings JsonSettings
= new() { NullValueHandling = NullValueHandling.Ignore };
public static ModMeta? LoadFromFile( string filePath )
{
try
{
var meta = JsonConvert.DeserializeObject< ModMeta >( File.ReadAllText( filePath ), JsonSettings );
if( meta != null )
{
meta.HasGroupWithConfig = meta.Groups.Count > 0
&& meta.Groups.Values.Any( G => G.SelectionType == SelectType.Multi || G.Options.Count > 1 );
}
return meta;
}
catch( Exception )
{
return null;
// todo: handle broken mods properly
}
}
private static bool ApplySingleGroupFiles( OptionGroup group, RelPath relPath, int selection, HashSet< GamePath > paths )
{
if( group.Options[ selection ].OptionFiles.TryGetValue( relPath, out var groupPaths ) )
{
paths.UnionWith( groupPaths );
return true;
}
for( var i = 0; i < group.Options.Count; ++i )
{
if( i == selection )
{
continue;
}
if( group.Options[ i ].OptionFiles.ContainsKey( relPath ) )
{
return true;
}
}
return false;
}
private static bool ApplyMultiGroupFiles( OptionGroup group, RelPath relPath, int selection, HashSet< GamePath > paths )
{
var doNotAdd = false;
for( var i = 0; i < group.Options.Count; ++i )
{
if( ( selection & ( 1 << i ) ) != 0 )
{
if( group.Options[ i ].OptionFiles.TryGetValue( relPath, out var groupPaths ) )
{
paths.UnionWith( groupPaths );
}
}
else if( group.Options[ i ].OptionFiles.ContainsKey( relPath ) )
{
doNotAdd = true;
}
}
return doNotAdd;
}
public (bool configChanged, HashSet< GamePath > paths) GetFilesForConfig( RelPath relPath, ModSettings settings )
{
var doNotAdd = false;
var configChanged = false;
HashSet< GamePath > paths = new();
foreach( var group in Groups.Values )
{
configChanged |= settings.FixSpecificSetting( this, group.GroupName );
if( group.Options.Count == 0 )
{
continue;
}
switch( group.SelectionType )
{
case SelectType.Single:
doNotAdd |= ApplySingleGroupFiles( group, relPath, settings.Settings[ group.GroupName ], paths );
break;
case SelectType.Multi:
doNotAdd |= ApplyMultiGroupFiles( group, relPath, settings.Settings[ group.GroupName ], paths );
break;
}
}
if( !doNotAdd )
{
paths.Add( new GamePath( relPath ) );
}
return ( configChanged, paths );
}
}
}

View file

@ -1,92 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System;
namespace Penumbra.Models
{
public class ModSettings
{
public int Priority { get; set; }
public Dictionary< string, int > Settings { get; set; } = new();
// For backwards compatibility
private Dictionary< string, int > Conf
{
set => Settings = value;
}
public static ModSettings CreateFrom( NamedModSettings n, ModMeta meta )
{
ModSettings ret = new()
{
Priority = n.Priority,
Settings = n.Settings.Keys.ToDictionary( K => K, K => 0 )
};
foreach( var kvp in n.Settings )
{
if( !meta.Groups.TryGetValue( kvp.Key, out var info ) )
{
continue;
}
if( info.SelectionType == SelectType.Single )
{
if( n.Settings[ kvp.Key ].Count == 0 )
{
ret.Settings[ kvp.Key ] = 0;
}
else
{
var idx = info.Options.FindIndex( O => O.OptionName == n.Settings[ kvp.Key ].Last() );
ret.Settings[ kvp.Key ] = idx < 0 ? 0 : idx;
}
}
else
{
foreach( var idx in n.Settings[ kvp.Key ]
.Select( option => info.Options.FindIndex( O => O.OptionName == option ) )
.Where( idx => idx >= 0 ) )
{
ret.Settings[ kvp.Key ] |= 1 << idx;
}
}
}
return ret;
}
public bool FixSpecificSetting( ModMeta meta, string name )
{
if( !meta.Groups.TryGetValue( name, out var group ) )
{
return Settings.Remove( name );
}
if( Settings.TryGetValue( name, out var oldSetting ) )
{
Settings[ name ] = group.SelectionType switch
{
SelectType.Single => Math.Min( Math.Max( oldSetting, 0 ), group.Options.Count - 1 ),
SelectType.Multi => Math.Min( Math.Max( oldSetting, 0 ), ( 1 << group.Options.Count ) - 1 ),
_ => Settings[ group.GroupName ]
};
return oldSetting != Settings[ group.GroupName ];
}
Settings[ name ] = 0;
return true;
}
public bool FixInvalidSettings( ModMeta meta )
{
if( meta.Groups.Count == 0 )
{
return false;
}
return Settings.Keys.ToArray().Union( meta.Groups.Keys )
.Aggregate( false, ( current, name ) => current | FixSpecificSetting( meta, name ) );
}
}
}

View file

@ -1,229 +1,236 @@
using Dalamud.Plugin;
using Newtonsoft.Json;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Dalamud.Plugin; using Penumbra.Mod;
using Newtonsoft.Json; using Penumbra.Util;
using Penumbra.Models;
namespace Penumbra.Mods namespace Penumbra.Mods
{ {
public class ModCollection public class ModCollection
{ {
private readonly DirectoryInfo _basePath; public const string DefaultCollection = "Default";
public List< ModInfo >? ModSettings { get; set; } public string Name { get; set; }
public ResourceMod[]? EnabledMods { get; set; }
public Dictionary< string, ModSettings > Settings { get; }
public ModCollection( DirectoryInfo basePath ) public ModCollection()
=> _basePath = basePath;
public void Load( bool invertOrder = false )
{ {
// find the collection json Name = DefaultCollection;
var collectionPath = Path.Combine( _basePath.FullName, "collection.json" ); Settings = new Dictionary< string, ModSettings >();
if( File.Exists( collectionPath ) )
{
try
{
ModSettings = JsonConvert.DeserializeObject< List< ModInfo > >( File.ReadAllText( collectionPath ) );
ModSettings = ModSettings.OrderBy( x => x.Priority ).ToList();
} }
catch( Exception e )
public ModCollection( string name, Dictionary< string, ModSettings > settings )
{ {
PluginLog.Error( $"failed to read log collection information, failed path: {collectionPath}, err: {e.Message}" ); Name = name;
Settings = settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.DeepCopy() );
}
private bool CleanUnavailableSettings( Dictionary< string, ModData > data )
{
if( Settings.Count <= data.Count )
{
return false;
}
List< string > removeList = new();
foreach( var settingKvp in Settings )
{
if( !data.ContainsKey( settingKvp.Key ) )
{
removeList.Add( settingKvp.Key );
} }
} }
#if DEBUG foreach( var s in removeList )
if( ModSettings != null )
{ {
foreach( var ms in ModSettings ) Settings.Remove( s );
{
PluginLog.Debug(
"mod: {ModName} Enabled: {Enabled} Priority: {Priority}",
ms.FolderName, ms.Enabled, ms.Priority
);
}
}
#endif
ModSettings ??= new List< ModInfo >();
var foundMods = new List< string >();
foreach( var modDir in _basePath.EnumerateDirectories() )
{
if( modDir.Name.ToLowerInvariant() == MetaManager.TmpDirectory )
{
continue;
} }
var metaFile = modDir.EnumerateFiles().FirstOrDefault( f => f.Name == "meta.json" ); return removeList.Count > 0;
if( metaFile == null )
{
#if DEBUG
PluginLog.Error( "mod meta is missing for resource mod: {ResourceModLocation}", modDir );
#else
PluginLog.Debug( "mod meta is missing for resource mod: {ResourceModLocation}", modDir );
#endif
continue;
} }
var meta = ModMeta.LoadFromFile( metaFile.FullName ) ?? new ModMeta(); public void CreateCache( DirectoryInfo modDirectory, Dictionary< string, ModData > data, bool cleanUnavailable = false )
var mod = new ResourceMod( meta, modDir );
FindOrCreateModSettings( mod );
foundMods.Add( modDir.Name );
mod.RefreshModFiles();
}
// remove any mods from the collection we didn't find
ModSettings = ModSettings.Where(
x =>
foundMods.Any(
fm => string.Equals( x.FolderName, fm, StringComparison.InvariantCultureIgnoreCase )
)
).ToList();
// if anything gets removed above, the priority ordering gets fucked, so we need to resort and reindex them otherwise BAD THINGS HAPPEN
ModSettings = ModSettings.OrderBy( x => x.Priority ).ToList();
var p = 0;
foreach( var modSetting in ModSettings )
{ {
modSetting.Priority = p++; Cache = new ModCollectionCache( Name, modDirectory );
} var changedSettings = false;
foreach( var modKvp in data )
// reorder the resourcemods list so we can just directly iterate
EnabledMods = GetOrderedAndEnabledModList( invertOrder ).ToArray();
// write the collection metadata back to disk
Save();
}
public void Save()
{ {
var collectionPath = Path.Combine( _basePath.FullName, "collection.json" ); if( Settings.TryGetValue( modKvp.Key, out var settings ) )
try
{ {
var data = JsonConvert.SerializeObject( ModSettings.OrderBy( x => x.Priority ).ToList() ); Cache.AvailableMods.Add( new Mod.Mod( settings, modKvp.Value ) );
File.WriteAllText( collectionPath, data );
}
catch( Exception e )
{
PluginLog.Error( $"failed to write log collection information, failed path: {collectionPath}, err: {e.Message}" );
}
}
private int CleanPriority( int priority )
=> priority < 0 ? 0 : priority >= ModSettings!.Count ? ModSettings.Count - 1 : priority;
public void ReorderMod( ModInfo info, int newPriority )
{
if( ModSettings == null )
{
return;
}
var oldPriority = info.Priority;
newPriority = CleanPriority( newPriority );
if( oldPriority == newPriority )
{
return;
}
info.Priority = newPriority;
if( newPriority < oldPriority )
{
for( var i = oldPriority - 1; i >= newPriority; --i )
{
++ModSettings![ i ].Priority;
ModSettings.Swap( i, i + 1 );
}
} }
else else
{ {
for( var i = oldPriority + 1; i <= newPriority; ++i ) changedSettings = true;
var newSettings = ModSettings.DefaultSettings( modKvp.Value.Meta );
Settings.Add( modKvp.Key, newSettings );
Cache.AvailableMods.Add( new Mod.Mod( newSettings, modKvp.Value ) );
}
}
if( cleanUnavailable )
{ {
--ModSettings![ i ].Priority; changedSettings |= CleanUnavailableSettings( data );
ModSettings.Swap( i - 1, i );
}
} }
EnabledMods = GetOrderedAndEnabledModList().ToArray(); if( changedSettings )
Save();
}
public void ReorderMod( ModInfo info, bool up )
=> ReorderMod( info, info.Priority + ( up ? 1 : -1 ) );
public ModInfo? FindModSettings( string name )
{ {
var settings = ModSettings?.FirstOrDefault( Save( Service< DalamudPluginInterface >.Get() );
x => string.Equals( x.FolderName, name, StringComparison.InvariantCultureIgnoreCase )
);
#if DEBUG
PluginLog.Information( "finding mod {ModName} - found: {ModSettingsExist}", name, settings != null );
#endif
return settings;
} }
public ModInfo AddModSettings( ResourceMod mod ) Cache.SortMods();
{ CalculateEffectiveFileList( modDirectory, true );
var entry = new ModInfo( mod )
{
Priority = ModSettings?.Count ?? 0,
FolderName = mod.ModBasePath.Name,
Enabled = true,
};
entry.FixInvalidSettings();
#if DEBUG
PluginLog.Information( "creating mod settings {ModName}", entry.FolderName );
#endif
ModSettings ??= new List< ModInfo >();
ModSettings.Add( entry );
return entry;
} }
public ModInfo FindOrCreateModSettings( ResourceMod mod ) public void UpdateSetting( ModData mod )
{ {
var settings = FindModSettings( mod.ModBasePath.Name ); if( !Settings.TryGetValue( mod.BasePath.Name, out var settings ) )
if( settings == null )
{ {
return AddModSettings( mod ); return;
} }
settings.Mod = mod; if( settings.FixInvalidSettings( mod.Meta ) )
settings.FixInvalidSettings();
return settings;
}
public IEnumerable< ModInfo > GetOrderedAndEnabledModSettings( bool invertOrder = false )
{ {
var query = ModSettings? Save( Service< DalamudPluginInterface >.Get() );
.Where( x => x.Enabled ) }
?? Enumerable.Empty< ModInfo >(); }
if( !invertOrder ) public void UpdateSettings()
{ {
return query.OrderBy( x => x.Priority ); if( Cache == null )
}
return query.OrderByDescending( x => x.Priority );
}
public IEnumerable< ResourceMod > GetOrderedAndEnabledModList( bool invertOrder = false )
{ {
return GetOrderedAndEnabledModSettings( invertOrder ) return;
.Select( x => x.Mod );
} }
public IEnumerable< (ResourceMod, ModInfo) > GetOrderedAndEnabledModListWithSettings( bool invertOrder = false ) var changes = false;
foreach( var mod in Cache.AvailableMods )
{ {
return GetOrderedAndEnabledModSettings( invertOrder ) changes |= mod.FixSettings();
.Select( x => ( x.Mod, x ) );
} }
if( changes )
{
Save( Service< DalamudPluginInterface >.Get() );
}
}
public void CalculateEffectiveFileList( DirectoryInfo modDir, bool withMetaManipulations )
{
Cache ??= new ModCollectionCache( Name, modDir );
UpdateSettings();
Cache.CalculateEffectiveFileList();
if( withMetaManipulations )
{
Cache.UpdateMetaManipulations();
}
}
[JsonIgnore]
public ModCollectionCache? Cache { get; private set; }
public static ModCollection? LoadFromFile( FileInfo file )
{
if( !file.Exists )
{
PluginLog.Error( $"Could not read collection because {file.FullName} does not exist." );
return null;
}
try
{
var collection = JsonConvert.DeserializeObject< ModCollection >( File.ReadAllText( file.FullName ) );
return collection;
}
catch( Exception e )
{
PluginLog.Error( $"Could not read collection information from {file.FullName}:\n{e}" );
}
return null;
}
private void SaveToFile( FileInfo file )
{
try
{
File.WriteAllText( file.FullName, JsonConvert.SerializeObject( this, Formatting.Indented ) );
}
catch( Exception e )
{
PluginLog.Error( $"Could not write collection {Name} to {file.FullName}:\n{e}" );
}
}
public static DirectoryInfo CollectionDir( DalamudPluginInterface pi )
=> new( Path.Combine( pi.GetPluginConfigDirectory(), "collections" ) );
private static FileInfo FileName( DirectoryInfo collectionDir, string name )
=> new( Path.Combine( collectionDir.FullName, $"{name.RemoveInvalidPathSymbols()}.json" ) );
public FileInfo FileName()
=> new( Path.Combine( Service< DalamudPluginInterface >.Get().GetPluginConfigDirectory(),
$"{Name.RemoveInvalidPathSymbols()}.json" ) );
public void Save( DalamudPluginInterface pi )
{
try
{
var dir = CollectionDir( pi );
dir.Create();
var file = FileName( dir, Name );
SaveToFile( file );
}
catch( Exception e )
{
PluginLog.Error( $"Could not save collection {Name}:\n{e}" );
}
}
public static ModCollection? Load( string name, DalamudPluginInterface pi )
{
var file = FileName( CollectionDir( pi ), name );
return file.Exists ? LoadFromFile( file ) : null;
}
public void Delete( DalamudPluginInterface pi )
{
var file = FileName( CollectionDir( pi ), Name );
if( file.Exists )
{
try
{
file.Delete();
}
catch( Exception e )
{
PluginLog.Error( $"Could not delete collection file {file} for {Name}:\n{e}" );
}
}
}
public void AddMod( ModData data )
{
if( Cache == null )
{
return;
}
if( Settings.TryGetValue( data.BasePath.Name, out var settings ) )
{
Cache.AddMod( settings, data );
}
else
{
Cache.AddMod( ModSettings.DefaultSettings( data.Meta ), data );
}
}
public string? ResolveSwappedOrReplacementPath( GamePath gameResourcePath )
=> Cache?.ResolveSwappedOrReplacementPath( gameResourcePath );
} }
} }

View file

@ -0,0 +1,167 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Penumbra.Meta;
using Penumbra.Mod;
using Penumbra.Util;
namespace Penumbra.Mods
{
public class ModCollectionCache
{
public readonly List< Mod.Mod > AvailableMods = new();
public readonly Dictionary< GamePath, FileInfo > ResolvedFiles = new();
public readonly Dictionary< GamePath, GamePath > SwappedFiles = new();
public readonly MetaManager MetaManipulations;
public ModCollectionCache( string collectionName, DirectoryInfo modDir )
=> MetaManipulations = new MetaManager( collectionName, ResolvedFiles, modDir );
public void SortMods()
{
AvailableMods.Sort( ( m1, m2 ) => string.Compare( m1.Data.Meta.Name, m2.Data.Meta.Name, StringComparison.InvariantCulture ) );
}
private void AddFiles( Dictionary< GamePath, Mod.Mod > registeredFiles, Mod.Mod mod )
{
foreach( var file in mod.Data.Resources.ModFiles )
{
var gamePaths = mod.GetFiles( file );
foreach( var gamePath in gamePaths )
{
if( !registeredFiles.TryGetValue( gamePath, out var oldMod ) )
{
registeredFiles.Add( gamePath, mod );
ResolvedFiles[ gamePath ] = file;
}
else
{
mod.Cache.AddConflict( oldMod, gamePath );
}
}
}
}
private void AddSwaps( Dictionary< GamePath, Mod.Mod > registeredFiles, Mod.Mod mod )
{
foreach( var swap in mod.Data.Meta.FileSwaps )
{
if( !registeredFiles.TryGetValue( swap.Key, out var oldMod ) )
{
registeredFiles.Add( swap.Key, mod );
SwappedFiles.Add( swap.Key, swap.Value );
}
else
{
mod.Cache.AddConflict( oldMod, swap.Key );
}
}
}
private void AddManipulations( Mod.Mod mod )
{
foreach( var manip in mod.Data.Resources.MetaManipulations.GetManipulationsForConfig( mod.Settings, mod.Data.Meta ) )
{
if( MetaManipulations.TryGetValue( manip, out var precedingMod ) )
{
mod.Cache.AddConflict( precedingMod, manip );
}
else
{
MetaManipulations.ApplyMod( manip, mod );
}
}
}
public void UpdateMetaManipulations()
{
MetaManipulations.Reset();
foreach( var mod in AvailableMods.Where( m => m.Settings.Enabled && m.Data.Resources.MetaManipulations.Count > 0 )
.OrderByDescending( m => m.Settings.Priority ) )
{
mod.Cache.ClearMetaConflicts();
AddManipulations( mod );
}
MetaManipulations.WriteNewFiles();
}
public void CalculateEffectiveFileList()
{
ResolvedFiles.Clear();
SwappedFiles.Clear();
var registeredFiles = new Dictionary< GamePath, Mod.Mod >();
foreach( var mod in AvailableMods.Where( m => m.Settings.Enabled ).OrderByDescending( m => m.Settings.Priority ) )
{
mod.Cache.ClearFileConflicts();
AddFiles( registeredFiles, mod );
AddSwaps( registeredFiles, mod );
}
}
public void RemoveMod( DirectoryInfo basePath )
{
var hadMeta = false;
var wasEnabled = false;
AvailableMods.RemoveAll( m =>
{
if( m.Settings.Enabled )
{
wasEnabled = true;
hadMeta |= m.Data.Resources.MetaManipulations.Count > 0;
}
return m.Data.BasePath.Name == basePath.Name;
} );
if( wasEnabled )
{
CalculateEffectiveFileList();
if( hadMeta )
{
UpdateMetaManipulations();
}
}
}
public void AddMod( ModSettings settings, ModData data )
{
AvailableMods.Add( new Mod.Mod( settings, data ) );
SortMods();
if( settings.Enabled )
{
CalculateEffectiveFileList();
if( data.Resources.MetaManipulations.Count > 0 )
{
UpdateMetaManipulations();
}
}
}
public FileInfo? GetCandidateForGameFile( GamePath gameResourcePath )
{
if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) )
{
return null;
}
candidate.Refresh();
if( candidate.FullName.Length >= 260 || !candidate.Exists )
{
return null;
}
return candidate;
}
public GamePath? GetSwappedFilePath( GamePath gameResourcePath )
=> SwappedFiles.TryGetValue( gameResourcePath, out var swappedPath ) ? swappedPath : null;
public string? ResolveSwappedOrReplacementPath( GamePath gameResourcePath )
=> GetCandidateForGameFile( gameResourcePath )?.FullName.Replace( '\\', '/' ) ?? GetSwappedFilePath( gameResourcePath ) ?? null;
}
}

View file

@ -1,207 +1,267 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using Dalamud.Plugin; using Dalamud.Plugin;
using Penumbra.Hooks; using Penumbra.Meta;
using Penumbra.Models; using Penumbra.Mod;
using Penumbra.Util; using Penumbra.Util;
namespace Penumbra.Mods namespace Penumbra.Mods
{ {
public class ModManager : IDisposable public class ModManager
{ {
private readonly Plugin _plugin; private readonly Plugin _plugin;
public readonly Dictionary< GamePath, FileInfo > ResolvedFiles = new(); public DirectoryInfo BasePath { get; private set; }
public readonly Dictionary< GamePath, GamePath > SwappedFiles = new();
public MetaManager? MetaManipulations;
public ModCollection? Mods { get; set; } public Dictionary< string, ModData > Mods { get; } = new();
private DirectoryInfo? _basePath; public Dictionary< string, ModCollection > Collections { get; } = new();
public ModCollection CurrentCollection { get; private set; }
public ModManager( Plugin plugin ) public ModManager( Plugin plugin )
=> _plugin = plugin;
public void DiscoverMods()
=> DiscoverMods( _basePath );
public void DiscoverMods( string? basePath )
=> DiscoverMods( basePath == null ? null : new DirectoryInfo( basePath ) );
public void DiscoverMods( DirectoryInfo? basePath )
{ {
_basePath = basePath; _plugin = plugin;
if( basePath == null || !basePath.Exists ) BasePath = new DirectoryInfo( plugin.Configuration!.ModDirectory );
ReadCollections();
CurrentCollection = Collections.Values.First();
if( !SetCurrentCollection( plugin.Configuration!.CurrentCollection ) )
{ {
Mods = null; PluginLog.Debug( "Last choice of collection {Name} is not available, reset to Default.",
return; plugin.Configuration!.CurrentCollection );
}
// FileSystemPasta(); if( SetCurrentCollection( ModCollection.DefaultCollection ) )
Mods = new ModCollection( basePath );
Mods.Load();
CalculateEffectiveFileList();
}
public void CalculateEffectiveFileList()
{ {
ResolvedFiles.Clear(); PluginLog.Error( "Could not load any collection. Default collection unavailable." );
SwappedFiles.Clear(); CurrentCollection = new ModCollection();
MetaManipulations?.Dispose();
if( Mods == null )
{
return;
}
MetaManipulations = new MetaManager( ResolvedFiles, _basePath! );
var changedSettings = false;
var registeredFiles = new Dictionary< GamePath, string >();
foreach( var (mod, settings) in Mods.GetOrderedAndEnabledModListWithSettings( _plugin!.Configuration!.InvertModListOrder ) )
{
mod.FileConflicts.Clear();
changedSettings |= ProcessModFiles( registeredFiles, mod, settings );
ProcessSwappedFiles( registeredFiles, mod, settings );
}
if( changedSettings )
{
Mods.Save();
}
MetaManipulations.WriteNewFiles();
Service< GameResourceManagement >.Get().ReloadPlayerResources();
}
private void ProcessSwappedFiles( Dictionary< GamePath, string > registeredFiles, ResourceMod mod, ModInfo settings )
{
foreach( var swap in mod.Meta.FileSwaps )
{
// just assume people put not fucked paths in here lol
if( !SwappedFiles.ContainsKey( swap.Value ) )
{
SwappedFiles[ swap.Key ] = swap.Value;
registeredFiles[ swap.Key ] = mod.Meta.Name;
}
else if( registeredFiles.TryGetValue( swap.Key, out var modName ) )
{
mod.AddConflict( modName, swap.Key );
} }
} }
} }
private bool ProcessModFiles( Dictionary< GamePath, string > registeredFiles, ResourceMod mod, ModInfo settings ) public bool SetCurrentCollection( string name )
{ {
var changedConfig = settings.FixInvalidSettings(); if( Collections.TryGetValue( name, out var collection ) )
foreach( var file in mod.ModFiles )
{ {
RelPath relativeFilePath = new( file, mod.ModBasePath ); CurrentCollection = collection;
var (configChanged, gamePaths) = mod.Meta.GetFilesForConfig( relativeFilePath, settings ); if( CurrentCollection.Cache == null )
changedConfig |= configChanged;
if( file.Extension == ".meta" && gamePaths.Count > 0 )
{ {
AddManipulations( file, mod ); CurrentCollection.CreateCache( BasePath, Mods );
}
return true;
}
return false;
}
public void ReadCollections()
{
var collectionDir = ModCollection.CollectionDir( _plugin.PluginInterface! );
if( collectionDir.Exists )
{
foreach( var file in collectionDir.EnumerateFiles( "*.json" ) )
{
var collection = ModCollection.LoadFromFile( file );
if( collection != null )
{
if( file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json" )
{
PluginLog.Warning( $"Collection {file.Name} does not correspond to {collection.Name}." );
}
if( Collections.ContainsKey( collection.Name ) )
{
PluginLog.Warning( $"Duplicate collection found: {collection.Name} already exists." );
} }
else else
{ {
AddFiles( gamePaths, file, registeredFiles, mod ); Collections.Add( collection.Name, collection );
} }
} }
return changedConfig;
}
private void AddFiles( IEnumerable< GamePath > gamePaths, FileInfo file, Dictionary< GamePath, string > registeredFiles,
ResourceMod mod )
{
foreach( var gamePath in gamePaths )
{
if( !ResolvedFiles.ContainsKey( gamePath ) )
{
ResolvedFiles[ gamePath ] = file;
registeredFiles[ gamePath ] = mod.Meta.Name;
}
else if( registeredFiles.TryGetValue( gamePath, out var modName ) )
{
mod.AddConflict( modName, gamePath );
}
} }
} }
private void AddManipulations( FileInfo file, ResourceMod mod ) if( !Collections.ContainsKey( ModCollection.DefaultCollection ) )
{ {
if( !mod.MetaManipulations.TryGetValue( file, out var meta ) ) var defaultCollection = new ModCollection();
{ SaveCollection( defaultCollection );
PluginLog.Error( $"{file.FullName} is a TexTools Meta File without meta information." ); Collections.Add( defaultCollection.Name, defaultCollection );
return;
}
foreach( var manipulation in meta.Manipulations )
{
MetaManipulations!.ApplyMod( manipulation );
} }
} }
public void ChangeModPriority( ModInfo info, int newPriority ) public void SaveCollection( ModCollection collection )
=> collection.Save( _plugin.PluginInterface! );
public bool AddCollection( string name, Dictionary< string, ModSettings > settings )
{ {
Mods!.ReorderMod( info, newPriority ); var nameFixed = name.RemoveInvalidPathSymbols().ToLowerInvariant();
CalculateEffectiveFileList(); if( Collections.Values.Any( c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == nameFixed ) )
{
PluginLog.Warning( $"The new collection {name} would lead to the same path as one that already exists." );
return false;
} }
public void ChangeModPriority( ModInfo info, bool up = false ) var newCollection = new ModCollection( name, settings );
{ Collections.Add( name, newCollection );
Mods!.ReorderMod( info, up ); SaveCollection( newCollection );
CalculateEffectiveFileList(); CurrentCollection = newCollection;
return true;
} }
public void DeleteMod( ResourceMod? mod ) public bool RemoveCollection( string name )
{ {
if( mod?.ModBasePath.Exists ?? false ) if( name == ModCollection.DefaultCollection )
{ {
try PluginLog.Error( "Can not remove the default collection." );
{ return false;
Directory.Delete( mod.ModBasePath.FullName, true );
}
catch( Exception e )
{
PluginLog.Error( $"Could not delete the mod {mod.ModBasePath.Name}:\n{e}" );
}
} }
if( Collections.TryGetValue( name, out var collection ) )
{
if( CurrentCollection == collection )
{
SetCurrentCollection( ModCollection.DefaultCollection );
}
collection.Delete( _plugin.PluginInterface! );
Collections.Remove( name );
return true;
}
return false;
}
public void DiscoverMods( DirectoryInfo basePath )
{
BasePath = basePath;
DiscoverMods(); DiscoverMods();
} }
public FileInfo? GetCandidateForGameFile( GamePath gameResourcePath ) public void DiscoverMods()
{ {
var val = ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ); Mods.Clear();
if( !val ) if( !BasePath.Exists )
{ {
return null; PluginLog.Debug( "The mod directory {Directory} does not exist.", BasePath.FullName );
try
{
Directory.CreateDirectory( BasePath.FullName );
}
catch( Exception e )
{
PluginLog.Error( $"The mod directory {BasePath.FullName} does not exist and could not be created:\n{e}" );
return;
}
} }
if( candidate.FullName.Length >= 260 || !candidate.Exists ) foreach( var modFolder in BasePath.EnumerateDirectories() )
{ {
return null; var mod = ModData.LoadMod( modFolder );
if( mod == null )
{
continue;
} }
return candidate; Mods.Add( modFolder.Name, mod );
} }
public GamePath? GetSwappedFilePath( GamePath gameResourcePath ) foreach( var collection in Collections.Values.Where( c => c.Cache != null ) )
=> SwappedFiles.TryGetValue( gameResourcePath, out var swappedPath ) ? swappedPath : null;
public string? ResolveSwappedOrReplacementFilePath( GamePath gameResourcePath )
=> GetCandidateForGameFile( gameResourcePath )?.FullName.Replace( '\\', '/' ) ?? GetSwappedFilePath( gameResourcePath ) ?? null;
public void Dispose()
{ {
MetaManipulations?.Dispose(); collection.CreateCache( BasePath, Mods );
// _fileSystemWatcher?.Dispose(); }
}
public void DeleteMod( DirectoryInfo modFolder )
{
modFolder.Refresh();
if( modFolder.Exists )
{
try
{
Directory.Delete( modFolder.FullName, true );
}
catch( Exception e )
{
PluginLog.Error( $"Could not delete the mod {modFolder.Name}:\n{e}" );
}
Mods.Remove( modFolder.Name );
foreach( var collection in Collections.Values.Where( c => c.Cache != null ) )
{
collection.Cache!.RemoveMod( modFolder );
}
}
}
public bool AddMod( DirectoryInfo modFolder )
{
var mod = ModData.LoadMod( modFolder );
if( mod == null )
{
return false;
}
if( Mods.ContainsKey( modFolder.Name ) )
{
return false;
}
Mods.Add( modFolder.Name, mod );
foreach( var collection in Collections.Values )
{
collection.AddMod( mod );
}
return true;
}
public bool UpdateMod( ModData mod, bool recomputeMeta = false )
{
var oldName = mod.Meta.Name;
var metaChanges = mod.Meta.RefreshFromFile( mod.MetaFile );
var fileChanges = mod.Resources.RefreshModFiles( mod.BasePath );
if( !( recomputeMeta || metaChanges || fileChanges == 0 ) )
{
return false;
}
var nameChange = !string.Equals( oldName, mod.Meta.Name, StringComparison.InvariantCulture );
recomputeMeta |= fileChanges.HasFlag( ResourceChange.Meta );
if( recomputeMeta )
{
mod.Resources.MetaManipulations.Update( mod.Resources.MetaFiles, mod.BasePath, mod.Meta );
mod.Resources.MetaManipulations.SaveToFile( MetaCollection.FileName( mod.BasePath ) );
}
foreach( var collection in Collections.Values )
{
if( metaChanges )
{
collection.UpdateSetting( mod );
if( nameChange )
{
collection.Cache?.SortMods();
}
}
if( fileChanges.HasFlag( ResourceChange.Files )
&& collection.Settings.TryGetValue( mod.BasePath.Name, out var settings )
&& settings.Enabled )
{
collection.Cache?.CalculateEffectiveFileList();
}
if( recomputeMeta )
{
collection.Cache?.UpdateMetaManipulations();
}
}
return true;
} }
// private void FileSystemWatcherOnChanged( object sender, FileSystemEventArgs e ) // private void FileSystemWatcherOnChanged( object sender, FileSystemEventArgs e )

View file

@ -0,0 +1,188 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using Dalamud.Plugin;
using Penumbra.Mod;
using Penumbra.Structs;
namespace Penumbra.Mods
{
public static class ModManagerEditExtensions
{
public static bool RenameMod( this ModManager manager, string newName, ModData mod )
{
if( newName.Length == 0 || string.Equals( newName, mod.Meta.Name, StringComparison.InvariantCulture ) )
{
return false;
}
mod.Meta.Name = newName;
mod.SaveMeta();
foreach( var collection in manager.Collections.Values.Where( c => c.Cache != null ) )
{
collection.Cache!.SortMods();
}
return true;
}
public static bool RenameModFolder( this ModManager manager, ModData mod, DirectoryInfo newDir, bool move = true )
{
if( move )
{
newDir.Refresh();
if( newDir.Exists )
{
return false;
}
var oldDir = new DirectoryInfo( mod.BasePath.FullName );
try
{
oldDir.MoveTo( newDir.FullName );
}
catch( Exception e )
{
PluginLog.Error( $"Error while renaming directory {oldDir.FullName} to {newDir.FullName}:\n{e}" );
return false;
}
}
manager.Mods.Remove( mod.BasePath.Name );
manager.Mods[ newDir.Name ] = mod;
var oldBasePath = mod.BasePath;
mod.BasePath = newDir;
mod.MetaFile = ModData.MetaFileInfo( newDir );
manager.UpdateMod( mod );
foreach( var collection in manager.Collections.Values )
{
if( collection.Settings.TryGetValue( oldBasePath.Name, out var settings ) )
{
collection.Settings[ newDir.Name ] = settings;
collection.Settings.Remove( oldBasePath.Name );
manager.SaveCollection( collection );
}
if( collection.Cache != null )
{
collection.Cache.RemoveMod( newDir );
collection.AddMod( mod );
}
}
return true;
}
public static bool ChangeModGroup( this ModManager manager, string oldGroupName, string newGroupName, ModData mod,
SelectType type = SelectType.Single )
{
if( newGroupName == oldGroupName || mod.Meta.Groups.ContainsKey( newGroupName ) )
{
return false;
}
if( mod.Meta.Groups.TryGetValue( oldGroupName, out var oldGroup ) )
{
if( newGroupName.Length > 0 )
{
mod.Meta.Groups[ newGroupName ] = new OptionGroup()
{
GroupName = newGroupName,
SelectionType = oldGroup.SelectionType,
Options = oldGroup.Options,
};
}
mod.Meta.Groups.Remove( oldGroupName );
}
else
{
if( newGroupName.Length == 0 )
{
return false;
}
mod.Meta.Groups[ newGroupName ] = new OptionGroup()
{
GroupName = newGroupName,
SelectionType = type,
Options = new List< Option >(),
};
}
mod.SaveMeta();
foreach( var collection in manager.Collections.Values )
{
if( !collection.Settings.TryGetValue( mod.BasePath.Name, out var settings ) )
{
continue;
}
if( newGroupName.Length > 0 )
{
settings.Settings[ newGroupName ] = settings.Settings.TryGetValue( oldGroupName, out var value ) ? value : 0;
}
settings.Settings.Remove( oldGroupName );
manager.SaveCollection( collection );
}
return true;
}
public static bool RemoveModOption( this ModManager manager, int optionIdx, OptionGroup group, ModData mod )
{
if( optionIdx < 0 || optionIdx >= group.Options.Count )
{
return false;
}
group.Options.RemoveAt( optionIdx );
mod.SaveMeta();
static int MoveMultiSetting( int oldSetting, int idx )
{
var bitmaskFront = ( 1 << idx ) - 1;
var bitmaskBack = ~( bitmaskFront | ( 1 << idx ) );
return ( oldSetting & bitmaskFront ) | ( ( oldSetting & bitmaskBack ) >> 1 );
}
foreach( var collection in manager.Collections.Values )
{
if( !collection.Settings.TryGetValue( mod.BasePath.Name, out var settings ) )
{
continue;
}
if( !settings.Settings.TryGetValue( group.GroupName, out var setting ) )
{
setting = 0;
}
var newSetting = group.SelectionType switch
{
SelectType.Single => setting >= optionIdx ? setting - 1 : setting,
SelectType.Multi => MoveMultiSetting( setting, optionIdx ),
_ => throw new InvalidEnumArgumentException(),
};
if( newSetting != setting )
{
settings.Settings[ group.GroupName ] = newSetting;
manager.SaveCollection( collection );
if( collection.Cache != null && settings.Enabled )
{
collection.CalculateEffectiveFileList( manager.BasePath, mod.Resources.MetaManipulations.Count > 0 );
}
}
}
return true;
}
}
}

View file

@ -1,70 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Plugin;
using Penumbra.Importer;
using Penumbra.Models;
using Penumbra.Util;
namespace Penumbra.Mods
{
public class ResourceMod
{
public ResourceMod( ModMeta meta, DirectoryInfo dir )
{
Meta = meta;
ModBasePath = dir;
}
public ModMeta Meta { get; set; }
public DirectoryInfo ModBasePath { get; set; }
public List< FileInfo > ModFiles { get; } = new();
public Dictionary< FileInfo, TexToolsMeta > MetaManipulations { get; } = new();
public Dictionary< string, List< GamePath > > FileConflicts { get; } = new();
public void RefreshModFiles()
{
FileConflicts.Clear();
ModFiles.Clear();
MetaManipulations.Clear();
// we don't care about any _files_ in the root dir, but any folders should be a game folder/file combo
foreach( var file in ModBasePath.EnumerateDirectories()
.SelectMany( dir => dir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) )
{
if( file.Extension == ".meta" )
{
try
{
MetaManipulations[ file ] = new TexToolsMeta( File.ReadAllBytes( file.FullName ) );
}
catch( Exception e )
{
PluginLog.Error( $"Could not parse meta file {file.FullName}:\n{e}" );
}
}
ModFiles.Add( file );
}
}
public void AddConflict( string modName, GamePath path )
{
if( FileConflicts.TryGetValue( modName, out var arr ) )
{
if( !arr.Contains( path ) )
{
arr.Add( path );
}
return;
}
FileConflicts[ modName ] = new List< GamePath > { path };
}
}
}

View file

@ -6,9 +6,10 @@ using EmbedIO.WebApi;
using Penumbra.API; using Penumbra.API;
using Penumbra.Game; using Penumbra.Game;
using Penumbra.Hooks; using Penumbra.Hooks;
using Penumbra.MetaData; using Penumbra.Meta.Files;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.UI; using Penumbra.UI;
using Penumbra.Util;
namespace Penumbra namespace Penumbra
{ {
@ -25,34 +26,35 @@ namespace Penumbra
private const string CommandName = "/penumbra"; private const string CommandName = "/penumbra";
public DalamudPluginInterface? PluginInterface { get; set; } public DalamudPluginInterface PluginInterface { get; set; } = null!;
public Configuration? Configuration { get; set; } public Configuration Configuration { get; set; } = null!;
public ResourceLoader? ResourceLoader { get; set; } public ResourceLoader ResourceLoader { get; set; } = null!;
public SettingsInterface? SettingsInterface { get; set; } public SettingsInterface SettingsInterface { get; set; } = null!;
public SoundShit? SoundShit { get; set; } public MusicManager SoundShit { get; set; } = null!;
private WebServer? _webServer; private WebServer? _webServer;
public void Initialize( DalamudPluginInterface pluginInterface ) public void Initialize( DalamudPluginInterface pluginInterface )
{ {
PluginInterface = pluginInterface; PluginInterface = pluginInterface;
Service< DalamudPluginInterface >.Set( PluginInterface );
Configuration = PluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); Configuration = Configuration.Load( PluginInterface );
Configuration.Initialize( PluginInterface );
SoundShit = new SoundShit( this );
SoundShit = new MusicManager( this );
SoundShit.DisableStreaming();
var gameUtils = Service< GameResourceManagement >.Set( PluginInterface ); var gameUtils = Service< GameResourceManagement >.Set( PluginInterface );
var modManager = Service< ModManager >.Set( this );
Service< MetaDefaults >.Set( PluginInterface ); Service< MetaDefaults >.Set( PluginInterface );
modManager.DiscoverMods( Configuration.CurrentCollection ); var modManager = Service< ModManager >.Set( this );
modManager.DiscoverMods();
ResourceLoader = new ResourceLoader( this ); ResourceLoader = new ResourceLoader( this );
PluginInterface.CommandManager.AddHandler( CommandName, new CommandInfo( OnCommand ) PluginInterface.CommandManager.AddHandler( CommandName, new CommandInfo( OnCommand )
{ {
HelpMessage = "/penumbra - toggle ui\n/penumbra reload - reload mod file lists & discover any new mods" HelpMessage = "/penumbra - toggle ui\n/penumbra reload - reload mod file lists & discover any new mods",
} ); } );
ResourceLoader.Init(); ResourceLoader.Init();
@ -96,14 +98,12 @@ namespace Penumbra
public void Dispose() public void Dispose()
{ {
// ModManager?.Dispose(); PluginInterface.UiBuilder.OnBuildUi -= SettingsInterface.Draw;
PluginInterface!.UiBuilder.OnBuildUi -= SettingsInterface!.Draw;
PluginInterface.CommandManager.RemoveHandler( CommandName ); PluginInterface.CommandManager.RemoveHandler( CommandName );
PluginInterface.Dispose(); PluginInterface.Dispose();
ResourceLoader?.Dispose(); ResourceLoader.Dispose();
ShutdownWebServer(); ShutdownWebServer();
} }
@ -118,8 +118,8 @@ namespace Penumbra
case "reload": case "reload":
{ {
Service< ModManager >.Get().DiscoverMods(); Service< ModManager >.Get().DiscoverMods();
PluginInterface!.Framework.Gui.Chat.Print( PluginInterface.Framework.Gui.Chat.Print(
$"Reloaded Penumbra mods. You have {Service< ModManager >.Get()?.Mods?.ModSettings?.Count ?? 0} mods, {Service< ModManager >.Get()?.Mods?.EnabledMods?.Length ?? 0} of which are enabled." $"Reloaded Penumbra mods. You have {Service< ModManager >.Get()?.Mods.Count} mods."
); );
break; break;
} }
@ -127,11 +127,11 @@ namespace Penumbra
{ {
if( args.Length > 1 ) if( args.Length > 1 )
{ {
RefreshActors.RedrawSpecific( PluginInterface!.ClientState.Actors, string.Join( " ", args.Skip( 1 ) ) ); RefreshActors.RedrawSpecific( PluginInterface.ClientState.Actors, string.Join( " ", args.Skip( 1 ) ) );
} }
else else
{ {
RefreshActors.RedrawAll( PluginInterface!.ClientState.Actors ); RefreshActors.RedrawAll( PluginInterface.ClientState.Actors );
} }
break; break;
@ -141,7 +141,7 @@ namespace Penumbra
return; return;
} }
SettingsInterface!.FlipVisibility(); SettingsInterface.FlipVisibility();
} }
} }
} }

View file

@ -7,6 +7,6 @@ namespace Penumbra.Structs
// some shit here, the game does some jump if its < 0xA for other files for some reason but there's no impl, probs debug? // some shit here, the game does some jump if its < 0xA for other files for some reason but there's no impl, probs debug?
LoadIndexResource = 0xA, // load index/index2 LoadIndexResource = 0xA, // load index/index2
LoadSqPackResource = 0xB LoadSqPackResource = 0xB,
} }
} }

View file

@ -0,0 +1,99 @@
using System.Collections.Generic;
using System.ComponentModel;
using Newtonsoft.Json;
using Penumbra.Util;
namespace Penumbra.Structs
{
public enum SelectType
{
Single,
Multi,
}
public struct Option
{
public string OptionName;
public string OptionDesc;
[JsonProperty( ItemConverterType = typeof( SingleOrArrayConverter< GamePath > ) )]
public Dictionary< RelPath, HashSet< GamePath > > OptionFiles;
public bool AddFile( RelPath filePath, GamePath gamePath )
{
if( OptionFiles.TryGetValue( filePath, out var set ) )
{
return set.Add( gamePath );
}
OptionFiles[ filePath ] = new HashSet< GamePath >() { gamePath };
return true;
}
}
public struct OptionGroup
{
public string GroupName;
[JsonConverter( typeof( Newtonsoft.Json.Converters.StringEnumConverter ) )]
public SelectType SelectionType;
public List< Option > Options;
private bool ApplySingleGroupFiles( RelPath relPath, int selection, HashSet< GamePath > paths )
{
if( Options[ selection ].OptionFiles.TryGetValue( relPath, out var groupPaths ) )
{
paths.UnionWith( groupPaths );
return true;
}
for( var i = 0; i < Options.Count; ++i )
{
if( i == selection )
{
continue;
}
if( Options[ i ].OptionFiles.ContainsKey( relPath ) )
{
return true;
}
}
return false;
}
private bool ApplyMultiGroupFiles( RelPath relPath, int selection, HashSet< GamePath > paths )
{
var doNotAdd = false;
for( var i = 0; i < Options.Count; ++i )
{
if( ( selection & ( 1 << i ) ) != 0 )
{
if( Options[ i ].OptionFiles.TryGetValue( relPath, out var groupPaths ) )
{
paths.UnionWith( groupPaths );
}
}
else if( Options[ i ].OptionFiles.ContainsKey( relPath ) )
{
doNotAdd = true;
}
}
return doNotAdd;
}
// Adds all game paths from the given option that correspond to the given RelPath to paths, if any exist.
internal bool ApplyGroupFiles( RelPath relPath, int selection, HashSet< GamePath > paths )
{
return SelectionType switch
{
SelectType.Single => ApplySingleGroupFiles( relPath, selection, paths ),
SelectType.Multi => ApplyMultiGroupFiles( relPath, selection, paths ),
_ => throw new InvalidEnumArgumentException( "Invalid option group type." ),
};
}
}
}

View file

@ -3,15 +3,21 @@ using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using ImGuiNET; using ImGuiNET;
namespace Penumbra.UI namespace Penumbra.UI.Custom
{ {
public static partial class ImGuiCustom public static partial class ImGuiCustom
{ {
public static void BeginFramedGroup( string label ) => BeginFramedGroupInternal( ref label, ZeroVector, false ); public static void BeginFramedGroup( string label )
public static void BeginFramedGroup( string label, Vector2 minSize ) => BeginFramedGroupInternal( ref label, minSize, false ); => BeginFramedGroupInternal( ref label, ZeroVector, false );
public static bool BeginFramedGroupEdit( ref string label ) => BeginFramedGroupInternal( ref label, ZeroVector, true ); public static void BeginFramedGroup( string label, Vector2 minSize )
public static bool BeginFramedGroupEdit( ref string label, Vector2 minSize ) => BeginFramedGroupInternal( ref label, minSize, true ); => BeginFramedGroupInternal( ref label, minSize, false );
public static bool BeginFramedGroupEdit( ref string label )
=> BeginFramedGroupInternal( ref label, ZeroVector, true );
public static bool BeginFramedGroupEdit( ref string label, Vector2 minSize )
=> BeginFramedGroupInternal( ref label, minSize, true );
private static bool BeginFramedGroupInternal( ref string label, Vector2 minSize, bool edit ) private static bool BeginFramedGroupInternal( ref string label, Vector2 minSize, bool edit )
{ {

View file

@ -1,6 +1,6 @@
using ImGuiNET; using ImGuiNET;
namespace Penumbra.UI namespace Penumbra.UI.Custom
{ {
public static partial class ImGuiCustom public static partial class ImGuiCustom
{ {

View file

@ -1,7 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using ImGuiNET; using ImGuiNET;
namespace Penumbra.UI namespace Penumbra.UI.Custom
{ {
public static partial class ImGuiCustom public static partial class ImGuiCustom
{ {
@ -16,8 +16,8 @@ namespace Penumbra.UI
return false; return false;
} }
public static bool ResizingTextInput( string label, ref string input, uint maxLength ) => public static bool ResizingTextInput( string label, ref string input, uint maxLength )
ResizingTextInputIntern( label, ref input, maxLength ).Item1; => ResizingTextInputIntern( label, ref input, maxLength ).Item1;
public static bool ResizingTextInput( ref string input, uint maxLength ) public static bool ResizingTextInput( ref string input, uint maxLength )
{ {

View file

@ -1,7 +1,7 @@
using System.Windows.Forms; using System.Windows.Forms;
using ImGuiNET; using ImGuiNET;
namespace Penumbra.UI namespace Penumbra.UI.Custom
{ {
public static partial class ImGuiCustom public static partial class ImGuiCustom
{ {

View file

@ -43,7 +43,7 @@ namespace Penumbra.UI
} }
var ss = ImGui.GetMainViewport().Size + ImGui.GetMainViewport().Pos; var ss = ImGui.GetMainViewport().Size + ImGui.GetMainViewport().Pos;
ImGui.SetNextWindowViewport(ImGui.GetMainViewport().ID); ImGui.SetNextWindowViewport( ImGui.GetMainViewport().ID );
ImGui.SetNextWindowPos( ss - WindowPosOffset, ImGuiCond.Always ); ImGui.SetNextWindowPos( ss - WindowPosOffset, ImGuiCond.Always );

View file

@ -19,7 +19,9 @@ namespace Penumbra.UI
#endif #endif
private readonly SettingsInterface _base; private readonly SettingsInterface _base;
public MenuBar( SettingsInterface ui ) => _base = ui;
public MenuBar( SettingsInterface ui )
=> _base = ui;
public void Draw() public void Draw()
{ {

View file

@ -0,0 +1,120 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Plugin;
using ImGuiNET;
using Penumbra.Hooks;
using Penumbra.Mod;
using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra.UI
{
public partial class SettingsInterface
{
private class TabCollections
{
private readonly Selector _selector;
private readonly ModManager _manager;
private string[] _collectionNames = null!;
private int _currentIndex = 0;
private string _newCollectionName = string.Empty;
private void UpdateNames()
=> _collectionNames = _manager.Collections.Values.Select( c => c.Name ).ToArray();
private void UpdateIndex()
{
_currentIndex = _collectionNames.IndexOf( c => c == _manager.CurrentCollection.Name );
if( _currentIndex < 0 )
{
PluginLog.Error( $"Current Collection {_manager.CurrentCollection.Name} is not found in collections." );
_currentIndex = 0;
}
}
public TabCollections( Selector selector )
{
_selector = selector;
_manager = Service< ModManager >.Get();
UpdateNames();
UpdateIndex();
}
private void CreateNewCollection( Dictionary< string, ModSettings > settings )
{
_manager.AddCollection( _newCollectionName, settings );
_manager.SetCurrentCollection( _newCollectionName );
_newCollectionName = string.Empty;
UpdateNames();
UpdateIndex();
}
private void DrawNewCollectionInput()
{
ImGui.InputTextWithHint( "##New Collection", "New Collection", ref _newCollectionName, 64 );
var changedStyle = false;
if( _newCollectionName.Length == 0 )
{
changedStyle = true;
ImGui.PushStyleVar( ImGuiStyleVar.Alpha, 0.5f );
}
if( ImGui.Button( "Create New Empty Collection" ) && _newCollectionName.Length > 0 )
{
CreateNewCollection( new Dictionary< string, ModSettings >() );
}
ImGui.SameLine();
if( ImGui.Button( "Duplicate Current Collection" ) && _newCollectionName.Length > 0 )
{
CreateNewCollection( _manager.CurrentCollection.Settings );
}
if( changedStyle )
{
ImGui.PopStyleVar();
}
if( _manager.Collections.Count > 1 )
{
ImGui.SameLine();
if( ImGui.Button( "Delete Current Collection" ) )
{
_manager.RemoveCollection( _manager.CurrentCollection.Name );
UpdateNames();
UpdateIndex();
}
}
}
public void Draw()
{
if( !ImGui.BeginTabItem( "Collections" ) )
{
return;
}
var index = _currentIndex;
if( ImGui.Combo( "Current Collection", ref index, _collectionNames, _collectionNames.Length ) )
{
if( index != _currentIndex && _manager.SetCurrentCollection( _collectionNames[ index ] ) )
{
_currentIndex = index;
_selector.ReloadSelection();
var resourceManager = Service< GameResourceManagement >.Get();
resourceManager.ReloadPlayerResources();
}
}
ImGui.Dummy( new Vector2( 0, 5 ) );
DrawNewCollectionInput();
ImGui.EndTabItem();
}
}
}
}

View file

@ -1,6 +1,7 @@
using System.IO; using System.IO;
using Dalamud.Interface; using Dalamud.Interface;
using ImGuiNET; using ImGuiNET;
using Penumbra.Meta;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Util; using Penumbra.Util;
@ -10,24 +11,40 @@ namespace Penumbra.UI
{ {
private class TabEffective private class TabEffective
{ {
private const string LabelTab = "Effective File List"; private const string LabelTab = "Effective Changes";
private const float TextSizePadding = 5f; private static readonly string LongArrowLeft = $"{( char )FontAwesomeIcon.LongArrowAltLeft}";
private readonly ModManager _modManager;
public TabEffective()
=> _modManager = Service< ModManager >.Get();
private ModManager _mods
=> Service< ModManager >.Get();
private static void DrawFileLine( FileInfo file, GamePath path ) private static void DrawFileLine( FileInfo file, GamePath path )
{ {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGuiCustom.CopyOnClickSelectable( path ); Custom.ImGuiCustom.CopyOnClickSelectable( path );
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.PushFont( UiBuilder.IconFont ); ImGui.PushFont( UiBuilder.IconFont );
ImGui.TextUnformatted( $"{( char )FontAwesomeIcon.LongArrowAltLeft}" ); ImGui.TextUnformatted( LongArrowLeft );
ImGui.PopFont(); ImGui.PopFont();
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGuiCustom.CopyOnClickSelectable( file.FullName ); Custom.ImGuiCustom.CopyOnClickSelectable( file.FullName );
}
private static void DrawManipulationLine( MetaManipulation manip, Mod.Mod mod )
{
ImGui.TableNextColumn();
ImGui.Selectable( manip.IdentifierString() );
ImGui.TableNextColumn();
ImGui.PushFont( UiBuilder.IconFont );
ImGui.TextUnformatted( LongArrowLeft );
ImGui.PopFont();
ImGui.TableNextColumn();
ImGui.Selectable( mod.Data.Meta.Name );
} }
public void Draw() public void Draw()
@ -40,14 +57,21 @@ namespace Penumbra.UI
const ImGuiTableFlags flags = ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX; const ImGuiTableFlags flags = ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX;
if( ImGui.BeginTable( "##effective_files", 3, flags, AutoFillSize ) ) if( ImGui.BeginTable( "##effective_changes", 3, flags, AutoFillSize ) )
{ {
foreach ( var file in _mods.ResolvedFiles ) var currentCollection = _modManager.CurrentCollection.Cache!;
foreach( var file in currentCollection.ResolvedFiles )
{ {
DrawFileLine( file.Value, file.Key ); DrawFileLine( file.Value, file.Key );
ImGui.TableNextRow(); ImGui.TableNextRow();
} }
foreach( var (manip, mod) in currentCollection.MetaManipulations.Manipulations )
{
DrawManipulationLine( manip, mod );
ImGui.TableNextRow();
}
ImGui.EndTable(); ImGui.EndTable();
} }

View file

@ -6,6 +6,7 @@ using System.Windows.Forms;
using Dalamud.Plugin; using Dalamud.Plugin;
using ImGuiNET; using ImGuiNET;
using Penumbra.Importer; using Penumbra.Importer;
using Penumbra.Util;
namespace Penumbra.UI namespace Penumbra.UI
{ {
@ -25,26 +26,30 @@ namespace Penumbra.UI
private static readonly Vector2 ImportBarSize = new( -1, 0 ); private static readonly Vector2 ImportBarSize = new( -1, 0 );
private bool _isImportRunning = false; private bool _isImportRunning;
private bool _hasError = false; private bool _hasError;
private TexToolsImport? _texToolsImport; private TexToolsImport? _texToolsImport;
private readonly SettingsInterface _base; private readonly SettingsInterface _base;
public TabImport( SettingsInterface ui ) => _base = ui; public TabImport( SettingsInterface ui )
=> _base = ui;
public bool IsImporting() => _isImportRunning; public bool IsImporting()
=> _isImportRunning;
private void RunImportTask() private void RunImportTask()
{ {
_isImportRunning = true; _isImportRunning = true;
Task.Run( async () => Task.Run( async () =>
{
try
{ {
var picker = new OpenFileDialog var picker = new OpenFileDialog
{ {
Multiselect = true, Multiselect = true,
Filter = FileTypeFilter, Filter = FileTypeFilter,
CheckFileExists = true, CheckFileExists = true,
Title = LabelFileDialog Title = LabelFileDialog,
}; };
var result = await picker.ShowDialogAsync(); var result = await picker.ShowDialogAsync();
@ -59,7 +64,7 @@ namespace Penumbra.UI
try try
{ {
_texToolsImport = new TexToolsImport( new DirectoryInfo( _base._plugin!.Configuration!.CurrentCollection ) ); _texToolsImport = new TexToolsImport( new DirectoryInfo( _base._plugin!.Configuration!.ModDirectory ) );
_texToolsImport.ImportModPack( new FileInfo( fileName ) ); _texToolsImport.ImportModPack( new FileInfo( fileName ) );
PluginLog.Log( $"-> {fileName} OK!" ); PluginLog.Log( $"-> {fileName} OK!" );
@ -74,6 +79,11 @@ namespace Penumbra.UI
_texToolsImport = null; _texToolsImport = null;
_base.ReloadMods(); _base.ReloadMods();
} }
}
catch( Exception e )
{
PluginLog.Error( $"Error opening file picker dialogue:\n{e}" );
}
_isImportRunning = false; _isImportRunning = false;
} ); } );
@ -98,8 +108,7 @@ namespace Penumbra.UI
switch( _texToolsImport.State ) switch( _texToolsImport.State )
{ {
case ImporterState.None: case ImporterState.None: break;
break;
case ImporterState.WritingPackToDisk: case ImporterState.WritingPackToDisk:
ImGui.Text( TooltipModpack1 ); ImGui.Text( TooltipModpack1 );
break; break;
@ -111,10 +120,8 @@ namespace Penumbra.UI
ImGui.ProgressBar( _texToolsImport.Progress, ImportBarSize, str ); ImGui.ProgressBar( _texToolsImport.Progress, ImportBarSize, str );
break; break;
} }
case ImporterState.Done: case ImporterState.Done: break;
break; default: throw new ArgumentOutOfRangeException();
default:
throw new ArgumentOutOfRangeException();
} }
} }

View file

@ -1,5 +1,6 @@
using ImGuiNET; using ImGuiNET;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra.UI namespace Penumbra.UI
{ {
@ -9,21 +10,21 @@ namespace Penumbra.UI
{ {
private const string LabelTab = "Installed Mods"; private const string LabelTab = "Installed Mods";
private readonly SettingsInterface _base; private readonly ModManager _modManager;
public readonly Selector Selector; public readonly Selector Selector;
public readonly ModPanel ModPanel; public readonly ModPanel ModPanel;
public TabInstalled( SettingsInterface ui ) public TabInstalled( SettingsInterface ui )
{ {
_base = ui; Selector = new Selector( ui );
Selector = new Selector( _base ); ModPanel = new ModPanel( ui, Selector );
ModPanel = new ModPanel( _base, Selector ); _modManager = Service< ModManager >.Get();
} }
private static void DrawNoModsAvailable() private static void DrawNoModsAvailable()
{ {
ImGui.Text( "You don't have any mods :(" ); ImGui.Text( "You don't have any mods :(" );
ImGuiCustom.VerticalDistance( 20f ); Custom.ImGuiCustom.VerticalDistance( 20f );
ImGui.Text( "You'll need to install them first by creating a folder close to the root of your drive (preferably an SSD)." ); ImGui.Text( "You'll need to install them first by creating a folder close to the root of your drive (preferably an SSD)." );
ImGui.Text( "For example: D:/ffxiv/mods/" ); ImGui.Text( "For example: D:/ffxiv/mods/" );
ImGui.Text( "And pasting that path into the settings tab and clicking the 'Rediscover Mods' button." ); ImGui.Text( "And pasting that path into the settings tab and clicking the 'Rediscover Mods' button." );
@ -38,7 +39,7 @@ namespace Penumbra.UI
return; return;
} }
if( Service< ModManager >.Get().Mods != null ) if( _modManager.Mods.Count > 0 )
{ {
Selector.Draw(); Selector.Draw();
ImGui.SameLine(); ImGui.SameLine();

View file

@ -1,10 +1,13 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.IO; using System.IO;
using System.Linq;
using Dalamud.Interface; using Dalamud.Interface;
using ImGuiNET; using ImGuiNET;
using Penumbra.Models; using Penumbra.Game.Enums;
using Penumbra.Meta;
using Penumbra.Mod;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Structs;
using Penumbra.Util; using Penumbra.Util;
namespace Penumbra.UI namespace Penumbra.UI
@ -35,7 +38,7 @@ namespace Penumbra.UI
private const string LabelChangedItemsHeader = "##changedItems"; private const string LabelChangedItemsHeader = "##changedItems";
private const string LabelChangedItemIdx = "##citem_"; private const string LabelChangedItemIdx = "##citem_";
private const string LabelChangedItemNew = "##citem_new"; private const string LabelChangedItemNew = "##citem_new";
private const string LabelConflictsTab = "File Conflicts"; private const string LabelConflictsTab = "Mod Conflicts";
private const string LabelConflictsHeader = "##conflicts"; private const string LabelConflictsHeader = "##conflicts";
private const string LabelFileSwapTab = "File Swaps"; private const string LabelFileSwapTab = "File Swaps";
private const string LabelFileSwapHeader = "##fileSwaps"; private const string LabelFileSwapHeader = "##fileSwaps";
@ -49,7 +52,6 @@ namespace Penumbra.UI
"Green files replace their standard game path counterpart (not in any option) or are in all options of a Single-Select option.\n" "Green files replace their standard game path counterpart (not in any option) or are in all options of a Single-Select option.\n"
+ "Yellow files are restricted to some options."; + "Yellow files are restricted to some options.";
private const float TextSizePadding = 5f;
private const float OptionSelectionWidth = 140f; private const float OptionSelectionWidth = 140f;
private const float CheckMarkSize = 50f; private const float CheckMarkSize = 50f;
private const uint ColorGreen = 0xFF00C800; private const uint ColorGreen = 0xFF00C800;
@ -68,11 +70,12 @@ namespace Penumbra.UI
private readonly Selector _selector; private readonly Selector _selector;
private readonly SettingsInterface _base; private readonly SettingsInterface _base;
private readonly ModManager _modManager;
private void SelectGroup( int idx ) private void SelectGroup( int idx )
{ {
// Not using the properties here because we need it to be not null forgiving in this case. // Not using the properties here because we need it to be not null forgiving in this case.
var numGroups = _selector.Mod?.Mod.Meta.Groups.Count ?? 0; var numGroups = _selector.Mod?.Data.Meta.Groups.Count ?? 0;
_selectedGroupIndex = idx; _selectedGroupIndex = idx;
if( _selectedGroupIndex >= numGroups ) if( _selectedGroupIndex >= numGroups )
{ {
@ -126,20 +129,19 @@ namespace Penumbra.UI
_base = ui; _base = ui;
_selector = s; _selector = s;
ResetState(); ResetState();
_modManager = Service< ModManager >.Get();
} }
// This is only drawn when we have a mod selected, so we can forgive nulls. // This is only drawn when we have a mod selected, so we can forgive nulls.
private ModInfo Mod private Mod.Mod Mod
=> _selector.Mod!; => _selector.Mod!;
private ModMeta Meta private ModMeta Meta
=> Mod.Mod.Meta; => Mod.Data.Meta;
private void Save() private void Save()
{ {
var modManager = Service< ModManager >.Get(); _modManager.CurrentCollection.Save( _base._plugin.PluginInterface! );
modManager.Mods?.Save();
modManager.CalculateEffectiveFileList();
} }
private void DrawAboutTab() private void DrawAboutTab()
@ -161,7 +163,8 @@ namespace Penumbra.UI
if( _editMode ) if( _editMode )
{ {
if( ImGui.InputTextMultiline( LabelDescEdit, ref desc, 1 << 16, AutoFillSize, flags ) ) if( ImGui.InputTextMultiline( LabelDescEdit, ref desc, 1 << 16,
AutoFillSize, flags ) )
{ {
Meta.Description = desc; Meta.Description = desc;
_selector.SaveCurrentMod(); _selector.SaveCurrentMod();
@ -194,6 +197,7 @@ namespace Penumbra.UI
if( ImGui.BeginTabItem( LabelChangedItemsTab ) ) if( ImGui.BeginTabItem( LabelChangedItemsTab ) )
{ {
ImGui.SetNextItemWidth( -1 ); ImGui.SetNextItemWidth( -1 );
var changedItems = false;
if( ImGui.BeginListBox( LabelChangedItemsHeader, AutoFillSize ) ) if( ImGui.BeginListBox( LabelChangedItemsHeader, AutoFillSize ) )
{ {
_changedItemsList ??= Meta.ChangedItems _changedItemsList ??= Meta.ChangedItems
@ -205,6 +209,7 @@ namespace Penumbra.UI
if( ImGui.InputText( _changedItemsList[ i ].label, ref _changedItemsList[ i ].name, 128, flags ) ) if( ImGui.InputText( _changedItemsList[ i ].label, ref _changedItemsList[ i ].name, 128, flags ) )
{ {
Meta.ChangedItems.RemoveOrChange( _changedItemsList[ i ].name, i ); Meta.ChangedItems.RemoveOrChange( _changedItemsList[ i ].name, i );
changedItems = true;
_selector.SaveCurrentMod(); _selector.SaveCurrentMod();
} }
} }
@ -217,10 +222,16 @@ namespace Penumbra.UI
&& newItem.Length > 0 ) && newItem.Length > 0 )
{ {
Meta.ChangedItems.Add( newItem ); Meta.ChangedItems.Add( newItem );
changedItems = true;
_selector.SaveCurrentMod(); _selector.SaveCurrentMod();
} }
} }
if( changedItems )
{
_changedItemsList = null;
}
ImGui.EndListBox(); ImGui.EndListBox();
} }
@ -234,7 +245,7 @@ namespace Penumbra.UI
private void DrawConflictTab() private void DrawConflictTab()
{ {
if( !Mod.Mod.FileConflicts.Any() || !ImGui.BeginTabItem( LabelConflictsTab ) ) if( !Mod.Cache.Conflicts.Any() || !ImGui.BeginTabItem( LabelConflictsTab ) )
{ {
return; return;
} }
@ -242,20 +253,28 @@ namespace Penumbra.UI
ImGui.SetNextItemWidth( -1 ); ImGui.SetNextItemWidth( -1 );
if( ImGui.BeginListBox( LabelConflictsHeader, AutoFillSize ) ) if( ImGui.BeginListBox( LabelConflictsHeader, AutoFillSize ) )
{ {
foreach( var kv in Mod.Mod.FileConflicts ) foreach( var kv in Mod.Cache.Conflicts )
{ {
var mod = kv.Key; var mod = kv.Key;
if( ImGui.Selectable( mod ) ) if( ImGui.Selectable( mod.Data.Meta.Name ) )
{ {
_selector.SelectModByName( mod ); _selector.SelectModByName( mod.Data.Meta.Name );
} }
ImGui.SameLine();
ImGui.Text( $"(Priority {mod.Settings.Priority})" );
ImGui.Indent( 15 ); ImGui.Indent( 15 );
foreach( var file in kv.Value ) foreach( var file in kv.Value.Files )
{ {
ImGui.Selectable( file ); ImGui.Selectable( file );
} }
foreach( var manip in kv.Value.Manipulations )
{
ImGui.Text( manip.IdentifierString() );
}
ImGui.Unindent( 15 ); ImGui.Unindent( 15 );
} }
@ -286,7 +305,7 @@ namespace Penumbra.UI
foreach( var file in Meta.FileSwaps ) foreach( var file in Meta.FileSwaps )
{ {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGuiCustom.CopyOnClickSelectable( file.Key ); Custom.ImGuiCustom.CopyOnClickSelectable( file.Key );
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.PushFont( UiBuilder.IconFont ); ImGui.PushFont( UiBuilder.IconFont );
@ -294,7 +313,7 @@ namespace Penumbra.UI
ImGui.PopFont(); ImGui.PopFont();
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGuiCustom.CopyOnClickSelectable( file.Value ); Custom.ImGuiCustom.CopyOnClickSelectable( file.Value );
ImGui.TableNextRow(); ImGui.TableNextRow();
} }
@ -312,16 +331,15 @@ namespace Penumbra.UI
return; return;
} }
var len = Mod.Mod.ModBasePath.FullName.Length; _fullFilenameList = Mod.Data.Resources.ModFiles
_fullFilenameList = Mod.Mod.ModFiles .Select( f => ( f, false, ColorGreen, new RelPath( f, Mod.Data.BasePath ) ) ).ToArray();
.Select( F => ( F, false, ColorGreen, new RelPath( F, Mod.Mod.ModBasePath ) ) ).ToArray();
if( Meta.Groups.Count == 0 ) if( Meta.Groups.Count == 0 )
{ {
return; return;
} }
for( var i = 0; i < Mod.Mod.ModFiles.Count; ++i ) for( var i = 0; i < Mod.Data.Resources.ModFiles.Count; ++i )
{ {
foreach( var group in Meta.Groups.Values ) foreach( var group in Meta.Groups.Values )
{ {
@ -362,10 +380,10 @@ namespace Penumbra.UI
if( ImGui.BeginListBox( LabelFileListHeader, AutoFillSize ) ) if( ImGui.BeginListBox( LabelFileListHeader, AutoFillSize ) )
{ {
UpdateFilenameList(); UpdateFilenameList();
foreach( var file in _fullFilenameList! ) foreach( var (name, _, color, _) in _fullFilenameList! )
{ {
ImGui.PushStyleColor( ImGuiCol.Text, file.color ); ImGui.PushStyleColor( ImGuiCol.Text, color );
ImGui.Selectable( file.name.FullName ); ImGui.Selectable( name.FullName );
ImGui.PopStyleColor(); ImGui.PopStyleColor();
} }
@ -379,10 +397,11 @@ namespace Penumbra.UI
ImGui.EndTabItem(); ImGui.EndTabItem();
} }
private int HandleDefaultString( GamePath[] gamePaths, out int removeFolders ) private static int HandleDefaultString( GamePath[] gamePaths, out int removeFolders )
{ {
removeFolders = 0; removeFolders = 0;
var defaultIndex = gamePaths.IndexOf( p => ( ( string )p ).StartsWith( TextDefaultGamePath ) ); var defaultIndex =
gamePaths.IndexOf( p => ( ( string )p ).StartsWith( TextDefaultGamePath ) );
if( defaultIndex < 0 ) if( defaultIndex < 0 )
{ {
return defaultIndex; return defaultIndex;
@ -412,7 +431,7 @@ namespace Penumbra.UI
var option = ( Option )_selectedOption; var option = ( Option )_selectedOption;
var gamePaths = _currentGamePaths.Split( ';' ).Select( P => new GamePath( P ) ).ToArray(); var gamePaths = _currentGamePaths.Split( ';' ).Select( p => new GamePath( p ) ).ToArray();
if( gamePaths.Length == 0 || ( ( string )gamePaths[ 0 ] ).Length == 0 ) if( gamePaths.Length == 0 || ( ( string )gamePaths[ 0 ] ).Length == 0 )
{ {
return; return;
@ -420,7 +439,7 @@ namespace Penumbra.UI
var defaultIndex = HandleDefaultString( gamePaths, out var removeFolders ); var defaultIndex = HandleDefaultString( gamePaths, out var removeFolders );
var changed = false; var changed = false;
for( var i = 0; i < Mod.Mod.ModFiles.Count; ++i ) for( var i = 0; i < Mod.Data.Resources.ModFiles.Count; ++i )
{ {
if( !_fullFilenameList![ i ].selected ) if( !_fullFilenameList![ i ].selected )
{ {
@ -435,7 +454,7 @@ namespace Penumbra.UI
if( remove && option.OptionFiles.TryGetValue( relName, out var setPaths ) ) if( remove && option.OptionFiles.TryGetValue( relName, out var setPaths ) )
{ {
if( setPaths.RemoveWhere( P => gamePaths.Contains( P ) ) > 0 ) if( setPaths.RemoveWhere( p => gamePaths.Contains( p ) ) > 0 )
{ {
changed = true; changed = true;
} }
@ -477,7 +496,8 @@ namespace Penumbra.UI
private void DrawGamePathInput() private void DrawGamePathInput()
{ {
ImGui.SetNextItemWidth( -1 ); ImGui.SetNextItemWidth( -1 );
ImGui.InputTextWithHint( LabelGamePathsEditBox, "Hover for help...", ref _currentGamePaths, 128 ); ImGui.InputTextWithHint( LabelGamePathsEditBox, "Hover for help...", ref _currentGamePaths,
128 );
if( ImGui.IsItemHovered() ) if( ImGui.IsItemHovered() )
{ {
ImGui.SetTooltip( TooltipGamePathsEdit ); ImGui.SetTooltip( TooltipGamePathsEdit );
@ -580,7 +600,7 @@ namespace Penumbra.UI
var oldEnabled = enabled; var oldEnabled = enabled;
if( ImGui.Checkbox( label, ref enabled ) && oldEnabled != enabled ) if( ImGui.Checkbox( label, ref enabled ) && oldEnabled != enabled )
{ {
Mod.Settings[ group.GroupName ] ^= 1 << idx; Mod.Settings.Settings[ group.GroupName ] ^= 1 << idx;
Save(); Save();
} }
} }
@ -592,14 +612,14 @@ namespace Penumbra.UI
return; return;
} }
ImGuiCustom.BeginFramedGroup( group.GroupName ); Custom.ImGuiCustom.BeginFramedGroup( group.GroupName );
for( var i = 0; i < group.Options.Count; ++i ) for( var i = 0; i < group.Options.Count; ++i )
{ {
DrawMultiSelectorCheckBox( group, i, Mod.Settings[ group.GroupName ], DrawMultiSelectorCheckBox( group, i, Mod.Settings.Settings[ group.GroupName ],
$"{group.Options[ i ].OptionName}##{group.GroupName}" ); $"{group.Options[ i ].OptionName}##{group.GroupName}" );
} }
ImGuiCustom.EndFramedGroup(); Custom.ImGuiCustom.EndFramedGroup();
} }
private void DrawSingleSelector( OptionGroup group ) private void DrawSingleSelector( OptionGroup group )
@ -609,11 +629,12 @@ namespace Penumbra.UI
return; return;
} }
var code = Mod.Settings[ group.GroupName ]; var code = Mod.Settings.Settings[ group.GroupName ];
if( ImGui.Combo( group.GroupName, ref code if( ImGui.Combo( group.GroupName, ref code
, group.Options.Select( x => x.OptionName ).ToArray(), group.Options.Count ) ) , group.Options.Select( x => x.OptionName ).ToArray(), group.Options.Count )
&& code != Mod.Settings.Settings[ group.GroupName ] )
{ {
Mod.Settings[ group.GroupName ] = code; Mod.Settings.Settings[ group.GroupName ] = code;
Save(); Save();
} }
} }
@ -633,7 +654,7 @@ namespace Penumbra.UI
private void DrawConfigurationTab() private void DrawConfigurationTab()
{ {
if( !_editMode && !Meta.HasGroupWithConfig ) if( !_editMode && !Meta.HasGroupsWithConfig )
{ {
return; return;
} }
@ -653,6 +674,178 @@ namespace Penumbra.UI
} }
} }
private static void DrawManipulationRow( MetaManipulation manip )
{
ImGui.TableNextColumn();
ImGui.Text( manip.Type.ToString() );
ImGui.TableNextColumn();
switch( manip.Type )
{
case MetaType.Eqp:
{
ImGui.Text( manip.EqpIdentifier.Slot.IsAccessory()
? ObjectType.Accessory.ToString()
: ObjectType.Equipment.ToString() );
ImGui.TableNextColumn();
ImGui.Text( manip.EqpIdentifier.SetId.ToString() );
ImGui.TableNextColumn();
ImGui.Text( manip.EqpIdentifier.Slot.ToString() );
break;
}
case MetaType.Gmp:
{
ImGui.Text( ObjectType.Equipment.ToString() );
ImGui.TableNextColumn();
ImGui.Text( manip.GmpIdentifier.SetId.ToString() );
ImGui.TableNextColumn();
ImGui.Text( EquipSlot.Head.ToString() );
break;
}
case MetaType.Eqdp:
{
ImGui.Text( manip.EqpIdentifier.Slot.IsAccessory()
? ObjectType.Accessory.ToString()
: ObjectType.Equipment.ToString() );
ImGui.TableNextColumn();
ImGui.Text( manip.EqdpIdentifier.SetId.ToString() );
ImGui.TableNextColumn();
ImGui.Text( manip.EqpIdentifier.Slot.ToString() );
ImGui.TableNextColumn();
var (gender, race) = manip.EqdpIdentifier.GenderRace.Split();
ImGui.Text( race.ToString() );
ImGui.TableNextColumn();
ImGui.Text( gender.ToString() );
break;
}
case MetaType.Est:
{
ImGui.Text( manip.EstIdentifier.ObjectType.ToString() );
ImGui.TableNextColumn();
ImGui.Text( manip.EstIdentifier.PrimaryId.ToString() );
ImGui.TableNextColumn();
ImGui.Text( manip.EstIdentifier.ObjectType == ObjectType.Equipment
? manip.EstIdentifier.EquipSlot.ToString()
: manip.EstIdentifier.BodySlot.ToString() );
ImGui.TableNextColumn();
var (gender, race) = manip.EstIdentifier.GenderRace.Split();
ImGui.Text( race.ToString() );
ImGui.TableNextColumn();
ImGui.Text( gender.ToString() );
break;
}
case MetaType.Imc:
{
ImGui.Text( manip.ImcIdentifier.ObjectType.ToString() );
ImGui.TableNextColumn();
ImGui.Text( manip.ImcIdentifier.PrimaryId.ToString() );
ImGui.TableNextColumn();
if( manip.ImcIdentifier.ObjectType == ObjectType.Accessory
|| manip.ImcIdentifier.ObjectType == ObjectType.Equipment )
{
ImGui.Text( manip.ImcIdentifier.ObjectType == ObjectType.Equipment
|| manip.ImcIdentifier.ObjectType == ObjectType.Accessory
? manip.ImcIdentifier.EquipSlot.ToString()
: manip.ImcIdentifier.BodySlot.ToString() );
}
ImGui.TableNextColumn();
ImGui.TableNextColumn();
ImGui.TableNextColumn();
if( manip.ImcIdentifier.ObjectType != ObjectType.Equipment
&& manip.ImcIdentifier.ObjectType != ObjectType.Accessory )
{
ImGui.Text( manip.ImcIdentifier.SecondaryId.ToString() );
}
ImGui.TableNextColumn();
ImGui.Text( manip.ImcIdentifier.Variant.ToString() );
break;
}
}
ImGui.TableSetColumnIndex( 9 );
ImGui.Text( manip.Value.ToString() );
ImGui.TableNextRow();
}
private static void DrawMetaManipulationsTable( string label, List< MetaManipulation > list )
{
if( list.Count == 0
|| !ImGui.BeginTable( label, 10,
ImGuiTableFlags.BordersInner | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ) )
{
return;
}
ImGui.TableNextColumn();
ImGui.TableHeader( $"Type##{label}" );
ImGui.TableNextColumn();
ImGui.TableHeader( $"Object Type##{label}" );
ImGui.TableNextColumn();
ImGui.TableHeader( $"Set##{label}" );
ImGui.TableNextColumn();
ImGui.TableHeader( $"Slot##{label}" );
ImGui.TableNextColumn();
ImGui.TableHeader( $"Race##{label}" );
ImGui.TableNextColumn();
ImGui.TableHeader( $"Gender##{label}" );
ImGui.TableNextColumn();
ImGui.TableHeader( $"Secondary ID##{label}" );
ImGui.TableNextColumn();
ImGui.TableHeader( $"Variant##{label}" );
ImGui.TableNextColumn();
ImGui.TableNextColumn();
ImGui.TableHeader( $"Value##{label}" );
ImGui.TableNextRow();
foreach( var manip in list )
{
DrawManipulationRow( manip );
}
ImGui.EndTable();
}
private void DrawMetaManipulationsTab()
{
if( Mod.Data.Resources.MetaManipulations.Count == 0 )
{
return;
}
if( !ImGui.BeginTabItem( "Meta Manipulations" ) )
{
return;
}
if( ImGui.BeginListBox( "##MetaManipulations", AutoFillSize ) )
{
var manips = Mod.Data.Resources.MetaManipulations;
if( manips.DefaultData.Count > 0 )
{
if( ImGui.CollapsingHeader( "Default" ) )
{
DrawMetaManipulationsTable( "##DefaultManips", manips.DefaultData );
}
}
foreach( var group in manips.GroupData )
{
foreach( var option in @group.Value )
{
if( ImGui.CollapsingHeader( $"{@group.Key} - {option.Key}" ) )
{
DrawMetaManipulationsTable( $"##{@group.Key}{option.Key}manips", option.Value );
}
}
}
ImGui.EndListBox();
}
ImGui.EndTabItem();
}
public void Draw( bool editMode ) public void Draw( bool editMode )
{ {
_editMode = editMode; _editMode = editMode;
@ -660,6 +853,7 @@ namespace Penumbra.UI
DrawAboutTab(); DrawAboutTab();
DrawChangedItemsTab(); DrawChangedItemsTab();
DrawConfigurationTab(); DrawConfigurationTab();
if( _editMode ) if( _editMode )
{ {
@ -671,8 +865,8 @@ namespace Penumbra.UI
} }
DrawFileSwapTab(); DrawFileSwapTab();
DrawMetaManipulationsTab();
DrawConflictTab(); DrawConflictTab();
ImGui.EndTabBar(); ImGui.EndTabBar();
} }
} }

View file

@ -3,7 +3,8 @@ using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Interface; using Dalamud.Interface;
using ImGuiNET; using ImGuiNET;
using Penumbra.Models; using Penumbra.Mods;
using Penumbra.Structs;
using Penumbra.Util; using Penumbra.Util;
namespace Penumbra.UI namespace Penumbra.UI
@ -44,7 +45,7 @@ namespace Penumbra.UI
} }
if( ImGui.Combo( LabelGroupSelect, ref _selectedGroupIndex if( ImGui.Combo( LabelGroupSelect, ref _selectedGroupIndex
, Meta.Groups.Values.Select( G => G.GroupName ).ToArray() , Meta.Groups.Values.Select( g => g.GroupName ).ToArray()
, Meta.Groups.Count ) ) , Meta.Groups.Count ) )
{ {
SelectGroup(); SelectGroup();
@ -65,7 +66,7 @@ namespace Penumbra.UI
} }
var group = ( OptionGroup )_selectedGroup!; var group = ( OptionGroup )_selectedGroup!;
if( ImGui.Combo( LabelOptionSelect, ref _selectedOptionIndex, group.Options.Select( O => O.OptionName ).ToArray(), if( ImGui.Combo( LabelOptionSelect, ref _selectedOptionIndex, group.Options.Select( o => o.OptionName ).ToArray(),
group.Options.Count ) ) group.Options.Count ) )
{ {
SelectOption(); SelectOption();
@ -87,7 +88,7 @@ namespace Penumbra.UI
ImGui.SetNextItemWidth( -1 ); ImGui.SetNextItemWidth( -1 );
if( ImGui.BeginListBox( LabelFileListHeader, AutoFillSize - new Vector2( 0, 1.5f * ImGui.GetTextLineHeight() ) ) ) if( ImGui.BeginListBox( LabelFileListHeader, AutoFillSize - new Vector2( 0, 1.5f * ImGui.GetTextLineHeight() ) ) )
{ {
for( var i = 0; i < Mod!.Mod.ModFiles.Count; ++i ) for( var i = 0; i < Mod!.Data.Resources.ModFiles.Count; ++i )
{ {
DrawFileAndGamePaths( i ); DrawFileAndGamePaths( i );
} }
@ -104,31 +105,13 @@ namespace Penumbra.UI
} }
} }
private bool DrawMultiSelectorEditBegin( OptionGroup group ) private void DrawMultiSelectorEditBegin( OptionGroup group )
{ {
var groupName = group.GroupName; var groupName = group.GroupName;
if( ImGuiCustom.BeginFramedGroupEdit( ref groupName ) if( Custom.ImGuiCustom.BeginFramedGroupEdit( ref groupName ) )
&& groupName != group.GroupName
&& !Meta!.Groups.ContainsKey( groupName ) )
{ {
var oldConf = Mod!.Settings[ group.GroupName ]; _modManager.ChangeModGroup( group.GroupName, groupName, Mod.Data );
Meta.Groups.Remove( group.GroupName );
Mod.FixSpecificSetting( group.GroupName );
if( groupName.Length > 0 )
{
Meta.Groups[ groupName ] = new OptionGroup()
{
GroupName = groupName,
SelectionType = SelectType.Multi,
Options = group.Options,
};
Mod.Settings[ groupName ] = oldConf;
} }
return true;
}
return false;
} }
private void DrawMultiSelectorEditAdd( OptionGroup group, float nameBoxStart ) private void DrawMultiSelectorEditAdd( OptionGroup group, float nameBoxStart )
@ -149,9 +132,9 @@ namespace Penumbra.UI
private void DrawMultiSelectorEdit( OptionGroup group ) private void DrawMultiSelectorEdit( OptionGroup group )
{ {
var nameBoxStart = CheckMarkSize; var nameBoxStart = CheckMarkSize;
var flag = Mod!.Settings[ group.GroupName ]; var flag = Mod!.Settings.Settings[ group.GroupName ];
var modChanged = DrawMultiSelectorEditBegin( group );
DrawMultiSelectorEditBegin( group );
for( var i = 0; i < group.Options.Count; ++i ) for( var i = 0; i < group.Options.Count; ++i )
{ {
var opt = group.Options[ i ]; var opt = group.Options[ i ];
@ -171,11 +154,7 @@ namespace Penumbra.UI
{ {
if( newName.Length == 0 ) if( newName.Length == 0 )
{ {
group.Options.RemoveAt( i ); _modManager.RemoveModOption( i, group, Mod.Data );
var bitmaskFront = ( 1 << i ) - 1;
var bitmaskBack = ~( bitmaskFront | ( 1 << i ) );
Mod.Settings[ group.GroupName ] = ( flag & bitmaskFront ) | ( ( flag & bitmaskBack ) >> 1 );
modChanged = true;
} }
else if( newName != opt.OptionName ) else if( newName != opt.OptionName )
{ {
@ -188,134 +167,74 @@ namespace Penumbra.UI
DrawMultiSelectorEditAdd( group, nameBoxStart ); DrawMultiSelectorEditAdd( group, nameBoxStart );
if( modChanged ) Custom.ImGuiCustom.EndFramedGroup();
{
_selector.SaveCurrentMod();
Save();
} }
ImGuiCustom.EndFramedGroup(); private void DrawSingleSelectorEditGroup( OptionGroup group )
}
private bool DrawSingleSelectorEditGroup( OptionGroup group, ref bool selectionChanged )
{ {
var groupName = group.GroupName; var groupName = group.GroupName;
if( ImGui.InputText( $"##{groupName}_add", ref groupName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) if( ImGui.InputText( $"##{groupName}_add", ref groupName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) )
&& !Meta!.Groups.ContainsKey( groupName ) )
{ {
var oldConf = Mod!.Settings[ group.GroupName ]; _modManager.ChangeModGroup( group.GroupName, groupName, Mod.Data );
if( groupName != group.GroupName )
{
Meta.Groups.Remove( group.GroupName );
selectionChanged |= Mod.FixSpecificSetting( group.GroupName );
} }
if( groupName.Length > 0 )
{
Meta.Groups.Add( groupName, new OptionGroup()
{
GroupName = groupName,
Options = group.Options,
SelectionType = SelectType.Single,
} );
Mod.Settings[ groupName ] = oldConf;
}
return true;
}
return false;
} }
private float DrawSingleSelectorEdit( OptionGroup group ) private float DrawSingleSelectorEdit( OptionGroup group )
{ {
var code = Mod!.Settings[ group.GroupName ]; var oldSetting = Mod!.Settings.Settings[ group.GroupName ];
var selectionChanged = false; var code = oldSetting;
var modChanged = false; if( Custom.ImGuiCustom.RenameableCombo( $"##{group.GroupName}", ref code, out var newName,
if( ImGuiCustom.RenameableCombo( $"##{group.GroupName}", ref code, out var newName,
group.Options.Select( x => x.OptionName ).ToArray(), group.Options.Count ) ) group.Options.Select( x => x.OptionName ).ToArray(), group.Options.Count ) )
{ {
if( code == group.Options.Count ) if( code == group.Options.Count )
{ {
if( newName.Length > 0 ) if( newName.Length > 0 )
{ {
selectionChanged = true; Mod.Settings.Settings[ group.GroupName ] = code;
modChanged = true;
Mod.Settings[ group.GroupName ] = code;
group.Options.Add( new Option() group.Options.Add( new Option()
{ {
OptionName = newName, OptionName = newName,
OptionDesc = "", OptionDesc = "",
OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(), OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(),
} ); } );
_selector.SaveCurrentMod();
} }
} }
else else
{ {
if( newName.Length == 0 ) if( newName.Length == 0 )
{ {
modChanged = true; _modManager.RemoveModOption( code, group, Mod.Data );
group.Options.RemoveAt( code );
} }
else else
{ {
if( newName != group.Options[ code ].OptionName ) if( newName != group.Options[ code ].OptionName )
{ {
modChanged = true;
group.Options[ code ] = new Option() group.Options[ code ] = new Option()
{ {
OptionName = newName, OptionDesc = group.Options[ code ].OptionDesc, OptionName = newName, OptionDesc = group.Options[ code ].OptionDesc,
OptionFiles = group.Options[ code ].OptionFiles, OptionFiles = group.Options[ code ].OptionFiles,
}; };
_selector.SaveCurrentMod();
}
}
}
} }
selectionChanged |= Mod.Settings[ group.GroupName ] != code; if( code != oldSetting )
Mod.Settings[ group.GroupName ] = code; {
} Save();
selectionChanged |= Mod.FixSpecificSetting( group.GroupName );
}
} }
ImGui.SameLine(); ImGui.SameLine();
var labelEditPos = ImGui.GetCursorPosX(); var labelEditPos = ImGui.GetCursorPosX();
modChanged |= DrawSingleSelectorEditGroup( group, ref selectionChanged ); DrawSingleSelectorEditGroup( group );
if( modChanged )
{
_selector.SaveCurrentMod();
}
if( selectionChanged )
{
Save();
}
return labelEditPos; return labelEditPos;
} }
private void AddNewGroup( string newGroup, SelectType selectType )
{
if( Meta!.Groups.ContainsKey( newGroup ) || newGroup.Length <= 0 )
{
return;
}
Meta.Groups[ newGroup ] = new OptionGroup()
{
GroupName = newGroup,
SelectionType = selectType,
Options = new List< Option >(),
};
Mod.Settings[ newGroup ] = 0;
_selector.SaveCurrentMod();
Save();
}
private void DrawAddSingleGroupField( float labelEditPos ) private void DrawAddSingleGroupField( float labelEditPos )
{ {
const string hint = "Add new Single Group...";
var newGroup = ""; var newGroup = "";
ImGui.SetCursorPosX( labelEditPos ); ImGui.SetCursorPosX( labelEditPos );
if( labelEditPos == CheckMarkSize ) if( labelEditPos == CheckMarkSize )
@ -323,9 +242,10 @@ namespace Penumbra.UI
ImGui.SetNextItemWidth( MultiEditBoxWidth ); ImGui.SetNextItemWidth( MultiEditBoxWidth );
} }
if( ImGui.InputTextWithHint( LabelNewSingleGroupEdit, hint, ref newGroup, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) if( ImGui.InputTextWithHint( LabelNewSingleGroupEdit, "Add new Single Group...", ref newGroup, 64,
ImGuiInputTextFlags.EnterReturnsTrue ) )
{ {
AddNewGroup( newGroup, SelectType.Single ); _modManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Single );
} }
} }
@ -337,21 +257,22 @@ namespace Penumbra.UI
if( ImGui.InputTextWithHint( LabelNewMultiGroup, "Add new Multi Group...", ref newGroup, 64, if( ImGui.InputTextWithHint( LabelNewMultiGroup, "Add new Multi Group...", ref newGroup, 64,
ImGuiInputTextFlags.EnterReturnsTrue ) ) ImGuiInputTextFlags.EnterReturnsTrue ) )
{ {
AddNewGroup( newGroup, SelectType.Multi ); _modManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Multi );
} }
} }
private void DrawGroupSelectorsEdit() private void DrawGroupSelectorsEdit()
{ {
var labelEditPos = CheckMarkSize; var labelEditPos = CheckMarkSize;
foreach( var g in Meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single ) ) var groups = Meta.Groups.Values.ToArray();
foreach( var g in groups.Where( g => g.SelectionType == SelectType.Single ) )
{ {
labelEditPos = DrawSingleSelectorEdit( g ); labelEditPos = DrawSingleSelectorEdit( g );
} }
DrawAddSingleGroupField( labelEditPos ); DrawAddSingleGroupField( labelEditPos );
foreach( var g in Meta.Groups.Values.Where( g => g.SelectionType == SelectType.Multi ) ) foreach( var g in groups.Where( g => g.SelectionType == SelectType.Multi ) )
{ {
DrawMultiSelectorEdit( g ); DrawMultiSelectorEdit( g );
} }
@ -403,7 +324,7 @@ namespace Penumbra.UI
} }
_selector.SaveCurrentMod(); _selector.SaveCurrentMod();
if( Mod.Enabled ) if( Mod.Settings.Enabled )
{ {
_selector.ReloadCurrentMod(); _selector.ReloadCurrentMod();
} }
@ -428,7 +349,7 @@ namespace Penumbra.UI
{ {
Meta.FileSwaps[ key ] = newValue; Meta.FileSwaps[ key ] = newValue;
_selector.SaveCurrentMod(); _selector.SaveCurrentMod();
if( Mod.Enabled ) if( Mod.Settings.Enabled )
{ {
_selector.ReloadCurrentMod(); _selector.ReloadCurrentMod();
} }

View file

@ -4,8 +4,9 @@ using System.IO;
using System.Numerics; using System.Numerics;
using Dalamud.Plugin; using Dalamud.Plugin;
using ImGuiNET; using ImGuiNET;
using Penumbra.Models; using Penumbra.Mod;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra.UI namespace Penumbra.UI
{ {
@ -20,6 +21,7 @@ namespace Penumbra.UI
private const string LabelEditWebsite = "##editWebsite"; private const string LabelEditWebsite = "##editWebsite";
private const string LabelModEnabled = "Enabled"; private const string LabelModEnabled = "Enabled";
private const string LabelEditingEnabled = "Enable Editing"; private const string LabelEditingEnabled = "Enable Editing";
private const string LabelOverWriteDir = "OverwriteDir";
private const string ButtonOpenWebsite = "Open Website"; private const string ButtonOpenWebsite = "Open Website";
private const string ButtonOpenModFolder = "Open Mod Folder"; private const string ButtonOpenModFolder = "Open Mod Folder";
private const string ButtonRenameModFolder = "Rename Mod Folder"; private const string ButtonRenameModFolder = "Rename Mod Folder";
@ -45,6 +47,7 @@ namespace Penumbra.UI
private readonly SettingsInterface _base; private readonly SettingsInterface _base;
private readonly Selector _selector; private readonly Selector _selector;
private readonly ModManager _modManager;
public readonly PluginDetails Details; public readonly PluginDetails Details;
private bool _editMode; private bool _editMode;
@ -57,23 +60,22 @@ namespace Penumbra.UI
_selector = s; _selector = s;
Details = new PluginDetails( _base, _selector ); Details = new PluginDetails( _base, _selector );
_currentWebsite = Meta?.Website ?? ""; _currentWebsite = Meta?.Website ?? "";
_modManager = Service< ModManager >.Get();
} }
private ModInfo? Mod private Mod.Mod? Mod
=> _selector.Mod; => _selector.Mod;
private ModMeta? Meta private ModMeta? Meta
=> Mod?.Mod.Meta; => Mod?.Data.Meta;
private void DrawName() private void DrawName()
{ {
var name = Meta!.Name; var name = Meta!.Name;
if( ImGuiCustom.InputOrText( _editMode, LabelEditName, ref name, 64 ) if( Custom.ImGuiCustom.InputOrText( _editMode, LabelEditName, ref name, 64 ) && _modManager.RenameMod( name, Mod!.Data ) )
&& name.Length > 0
&& name != Meta.Name )
{ {
Meta.Name = name; _selector.RenameCurrentModLower( name );
_selector.SaveCurrentMod(); _selector.SelectModByDir( Mod.Data.BasePath.Name );
} }
} }
@ -87,7 +89,7 @@ namespace Penumbra.UI
ImGui.PushStyleVar( ImGuiStyleVar.ItemSpacing, ZeroVector ); ImGui.PushStyleVar( ImGuiStyleVar.ItemSpacing, ZeroVector );
ImGui.SameLine(); ImGui.SameLine();
var version = Meta!.Version; var version = Meta!.Version;
if( ImGuiCustom.ResizingTextInput( LabelEditVersion, ref version, 16 ) if( Custom.ImGuiCustom.ResizingTextInput( LabelEditVersion, ref version, 16 )
&& version != Meta.Version ) && version != Meta.Version )
{ {
Meta.Version = version; Meta.Version = version;
@ -112,7 +114,7 @@ namespace Penumbra.UI
ImGui.SameLine(); ImGui.SameLine();
var author = Meta!.Author; var author = Meta!.Author;
if( ImGuiCustom.InputOrText( _editMode, LabelEditAuthor, ref author, 64 ) if( Custom.ImGuiCustom.InputOrText( _editMode, LabelEditAuthor, ref author, 64 )
&& author != Meta.Author ) && author != Meta.Author )
{ {
Meta.Author = author; Meta.Author = author;
@ -130,7 +132,7 @@ namespace Penumbra.UI
ImGui.TextColored( GreyColor, "from" ); ImGui.TextColored( GreyColor, "from" );
ImGui.SameLine(); ImGui.SameLine();
var website = Meta!.Website; var website = Meta!.Website;
if( ImGuiCustom.ResizingTextInput( LabelEditWebsite, ref website, 512 ) if( Custom.ImGuiCustom.ResizingTextInput( LabelEditWebsite, ref website, 512 )
&& website != Meta.Website ) && website != Meta.Website )
{ {
Meta.Website = website; Meta.Website = website;
@ -191,15 +193,34 @@ namespace Penumbra.UI
DrawWebsite(); DrawWebsite();
} }
private void DrawPriority()
{
var priority = Mod!.Settings.Priority;
ImGui.SetNextItemWidth( 50 );
if( ImGui.InputInt( "Priority", ref priority, 0 ) && priority != Mod!.Settings.Priority )
{
Mod.Settings.Priority = priority;
var collection = _modManager.CurrentCollection;
collection.Save( _base._plugin.PluginInterface! );
collection.CalculateEffectiveFileList( _modManager.BasePath, Mod.Data.Resources.MetaManipulations.Count > 0 );
}
if( ImGui.IsItemHovered() )
{
ImGui.SetTooltip( "Higher priority mods take precedence over other mods in the case of file conflicts.\n"
+ "In case of identical priority, the alphabetically first mod takes precedence." );
}
}
private void DrawEnabledMark() private void DrawEnabledMark()
{ {
var enabled = Mod!.Enabled; var enabled = Mod!.Settings.Enabled;
if( ImGui.Checkbox( LabelModEnabled, ref enabled ) ) if( ImGui.Checkbox( LabelModEnabled, ref enabled ) )
{ {
Mod.Enabled = enabled; Mod.Settings.Enabled = enabled;
var modManager = Service< ModManager >.Get(); var collection = _modManager.CurrentCollection;
modManager.Mods!.Save(); collection.Save( _base._plugin.PluginInterface! );
modManager.CalculateEffectiveFileList(); collection.CalculateEffectiveFileList( _modManager.BasePath, Mod.Data.Resources.MetaManipulations.Count > 0 );
} }
} }
@ -212,7 +233,7 @@ namespace Penumbra.UI
{ {
if( ImGui.Button( ButtonOpenModFolder ) ) if( ImGui.Button( ButtonOpenModFolder ) )
{ {
Process.Start( Mod!.Mod.ModBasePath.FullName ); Process.Start( Mod!.Data.BasePath.FullName );
} }
if( ImGui.IsItemHovered() ) if( ImGui.IsItemHovered() )
@ -224,7 +245,98 @@ namespace Penumbra.UI
private string _newName = ""; private string _newName = "";
private bool _keyboardFocus = true; private bool _keyboardFocus = true;
private void DrawRenameModFolderButton() private void RenameModFolder( string newName )
{
_newName = newName.RemoveNonAsciiSymbols().RemoveInvalidPathSymbols();
if( _newName.Length == 0 )
{
PluginLog.Debug( "New Directory name {NewName} was empty after removing invalid symbols.", newName );
ImGui.CloseCurrentPopup();
}
else if( !string.Equals( _newName, Mod!.Data.BasePath.Name, StringComparison.InvariantCultureIgnoreCase ) )
{
DirectoryInfo dir = Mod!.Data.BasePath;
DirectoryInfo newDir = new( Path.Combine( dir.Parent!.FullName, _newName ) );
if( newDir.Exists )
{
ImGui.OpenPopup( LabelOverWriteDir );
}
else if( Service< ModManager >.Get()!.RenameModFolder( Mod.Data, newDir ) )
{
_selector.ReloadCurrentMod();
ImGui.CloseCurrentPopup();
}
}
}
private static bool MergeFolderInto( DirectoryInfo source, DirectoryInfo target )
{
try
{
foreach( var file in source.EnumerateFiles( "*", SearchOption.AllDirectories ) )
{
var targetFile = new FileInfo( Path.Combine( target.FullName, file.FullName.Substring( source.FullName.Length + 1 ) ) );
if( targetFile.Exists )
{
targetFile.Delete();
}
targetFile.Directory?.Create();
file.MoveTo( targetFile.FullName );
}
source.Delete( true );
return true;
}
catch( Exception e )
{
PluginLog.Error( $"Could not merge directory {source.FullName} into {target.FullName}:\n{e}" );
}
return false;
}
private bool OverwriteDirPopup()
{
var closeParent = false;
var _ = true;
if( ImGui.BeginPopupModal( LabelOverWriteDir, ref _, ImGuiWindowFlags.AlwaysAutoResize ) )
{
DirectoryInfo dir = Mod!.Data.BasePath;
DirectoryInfo newDir = new( Path.Combine( dir.Parent!.FullName, _newName ) );
ImGui.Text(
$"The mod directory {newDir} already exists.\nDo you want to merge / overwrite both mods?\nThis may corrupt the resulting mod in irrecoverable ways." );
var buttonSize = new Vector2( 120, 0 );
if( ImGui.Button( "Yes", buttonSize ) )
{
if( MergeFolderInto( dir, newDir ) )
{
Service< ModManager >.Get()!.RenameModFolder( Mod.Data, newDir, false );
_selector.ResetModNamesLower();
_selector.SelectModByDir( _newName );
closeParent = true;
ImGui.CloseCurrentPopup();
}
}
ImGui.SameLine();
if( ImGui.Button( "Cancel", buttonSize ) )
{
_keyboardFocus = true;
ImGui.CloseCurrentPopup();
}
ImGui.EndPopup();
}
return closeParent;
}
private void DrawRenameModFolderPopup()
{ {
var _ = true; var _ = true;
_keyboardFocus |= !ImGui.IsPopupOpen( PopupRenameFolder ); _keyboardFocus |= !ImGui.IsPopupOpen( PopupRenameFolder );
@ -237,121 +349,38 @@ namespace Penumbra.UI
ImGui.CloseCurrentPopup(); ImGui.CloseCurrentPopup();
} }
var newName = Mod!.FolderName; var newName = Mod!.Data.BasePath.Name;
if( _keyboardFocus ) if( _keyboardFocus )
{ {
PluginLog.Log( "Fuck you" );
ImGui.SetKeyboardFocusHere(); ImGui.SetKeyboardFocusHere();
_keyboardFocus = false; _keyboardFocus = false;
} }
if( ImGui.InputText( "New Folder Name##RenameFolderInput", ref newName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) if( ImGui.InputText( "New Folder Name##RenameFolderInput", ref newName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) )
{ {
_newName = newName.RemoveNonAsciiSymbols().RemoveInvalidPathSymbols(); RenameModFolder( newName );
if( _newName.Length == 0 )
{
ImGui.CloseCurrentPopup();
}
else if( !string.Equals( _newName, Mod!.FolderName, StringComparison.InvariantCultureIgnoreCase ) )
{
DirectoryInfo dir = Mod!.Mod.ModBasePath;
DirectoryInfo newDir = new( Path.Combine( dir.Parent!.FullName, _newName ) );
if( newDir.Exists )
{
PluginLog.Error( "GOTT" );
ImGui.OpenPopup( "OverwriteDir" );
}
else
{
try
{
dir.MoveTo( newDir.FullName );
}
catch( Exception e )
{
PluginLog.Error( $"Error while renaming directory {dir.FullName} to {newDir.FullName}:\n{e}" );
}
Mod!.FolderName = _newName;
Mod!.Mod.ModBasePath = newDir;
_selector.ReloadCurrentMod();
Service< ModManager >.Get()!.Mods!.Save();
ImGui.CloseCurrentPopup();
}
}
} }
ImGui.TextColored( GreyColor, ImGui.TextColored( GreyColor,
"Please restrict yourself to ascii symbols that are valid in a windows path,\nother symbols will be replaced by underscores." ); "Please restrict yourself to ascii symbols that are valid in a windows path,\nother symbols will be replaced by underscores." );
var closeParent = false;
_ = true;
ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, Vector2.One / 2 ); ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, Vector2.One / 2 );
if( ImGui.BeginPopupModal( "OverwriteDir", ref _, ImGuiWindowFlags.AlwaysAutoResize ) )
{
DirectoryInfo dir = Mod!.Mod.ModBasePath;
DirectoryInfo newDir = new( Path.Combine( dir.Parent!.FullName, _newName ) );
ImGui.Text(
$"The mod directory {newDir} already exists.\nDo you want to merge / overwrite both mods?\nThis may corrupt the resulting mod in irrecoverable ways." );
var buttonSize = new Vector2( 120, 0 );
if( ImGui.Button( "Yes", buttonSize ) )
{
try
{
foreach( var file in dir.EnumerateFiles( "*", SearchOption.AllDirectories ) )
{
var target = new FileInfo( Path.Combine( newDir.FullName,
file.FullName.Substring( dir.FullName.Length ) ) );
if( target.Exists )
{
target.Delete();
}
target.Directory?.Create();
file.MoveTo( target.FullName );
}
dir.Delete( true ); if( OverwriteDirPopup() )
var mod = Service< ModManager >.Get()!.Mods!.ModSettings!
.RemoveAll( m => m.FolderName == _newName );
Mod!.FolderName = _newName;
Mod!.Mod.ModBasePath = newDir;
Service< ModManager >.Get()!.Mods!.Save();
_base.ReloadMods();
_selector.SelectModByDir( _newName );
}
catch( Exception e )
{
PluginLog.Error( $"Error while renaming directory {dir.FullName} to {newDir.FullName}:\n{e}" );
}
closeParent = true;
ImGui.CloseCurrentPopup();
}
ImGui.SameLine();
if( ImGui.Button( "Cancel", buttonSize ) )
{
PluginLog.Error( "FUCKFUCK" );
_keyboardFocus = true;
ImGui.CloseCurrentPopup();
}
ImGui.EndPopup();
}
if( closeParent )
{ {
ImGui.CloseCurrentPopup(); ImGui.CloseCurrentPopup();
} }
ImGui.EndPopup(); ImGui.EndPopup();
} }
}
private void DrawRenameModFolderButton()
{
DrawRenameModFolderPopup();
if( ImGui.Button( ButtonRenameModFolder ) ) if( ImGui.Button( ButtonRenameModFolder ) )
{ {
ImGui.OpenPopup( PopupRenameFolder ); ImGui.OpenPopup( PopupRenameFolder );
@ -367,7 +396,8 @@ namespace Penumbra.UI
{ {
if( ImGui.Button( ButtonEditJson ) ) if( ImGui.Button( ButtonEditJson ) )
{ {
Process.Start( _selector.SaveCurrentMod() ); _selector.SaveCurrentMod();
Process.Start( Mod!.Data.MetaFile.FullName );
} }
if( ImGui.IsItemHovered() ) if( ImGui.IsItemHovered() )
@ -389,14 +419,27 @@ namespace Penumbra.UI
} }
} }
private void DrawResetMetaButton()
{
if( ImGui.Button( "Recompute Metadata" ) )
{
_selector.ReloadCurrentMod( true );
}
if( ImGui.IsItemHovered() )
{
ImGui.SetTooltip(
"Force a recomputation of the metadata_manipulations.json file from all .meta files in the folder.\nAlso reloads the mod." );
}
}
private void DrawDeduplicateButton() private void DrawDeduplicateButton()
{ {
if( ImGui.Button( ButtonDeduplicate ) ) if( ImGui.Button( ButtonDeduplicate ) )
{ {
ModCleanup.Deduplicate( Mod!.Mod.ModBasePath, Meta! ); ModCleanup.Deduplicate( Mod!.Data.BasePath, Meta! );
_selector.SaveCurrentMod(); _selector.SaveCurrentMod();
Mod.Mod.RefreshModFiles(); _selector.ReloadCurrentMod();
Service< ModManager >.Get().CalculateEffectiveFileList();
} }
if( ImGui.IsItemHovered() ) if( ImGui.IsItemHovered() )
@ -409,10 +452,9 @@ namespace Penumbra.UI
{ {
if( ImGui.Button( ButtonNormalize ) ) if( ImGui.Button( ButtonNormalize ) )
{ {
ModCleanup.Normalize( Mod!.Mod.ModBasePath, Meta! ); ModCleanup.Normalize( Mod!.Data.BasePath, Meta! );
_selector.SaveCurrentMod(); _selector.SaveCurrentMod();
Mod.Mod.RefreshModFiles(); _selector.ReloadCurrentMod();
Service< ModManager >.Get().CalculateEffectiveFileList();
} }
if( ImGui.IsItemHovered() ) if( ImGui.IsItemHovered() )
@ -430,6 +472,8 @@ namespace Penumbra.UI
DrawEditJsonButton(); DrawEditJsonButton();
ImGui.SameLine(); ImGui.SameLine();
DrawReloadJsonButton(); DrawReloadJsonButton();
DrawResetMetaButton();
ImGui.SameLine(); ImGui.SameLine();
DrawDeduplicateButton(); DrawDeduplicateButton();
ImGui.SameLine(); ImGui.SameLine();
@ -454,9 +498,11 @@ namespace Penumbra.UI
DrawHeaderLine(); DrawHeaderLine();
// Next line with fixed distance. // Next line with fixed distance.
ImGuiCustom.VerticalDistance( HeaderLineDistance ); Custom.ImGuiCustom.VerticalDistance( HeaderLineDistance );
DrawEnabledMark(); DrawEnabledMark();
ImGui.SameLine();
DrawPriority();
if( _base._plugin!.Configuration!.ShowAdvanced ) if( _base._plugin!.Configuration!.ShowAdvanced )
{ {
ImGui.SameLine(); ImGui.SameLine();

View file

@ -1,14 +1,15 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
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.Mod;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra.UI namespace Penumbra.UI
{ {
@ -16,80 +17,92 @@ namespace Penumbra.UI
{ {
private class Selector private class Selector
{ {
[Flags]
private enum ModFilter
{
Enabled = 1 << 0,
Disabled = 1 << 1,
NoConflict = 1 << 2,
SolvedConflict = 1 << 3,
UnsolvedConflict = 1 << 4,
HasNoMetaManipulations = 1 << 5,
HasMetaManipulations = 1 << 6,
HasNoFileSwaps = 1 << 7,
HasFileSwaps = 1 << 8,
HasConfig = 1 << 9,
HasNoConfig = 1 << 10,
HasNoFiles = 1 << 11,
HasFiles = 1 << 12,
};
private const ModFilter UnfilteredStateMods = ( ModFilter )( ( 1 << 13 ) - 1 );
private static readonly Dictionary< ModFilter, string > ModFilterNames = new()
{
{ ModFilter.Enabled, "Enabled" },
{ ModFilter.Disabled, "Disabled" },
{ ModFilter.NoConflict, "No Conflicts" },
{ ModFilter.SolvedConflict, "Solved Conflicts" },
{ ModFilter.UnsolvedConflict, "Unsolved Conflicts" },
{ ModFilter.HasNoMetaManipulations, "No Meta Manipulations" },
{ ModFilter.HasMetaManipulations, "Meta Manipulations" },
{ ModFilter.HasNoFileSwaps, "No File Swaps" },
{ ModFilter.HasFileSwaps, "File Swaps" },
{ ModFilter.HasNoConfig, "No Configuration" },
{ ModFilter.HasConfig, "Configuration" },
{ ModFilter.HasNoFiles, "No Files" },
{ ModFilter.HasFiles, "Files" },
};
private const string LabelSelectorList = "##availableModList"; private const string LabelSelectorList = "##availableModList";
private const string LabelModFilter = "##ModFilter"; private const string LabelModFilter = "##ModFilter";
private const string LabelPriorityPopup = "Priority";
private const string LabelAddModPopup = "AddMod"; private const string LabelAddModPopup = "AddMod";
private const string TooltipModFilter = "Filter mods for those containing the given substring."; private const string TooltipModFilter = "Filter mods for those containing the given substring.";
private const string TooltipMoveDown = "Move the selected mod down in priority";
private const string TooltipMoveUp = "Move the selected mod up in priority";
private const string TooltipDelete = "Delete the selected mod"; private const string TooltipDelete = "Delete the selected mod";
private const string TooltipAdd = "Add an empty mod"; private const string TooltipAdd = "Add an empty mod";
private const string DialogDeleteMod = "PenumbraDeleteMod"; private const string DialogDeleteMod = "PenumbraDeleteMod";
private const string ButtonYesDelete = "Yes, delete it"; private const string ButtonYesDelete = "Yes, delete it";
private const string ButtonNoDelete = "No, keep it"; private const string ButtonNoDelete = "No, keep it";
private const string DescPriorityPopup = "New Priority:";
private const float SelectorPanelWidth = 240f; private const float SelectorPanelWidth = 240f;
private const uint DisabledModColor = 0xFF666666; private const uint DisabledModColor = 0xFF666666;
private const uint ConflictingModColor = 0xFFAAAAFF; private const uint ConflictingModColor = 0xFFAAAAFF;
private const uint HandledConflictModColor = 0xFF88DDDD;
private static readonly Vector2 SelectorButtonSizes = new( 60, 0 ); private static readonly Vector2 SelectorButtonSizes = new( 120, 0 );
private static readonly string ArrowUpString = FontAwesomeIcon.ArrowUp.ToIconString();
private static readonly string ArrowDownString = FontAwesomeIcon.ArrowDown.ToIconString();
private readonly SettingsInterface _base; private readonly SettingsInterface _base;
private readonly ModManager _modManager;
private static ModCollection? Mods private List< Mod.Mod >? Mods
=> Service< ModManager >.Get().Mods; => _modManager.CurrentCollection.Cache?.AvailableMods;
public ModInfo? Mod { get; private set; } public Mod.Mod? Mod { get; private set; }
private int _index; private int _index;
private int? _deleteIndex; private int? _deleteIndex;
private string _modFilter = ""; private string _modFilter = "";
private string[]? _modNamesLower; private string[] _modNamesLower;
private ModFilter _stateFilter = UnfilteredStateMods;
public Selector( SettingsInterface ui ) public Selector( SettingsInterface ui )
{ {
_base = ui; _base = ui;
_modNamesLower = Array.Empty< string >();
_modManager = Service<ModManager>.Get();
ResetModNamesLower(); ResetModNamesLower();
} }
public void ResetModNamesLower() public void ResetModNamesLower()
{ {
_modNamesLower = Mods?.ModSettings?.Where( I => I.Mod != null ) _modNamesLower = Mods?.Select( m => m.Data.Meta.Name.ToLowerInvariant() ).ToArray()
.Select( I => I.Mod!.Meta.Name.ToLowerInvariant() ).ToArray() ?? Array.Empty< string >();
?? new string[] { };
} }
private void DrawPriorityChangeButton( string iconString, bool up, int unavailableWhen ) public void RenameCurrentModLower( string newName )
{ {
ImGui.PushFont( UiBuilder.IconFont ); if( _index >= 0 )
if( _index != unavailableWhen )
{ {
if( ImGui.Button( iconString, SelectorButtonSizes ) ) _modNamesLower[ _index ] = newName.ToLowerInvariant();
{
SetSelection( _index );
Service< ModManager >.Get().ChangeModPriority( Mod!, up );
_modNamesLower!.Swap( _index, _index + ( up ? 1 : -1 ) );
_index += up ? 1 : -1;
}
}
else
{
ImGui.PushStyleVar( ImGuiStyleVar.Alpha, 0.5f );
ImGui.Button( iconString, SelectorButtonSizes );
ImGui.PopStyleVar();
}
ImGui.PopFont();
if( ImGui.IsItemHovered() )
{
ImGui.SetTooltip(
_base._plugin!.Configuration!.InvertModListOrder ^ up ? TooltipMoveDown : TooltipMoveUp
);
} }
} }
@ -127,7 +140,7 @@ namespace Penumbra.UI
{ {
try try
{ {
var newDir = TexToolsImport.CreateModFolder( new DirectoryInfo( _base._plugin.Configuration!.CurrentCollection ), var newDir = TexToolsImport.CreateModFolder( new DirectoryInfo( _base._plugin.Configuration!.ModDirectory ),
newName ); newName );
var modMeta = new ModMeta var modMeta = new ModMeta
{ {
@ -135,9 +148,10 @@ namespace Penumbra.UI
Name = newName, Name = newName,
Description = string.Empty, Description = string.Empty,
}; };
var metaPath = Path.Combine( newDir.FullName, "meta.json" );
File.WriteAllText( metaPath, JsonConvert.SerializeObject( modMeta, Formatting.Indented ) ); var metaFile = new FileInfo( Path.Combine( newDir.FullName, "meta.json" ) );
_base.ReloadMods(); modMeta.SaveToFile( metaFile );
_modManager.AddMod( newDir );
SelectModByDir( newDir.Name ); SelectModByDir( newDir.Name );
} }
catch( Exception e ) catch( Exception e )
@ -174,7 +188,7 @@ namespace Penumbra.UI
private void DrawModsSelectorFilter() private void DrawModsSelectorFilter()
{ {
ImGui.SetNextItemWidth( SelectorButtonSizes.X * 4 ); ImGui.SetNextItemWidth( SelectorButtonSizes.X * 2 - 22 );
var tmp = _modFilter; var tmp = _modFilter;
if( ImGui.InputTextWithHint( LabelModFilter, "Filter Mods...", ref tmp, 256 ) ) if( ImGui.InputTextWithHint( LabelModFilter, "Filter Mods...", ref tmp, 256 ) )
{ {
@ -185,6 +199,26 @@ namespace Penumbra.UI
{ {
ImGui.SetTooltip( TooltipModFilter ); ImGui.SetTooltip( TooltipModFilter );
} }
ImGui.SameLine();
if( ImGui.BeginCombo( "##ModStateFilter", "",
ImGuiComboFlags.NoPreview | ImGuiComboFlags.PopupAlignLeft | ImGuiComboFlags.HeightLargest ) )
{
var flags = ( int )_stateFilter;
foreach( ModFilter flag in Enum.GetValues( typeof( ModFilter ) ) )
{
ImGui.CheckboxFlags( ModFilterNames[ flag ], ref flags, ( int )flag );
}
_stateFilter = ( ModFilter )flags;
ImGui.EndCombo();
}
if( ImGui.IsItemHovered() )
{
ImGui.SetTooltip( "Filter mods for their activation status." );
}
} }
private void DrawModsSelectorButtons() private void DrawModsSelectorButtons()
@ -193,10 +227,6 @@ namespace Penumbra.UI
ImGui.PushStyleVar( ImGuiStyleVar.WindowPadding, ZeroVector ); ImGui.PushStyleVar( ImGuiStyleVar.WindowPadding, ZeroVector );
ImGui.PushStyleVar( ImGuiStyleVar.FrameRounding, 0 ); ImGui.PushStyleVar( ImGuiStyleVar.FrameRounding, 0 );
DrawPriorityChangeButton( ArrowUpString, false, 0 );
ImGui.SameLine();
DrawPriorityChangeButton( ArrowDownString, true, Mods?.ModSettings?.Count - 1 ?? 0 );
ImGui.SameLine();
DrawModTrashButton(); DrawModTrashButton();
ImGui.SameLine(); ImGui.SameLine();
DrawModAddButton(); DrawModAddButton();
@ -221,7 +251,7 @@ namespace Penumbra.UI
return; return;
} }
if( Mod?.Mod == null ) if( Mod == null )
{ {
ImGui.CloseCurrentPopup(); ImGui.CloseCurrentPopup();
ImGui.EndPopup(); ImGui.EndPopup();
@ -231,16 +261,15 @@ namespace Penumbra.UI
ImGui.Text( "Are you sure you want to delete the following mod:" ); ImGui.Text( "Are you sure you want to delete the following mod:" );
// todo: why the fuck does this become null?????? // todo: why the fuck does this become null??????
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
ImGui.TextColored( new Vector4( 0.7f, 0.1f, 0.1f, 1 ), Mod?.Mod?.Meta?.Name ?? "Unknown" ); ImGui.TextColored( new Vector4( 0.7f, 0.1f, 0.1f, 1 ), Mod.Data.Meta.Name ?? "Unknown" );
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() ) / 2 ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() ) / 2 );
var buttonSize = new Vector2( 120, 0 ); var buttonSize = new Vector2( 120, 0 );
if( ImGui.Button( ButtonYesDelete, buttonSize ) ) if( ImGui.Button( ButtonYesDelete, buttonSize ) )
{ {
ImGui.CloseCurrentPopup(); ImGui.CloseCurrentPopup();
Service< ModManager >.Get().DeleteMod( Mod?.Mod ); _modManager.DeleteMod( Mod.Data.BasePath );
ClearSelection(); ClearSelection();
_base.ReloadMods();
} }
ImGui.SameLine(); ImGui.SameLine();
@ -254,7 +283,93 @@ namespace Penumbra.UI
ImGui.EndPopup(); ImGui.EndPopup();
} }
private int _priorityPopupIdx = 0; private bool CheckFlags( int count, ModFilter hasNoFlag, ModFilter hasFlag )
{
if( count == 0 )
{
if( _stateFilter.HasFlag( hasNoFlag ) )
{
return false;
}
}
else if( _stateFilter.HasFlag( hasFlag ) )
{
return false;
}
return true;
}
public void DrawMod( Mod.Mod mod, int modIndex )
{
if( _modFilter.Length > 0 && !_modNamesLower[ modIndex ].Contains( _modFilter )
|| CheckFlags( mod.Data.Resources.ModFiles.Count, ModFilter.HasNoFiles, ModFilter.HasFiles )
|| CheckFlags( mod.Data.Meta.FileSwaps.Count, ModFilter.HasNoFileSwaps, ModFilter.HasFileSwaps )
|| CheckFlags( mod.Data.Resources.MetaManipulations.Count, ModFilter.HasNoMetaManipulations, ModFilter.HasMetaManipulations )
|| CheckFlags( mod.Data.Meta.HasGroupsWithConfig ? 1 : 0, ModFilter.HasNoConfig, ModFilter.HasConfig ) )
{
return;
}
var changedColour = false;
if( !mod.Settings.Enabled )
{
if( !_stateFilter.HasFlag( ModFilter.Disabled ) || !_stateFilter.HasFlag( ModFilter.NoConflict ) )
{
return;
}
ImGui.PushStyleColor( ImGuiCol.Text, DisabledModColor );
changedColour = true;
}
else
{
if( !_stateFilter.HasFlag( ModFilter.Enabled ) )
{
return;
}
if( mod.Cache.Conflicts.Any() )
{
if( mod.Cache.Conflicts.Keys.Any( m => m.Settings.Priority == mod.Settings.Priority ) )
{
if( !_stateFilter.HasFlag( ModFilter.UnsolvedConflict ) )
{
return;
}
ImGui.PushStyleColor( ImGuiCol.Text, ConflictingModColor );
}
else
{
if( !_stateFilter.HasFlag( ModFilter.SolvedConflict ) )
{
return;
}
ImGui.PushStyleColor( ImGuiCol.Text, HandledConflictModColor );
}
changedColour = true;
}
else if( !_stateFilter.HasFlag( ModFilter.NoConflict ) )
{
return;
}
}
var selected = ImGui.Selectable( $"{mod.Data.Meta.Name}##{modIndex}", modIndex == _index );
if( changedColour )
{
ImGui.PopStyleColor();
}
if( selected )
{
SetSelection( modIndex, mod );
}
}
public void Draw() public void Draw()
{ {
@ -271,61 +386,9 @@ namespace Penumbra.UI
// Inlay selector list // Inlay selector list
ImGui.BeginChild( LabelSelectorList, new Vector2( SelectorPanelWidth, -ImGui.GetFrameHeightWithSpacing() ), true ); ImGui.BeginChild( LabelSelectorList, new Vector2( SelectorPanelWidth, -ImGui.GetFrameHeightWithSpacing() ), true );
if( Mods.ModSettings != null ) for( var modIndex = 0; modIndex < Mods.Count; modIndex++ )
{ {
for( var modIndex = 0; modIndex < Mods.ModSettings.Count; modIndex++ ) DrawMod( Mods[ modIndex ], modIndex );
{
var settings = Mods.ModSettings[ modIndex ];
var modName = settings.Mod.Meta.Name;
if( _modFilter.Length > 0 && !_modNamesLower![ modIndex ].Contains( _modFilter ) )
{
continue;
}
var changedColour = false;
if( !settings.Enabled )
{
ImGui.PushStyleColor( ImGuiCol.Text, DisabledModColor );
changedColour = true;
}
else if( settings.Mod.FileConflicts.Any() )
{
ImGui.PushStyleColor( ImGuiCol.Text, ConflictingModColor );
changedColour = true;
}
#if DEBUG
var selected = ImGui.Selectable(
$"id={modIndex} {modName}",
modIndex == _index
);
#else
var selected = ImGui.Selectable( modName, modIndex == _index );
#endif
if( ImGui.IsItemClicked( ImGuiMouseButton.Right ) )
{
if( ImGui.IsPopupOpen( LabelPriorityPopup ) )
{
ImGui.CloseCurrentPopup();
}
_priorityPopupIdx = modIndex;
_keyboardFocus = true;
ImGui.OpenPopup( LabelPriorityPopup );
}
ImGui.OpenPopupOnItemClick( LabelPriorityPopup, ImGuiPopupFlags.MouseButtonRight );
if( changedColour )
{
ImGui.PopStyleColor();
}
if( selected )
{
SetSelection( modIndex, settings );
}
}
} }
ImGui.EndChild(); ImGui.EndChild();
@ -334,52 +397,9 @@ namespace Penumbra.UI
ImGui.EndGroup(); ImGui.EndGroup();
DrawDeleteModal(); DrawDeleteModal();
DrawPriorityPopup();
} }
private void DrawPriorityPopup() private void SetSelection( int idx, Mod.Mod? info )
{
if( !ImGui.BeginPopupContextItem( LabelPriorityPopup ) )
{
return;
}
var size = ImGui.CalcTextSize( DescPriorityPopup ).X;
//ImGui.Text( DescPriorityPopup );
var newPriority = _priorityPopupIdx;
if( _keyboardFocus )
{
ImGui.SetKeyboardFocusHere( -1 );
_keyboardFocus = false;
}
ImGui.SetNextItemWidth( size );
if( ImGui.InputInt( "New Priority", ref newPriority, 0, 0,
ImGuiInputTextFlags.EnterReturnsTrue )
&& newPriority != _priorityPopupIdx )
{
Service< ModManager >.Get().ChangeModPriority( Mods!.ModSettings![ _priorityPopupIdx ], newPriority );
ResetModNamesLower();
if( _priorityPopupIdx == _index )
{
_index = newPriority;
SetSelection( _index );
}
ImGui.CloseCurrentPopup();
}
if( ImGui.IsKeyPressed( ImGui.GetKeyIndex( ImGuiKey.Escape ) ) )
{
ImGui.CloseCurrentPopup();
}
ImGui.EndPopup();
}
private void SetSelection( int idx, ModInfo? info )
{ {
Mod = info; Mod = info;
if( idx != _index ) if( idx != _index )
@ -393,7 +413,7 @@ namespace Penumbra.UI
private void SetSelection( int idx ) private void SetSelection( int idx )
{ {
if( idx >= ( Mods?.ModSettings?.Count ?? 0 ) ) if( idx >= ( Mods?.Count ?? 0 ) )
{ {
idx = -1; idx = -1;
} }
@ -404,58 +424,45 @@ namespace Penumbra.UI
} }
else else
{ {
SetSelection( idx, Mods!.ModSettings![ idx ] ); SetSelection( idx, Mods![ idx ] );
} }
} }
public void ReloadSelection()
=> SetSelection( _index, Mods![ _index ] );
public void ClearSelection() public void ClearSelection()
=> SetSelection( -1 ); => SetSelection( -1 );
public void SelectModByName( string name ) public void SelectModByName( string name )
{ {
var idx = Mods?.ModSettings?.FindIndex( mod => mod.Mod.Meta.Name == name ) ?? -1; var idx = Mods?.FindIndex( mod => mod.Data.Meta.Name == name ) ?? -1;
SetSelection( idx ); SetSelection( idx );
} }
public void SelectModByDir( string name ) public void SelectModByDir( string name )
{ {
var idx = Mods?.ModSettings?.FindIndex( mod => mod.FolderName == name ) ?? -1; var idx = Mods?.FindIndex( mod => mod.Data.BasePath.Name == name ) ?? -1;
SetSelection( idx ); SetSelection( idx );
} }
private string GetCurrentModMetaFile() public void ReloadCurrentMod( bool recomputeMeta = false )
=> Mod == null ? "" : Path.Combine( Mod.Mod.ModBasePath.FullName, "meta.json" );
public void ReloadCurrentMod()
{
var metaPath = GetCurrentModMetaFile();
if( metaPath.Length > 0 && File.Exists( metaPath ) )
{
Mod!.Mod.Meta = ModMeta.LoadFromFile( metaPath ) ?? Mod.Mod.Meta;
_base._menu.InstalledTab.ModPanel.Details.ResetState();
}
Mod!.Mod.RefreshModFiles();
Service< ModManager >.Get().CalculateEffectiveFileList();
ResetModNamesLower();
}
public string SaveCurrentMod()
{ {
if( Mod == null ) if( Mod == null )
{ {
return ""; return;
} }
var metaPath = GetCurrentModMetaFile(); if( _index >= 0 && _modManager.UpdateMod( Mod.Data, recomputeMeta ) )
if( metaPath.Length > 0 )
{ {
File.WriteAllText( metaPath, JsonConvert.SerializeObject( Mod.Mod.Meta, Formatting.Indented ) ); ResetModNamesLower();
SelectModByDir( Mod.Data.BasePath.Name );
_base._menu.InstalledTab.ModPanel.Details.ResetState();
}
} }
_base._menu.InstalledTab.ModPanel.Details.ResetState(); public void SaveCurrentMod()
return metaPath; => Mod?.Data.SaveMeta();
}
} }
} }
} }

View file

@ -1,6 +1,10 @@
using System;
using System.Diagnostics; using System.Diagnostics;
using System.Text.RegularExpressions;
using Dalamud.Plugin;
using ImGuiNET; using ImGuiNET;
using Penumbra.Hooks; using Penumbra.Hooks;
using Penumbra.Util;
namespace Penumbra.UI namespace Penumbra.UI
{ {
@ -13,7 +17,6 @@ namespace Penumbra.UI
private const string LabelRediscoverButton = "Rediscover Mods"; private const string LabelRediscoverButton = "Rediscover Mods";
private const string LabelOpenFolder = "Open Mods Folder"; private const string LabelOpenFolder = "Open Mods Folder";
private const string LabelEnabled = "Enable Mods"; private const string LabelEnabled = "Enable Mods";
private const string LabelInvertModOrder = "Invert mod load order (mods are loaded bottom up)";
private const string LabelShowAdvanced = "Show Advanced Settings"; private const string LabelShowAdvanced = "Show Advanced Settings";
private const string LabelLogLoadedFiles = "Log all loaded files"; private const string LabelLogLoadedFiles = "Log all loaded files";
private const string LabelDisableNotifications = "Disable filesystem change notifications"; private const string LabelDisableNotifications = "Disable filesystem change notifications";
@ -33,10 +36,10 @@ namespace Penumbra.UI
private void DrawRootFolder() private void DrawRootFolder()
{ {
var basePath = _config.CurrentCollection; var basePath = _config.ModDirectory;
if( ImGui.InputText( LabelRootFolder, ref basePath, 255 ) && _config.CurrentCollection != basePath ) if( ImGui.InputText( LabelRootFolder, ref basePath, 255 ) && _config.ModDirectory != basePath )
{ {
_config.CurrentCollection = basePath; _config.ModDirectory = basePath;
_configChanged = true; _configChanged = true;
} }
} }
@ -54,7 +57,7 @@ namespace Penumbra.UI
{ {
if( ImGui.Button( LabelOpenFolder ) ) if( ImGui.Button( LabelOpenFolder ) )
{ {
Process.Start( _config.CurrentCollection ); Process.Start( _config.ModDirectory );
} }
} }
@ -69,17 +72,6 @@ namespace Penumbra.UI
} }
} }
private void DrawInvertModOrderBox()
{
var invertOrder = _config.InvertModListOrder;
if( ImGui.Checkbox( LabelInvertModOrder, ref invertOrder ) )
{
_config.InvertModListOrder = invertOrder;
_base.ReloadMods();
_configChanged = true;
}
}
private void DrawShowAdvancedBox() private void DrawShowAdvancedBox()
{ {
var showAdvanced = _config.ShowAdvanced; var showAdvanced = _config.ShowAdvanced;
@ -91,10 +83,22 @@ namespace Penumbra.UI
} }
private void DrawLogLoadedFilesBox() private void DrawLogLoadedFilesBox()
{
if( _base._plugin.ResourceLoader != null )
{ {
ImGui.Checkbox( LabelLogLoadedFiles, ref _base._plugin.ResourceLoader.LogAllFiles ); ImGui.Checkbox( LabelLogLoadedFiles, ref _base._plugin.ResourceLoader.LogAllFiles );
ImGui.SameLine();
var regex = _base._plugin.ResourceLoader.LogFileFilter?.ToString() ?? string.Empty;
var tmp = regex;
if( ImGui.InputTextWithHint( "##LogFilter", "Matching this Regex...", ref tmp, 64 ) && tmp != regex )
{
try
{
var newRegex = tmp.Length > 0 ? new Regex( tmp, RegexOptions.Compiled ) : null;
_base._plugin.ResourceLoader.LogFileFilter = newRegex;
}
catch( Exception e )
{
PluginLog.Debug( "Could not create regex:\n{Exception}", e );
}
} }
} }
@ -127,11 +131,11 @@ namespace Penumbra.UI
} }
} }
private void DrawReloadResourceButton() private static void DrawReloadResourceButton()
{ {
if( ImGui.Button( LabelReloadResource ) ) if( ImGui.Button( LabelReloadResource ) )
{ {
Service<GameResourceManagement>.Get().ReloadPlayerResources(); Service< GameResourceManagement >.Get().ReloadPlayerResources();
} }
} }
@ -157,13 +161,10 @@ namespace Penumbra.UI
ImGui.SameLine(); ImGui.SameLine();
DrawOpenModsButton(); DrawOpenModsButton();
ImGuiCustom.VerticalDistance( DefaultVerticalSpace ); Custom.ImGuiCustom.VerticalDistance( DefaultVerticalSpace );
DrawEnabledBox(); DrawEnabledBox();
ImGuiCustom.VerticalDistance( DefaultVerticalSpace ); Custom.ImGuiCustom.VerticalDistance( DefaultVerticalSpace );
DrawInvertModOrderBox();
ImGuiCustom.VerticalDistance( DefaultVerticalSpace );
DrawShowAdvancedBox(); DrawShowAdvancedBox();
if( _config.ShowAdvanced ) if( _config.ShowAdvanced )

View file

@ -1,6 +1,7 @@
using System.IO; using System.IO;
using System.Numerics; using System.Numerics;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra.UI namespace Penumbra.UI
{ {
@ -25,7 +26,8 @@ namespace Penumbra.UI
_menu = new SettingsMenu( this ); _menu = new SettingsMenu( this );
} }
public void FlipVisibility() => _menu.Visible = !_menu.Visible; public void FlipVisibility()
=> _menu.Visible = !_menu.Visible;
public void Draw() public void Draw()
{ {
@ -39,10 +41,10 @@ namespace Penumbra.UI
_menu.InstalledTab.Selector.ResetModNamesLower(); _menu.InstalledTab.Selector.ResetModNamesLower();
_menu.InstalledTab.Selector.ClearSelection(); _menu.InstalledTab.Selector.ClearSelection();
// create the directory if it doesn't exist // create the directory if it doesn't exist
Directory.CreateDirectory( _plugin!.Configuration!.CurrentCollection ); Directory.CreateDirectory( _plugin!.Configuration!.ModDirectory );
var modManager = Service< ModManager >.Get(); var modManager = Service< ModManager >.Get();
modManager.DiscoverMods( _plugin.Configuration.CurrentCollection ); modManager.DiscoverMods( new DirectoryInfo( _plugin.Configuration.ModDirectory ) );
_menu.InstalledTab.Selector.ResetModNamesLower(); _menu.InstalledTab.Selector.ResetModNamesLower();
} }
} }

View file

@ -16,8 +16,9 @@ namespace Penumbra.UI
private readonly TabSettings _settingsTab; private readonly TabSettings _settingsTab;
private readonly TabImport _importTab; private readonly TabImport _importTab;
private readonly TabBrowser _browserTab; private readonly TabBrowser _browserTab;
private readonly TabCollections _collectionsTab;
public readonly TabInstalled InstalledTab; public readonly TabInstalled InstalledTab;
public readonly TabEffective EffectiveTab; private readonly TabEffective _effectiveTab;
public SettingsMenu( SettingsInterface ui ) public SettingsMenu( SettingsInterface ui )
{ {
@ -26,7 +27,8 @@ namespace Penumbra.UI
_importTab = new TabImport( _base ); _importTab = new TabImport( _base );
_browserTab = new TabBrowser(); _browserTab = new TabBrowser();
InstalledTab = new TabInstalled( _base ); InstalledTab = new TabInstalled( _base );
EffectiveTab = new TabEffective(); _collectionsTab = new TabCollections( InstalledTab.Selector );
_effectiveTab = new TabEffective();
} }
#if DEBUG #if DEBUG
@ -57,6 +59,7 @@ namespace Penumbra.UI
ImGui.BeginTabBar( PenumbraSettingsLabel ); ImGui.BeginTabBar( PenumbraSettingsLabel );
_settingsTab.Draw(); _settingsTab.Draw();
_collectionsTab.Draw();
_importTab.Draw(); _importTab.Draw();
if( !_importTab.IsImporting() ) if( !_importTab.IsImporting() )
@ -66,7 +69,7 @@ namespace Penumbra.UI
if( _base._plugin!.Configuration!.ShowAdvanced ) if( _base._plugin!.Configuration!.ShowAdvanced )
{ {
EffectiveTab.Draw(); _effectiveTab.Draw();
} }
} }

View file

@ -1,7 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace Penumbra namespace Penumbra.Util
{ {
public static class ArrayExtensions public static class ArrayExtensions
{ {

View file

@ -1,11 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Threading.Tasks;
namespace Penumbra.Util namespace Penumbra.Util
{ {
@ -38,7 +36,7 @@ namespace Penumbra.Util
var list = new List< T >( count ); var list = new List< T >( count );
for( int i = 0; i < count; i++ ) for( var i = 0; i < count; i++ )
{ {
var offset = size * i; var offset = size * i;
var span = new ReadOnlySpan< byte >( data, offset, size ); var span = new ReadOnlySpan< byte >( data, offset, size );
@ -55,9 +53,9 @@ namespace Penumbra.Util
var data = br.ReadBytes( size * count ); var data = br.ReadBytes( size * count );
// im a pirate arr // im a pirate arr
var arr = new T[ count ]; var arr = new T[count];
for( int i = 0; i < count; i++ ) for( var i = 0; i < count; i++ )
{ {
var offset = size * i; var offset = size * i;
var span = new ReadOnlySpan< byte >( data, offset, size ); var span = new ReadOnlySpan< byte >( data, offset, size );
@ -76,9 +74,7 @@ namespace Penumbra.Util
/// <param name="offset">The offset to read a string starting from.</param> /// <param name="offset">The offset to read a string starting from.</param>
/// <returns></returns> /// <returns></returns>
public static string ReadStringOffsetData( this BinaryReader br, long offset ) public static string ReadStringOffsetData( this BinaryReader br, long offset )
{ => Encoding.UTF8.GetString( ReadRawOffsetData( br, offset ) );
return Encoding.UTF8.GetString( ReadRawOffsetData( br, offset ) );
}
/// <summary> /// <summary>
/// Moves the BinaryReader position to offset, reads raw bytes until a null byte, then /// Moves the BinaryReader position to offset, reads raw bytes until a null byte, then
@ -108,7 +104,8 @@ namespace Penumbra.Util
/// <summary> /// <summary>
/// Seeks this BinaryReader's position to the given offset. Syntactic sugar. /// Seeks this BinaryReader's position to the given offset. Syntactic sugar.
/// </summary> /// </summary>
public static void Seek( this BinaryReader br, long offset ) { public static void Seek( this BinaryReader br, long offset )
{
br.BaseStream.Position = offset; br.BaseStream.Position = offset;
} }

View file

@ -22,7 +22,8 @@ namespace Penumbra.Util
return k; return k;
} ).ToArray(); } ).ToArray();
public uint Checksum => ~_crc32; public uint Checksum
=> ~_crc32;
private uint _crc32 = 0xFFFFFFFF; private uint _crc32 = 0xFFFFFFFF;
@ -50,8 +51,7 @@ namespace Penumbra.Util
[MethodImpl( MethodImplOptions.AggressiveInlining )] [MethodImpl( MethodImplOptions.AggressiveInlining )]
public void Update( byte b ) public void Update( byte b )
{ {
_crc32 = CrcArray[ ( _crc32 ^ b ) & 0xFF ] ^ _crc32 = CrcArray[ ( _crc32 ^ b ) & 0xFF ] ^ ( ( _crc32 >> 8 ) & 0x00FFFFFF );
( ( _crc32 >> 8 ) & 0x00FFFFFF );
} }
} }
} }

View file

@ -5,7 +5,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
namespace Penumbra namespace Penumbra.Util
{ {
public static class DialogExtensions public static class DialogExtensions
{ {
@ -38,7 +38,8 @@ namespace Penumbra
{ {
public IntPtr Handle { get; set; } public IntPtr Handle { get; set; }
public DialogHandle( IntPtr handle ) => Handle = handle; public DialogHandle( IntPtr handle )
=> Handle = handle;
} }
public class HiddenForm : Form public class HiddenForm : Form

View file

@ -1,9 +1,4 @@
using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Penumbra.Util namespace Penumbra.Util
{ {

View file

@ -171,7 +171,7 @@ namespace Penumbra.Util
{ {
if( value != null ) if( value != null )
{ {
var v = ( GamePath) value; var v = ( GamePath )value;
serializer.Serialize( writer, v.ToString() ); serializer.Serialize( writer, v.ToString() );
} }
} }

View file

@ -1,6 +1,6 @@
using System; using System;
namespace Penumbra namespace Penumbra.Util
{ {
/// <summary> /// <summary>
/// Basic service locator /// Basic service locator
@ -11,8 +11,7 @@ namespace Penumbra
private static T? _object; private static T? _object;
static Service() static Service()
{ { }
}
public static void Set( T obj ) public static void Set( T obj )
{ {

View file

@ -7,7 +7,8 @@ namespace Penumbra.Util
{ {
public class SingleOrArrayConverter< T > : JsonConverter public class SingleOrArrayConverter< T > : JsonConverter
{ {
public override bool CanConvert( Type objectType ) => objectType == typeof( HashSet< T > ); public override bool CanConvert( Type objectType )
=> objectType == typeof( HashSet< T > );
public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer )
{ {
@ -15,7 +16,7 @@ namespace Penumbra.Util
if( token.Type == JTokenType.Array ) if( token.Type == JTokenType.Array )
{ {
return token.ToObject< HashSet< T > >() ?? new HashSet<T>(); return token.ToObject< HashSet< T > >() ?? new HashSet< T >();
} }
var tmp = token.ToObject< T >(); var tmp = token.ToObject< T >();
@ -24,7 +25,8 @@ namespace Penumbra.Util
: new HashSet< T >(); : new HashSet< T >();
} }
public override bool CanWrite => true; public override bool CanWrite
=> true;
public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer )
{ {
@ -37,6 +39,7 @@ namespace Penumbra.Util
serializer.Serialize( writer, val?.ToString() ); serializer.Serialize( writer, val?.ToString() );
} }
} }
writer.WriteEndArray(); writer.WriteEndArray();
} }
} }

View file

@ -1,18 +1,17 @@
using System.IO; using System.IO;
using System.Linq;
using System.Text; using System.Text;
namespace Penumbra namespace Penumbra.Util
{ {
public static class StringPathExtensions public static class StringPathExtensions
{ {
private static readonly char[] _invalid = Path.GetInvalidFileNameChars(); private static readonly char[] Invalid = Path.GetInvalidFileNameChars();
public static string ReplaceInvalidPathSymbols( this string s, string replacement = "_" ) public static string ReplaceInvalidPathSymbols( this string s, string replacement = "_" )
=> string.Join( replacement, s.Split( _invalid ) ); => string.Join( replacement, s.Split( Invalid ) );
public static string RemoveInvalidPathSymbols( this string s ) public static string RemoveInvalidPathSymbols( this string s )
=> string.Concat( s.Split( _invalid ) ); => string.Concat( s.Split( Invalid ) );
public static string RemoveNonAsciiSymbols( this string s, string replacement = "_" ) public static string RemoveNonAsciiSymbols( this string s, string replacement = "_" )
{ {