mirror of
https://github.com/xivdev/Penumbra.git
synced 2026-01-03 14:23:43 +01:00
This commit is contained in:
parent
7b451c5097
commit
bb957c1119
50 changed files with 2016 additions and 1247 deletions
2
Luna
2
Luna
|
|
@ -1 +1 @@
|
|||
Subproject commit 235825613cf1a2c95173d0a1ae43f2c6601a9df6
|
||||
Subproject commit fbfe51b4c39b0f094678f1017f7688632e8f61b8
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit d52071290b48a1f2292023675b4b72365aef4cc0
|
||||
Subproject commit 66a11d4c886d64da6ecb1a0ec4c8306b99167be1
|
||||
|
|
@ -70,6 +70,27 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
|
|||
return (ret.Item1, (ret.Item2.Value.Item1, ret.Item2.Value.Item2, ret.Item2.Value.Item3, ret.Item2.Value.Item4));
|
||||
}
|
||||
|
||||
public PenumbraApiEc GetSettingsInAllCollections(string modDirectory, string modName,
|
||||
out Dictionary<Guid, (bool, int, Dictionary<string, List<string>>, bool, bool)> settings,
|
||||
bool ignoreTemporaryCollections = false)
|
||||
{
|
||||
settings = [];
|
||||
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
|
||||
return PenumbraApiEc.ModMissing;
|
||||
|
||||
var collections = ignoreTemporaryCollections
|
||||
? _collectionManager.Storage.Where(c => c != ModCollection.Empty)
|
||||
: _collectionManager.Storage.Where(c => c != ModCollection.Empty).Concat(_collectionManager.Temp.Values);
|
||||
settings = [];
|
||||
foreach (var collection in collections)
|
||||
{
|
||||
if (GetCurrentSettings(collection, mod, false, false, 0) is { } s)
|
||||
settings.Add(collection.Identity.Id, s);
|
||||
}
|
||||
|
||||
return PenumbraApiEc.Success;
|
||||
}
|
||||
|
||||
public (PenumbraApiEc, (bool, int, Dictionary<string, List<string>>, bool, bool)?) GetCurrentModSettingsWithTemp(Guid collectionId,
|
||||
string modDirectory, string modName, bool ignoreInheritance, bool ignoreTemporary, int key)
|
||||
{
|
||||
|
|
@ -269,7 +290,8 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
|
|||
}
|
||||
|
||||
private void OnModSettingChange(in ModSettingChanged.Arguments arguments)
|
||||
=> ModSettingChanged?.Invoke(arguments.Type, arguments.Collection.Identity.Id, arguments.Mod?.Identifier ?? string.Empty, arguments.Inherited);
|
||||
=> ModSettingChanged?.Invoke(arguments.Type, arguments.Collection.Identity.Id, arguments.Mod?.Identifier ?? string.Empty,
|
||||
arguments.Inherited);
|
||||
|
||||
private void OnModOptionEdited(in ModOptionChanged.Arguments arguments)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ public class PenumbraApi(
|
|||
UiApi ui) : IDisposable, Luna.IApiService, IPenumbraApi
|
||||
{
|
||||
public const int BreakingVersion = 5;
|
||||
public const int FeatureVersion = 12;
|
||||
public const int FeatureVersion = 13;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.Interop.PathResolving;
|
||||
|
|
@ -40,6 +41,19 @@ public class ResolveApi(
|
|||
return ret.Select(r => r.ToString()).ToArray();
|
||||
}
|
||||
|
||||
public PenumbraApiEc ResolvePath(Guid collectionId, string gamePath, out string resolvedPath)
|
||||
{
|
||||
resolvedPath = gamePath;
|
||||
if (!collectionManager.Storage.ById(collectionId, out var collection))
|
||||
return PenumbraApiEc.CollectionMissing;
|
||||
|
||||
if (!collection.HasCache)
|
||||
return PenumbraApiEc.CollectionInactive;
|
||||
|
||||
resolvedPath = ResolvePath(gamePath, modManager, collection);
|
||||
return PenumbraApiEc.Success;
|
||||
}
|
||||
|
||||
public string[] ReverseResolvePlayerPath(string moddedPath)
|
||||
{
|
||||
if (!config.EnableMods)
|
||||
|
|
@ -63,6 +77,26 @@ public class ResolveApi(
|
|||
return (resolved, reverseResolved.Select(a => a.Select(p => p.ToString()).ToArray()).ToArray());
|
||||
}
|
||||
|
||||
public PenumbraApiEc ResolvePaths(Guid collectionId, string[] forward, string[] reverse, out string[] resolvedForward,
|
||||
out string[][] resolvedReverse)
|
||||
{
|
||||
resolvedForward = forward;
|
||||
resolvedReverse = [];
|
||||
if (!config.EnableMods)
|
||||
return PenumbraApiEc.Success;
|
||||
|
||||
if (!collectionManager.Storage.ById(collectionId, out var collection))
|
||||
return PenumbraApiEc.CollectionMissing;
|
||||
|
||||
if (!collection.HasCache)
|
||||
return PenumbraApiEc.CollectionInactive;
|
||||
|
||||
resolvedForward = forward.Select(p => ResolvePath(p, modManager, collection)).ToArray();
|
||||
var reverseResolved = collection.ReverseResolvePaths(reverse);
|
||||
resolvedReverse = reverseResolved.Select(a => a.Select(p => p.ToString()).ToArray()).ToArray();
|
||||
return PenumbraApiEc.Success;
|
||||
}
|
||||
|
||||
public async Task<(string[], string[][])> ResolvePlayerPathsAsync(string[] forward, string[] reverse)
|
||||
{
|
||||
if (!config.EnableMods)
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ public sealed class IpcProviders : IDisposable, IApiService, IRequiredService
|
|||
IpcSubscribers.GetCurrentModSettings.Provider(pi, api.ModSettings),
|
||||
IpcSubscribers.GetCurrentModSettingsWithTemp.Provider(pi, api.ModSettings),
|
||||
IpcSubscribers.GetAllModSettings.Provider(pi, api.ModSettings),
|
||||
IpcSubscribers.GetSettingsInAllCollections.Provider(pi, api.ModSettings),
|
||||
IpcSubscribers.TryInheritMod.Provider(pi, api.ModSettings),
|
||||
IpcSubscribers.TrySetMod.Provider(pi, api.ModSettings),
|
||||
IpcSubscribers.TrySetModPriority.Provider(pi, api.ModSettings),
|
||||
|
|
@ -97,6 +98,8 @@ public sealed class IpcProviders : IDisposable, IApiService, IRequiredService
|
|||
IpcSubscribers.ReverseResolvePlayerPath.Provider(pi, api.Resolve),
|
||||
IpcSubscribers.ResolvePlayerPaths.Provider(pi, api.Resolve),
|
||||
IpcSubscribers.ResolvePlayerPathsAsync.Provider(pi, api.Resolve),
|
||||
IpcSubscribers.ResolvePath.Provider(pi, api.Resolve),
|
||||
IpcSubscribers.ResolvePaths.Provider(pi, api.Resolve),
|
||||
|
||||
IpcSubscribers.GetGameObjectResourcePaths.Provider(pi, api.ResourceTree),
|
||||
IpcSubscribers.GetPlayerResourcePaths.Provider(pi, api.ResourceTree),
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using Penumbra.Api.Api;
|
|||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.Mods.Settings;
|
||||
|
||||
namespace Penumbra.Communication;
|
||||
|
|
@ -14,7 +15,10 @@ public sealed class ModSettingChanged(Logger log)
|
|||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="ModSettingsApi.OnModSettingChange"/>
|
||||
Api = int.MinValue,
|
||||
Api = int.MinValue,
|
||||
|
||||
/// <seealso cref="Mods.Manager.ModConfigUpdater.OnModSettingChanged"/>
|
||||
ModConfigUpdater = -10,
|
||||
|
||||
/// <seealso cref="Collections.Cache.CollectionCacheManager.OnModSettingChange"/>
|
||||
CollectionCacheManager = 0,
|
||||
|
|
@ -26,7 +30,7 @@ public sealed class ModSettingChanged(Logger log)
|
|||
ModFileSystemSelector = 0,
|
||||
|
||||
/// <seealso cref="Mods.ModSelection.OnSettingChange"/>
|
||||
ModSelection = 10,
|
||||
ModSelection = 10,
|
||||
}
|
||||
|
||||
/// <summary> The arguments for a ModSettingChanged event. </summary>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using Dalamud.Configuration;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using ImSharp;
|
||||
using Luna;
|
||||
using Newtonsoft.Json;
|
||||
using Penumbra.Import.Structs;
|
||||
|
|
|
|||
|
|
@ -1,169 +1,172 @@
|
|||
using Penumbra.Communication;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.Mods.Manager.OptionEditor;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mods.Manager;
|
||||
|
||||
public class ModCacheManager : IDisposable, Luna.IRequiredService
|
||||
{
|
||||
private readonly Configuration _config;
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly ObjectIdentification _identifier;
|
||||
private readonly ModStorage _modManager;
|
||||
private bool _updatingItems;
|
||||
|
||||
public ModCacheManager(CommunicatorService communicator, ObjectIdentification identifier, ModStorage modStorage, Configuration config)
|
||||
{
|
||||
_communicator = communicator;
|
||||
_identifier = identifier;
|
||||
_modManager = modStorage;
|
||||
_config = config;
|
||||
|
||||
_communicator.ModOptionChanged.Subscribe(OnModOptionChange, ModOptionChanged.Priority.ModCacheManager);
|
||||
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModCacheManager);
|
||||
_communicator.ModDataChanged.Subscribe(OnModDataChange, ModDataChanged.Priority.ModCacheManager);
|
||||
_communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished, ModDiscoveryFinished.Priority.ModCacheManager);
|
||||
identifier.Awaiter.ContinueWith(_ => OnIdentifierCreation(), TaskScheduler.Default);
|
||||
OnModDiscoveryFinished();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_communicator.ModOptionChanged.Unsubscribe(OnModOptionChange);
|
||||
_communicator.ModPathChanged.Unsubscribe(OnModPathChange);
|
||||
_communicator.ModDataChanged.Unsubscribe(OnModDataChange);
|
||||
_communicator.ModDiscoveryFinished.Unsubscribe(OnModDiscoveryFinished);
|
||||
}
|
||||
|
||||
private void OnModOptionChange(in ModOptionChanged.Arguments arguments)
|
||||
{
|
||||
switch (arguments.Type)
|
||||
{
|
||||
case ModOptionChangeType.GroupAdded:
|
||||
case ModOptionChangeType.GroupDeleted:
|
||||
case ModOptionChangeType.OptionAdded:
|
||||
case ModOptionChangeType.OptionDeleted:
|
||||
UpdateChangedItems(arguments.Mod);
|
||||
UpdateCounts(arguments.Mod);
|
||||
break;
|
||||
case ModOptionChangeType.GroupTypeChanged:
|
||||
UpdateHasOptions(arguments.Mod);
|
||||
break;
|
||||
case ModOptionChangeType.OptionFilesChanged:
|
||||
case ModOptionChangeType.OptionFilesAdded:
|
||||
UpdateChangedItems(arguments.Mod);
|
||||
UpdateFileCount(arguments.Mod);
|
||||
break;
|
||||
case ModOptionChangeType.OptionSwapsChanged:
|
||||
UpdateChangedItems(arguments.Mod);
|
||||
UpdateSwapCount(arguments.Mod);
|
||||
break;
|
||||
case ModOptionChangeType.OptionMetaChanged:
|
||||
UpdateChangedItems(arguments.Mod);
|
||||
UpdateMetaCount(arguments.Mod);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnModPathChange(in ModPathChanged.Arguments arguments)
|
||||
{
|
||||
switch (arguments.Type)
|
||||
{
|
||||
case ModPathChangeType.Added:
|
||||
case ModPathChangeType.Reloaded:
|
||||
RefreshWithChangedItems(arguments.Mod);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnModDataChange(in ModDataChanged.Arguments arguments)
|
||||
{
|
||||
if ((arguments.Type & (ModDataChangeType.LocalTags | ModDataChangeType.ModTags)) is not 0)
|
||||
UpdateTags(arguments.Mod);
|
||||
}
|
||||
|
||||
private void OnModDiscoveryFinished()
|
||||
{
|
||||
if (!_identifier.Awaiter.IsCompletedSuccessfully || _updatingItems)
|
||||
{
|
||||
Parallel.ForEach(_modManager, RefreshWithoutChangedItems);
|
||||
}
|
||||
else
|
||||
{
|
||||
_updatingItems = true;
|
||||
Parallel.ForEach(_modManager, RefreshWithChangedItems);
|
||||
_updatingItems = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnIdentifierCreation()
|
||||
{
|
||||
if (_updatingItems)
|
||||
return;
|
||||
|
||||
_updatingItems = true;
|
||||
Parallel.ForEach(_modManager, UpdateChangedItems);
|
||||
_updatingItems = false;
|
||||
}
|
||||
|
||||
private static void UpdateFileCount(Mod mod)
|
||||
=> mod.TotalFileCount = mod.AllDataContainers.Sum(s => s.Files.Count);
|
||||
|
||||
private static void UpdateSwapCount(Mod mod)
|
||||
=> mod.TotalSwapCount = mod.AllDataContainers.Sum(s => s.FileSwaps.Count);
|
||||
|
||||
private static void UpdateMetaCount(Mod mod)
|
||||
=> mod.TotalManipulations = mod.AllDataContainers.Sum(s => s.Manipulations.Count);
|
||||
|
||||
private static void UpdateHasOptions(Mod mod)
|
||||
=> mod.HasOptions = mod.Groups.Any(o => o.IsOption);
|
||||
|
||||
private static void UpdateTags(Mod mod)
|
||||
=> mod.AllTagsLower = string.Join('\0', mod.ModTags.Concat(mod.LocalTags).Select(s => s.ToLowerInvariant()));
|
||||
|
||||
private void UpdateChangedItems(Mod mod)
|
||||
{
|
||||
mod.ChangedItems.Clear();
|
||||
|
||||
_identifier.AddChangedItems(mod.Default, mod.ChangedItems);
|
||||
foreach (var group in mod.Groups)
|
||||
group.AddChangedItems(_identifier, mod.ChangedItems);
|
||||
|
||||
if (_config.HideMachinistOffhandFromChangedItems)
|
||||
mod.ChangedItems.RemoveMachinistOffhands();
|
||||
|
||||
mod.LowerChangedItemsString = string.Join("\0", mod.ChangedItems.Keys.Select(k => k.ToLowerInvariant()));
|
||||
++mod.LastChangedItemsUpdate;
|
||||
}
|
||||
|
||||
private static void UpdateCounts(Mod mod)
|
||||
{
|
||||
mod.TotalFileCount = mod.Default.Files.Count;
|
||||
mod.TotalSwapCount = mod.Default.FileSwaps.Count;
|
||||
mod.TotalManipulations = mod.Default.Manipulations.Count;
|
||||
mod.HasOptions = false;
|
||||
foreach (var group in mod.Groups)
|
||||
{
|
||||
mod.HasOptions |= group.IsOption;
|
||||
var (files, swaps, manips) = group.GetCounts();
|
||||
mod.TotalFileCount += files;
|
||||
mod.TotalSwapCount += swaps;
|
||||
mod.TotalManipulations += manips;
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshWithChangedItems(Mod mod)
|
||||
{
|
||||
UpdateTags(mod);
|
||||
UpdateCounts(mod);
|
||||
UpdateChangedItems(mod);
|
||||
}
|
||||
|
||||
private void RefreshWithoutChangedItems(Mod mod)
|
||||
{
|
||||
UpdateTags(mod);
|
||||
UpdateCounts(mod);
|
||||
}
|
||||
}
|
||||
using Luna;
|
||||
using Penumbra.Communication;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.Mods.Manager.OptionEditor;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mods.Manager;
|
||||
|
||||
public class ModCacheManager : IDisposable, IRequiredService
|
||||
{
|
||||
private readonly Configuration _config;
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly ObjectIdentification _identifier;
|
||||
private readonly ModStorage _modManager;
|
||||
private readonly SaveService _saveService;
|
||||
private bool _updatingItems;
|
||||
|
||||
public ModCacheManager(CommunicatorService communicator, ObjectIdentification identifier, ModStorage modStorage, Configuration config,
|
||||
SaveService saveService)
|
||||
{
|
||||
_communicator = communicator;
|
||||
_identifier = identifier;
|
||||
_modManager = modStorage;
|
||||
_config = config;
|
||||
_saveService = saveService;
|
||||
|
||||
_communicator.ModOptionChanged.Subscribe(OnModOptionChange, ModOptionChanged.Priority.ModCacheManager);
|
||||
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModCacheManager);
|
||||
_communicator.ModDataChanged.Subscribe(OnModDataChange, ModDataChanged.Priority.ModCacheManager);
|
||||
_communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished, ModDiscoveryFinished.Priority.ModCacheManager);
|
||||
|
||||
identifier.Awaiter.ContinueWith(_ => OnIdentifierCreation(), TaskScheduler.Default);
|
||||
OnModDiscoveryFinished();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_communicator.ModOptionChanged.Unsubscribe(OnModOptionChange);
|
||||
_communicator.ModPathChanged.Unsubscribe(OnModPathChange);
|
||||
_communicator.ModDataChanged.Unsubscribe(OnModDataChange);
|
||||
_communicator.ModDiscoveryFinished.Unsubscribe(OnModDiscoveryFinished);
|
||||
}
|
||||
|
||||
private void OnModOptionChange(in ModOptionChanged.Arguments arguments)
|
||||
{
|
||||
switch (arguments.Type)
|
||||
{
|
||||
case ModOptionChangeType.GroupAdded:
|
||||
case ModOptionChangeType.GroupDeleted:
|
||||
case ModOptionChangeType.OptionAdded:
|
||||
case ModOptionChangeType.OptionDeleted:
|
||||
UpdateChangedItems(arguments.Mod);
|
||||
UpdateCounts(arguments.Mod);
|
||||
break;
|
||||
case ModOptionChangeType.GroupTypeChanged: UpdateHasOptions(arguments.Mod); break;
|
||||
case ModOptionChangeType.OptionFilesChanged:
|
||||
case ModOptionChangeType.OptionFilesAdded:
|
||||
UpdateChangedItems(arguments.Mod);
|
||||
UpdateFileCount(arguments.Mod);
|
||||
break;
|
||||
case ModOptionChangeType.OptionSwapsChanged:
|
||||
UpdateChangedItems(arguments.Mod);
|
||||
UpdateSwapCount(arguments.Mod);
|
||||
break;
|
||||
case ModOptionChangeType.OptionMetaChanged:
|
||||
UpdateChangedItems(arguments.Mod);
|
||||
UpdateMetaCount(arguments.Mod);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnModPathChange(in ModPathChanged.Arguments arguments)
|
||||
{
|
||||
switch (arguments.Type)
|
||||
{
|
||||
case ModPathChangeType.Added:
|
||||
case ModPathChangeType.Reloaded:
|
||||
RefreshWithChangedItems(arguments.Mod);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnModDataChange(in ModDataChanged.Arguments arguments)
|
||||
{
|
||||
if ((arguments.Type & (ModDataChangeType.LocalTags | ModDataChangeType.ModTags)) is not 0)
|
||||
UpdateTags(arguments.Mod);
|
||||
}
|
||||
|
||||
private void OnModDiscoveryFinished()
|
||||
{
|
||||
if (!_identifier.Awaiter.IsCompletedSuccessfully || _updatingItems)
|
||||
{
|
||||
Parallel.ForEach(_modManager, RefreshWithoutChangedItems);
|
||||
}
|
||||
else
|
||||
{
|
||||
_updatingItems = true;
|
||||
Parallel.ForEach(_modManager, RefreshWithChangedItems);
|
||||
_updatingItems = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnIdentifierCreation()
|
||||
{
|
||||
if (_updatingItems)
|
||||
return;
|
||||
|
||||
_updatingItems = true;
|
||||
Parallel.ForEach(_modManager, UpdateChangedItems);
|
||||
_updatingItems = false;
|
||||
}
|
||||
|
||||
private static void UpdateFileCount(Mod mod)
|
||||
=> mod.TotalFileCount = mod.AllDataContainers.Sum(s => s.Files.Count);
|
||||
|
||||
private static void UpdateSwapCount(Mod mod)
|
||||
=> mod.TotalSwapCount = mod.AllDataContainers.Sum(s => s.FileSwaps.Count);
|
||||
|
||||
private static void UpdateMetaCount(Mod mod)
|
||||
=> mod.TotalManipulations = mod.AllDataContainers.Sum(s => s.Manipulations.Count);
|
||||
|
||||
private static void UpdateHasOptions(Mod mod)
|
||||
=> mod.HasOptions = mod.Groups.Any(o => o.IsOption);
|
||||
|
||||
private static void UpdateTags(Mod mod)
|
||||
=> mod.AllTagsLower = string.Join('\0', mod.ModTags.Concat(mod.LocalTags).Select(s => s.ToLowerInvariant()));
|
||||
|
||||
private void UpdateChangedItems(Mod mod)
|
||||
{
|
||||
mod.ChangedItems.Clear();
|
||||
|
||||
_identifier.AddChangedItems(mod.Default, mod.ChangedItems);
|
||||
foreach (var group in mod.Groups)
|
||||
group.AddChangedItems(_identifier, mod.ChangedItems);
|
||||
|
||||
if (_config.HideMachinistOffhandFromChangedItems)
|
||||
mod.ChangedItems.RemoveMachinistOffhands();
|
||||
|
||||
mod.LowerChangedItemsString = string.Join("\0", mod.ChangedItems.Keys.Select(k => k.ToLowerInvariant()));
|
||||
++mod.LastChangedItemsUpdate;
|
||||
}
|
||||
|
||||
private static void UpdateCounts(Mod mod)
|
||||
{
|
||||
mod.TotalFileCount = mod.Default.Files.Count;
|
||||
mod.TotalSwapCount = mod.Default.FileSwaps.Count;
|
||||
mod.TotalManipulations = mod.Default.Manipulations.Count;
|
||||
mod.HasOptions = false;
|
||||
foreach (var group in mod.Groups)
|
||||
{
|
||||
mod.HasOptions |= group.IsOption;
|
||||
var (files, swaps, manips) = group.GetCounts();
|
||||
mod.TotalFileCount += files;
|
||||
mod.TotalSwapCount += swaps;
|
||||
mod.TotalManipulations += manips;
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshWithChangedItems(Mod mod)
|
||||
{
|
||||
UpdateTags(mod);
|
||||
UpdateCounts(mod);
|
||||
UpdateChangedItems(mod);
|
||||
}
|
||||
|
||||
private void RefreshWithoutChangedItems(Mod mod)
|
||||
{
|
||||
UpdateTags(mod);
|
||||
UpdateCounts(mod);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
74
Penumbra/Mods/Manager/ModConfigUpdater.cs
Normal file
74
Penumbra/Mods/Manager/ModConfigUpdater.cs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
using Luna;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.Communication;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Mods.Manager;
|
||||
|
||||
public class ModConfigUpdater : IDisposable, IRequiredService
|
||||
{
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly SaveService _saveService;
|
||||
private readonly ModStorage _mods;
|
||||
private readonly CollectionStorage _collections;
|
||||
|
||||
public ModConfigUpdater(CommunicatorService communicator, SaveService saveService, ModStorage mods, CollectionStorage collections)
|
||||
{
|
||||
_communicator = communicator;
|
||||
_saveService = saveService;
|
||||
_mods = mods;
|
||||
_collections = collections;
|
||||
|
||||
_communicator.ModSettingChanged.Subscribe(OnModSettingChanged, ModSettingChanged.Priority.ModConfigUpdater);
|
||||
}
|
||||
|
||||
public IEnumerable<Mod> ListUnusedMods(TimeSpan age)
|
||||
{
|
||||
var cutoff = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - (int)age.TotalMilliseconds;
|
||||
foreach (var mod in _mods)
|
||||
{
|
||||
// Skip actively ignored mods.
|
||||
if (mod.IgnoreLastConfig)
|
||||
continue;
|
||||
|
||||
// Skip mods that had settings changed since the given maximum age.
|
||||
if (mod.LastConfigEdit >= cutoff)
|
||||
continue;
|
||||
|
||||
// Skip mods that are currently permanently enabled or have any temporary settings.
|
||||
if (_collections.Any(c => c.GetOwnSettings(mod.Index)?.Enabled is true || c.GetTempSettings(mod.Index) is not null))
|
||||
continue;
|
||||
|
||||
yield return mod;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnModSettingChanged(in ModSettingChanged.Arguments arguments)
|
||||
{
|
||||
if (arguments.Inherited)
|
||||
return;
|
||||
|
||||
switch (arguments.Type)
|
||||
{
|
||||
case ModSettingChange.Inheritance:
|
||||
case ModSettingChange.MultiInheritance:
|
||||
case ModSettingChange.MultiEnableState:
|
||||
case ModSettingChange.TemporaryMod:
|
||||
case ModSettingChange.Edited:
|
||||
return;
|
||||
}
|
||||
|
||||
if (arguments.Mod is { } mod)
|
||||
{
|
||||
mod.LastConfigEdit = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
_communicator.ModDataChanged.Invoke(new ModDataChanged.Arguments(ModDataChangeType.LastConfigEdit, mod, null));
|
||||
_saveService.Save(SaveType.Delay, new ModLocalData(mod));
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_communicator.ModSettingChanged.Unsubscribe(OnModSettingChanged);
|
||||
}
|
||||
}
|
||||
|
|
@ -27,7 +27,8 @@ public enum ModDataChangeType : uint
|
|||
PreferredChangedItems = 0x004000,
|
||||
RequiredFeatures = 0x008000,
|
||||
FileSystemFolder = 0x010000,
|
||||
FileSystemSortOrder = 0x020000,
|
||||
FileSystemSortOrder = 0x020000,
|
||||
LastConfigEdit = 0x040000,
|
||||
}
|
||||
|
||||
public class ModDataEditor(SaveService saveService, CommunicatorService communicatorService, ItemData itemData) : Luna.IService
|
||||
|
|
|
|||
|
|
@ -17,13 +17,14 @@ public sealed class ModTab : TwoPanelLayout, ITab<TabType>
|
|||
public override ReadOnlySpan<byte> Label
|
||||
=> "Mods2"u8;
|
||||
|
||||
public ModTab(ModFileSystemDrawer drawer, ModPanel panel, CollectionSelectHeader collectionHeader)
|
||||
public ModTab(ModFileSystemDrawer drawer, ModPanel panel, CollectionSelectHeader collectionHeader, RedrawFooter redrawFooter)
|
||||
{
|
||||
LeftHeader = drawer.Header;
|
||||
LeftFooter = drawer.Footer;
|
||||
LeftPanel = drawer;
|
||||
RightPanel = panel;
|
||||
RightHeader = collectionHeader;
|
||||
RightFooter = redrawFooter;
|
||||
}
|
||||
|
||||
public void DrawContent()
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@ public sealed class Mod : IMod, IFileSystemValue<Mod>
|
|||
// Local Data
|
||||
public DataPath Path { get; } = new();
|
||||
public long ImportDate { get; internal set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds();
|
||||
public long LastConfigEdit { get; internal set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds();
|
||||
public bool IgnoreLastConfig { get; internal set; } = false;
|
||||
public IReadOnlyList<string> LocalTags { get; internal set; } = [];
|
||||
public string Note { get; internal set; } = string.Empty;
|
||||
public HashSet<CustomItemId> PreferredChangedItems { get; internal set; } = [];
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ public readonly struct ModLocalData(Mod mod) : ISavable
|
|||
{ nameof(Mod.Note), JToken.FromObject(mod.Note) },
|
||||
{ nameof(Mod.Favorite), JToken.FromObject(mod.Favorite) },
|
||||
{ nameof(Mod.PreferredChangedItems), JToken.FromObject(mod.PreferredChangedItems) },
|
||||
{ nameof(Mod.LastConfigEdit), JToken.FromObject(mod.LastConfigEdit) },
|
||||
};
|
||||
|
||||
if (mod.Path.Folder.Length > 0)
|
||||
|
|
@ -40,12 +41,14 @@ public readonly struct ModLocalData(Mod mod) : ISavable
|
|||
{
|
||||
var dataFile = editor.SaveService.FileNames.LocalDataFile(mod);
|
||||
|
||||
var importDate = 0L;
|
||||
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
var importDate = now;
|
||||
var localTags = Enumerable.Empty<string>();
|
||||
var favorite = false;
|
||||
var note = string.Empty;
|
||||
var fileSystemFolder = string.Empty;
|
||||
string? sortOrderName = null;
|
||||
var lastConfigEdit = now;
|
||||
|
||||
HashSet<CustomItemId> preferredChangedItems = [];
|
||||
|
||||
|
|
@ -56,10 +59,11 @@ public readonly struct ModLocalData(Mod mod) : ISavable
|
|||
var text = File.ReadAllText(dataFile);
|
||||
var json = JObject.Parse(text);
|
||||
|
||||
importDate = json[nameof(Mod.ImportDate)]?.Value<long>() ?? importDate;
|
||||
favorite = json[nameof(Mod.Favorite)]?.Value<bool>() ?? favorite;
|
||||
note = json[nameof(Mod.Note)]?.Value<string>() ?? note;
|
||||
localTags = (json[nameof(Mod.LocalTags)] as JArray)?.Values<string>().OfType<string>() ?? localTags;
|
||||
importDate = json[nameof(Mod.ImportDate)]?.Value<long>() ?? importDate;
|
||||
lastConfigEdit = json[nameof(Mod.LastConfigEdit)]?.Value<long>() ?? lastConfigEdit;
|
||||
favorite = json[nameof(Mod.Favorite)]?.Value<bool>() ?? favorite;
|
||||
note = json[nameof(Mod.Note)]?.Value<string>() ?? note;
|
||||
localTags = (json[nameof(Mod.LocalTags)] as JArray)?.Values<string>().OfType<string>() ?? localTags;
|
||||
preferredChangedItems =
|
||||
(json[nameof(Mod.PreferredChangedItems)] as JArray)?.Values<ulong>().Select(i => (CustomItemId)i).ToHashSet()
|
||||
?? mod.DefaultPreferredItems;
|
||||
|
|
@ -84,6 +88,12 @@ public readonly struct ModLocalData(Mod mod) : ISavable
|
|||
changes |= ModDataChangeType.ImportDate;
|
||||
}
|
||||
|
||||
if (mod.LastConfigEdit != lastConfigEdit)
|
||||
{
|
||||
mod.LastConfigEdit = lastConfigEdit;
|
||||
changes |= ModDataChangeType.LastConfigEdit;
|
||||
}
|
||||
|
||||
changes |= UpdateTags(mod, null, localTags);
|
||||
|
||||
if (mod.Favorite != favorite)
|
||||
|
|
@ -124,11 +134,11 @@ public readonly struct ModLocalData(Mod mod) : ISavable
|
|||
|
||||
internal static ModDataChangeType UpdateTags(Mod mod, IEnumerable<string>? newModTags, IEnumerable<string>? newLocalTags)
|
||||
{
|
||||
if (newModTags == null && newLocalTags == null)
|
||||
if (newModTags is null && newLocalTags is null)
|
||||
return 0;
|
||||
|
||||
ModDataChangeType type = 0;
|
||||
if (newModTags != null)
|
||||
if (newModTags is not null)
|
||||
{
|
||||
var modTags = newModTags.Where(t => t.Length > 0).Distinct().ToArray();
|
||||
if (!modTags.SequenceEqual(mod.ModTags))
|
||||
|
|
@ -139,7 +149,7 @@ public readonly struct ModLocalData(Mod mod) : ISavable
|
|||
}
|
||||
}
|
||||
|
||||
if (newLocalTags != null)
|
||||
if (newLocalTags is not null)
|
||||
{
|
||||
var localTags = newLocalTags!.Where(t => t.Length > 0 && !mod.ModTags.Contains(t)).Distinct().ToArray();
|
||||
if (!localTags.SequenceEqual(mod.LocalTags))
|
||||
|
|
|
|||
|
|
@ -20,10 +20,9 @@ using Penumbra.Services;
|
|||
using Penumbra.UI;
|
||||
using Penumbra.UI.AdvancedWindow;
|
||||
using Penumbra.UI.MainWindow;
|
||||
using Penumbra.UI.Tabs;
|
||||
using ChangedItemClick = Penumbra.Communication.ChangedItemClick;
|
||||
using ChangedItemHover = Penumbra.Communication.ChangedItemHover;
|
||||
using DynamisIpc = OtterGui.Services.DynamisIpc;
|
||||
using DynamisIpc = Luna.DynamisIpc;
|
||||
using MessageService = Penumbra.Services.MessageService;
|
||||
using MouseButton = Penumbra.Api.Enums.MouseButton;
|
||||
using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager;
|
||||
|
|
@ -34,7 +33,7 @@ public class Penumbra : IDalamudPlugin
|
|||
{
|
||||
public static readonly OtterGui.Log.Logger Log = new();
|
||||
public static MessageService Messager { get; private set; } = null!;
|
||||
public static DynamisIpc Dynamis { get; private set; } = null!;
|
||||
public static DynamisIpc Dynamis { get; private set; } = null!;
|
||||
|
||||
private readonly ValidityChecker _validityChecker;
|
||||
private readonly ResidentResourceManager _residentResources;
|
||||
|
|
@ -293,8 +292,10 @@ public class Penumbra : IDalamudPlugin
|
|||
return sb.ToString();
|
||||
|
||||
void PrintCollection(ModCollection c, CollectionCache _)
|
||||
=> sb.Append(
|
||||
{
|
||||
sb.Append(
|
||||
$"> **`Collection {c.Identity.AnonymizedName + ':',-18}`** Inheritances: `{c.Inheritance.DirectlyInheritsFrom.Count,3}`, Enabled Mods: `{c.ActualSettings.Count(s => s is { Enabled: true }),4}`, Conflicts: `{c.AllConflicts.SelectMany(x => x).Sum(x => x is { HasPriority: true, Solved: true } ? x.Conflicts.Count : 0),5}/{c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority ? x.Conflicts.Count : 0),5}`\n");
|
||||
}
|
||||
}
|
||||
|
||||
private static string CollectLocaleEnvironmentVariables()
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@
|
|||
<ProjectReference Include="..\Penumbra.Api\Penumbra.Api.csproj" />
|
||||
<ProjectReference Include="..\Penumbra.String\Penumbra.String.csproj" />
|
||||
|
||||
<ProjectReference Include="..\Luna\Luna.Generators\Luna.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
|
||||
<ProjectReference Include="..\Luna\Luna.Generators\Luna.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="GetGitHash" BeforeTargets="GetAssemblyVersion" Returns="InformationalVersion">
|
||||
|
|
|
|||
3
Penumbra/Penumbra.csproj.DotSettings
Normal file
3
Penumbra/Penumbra.csproj.DotSettings
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=ui_005Cmodstab_005Cselector_005Cbuttons/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=ui_005Cmodstab_005Cselector_005Cfilter/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
|
|
@ -1,324 +1,324 @@
|
|||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Dalamud.Plugin.Services;
|
||||
using ImSharp;
|
||||
using Luna;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Widgets;
|
||||
using Penumbra.Communication;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.Mods.Editor;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.String.Classes;
|
||||
using Penumbra.UI.Classes;
|
||||
using MouseWheelType = OtterGui.Widgets.MouseWheelType;
|
||||
|
||||
namespace Penumbra.UI.AdvancedWindow;
|
||||
|
||||
public class FileEditor<T>(
|
||||
ModEditWindow owner,
|
||||
CommunicatorService communicator,
|
||||
IDataManager gameData,
|
||||
Configuration config,
|
||||
FileCompactor compactor,
|
||||
FileDialogService fileDialog,
|
||||
string tabName,
|
||||
string fileType,
|
||||
Func<IReadOnlyList<FileRegistry>> getFiles,
|
||||
Func<T, bool, bool> drawEdit,
|
||||
Func<string> getInitialPath,
|
||||
Func<byte[], string, bool, T?> parseFile)
|
||||
: IDisposable
|
||||
where T : class, IWritable
|
||||
{
|
||||
public void Draw()
|
||||
{
|
||||
using var tab = Im.TabBar.BeginItem(tabName);
|
||||
if (!tab)
|
||||
{
|
||||
_quickImport = null;
|
||||
return;
|
||||
}
|
||||
|
||||
Im.Line.New();
|
||||
DrawFileSelectCombo();
|
||||
SaveButton();
|
||||
Im.Line.Same();
|
||||
ResetButton();
|
||||
Im.Line.Same();
|
||||
RedrawOnSaveBox();
|
||||
Im.Line.Same();
|
||||
DefaultInput();
|
||||
Im.Dummy(new Vector2(Im.Style.TextHeight / 2));
|
||||
|
||||
DrawFilePanel();
|
||||
}
|
||||
|
||||
private void RedrawOnSaveBox()
|
||||
{
|
||||
var redraw = config.Ephemeral.ForceRedrawOnFileChange;
|
||||
if (Im.Checkbox("Redraw on Save"u8, ref redraw))
|
||||
{
|
||||
config.Ephemeral.ForceRedrawOnFileChange = redraw;
|
||||
config.Ephemeral.Save();
|
||||
}
|
||||
|
||||
Im.Tooltip.OnHover("Force a redraw of your player character whenever you save a file here."u8);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
(_currentFile as IDisposable)?.Dispose();
|
||||
_currentFile = null;
|
||||
(_defaultFile as IDisposable)?.Dispose();
|
||||
_defaultFile = null;
|
||||
}
|
||||
|
||||
private FileRegistry? _currentPath;
|
||||
private T? _currentFile;
|
||||
private Exception? _currentException;
|
||||
private bool _changed;
|
||||
|
||||
private string _defaultPath = typeof(T) == typeof(ModEditWindow.PbdTab) ? GamePaths.Pbd.Path : string.Empty;
|
||||
private bool _inInput;
|
||||
private Utf8GamePath _defaultPathUtf8;
|
||||
private bool _isDefaultPathUtf8Valid;
|
||||
private T? _defaultFile;
|
||||
private Exception? _defaultException;
|
||||
|
||||
private readonly Combo _combo = new(getFiles);
|
||||
|
||||
private ModEditWindow.QuickImportAction? _quickImport;
|
||||
|
||||
private void DefaultInput()
|
||||
{
|
||||
using var spacing = ImStyleDouble.ItemSpacing.PushX(Im.Style.GlobalScale * 3);
|
||||
Im.Item.SetNextWidth(Im.ContentRegion.Available.X - 2 * (Im.Style.GlobalScale * 3 + Im.Style.FrameHeight));
|
||||
Im.Input.Text("##defaultInput"u8, ref _defaultPath, "Input game path to compare..."u8, maxLength: Utf8GamePath.MaxGamePathLength);
|
||||
_inInput = Im.Item.Active;
|
||||
if (Im.Item.DeactivatedAfterEdit && _defaultPath.Length > 0)
|
||||
{
|
||||
_isDefaultPathUtf8Valid = Utf8GamePath.FromString(_defaultPath, out _defaultPathUtf8);
|
||||
_quickImport = null;
|
||||
fileDialog.Reset();
|
||||
try
|
||||
{
|
||||
var file = gameData.GetFile(_defaultPath);
|
||||
if (file is not null)
|
||||
{
|
||||
_defaultException = null;
|
||||
(_defaultFile as IDisposable)?.Dispose();
|
||||
_defaultFile = null; // Avoid double disposal if an exception occurs during the parsing of the new file.
|
||||
_defaultFile = parseFile(file.Data, _defaultPath, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_defaultFile = null;
|
||||
_defaultException = new Exception("File does not exist.");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_defaultFile = null;
|
||||
_defaultException = e;
|
||||
}
|
||||
}
|
||||
|
||||
Im.Line.Same();
|
||||
if (ImEx.Icon.Button(LunaStyle.SaveIcon, "Export this file."u8, _defaultFile is null))
|
||||
fileDialog.OpenSavePicker($"Export {_defaultPath} to...", fileType, Path.GetFileNameWithoutExtension(_defaultPath), fileType,
|
||||
(success, name) =>
|
||||
{
|
||||
if (!success)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
compactor.WriteAllBytes(name, _defaultFile?.Write() ?? throw new Exception("File invalid."));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Messager.NotificationMessage(e, $"Could not export {_defaultPath}.", NotificationType.Error);
|
||||
}
|
||||
}, getInitialPath(), false);
|
||||
|
||||
_quickImport ??=
|
||||
ModEditWindow.QuickImportAction.Prepare(owner, _isDefaultPathUtf8Valid ? _defaultPathUtf8 : Utf8GamePath.Empty, _defaultFile);
|
||||
Im.Line.Same();
|
||||
if (ImEx.Icon.Button(LunaStyle.ImportIcon, $"Add a copy of this file to {_quickImport.OptionName}.", !_quickImport.CanExecute))
|
||||
{
|
||||
try
|
||||
{
|
||||
UpdateCurrentFile(_quickImport.Execute());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Log.Error($"Could not add a copy of {_quickImport.GamePath} to {_quickImport.OptionName}:\n{e}");
|
||||
}
|
||||
|
||||
_quickImport = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_currentException = null;
|
||||
_currentPath = null;
|
||||
(_currentFile as IDisposable)?.Dispose();
|
||||
_currentFile = null;
|
||||
_changed = false;
|
||||
}
|
||||
|
||||
private void DrawFileSelectCombo()
|
||||
{
|
||||
if (_combo.Draw("##fileSelect", _currentPath?.RelPath.ToString() ?? $"Select {fileType} File...", string.Empty,
|
||||
Im.ContentRegion.Available.X, Im.Style.TextHeight)
|
||||
&& _combo.CurrentSelection != null)
|
||||
UpdateCurrentFile(_combo.CurrentSelection);
|
||||
}
|
||||
|
||||
private void UpdateCurrentFile(FileRegistry path)
|
||||
{
|
||||
if (ReferenceEquals(_currentPath, path))
|
||||
return;
|
||||
|
||||
_changed = false;
|
||||
_currentPath = path;
|
||||
_currentException = null;
|
||||
try
|
||||
{
|
||||
var bytes = File.ReadAllBytes(_currentPath.File.FullName);
|
||||
(_currentFile as IDisposable)?.Dispose();
|
||||
_currentFile = null; // Avoid double disposal if an exception occurs during the parsing of the new file.
|
||||
_currentFile = parseFile(bytes, _currentPath.File.FullName, true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
(_currentFile as IDisposable)?.Dispose();
|
||||
_currentFile = null;
|
||||
_currentException = e;
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveButton()
|
||||
{
|
||||
var canSave = _changed && _currentFile is { Valid: true };
|
||||
if (ImEx.Button("Save to File"u8, Vector2.Zero,
|
||||
$"Save the selected {fileType} file with all changes applied. This is not revertible.", !canSave))
|
||||
SaveFile();
|
||||
}
|
||||
|
||||
public void SaveFile()
|
||||
{
|
||||
compactor.WriteAllBytes(_currentPath!.File.FullName, _currentFile!.Write());
|
||||
if (owner.Mod is not null)
|
||||
communicator.ModFileChanged.Invoke(new ModFileChanged.Arguments(owner.Mod, _currentPath));
|
||||
_changed = false;
|
||||
}
|
||||
|
||||
private void ResetButton()
|
||||
{
|
||||
if (ImEx.Button("Reset Changes"u8, Vector2.Zero,
|
||||
$"Reset all changes made to the {fileType} file.", !_changed))
|
||||
{
|
||||
var tmp = _currentPath;
|
||||
_currentPath = null;
|
||||
UpdateCurrentFile(tmp!);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawFilePanel()
|
||||
{
|
||||
using var child = Im.Child.Begin("##filePanel"u8, Im.ContentRegion.Available, true);
|
||||
if (!child)
|
||||
return;
|
||||
|
||||
if (_currentPath is not null)
|
||||
{
|
||||
if (_currentFile is null)
|
||||
{
|
||||
Im.Text($"Could not parse selected {fileType} file.");
|
||||
if (_currentException is not null)
|
||||
{
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Dalamud.Plugin.Services;
|
||||
using ImSharp;
|
||||
using Luna;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Widgets;
|
||||
using Penumbra.Communication;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.Mods.Editor;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.String.Classes;
|
||||
using Penumbra.UI.Classes;
|
||||
using MouseWheelType = OtterGui.Widgets.MouseWheelType;
|
||||
|
||||
namespace Penumbra.UI.AdvancedWindow;
|
||||
|
||||
public class FileEditor<T>(
|
||||
ModEditWindow owner,
|
||||
CommunicatorService communicator,
|
||||
IDataManager gameData,
|
||||
Configuration config,
|
||||
FileCompactor compactor,
|
||||
FileDialogService fileDialog,
|
||||
string tabName,
|
||||
string fileType,
|
||||
Func<IReadOnlyList<FileRegistry>> getFiles,
|
||||
Func<T, bool, bool> drawEdit,
|
||||
Func<string> getInitialPath,
|
||||
Func<byte[], string, bool, T?> parseFile)
|
||||
: IDisposable
|
||||
where T : class, IWritable
|
||||
{
|
||||
public void Draw()
|
||||
{
|
||||
using var tab = Im.TabBar.BeginItem(tabName);
|
||||
if (!tab)
|
||||
{
|
||||
_quickImport = null;
|
||||
return;
|
||||
}
|
||||
|
||||
Im.Line.New();
|
||||
DrawFileSelectCombo();
|
||||
SaveButton();
|
||||
Im.Line.Same();
|
||||
ResetButton();
|
||||
Im.Line.Same();
|
||||
RedrawOnSaveBox();
|
||||
Im.Line.Same();
|
||||
DefaultInput();
|
||||
Im.Dummy(new Vector2(Im.Style.TextHeight / 2));
|
||||
|
||||
DrawFilePanel();
|
||||
}
|
||||
|
||||
private void RedrawOnSaveBox()
|
||||
{
|
||||
var redraw = config.Ephemeral.ForceRedrawOnFileChange;
|
||||
if (Im.Checkbox("Redraw on Save"u8, ref redraw))
|
||||
{
|
||||
config.Ephemeral.ForceRedrawOnFileChange = redraw;
|
||||
config.Ephemeral.Save();
|
||||
}
|
||||
|
||||
Im.Tooltip.OnHover("Force a redraw of your player character whenever you save a file here."u8);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
(_currentFile as IDisposable)?.Dispose();
|
||||
_currentFile = null;
|
||||
(_defaultFile as IDisposable)?.Dispose();
|
||||
_defaultFile = null;
|
||||
}
|
||||
|
||||
private FileRegistry? _currentPath;
|
||||
private T? _currentFile;
|
||||
private Exception? _currentException;
|
||||
private bool _changed;
|
||||
|
||||
private string _defaultPath = typeof(T) == typeof(ModEditWindow.PbdTab) ? GamePaths.Pbd.Path : string.Empty;
|
||||
private bool _inInput;
|
||||
private Utf8GamePath _defaultPathUtf8;
|
||||
private bool _isDefaultPathUtf8Valid;
|
||||
private T? _defaultFile;
|
||||
private Exception? _defaultException;
|
||||
|
||||
private readonly Combo _combo = new(getFiles);
|
||||
|
||||
private ModEditWindow.QuickImportAction? _quickImport;
|
||||
|
||||
private void DefaultInput()
|
||||
{
|
||||
using var spacing = ImStyleDouble.ItemSpacing.PushX(Im.Style.GlobalScale * 3);
|
||||
Im.Item.SetNextWidth(Im.ContentRegion.Available.X - 2 * (Im.Style.GlobalScale * 3 + Im.Style.FrameHeight));
|
||||
Im.Input.Text("##defaultInput"u8, ref _defaultPath, "Input game path to compare..."u8, maxLength: Utf8GamePath.MaxGamePathLength);
|
||||
_inInput = Im.Item.Active;
|
||||
if (Im.Item.DeactivatedAfterEdit && _defaultPath.Length > 0)
|
||||
{
|
||||
_isDefaultPathUtf8Valid = Utf8GamePath.FromString(_defaultPath, out _defaultPathUtf8);
|
||||
_quickImport = null;
|
||||
fileDialog.Reset();
|
||||
try
|
||||
{
|
||||
var file = gameData.GetFile(_defaultPath);
|
||||
if (file is not null)
|
||||
{
|
||||
_defaultException = null;
|
||||
(_defaultFile as IDisposable)?.Dispose();
|
||||
_defaultFile = null; // Avoid double disposal if an exception occurs during the parsing of the new file.
|
||||
_defaultFile = parseFile(file.Data, _defaultPath, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_defaultFile = null;
|
||||
_defaultException = new Exception("File does not exist.");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_defaultFile = null;
|
||||
_defaultException = e;
|
||||
}
|
||||
}
|
||||
|
||||
Im.Line.Same();
|
||||
if (ImEx.Icon.Button(LunaStyle.SaveIcon, "Export this file."u8, _defaultFile is null))
|
||||
fileDialog.OpenSavePicker($"Export {_defaultPath} to...", fileType, Path.GetFileNameWithoutExtension(_defaultPath), fileType,
|
||||
(success, name) =>
|
||||
{
|
||||
if (!success)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
compactor.WriteAllBytes(name, _defaultFile?.Write() ?? throw new Exception("File invalid."));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Messager.NotificationMessage(e, $"Could not export {_defaultPath}.", NotificationType.Error);
|
||||
}
|
||||
}, getInitialPath(), false);
|
||||
|
||||
_quickImport ??=
|
||||
ModEditWindow.QuickImportAction.Prepare(owner, _isDefaultPathUtf8Valid ? _defaultPathUtf8 : Utf8GamePath.Empty, _defaultFile);
|
||||
Im.Line.Same();
|
||||
if (ImEx.Icon.Button(LunaStyle.ImportIcon, $"Add a copy of this file to {_quickImport.OptionName}.", !_quickImport.CanExecute))
|
||||
{
|
||||
try
|
||||
{
|
||||
UpdateCurrentFile(_quickImport.Execute());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Log.Error($"Could not add a copy of {_quickImport.GamePath} to {_quickImport.OptionName}:\n{e}");
|
||||
}
|
||||
|
||||
_quickImport = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_currentException = null;
|
||||
_currentPath = null;
|
||||
(_currentFile as IDisposable)?.Dispose();
|
||||
_currentFile = null;
|
||||
_changed = false;
|
||||
}
|
||||
|
||||
private void DrawFileSelectCombo()
|
||||
{
|
||||
if (_combo.Draw("##fileSelect", _currentPath?.RelPath.ToString() ?? $"Select {fileType} File...", string.Empty,
|
||||
Im.ContentRegion.Available.X, Im.Style.TextHeight)
|
||||
&& _combo.CurrentSelection != null)
|
||||
UpdateCurrentFile(_combo.CurrentSelection);
|
||||
}
|
||||
|
||||
private void UpdateCurrentFile(FileRegistry path)
|
||||
{
|
||||
if (ReferenceEquals(_currentPath, path))
|
||||
return;
|
||||
|
||||
_changed = false;
|
||||
_currentPath = path;
|
||||
_currentException = null;
|
||||
try
|
||||
{
|
||||
var bytes = File.ReadAllBytes(_currentPath.File.FullName);
|
||||
(_currentFile as IDisposable)?.Dispose();
|
||||
_currentFile = null; // Avoid double disposal if an exception occurs during the parsing of the new file.
|
||||
_currentFile = parseFile(bytes, _currentPath.File.FullName, true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
(_currentFile as IDisposable)?.Dispose();
|
||||
_currentFile = null;
|
||||
_currentException = e;
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveButton()
|
||||
{
|
||||
var canSave = _changed && _currentFile is { Valid: true };
|
||||
if (ImEx.Button("Save to File"u8, Vector2.Zero,
|
||||
$"Save the selected {fileType} file with all changes applied. This is not revertible.", !canSave))
|
||||
SaveFile();
|
||||
}
|
||||
|
||||
public void SaveFile()
|
||||
{
|
||||
compactor.WriteAllBytes(_currentPath!.File.FullName, _currentFile!.Write());
|
||||
if (owner.Mod is not null)
|
||||
communicator.ModFileChanged.Invoke(new ModFileChanged.Arguments(owner.Mod, _currentPath));
|
||||
_changed = false;
|
||||
}
|
||||
|
||||
private void ResetButton()
|
||||
{
|
||||
if (ImEx.Button("Reset Changes"u8, Vector2.Zero,
|
||||
$"Reset all changes made to the {fileType} file.", !_changed))
|
||||
{
|
||||
var tmp = _currentPath;
|
||||
_currentPath = null;
|
||||
UpdateCurrentFile(tmp!);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawFilePanel()
|
||||
{
|
||||
using var child = Im.Child.Begin("##filePanel"u8, Im.ContentRegion.Available, true);
|
||||
if (!child)
|
||||
return;
|
||||
|
||||
if (_currentPath is not null)
|
||||
{
|
||||
if (_currentFile is null)
|
||||
{
|
||||
Im.Text($"Could not parse selected {fileType} file.");
|
||||
if (_currentException is not null)
|
||||
{
|
||||
using var tab = Im.Indent();
|
||||
Im.TextWrapped($"{_currentException}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using var id = Im.Id.Push(0);
|
||||
_changed |= drawEdit(_currentFile, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!_inInput && _defaultPath.Length > 0)
|
||||
{
|
||||
if (_currentPath is not null)
|
||||
{
|
||||
Im.Line.New();
|
||||
Im.Line.New();
|
||||
Im.Text($"Preview of {_defaultPath}:");
|
||||
Im.Separator();
|
||||
}
|
||||
|
||||
if (_defaultFile == null)
|
||||
{
|
||||
Im.Text($"Could not parse provided {fileType} game file:\n");
|
||||
if (_defaultException is not null)
|
||||
{
|
||||
using var tab = Im.Indent();
|
||||
Im.TextWrapped($"{_defaultException}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using var id = Im.Id.Push(1);
|
||||
drawEdit(_defaultFile, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class Combo(Func<IReadOnlyList<FileRegistry>> generator)
|
||||
: FilterComboCache<FileRegistry>(generator, MouseWheelType.None, Penumbra.Log)
|
||||
{
|
||||
protected override bool DrawSelectable(int globalIdx, bool selected)
|
||||
{
|
||||
var file = Items[globalIdx];
|
||||
bool ret;
|
||||
using (ImGuiColor.Text.Push(ColorId.HandledConflictMod.Value(), file.IsOnPlayer))
|
||||
{
|
||||
ret = Im.Selectable(file.RelPath.ToString(), selected);
|
||||
}
|
||||
|
||||
if (Im.Item.Hovered())
|
||||
{
|
||||
using var tt = Im.Tooltip.Begin();
|
||||
Im.Text("All Game Paths"u8);
|
||||
Im.Separator();
|
||||
using var t = Im.Table.Begin("##Tooltip"u8, 2, TableFlags.SizingFixedFit);
|
||||
if (t)
|
||||
{
|
||||
foreach (var (option, gamePath) in file.SubModUsage)
|
||||
{
|
||||
Im.TextWrapped($"{_currentException}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using var id = Im.Id.Push(0);
|
||||
_changed |= drawEdit(_currentFile, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!_inInput && _defaultPath.Length > 0)
|
||||
{
|
||||
if (_currentPath is not null)
|
||||
{
|
||||
Im.Line.New();
|
||||
Im.Line.New();
|
||||
Im.Text($"Preview of {_defaultPath}:");
|
||||
Im.Separator();
|
||||
}
|
||||
|
||||
if (_defaultFile == null)
|
||||
{
|
||||
Im.Text($"Could not parse provided {fileType} game file:\n");
|
||||
if (_defaultException is not null)
|
||||
{
|
||||
using var tab = Im.Indent();
|
||||
Im.TextWrapped($"{_defaultException}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using var id = Im.Id.Push(1);
|
||||
drawEdit(_defaultFile, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class Combo(Func<IReadOnlyList<FileRegistry>> generator)
|
||||
: FilterComboCache<FileRegistry>(generator, MouseWheelType.None, Penumbra.Log)
|
||||
{
|
||||
protected override bool DrawSelectable(int globalIdx, bool selected)
|
||||
{
|
||||
var file = Items[globalIdx];
|
||||
bool ret;
|
||||
using (ImGuiColor.Text.Push(ColorId.HandledConflictMod.Value(), file.IsOnPlayer))
|
||||
{
|
||||
ret = Im.Selectable(file.RelPath.ToString(), selected);
|
||||
}
|
||||
|
||||
if (Im.Item.Hovered())
|
||||
{
|
||||
using var tt = Im.Tooltip.Begin();
|
||||
Im.Text("All Game Paths"u8);
|
||||
Im.Separator();
|
||||
using var t = Im.Table.Begin("##Tooltip"u8, 2, TableFlags.SizingFixedFit);
|
||||
if (t)
|
||||
{
|
||||
foreach (var (option, gamePath) in file.SubModUsage)
|
||||
{
|
||||
t.DrawColumn(gamePath.Path.Span);
|
||||
using var color = ImGuiColor.Text.Push(ColorId.ItemId.Value());
|
||||
t.DrawColumn(option.GetFullName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (file.SubModUsage.Count > 0)
|
||||
{
|
||||
Im.Line.Same();
|
||||
using var color = ImGuiColor.Text.Push(ColorId.ItemId.Value());
|
||||
ImEx.TextRightAligned($"{file.SubModUsage[0].Item2.Path}");
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
protected override bool IsVisible(int globalIndex, LowerString filter)
|
||||
=> filter.IsContained(Items[globalIndex].File.FullName)
|
||||
|| Items[globalIndex].SubModUsage.Any(f => filter.IsContained(f.Item2.ToString()));
|
||||
}
|
||||
}
|
||||
using var color = ImGuiColor.Text.Push(ColorId.ItemId.Value());
|
||||
t.DrawColumn(option.GetFullName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (file.SubModUsage.Count > 0)
|
||||
{
|
||||
Im.Line.Same();
|
||||
using var color = ImGuiColor.Text.Push(ColorId.ItemId.Value());
|
||||
ImEx.TextRightAligned($"{file.SubModUsage[0].Item2.Path}");
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
protected override bool IsVisible(int globalIndex, LowerString filter)
|
||||
=> filter.IsContained(Items[globalIndex].File.FullName)
|
||||
|| Items[globalIndex].SubModUsage.Any(f => filter.IsContained(f.Item2.ToString()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,280 +1,284 @@
|
|||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using ImSharp;
|
||||
using Luna;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Penumbra.Collections.Cache;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.GameData.Files.AtchStructs;
|
||||
using Penumbra.Meta;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods.Editor;
|
||||
using Penumbra.UI.Combos;
|
||||
using Notification = Luna.Notification;
|
||||
|
||||
namespace Penumbra.UI.AdvancedWindow.Meta;
|
||||
|
||||
public sealed class AtchMetaDrawer : MetaDrawer<AtchIdentifier, AtchEntry>
|
||||
{
|
||||
public override ReadOnlySpan<byte> Label
|
||||
=> "Attachment Points (ATCH)###ATCH"u8;
|
||||
|
||||
public override int NumColumns
|
||||
=> 10;
|
||||
|
||||
public override float ColumnHeight
|
||||
=> 2 * Im.Style.FrameHeightWithSpacing;
|
||||
|
||||
private AtchFile? _currentBaseAtchFile;
|
||||
private AtchPoint? _currentBaseAtchPoint;
|
||||
private readonly AtchPointCombo _combo;
|
||||
|
||||
public AtchMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles)
|
||||
: base(editor, metaFiles)
|
||||
{
|
||||
_combo = new AtchPointCombo(() => _currentBaseAtchFile?.Points.Select(p => p.Type).ToList() ?? []);
|
||||
}
|
||||
|
||||
private sealed class RaceCodeException(string filePath) : Exception($"Could not identify race code from path {filePath}.");
|
||||
|
||||
public void ImportFile(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (filePath.Length == 0 || !File.Exists(filePath))
|
||||
throw new FileNotFoundException();
|
||||
|
||||
var gr = Parser.ParseRaceCode(filePath);
|
||||
if (gr is GenderRace.Unknown)
|
||||
throw new RaceCodeException(filePath);
|
||||
|
||||
var text = File.ReadAllBytes(filePath);
|
||||
var file = new AtchFile(text);
|
||||
foreach (var point in file.Points)
|
||||
{
|
||||
foreach (var (index, entry) in point.Entries.Index())
|
||||
{
|
||||
var identifier = new AtchIdentifier(point.Type, gr, (ushort)index);
|
||||
var defaultValue = AtchCache.GetDefault(MetaFiles, identifier);
|
||||
if (defaultValue is null)
|
||||
continue;
|
||||
|
||||
if (defaultValue.Value.Equals(entry))
|
||||
Editor.Changes |= Editor.Remove(identifier);
|
||||
else
|
||||
Editor.Changes |= Editor.TryAdd(identifier, entry) || Editor.Update(identifier, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (RaceCodeException ex)
|
||||
{
|
||||
Penumbra.Messager.AddMessage(new Notification(ex, "The imported .atch file does not contain a race code (cXXXX) in its name.",
|
||||
"Could not import .atch file:", NotificationType.Warning));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Penumbra.Messager.AddMessage(new Notification(ex, "Unable to import .atch file.", "Could not import .atch file:", NotificationType.Warning));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected override void DrawNew()
|
||||
{
|
||||
Im.Table.NextColumn();
|
||||
CopyToClipboardButton("Copy all current ATCH manipulations to clipboard."u8,
|
||||
new Lazy<JToken?>(() => MetaDictionary.SerializeTo([], Editor.Atch)));
|
||||
|
||||
Im.Table.NextColumn();
|
||||
var canAdd = !Editor.Contains(Identifier);
|
||||
var tt = canAdd ? "Stage this edit."u8 : "This entry is already edited."u8;
|
||||
if (ImEx.Icon.Button(LunaStyle.AddObjectIcon, tt, disabled: !canAdd))
|
||||
Editor.Changes |= Editor.TryAdd(Identifier, Entry);
|
||||
|
||||
if (DrawIdentifierInput(ref Identifier))
|
||||
UpdateEntry();
|
||||
|
||||
var defaultEntry = AtchCache.GetDefault(MetaFiles, Identifier) ?? default;
|
||||
DrawEntry(defaultEntry, ref defaultEntry, true);
|
||||
}
|
||||
|
||||
private void UpdateEntry()
|
||||
=> Entry = _currentBaseAtchPoint!.Entries[Identifier.EntryIndex];
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
_currentBaseAtchFile = MetaFiles.AtchManager.AtchFileBase[GenderRace.MidlanderMale];
|
||||
_currentBaseAtchPoint = _currentBaseAtchFile.Points.First();
|
||||
Identifier = new AtchIdentifier(_currentBaseAtchPoint.Type, GenderRace.MidlanderMale, 0);
|
||||
Entry = _currentBaseAtchPoint.Entries[0];
|
||||
}
|
||||
|
||||
protected override void DrawEntry(AtchIdentifier identifier, AtchEntry entry)
|
||||
{
|
||||
DrawMetaButtons(identifier, entry);
|
||||
DrawIdentifier(identifier);
|
||||
|
||||
var defaultEntry = AtchCache.GetDefault(MetaFiles, identifier) ?? default;
|
||||
if (DrawEntry(defaultEntry, ref entry, false))
|
||||
Editor.Changes |= Editor.Update(identifier, entry);
|
||||
}
|
||||
|
||||
protected override IEnumerable<(AtchIdentifier, AtchEntry)> Enumerate()
|
||||
=> Editor.Atch.Select(kvp => (kvp.Key, kvp.Value))
|
||||
.OrderBy(p => p.Key.GenderRace)
|
||||
.ThenBy(p => p.Key.Type)
|
||||
.ThenBy(p => p.Key.EntryIndex);
|
||||
|
||||
protected override int Count
|
||||
=> Editor.Atch.Count;
|
||||
|
||||
private bool DrawIdentifierInput(ref AtchIdentifier identifier)
|
||||
{
|
||||
var changes = false;
|
||||
Im.Table.NextColumn();
|
||||
changes |= DrawRace(ref identifier);
|
||||
Im.Table.NextColumn();
|
||||
changes |= DrawGender(ref identifier, false);
|
||||
if (changes)
|
||||
UpdateFile();
|
||||
Im.Table.NextColumn();
|
||||
if (DrawPointInput(ref identifier, _combo))
|
||||
{
|
||||
_currentBaseAtchPoint = _currentBaseAtchFile?.GetPoint(identifier.Type);
|
||||
changes = true;
|
||||
}
|
||||
|
||||
Im.Table.NextColumn();
|
||||
changes |= DrawEntryIndexInput(ref identifier, _currentBaseAtchPoint!);
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
private void UpdateFile()
|
||||
{
|
||||
_currentBaseAtchFile = MetaFiles.AtchManager.AtchFileBase[Identifier.GenderRace];
|
||||
_currentBaseAtchPoint = _currentBaseAtchFile.GetPoint(Identifier.Type);
|
||||
if (_currentBaseAtchPoint is null)
|
||||
{
|
||||
_currentBaseAtchPoint = _currentBaseAtchFile.Points.First();
|
||||
Identifier = Identifier with { Type = _currentBaseAtchPoint.Type };
|
||||
}
|
||||
|
||||
if (Identifier.EntryIndex >= _currentBaseAtchPoint.Entries.Length)
|
||||
Identifier = Identifier with { EntryIndex = 0 };
|
||||
}
|
||||
|
||||
private static void DrawIdentifier(AtchIdentifier identifier)
|
||||
{
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using ImSharp;
|
||||
using Luna;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Penumbra.Collections.Cache;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.GameData.Files.AtchStructs;
|
||||
using Penumbra.Meta;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods.Editor;
|
||||
using Penumbra.UI.Combos;
|
||||
using Notification = Luna.Notification;
|
||||
|
||||
namespace Penumbra.UI.AdvancedWindow.Meta;
|
||||
|
||||
public sealed class AtchMetaDrawer : MetaDrawer<AtchIdentifier, AtchEntry>
|
||||
{
|
||||
public override ReadOnlySpan<byte> Label
|
||||
=> "Attachment Points (ATCH)###ATCH"u8;
|
||||
|
||||
public override int NumColumns
|
||||
=> 10;
|
||||
|
||||
public override float ColumnHeight
|
||||
=> 2 * Im.Style.FrameHeightWithSpacing;
|
||||
|
||||
private AtchFile? _currentBaseAtchFile;
|
||||
private AtchPoint? _currentBaseAtchPoint;
|
||||
private readonly AtchPointCombo _combo;
|
||||
|
||||
public AtchMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles)
|
||||
: base(editor, metaFiles)
|
||||
{
|
||||
_combo = new AtchPointCombo(this);
|
||||
}
|
||||
|
||||
public IEnumerable<AtchType> GetPoints()
|
||||
=> _currentBaseAtchFile?.Points.Select(p => p.Type) ?? [];
|
||||
|
||||
private sealed class RaceCodeException(string filePath) : Exception($"Could not identify race code from path {filePath}.");
|
||||
|
||||
public void ImportFile(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (filePath.Length == 0 || !File.Exists(filePath))
|
||||
throw new FileNotFoundException();
|
||||
|
||||
var gr = Parser.ParseRaceCode(filePath);
|
||||
if (gr is GenderRace.Unknown)
|
||||
throw new RaceCodeException(filePath);
|
||||
|
||||
var text = File.ReadAllBytes(filePath);
|
||||
var file = new AtchFile(text);
|
||||
foreach (var point in file.Points)
|
||||
{
|
||||
foreach (var (index, entry) in point.Entries.Index())
|
||||
{
|
||||
var identifier = new AtchIdentifier(point.Type, gr, (ushort)index);
|
||||
var defaultValue = AtchCache.GetDefault(MetaFiles, identifier);
|
||||
if (defaultValue is null)
|
||||
continue;
|
||||
|
||||
if (defaultValue.Value.Equals(entry))
|
||||
Editor.Changes |= Editor.Remove(identifier);
|
||||
else
|
||||
Editor.Changes |= Editor.TryAdd(identifier, entry) || Editor.Update(identifier, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (RaceCodeException ex)
|
||||
{
|
||||
Penumbra.Messager.AddMessage(new Notification(ex, "The imported .atch file does not contain a race code (cXXXX) in its name.",
|
||||
"Could not import .atch file:", NotificationType.Warning));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Penumbra.Messager.AddMessage(new Notification(ex, "Unable to import .atch file.", "Could not import .atch file:",
|
||||
NotificationType.Warning));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected override void DrawNew()
|
||||
{
|
||||
Im.Table.NextColumn();
|
||||
ImEx.TextFramed(identifier.Race.ToNameU8(), default, FrameColor);
|
||||
Im.Tooltip.OnHover("Model Race"u8);
|
||||
|
||||
Im.Table.NextColumn();
|
||||
DrawGender(ref identifier, true);
|
||||
|
||||
Im.Table.NextColumn();
|
||||
ImEx.TextFramed(identifier.Type.ToName(), default, FrameColor);
|
||||
Im.Tooltip.OnHover("Attachment Point Type"u8);
|
||||
|
||||
Im.Table.NextColumn();
|
||||
ImEx.TextFramed($"{identifier.EntryIndex}", default, FrameColor);
|
||||
Im.Tooltip.OnHover("State Entry Index"u8);
|
||||
}
|
||||
|
||||
private static bool DrawEntry(in AtchEntry defaultEntry, ref AtchEntry entry, bool disabled)
|
||||
{
|
||||
var changes = false;
|
||||
using var dis = Im.Disabled(disabled);
|
||||
if (defaultEntry.Bone.Length == 0)
|
||||
return false;
|
||||
|
||||
Im.Table.NextColumn();
|
||||
Im.Item.SetNextWidthScaled(200);
|
||||
if (Im.Input.Text("##BoneName"u8, entry.FullSpan, out StringU8 newBone))
|
||||
{
|
||||
entry.SetBoneName(newBone);
|
||||
changes = true;
|
||||
}
|
||||
|
||||
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Bone Name"u8);
|
||||
|
||||
Im.Item.SetNextWidthScaled(200);
|
||||
changes |= Im.Input.Scalar("##AtchScale"u8, ref entry.Scale);
|
||||
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Scale"u8);
|
||||
|
||||
Im.Table.NextColumn();
|
||||
Im.Item.SetNextWidthScaled(120);
|
||||
changes |= Im.Input.Scalar("##AtchOffsetX"u8, ref entry.OffsetX);
|
||||
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Offset X-Coordinate"u8);
|
||||
Im.Item.SetNextWidthScaled(120);
|
||||
changes |= Im.Input.Scalar("##AtchRotationX"u8, ref entry.RotationX);
|
||||
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Rotation X-Axis"u8);
|
||||
|
||||
Im.Table.NextColumn();
|
||||
Im.Item.SetNextWidthScaled(120);
|
||||
changes |= Im.Input.Scalar("##AtchOffsetY"u8, ref entry.OffsetY);
|
||||
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Offset Y-Coordinate"u8);
|
||||
Im.Item.SetNextWidthScaled(120);
|
||||
changes |= Im.Input.Scalar("##AtchRotationY"u8, ref entry.RotationY);
|
||||
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Rotation Y-Axis"u8);
|
||||
|
||||
Im.Table.NextColumn();
|
||||
Im.Item.SetNextWidthScaled(120);
|
||||
changes |= Im.Input.Scalar("##AtchOffsetZ"u8, ref entry.OffsetZ);
|
||||
Im.Tooltip.OnHover("Offset Z-Coordinate"u8);
|
||||
Im.Item.SetNextWidthScaled(120);
|
||||
changes |= Im.Input.Scalar("##AtchRotationZ"u8, ref entry.RotationZ);
|
||||
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Rotation Z-Axis"u8);
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
private static bool DrawRace(ref AtchIdentifier identifier, float unscaledWidth = 100)
|
||||
{
|
||||
var ret = Combos.Combos.Race("##atchRace", identifier.Race, out var race, unscaledWidth);
|
||||
Im.Tooltip.OnHover("Model Race"u8);
|
||||
if (ret)
|
||||
identifier = identifier with { GenderRace = Names.CombinedRace(identifier.Gender, race) };
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static bool DrawGender(ref AtchIdentifier identifier, bool disabled)
|
||||
{
|
||||
var isMale = identifier.Gender is Gender.Male;
|
||||
|
||||
if (!ImEx.Icon.Button(isMale ? FontAwesomeIcon.Mars.Icon() : FontAwesomeIcon.Venus.Icon(), "Gender"u8, buttonColor: disabled ? 0x000F0000u : 0)
|
||||
|| disabled)
|
||||
return false;
|
||||
|
||||
identifier = identifier with { GenderRace = Names.CombinedRace(isMale ? Gender.Female : Gender.Male, identifier.Race) };
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool DrawPointInput(ref AtchIdentifier identifier, AtchPointCombo combo)
|
||||
{
|
||||
if (!combo.Draw("##AtchPoint", identifier.Type.ToName(), "Attachment Point Type", 160 * Im.Style.GlobalScale,
|
||||
Im.Style.TextHeightWithSpacing))
|
||||
return false;
|
||||
|
||||
identifier = identifier with { Type = combo.CurrentSelection };
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool DrawEntryIndexInput(ref AtchIdentifier identifier, AtchPoint currentAtchPoint)
|
||||
{
|
||||
var index = identifier.EntryIndex;
|
||||
Im.Item.SetNextWidth(40 * Im.Style.GlobalScale);
|
||||
var ret = Im.Drag("##AtchEntry"u8, ref index, 0, (ushort)(currentAtchPoint.Entries.Length - 1), 0.05f,
|
||||
SliderFlags.AlwaysClamp);
|
||||
Im.Tooltip.OnHover("State Entry Index"u8);
|
||||
if (!ret)
|
||||
return false;
|
||||
|
||||
index = Math.Clamp(index, (ushort)0, (ushort)(currentAtchPoint.Entries.Length - 1));
|
||||
identifier = identifier with { EntryIndex = index };
|
||||
return true;
|
||||
}
|
||||
CopyToClipboardButton("Copy all current ATCH manipulations to clipboard."u8,
|
||||
new Lazy<JToken?>(() => MetaDictionary.SerializeTo([], Editor.Atch)));
|
||||
|
||||
Im.Table.NextColumn();
|
||||
var canAdd = !Editor.Contains(Identifier);
|
||||
var tt = canAdd ? "Stage this edit."u8 : "This entry is already edited."u8;
|
||||
if (ImEx.Icon.Button(LunaStyle.AddObjectIcon, tt, !canAdd))
|
||||
Editor.Changes |= Editor.TryAdd(Identifier, Entry);
|
||||
|
||||
if (DrawIdentifierInput(ref Identifier))
|
||||
UpdateEntry();
|
||||
|
||||
var defaultEntry = AtchCache.GetDefault(MetaFiles, Identifier) ?? default;
|
||||
DrawEntry(defaultEntry, ref defaultEntry, true);
|
||||
}
|
||||
|
||||
private void UpdateEntry()
|
||||
=> Entry = _currentBaseAtchPoint!.Entries[Identifier.EntryIndex];
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
_currentBaseAtchFile = MetaFiles.AtchManager.AtchFileBase[GenderRace.MidlanderMale];
|
||||
_currentBaseAtchPoint = _currentBaseAtchFile.Points.First();
|
||||
Identifier = new AtchIdentifier(_currentBaseAtchPoint.Type, GenderRace.MidlanderMale, 0);
|
||||
Entry = _currentBaseAtchPoint.Entries[0];
|
||||
}
|
||||
|
||||
protected override void DrawEntry(AtchIdentifier identifier, AtchEntry entry)
|
||||
{
|
||||
DrawMetaButtons(identifier, entry);
|
||||
DrawIdentifier(identifier);
|
||||
|
||||
var defaultEntry = AtchCache.GetDefault(MetaFiles, identifier) ?? default;
|
||||
if (DrawEntry(defaultEntry, ref entry, false))
|
||||
Editor.Changes |= Editor.Update(identifier, entry);
|
||||
}
|
||||
|
||||
protected override IEnumerable<(AtchIdentifier, AtchEntry)> Enumerate()
|
||||
=> Editor.Atch.Select(kvp => (kvp.Key, kvp.Value))
|
||||
.OrderBy(p => p.Key.GenderRace)
|
||||
.ThenBy(p => p.Key.Type)
|
||||
.ThenBy(p => p.Key.EntryIndex);
|
||||
|
||||
protected override int Count
|
||||
=> Editor.Atch.Count;
|
||||
|
||||
private bool DrawIdentifierInput(ref AtchIdentifier identifier)
|
||||
{
|
||||
var changes = false;
|
||||
Im.Table.NextColumn();
|
||||
changes |= DrawRace(ref identifier);
|
||||
Im.Table.NextColumn();
|
||||
changes |= DrawGender(ref identifier, false);
|
||||
if (changes)
|
||||
UpdateFile();
|
||||
Im.Table.NextColumn();
|
||||
if (DrawPointInput(ref identifier, _combo))
|
||||
{
|
||||
_currentBaseAtchPoint = _currentBaseAtchFile?.GetPoint(identifier.Type);
|
||||
changes = true;
|
||||
}
|
||||
|
||||
Im.Table.NextColumn();
|
||||
changes |= DrawEntryIndexInput(ref identifier, _currentBaseAtchPoint!);
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
private void UpdateFile()
|
||||
{
|
||||
_currentBaseAtchFile = MetaFiles.AtchManager.AtchFileBase[Identifier.GenderRace];
|
||||
_currentBaseAtchPoint = _currentBaseAtchFile.GetPoint(Identifier.Type);
|
||||
if (_currentBaseAtchPoint is null)
|
||||
{
|
||||
_currentBaseAtchPoint = _currentBaseAtchFile.Points.First();
|
||||
Identifier = Identifier with { Type = _currentBaseAtchPoint.Type };
|
||||
}
|
||||
|
||||
if (Identifier.EntryIndex >= _currentBaseAtchPoint.Entries.Length)
|
||||
Identifier = Identifier with { EntryIndex = 0 };
|
||||
}
|
||||
|
||||
private static void DrawIdentifier(AtchIdentifier identifier)
|
||||
{
|
||||
Im.Table.NextColumn();
|
||||
ImEx.TextFramed(identifier.Race.ToNameU8(), default, FrameColor);
|
||||
Im.Tooltip.OnHover("Model Race"u8);
|
||||
|
||||
Im.Table.NextColumn();
|
||||
DrawGender(ref identifier, true);
|
||||
|
||||
Im.Table.NextColumn();
|
||||
ImEx.TextFramed(identifier.Type.ToName(), default, FrameColor);
|
||||
Im.Tooltip.OnHover("Attachment Point Type"u8);
|
||||
|
||||
Im.Table.NextColumn();
|
||||
ImEx.TextFramed($"{identifier.EntryIndex}", default, FrameColor);
|
||||
Im.Tooltip.OnHover("State Entry Index"u8);
|
||||
}
|
||||
|
||||
private static bool DrawEntry(in AtchEntry defaultEntry, ref AtchEntry entry, bool disabled)
|
||||
{
|
||||
var changes = false;
|
||||
using var dis = Im.Disabled(disabled);
|
||||
if (defaultEntry.Bone.Length == 0)
|
||||
return false;
|
||||
|
||||
Im.Table.NextColumn();
|
||||
Im.Item.SetNextWidthScaled(200);
|
||||
if (Im.Input.Text("##BoneName"u8, entry.FullSpan, out StringU8 newBone))
|
||||
{
|
||||
entry.SetBoneName(newBone);
|
||||
changes = true;
|
||||
}
|
||||
|
||||
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Bone Name"u8);
|
||||
|
||||
Im.Item.SetNextWidthScaled(200);
|
||||
changes |= Im.Input.Scalar("##AtchScale"u8, ref entry.Scale);
|
||||
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Scale"u8);
|
||||
|
||||
Im.Table.NextColumn();
|
||||
Im.Item.SetNextWidthScaled(120);
|
||||
changes |= Im.Input.Scalar("##AtchOffsetX"u8, ref entry.OffsetX);
|
||||
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Offset X-Coordinate"u8);
|
||||
Im.Item.SetNextWidthScaled(120);
|
||||
changes |= Im.Input.Scalar("##AtchRotationX"u8, ref entry.RotationX);
|
||||
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Rotation X-Axis"u8);
|
||||
|
||||
Im.Table.NextColumn();
|
||||
Im.Item.SetNextWidthScaled(120);
|
||||
changes |= Im.Input.Scalar("##AtchOffsetY"u8, ref entry.OffsetY);
|
||||
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Offset Y-Coordinate"u8);
|
||||
Im.Item.SetNextWidthScaled(120);
|
||||
changes |= Im.Input.Scalar("##AtchRotationY"u8, ref entry.RotationY);
|
||||
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Rotation Y-Axis"u8);
|
||||
|
||||
Im.Table.NextColumn();
|
||||
Im.Item.SetNextWidthScaled(120);
|
||||
changes |= Im.Input.Scalar("##AtchOffsetZ"u8, ref entry.OffsetZ);
|
||||
Im.Tooltip.OnHover("Offset Z-Coordinate"u8);
|
||||
Im.Item.SetNextWidthScaled(120);
|
||||
changes |= Im.Input.Scalar("##AtchRotationZ"u8, ref entry.RotationZ);
|
||||
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Rotation Z-Axis"u8);
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
private static bool DrawRace(ref AtchIdentifier identifier, float unscaledWidth = 100)
|
||||
{
|
||||
var ret = Combos.Combos.Race("##atchRace", identifier.Race, out var race, unscaledWidth);
|
||||
Im.Tooltip.OnHover("Model Race"u8);
|
||||
if (ret)
|
||||
identifier = identifier with { GenderRace = Names.CombinedRace(identifier.Gender, race) };
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static bool DrawGender(ref AtchIdentifier identifier, bool disabled)
|
||||
{
|
||||
var isMale = identifier.Gender is Gender.Male;
|
||||
|
||||
if (!ImEx.Icon.Button(isMale ? FontAwesomeIcon.Mars.Icon() : FontAwesomeIcon.Venus.Icon(), "Gender"u8,
|
||||
buttonColor: disabled ? 0x000F0000u : 0)
|
||||
|| disabled)
|
||||
return false;
|
||||
|
||||
identifier = identifier with { GenderRace = Names.CombinedRace(isMale ? Gender.Female : Gender.Male, identifier.Race) };
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool DrawPointInput(ref AtchIdentifier identifier, AtchPointCombo combo)
|
||||
{
|
||||
if (!combo.Draw("##AtchPoint"u8, identifier.Type, "Attachment Point Type"u8, 160 * Im.Style.GlobalScale, out var newType))
|
||||
return false;
|
||||
|
||||
identifier = identifier with { Type = newType };
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool DrawEntryIndexInput(ref AtchIdentifier identifier, AtchPoint currentAtchPoint)
|
||||
{
|
||||
var index = identifier.EntryIndex;
|
||||
Im.Item.SetNextWidth(40 * Im.Style.GlobalScale);
|
||||
var ret = Im.Drag("##AtchEntry"u8, ref index, 0, (ushort)(currentAtchPoint.Entries.Length - 1), 0.05f,
|
||||
SliderFlags.AlwaysClamp);
|
||||
Im.Tooltip.OnHover("State Entry Index"u8);
|
||||
if (!ret)
|
||||
return false;
|
||||
|
||||
index = Math.Clamp(index, (ushort)0, (ushort)(currentAtchPoint.Entries.Length - 1));
|
||||
identifier = identifier with { EntryIndex = index };
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ using ImSharp;
|
|||
using Luna;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui;
|
||||
using OtterGui.Text;
|
||||
using Penumbra.Meta;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods.Editor;
|
||||
|
|
@ -58,7 +57,7 @@ public abstract class MetaDrawer<TIdentifier, TEntry>(ModMetaEditor editor, Meta
|
|||
public abstract int NumColumns { get; }
|
||||
|
||||
public virtual float ColumnHeight
|
||||
=> ImUtf8.FrameHeightSpacing;
|
||||
=> Im.Style.FrameHeightWithSpacing;
|
||||
|
||||
protected abstract void DrawNew();
|
||||
protected abstract void Initialize();
|
||||
|
|
@ -78,7 +77,7 @@ public abstract class MetaDrawer<TIdentifier, TEntry>(ModMetaEditor editor, Meta
|
|||
int tmp = currentId;
|
||||
Im.Item.SetNextWidth(unscaledWidth * Im.Style.GlobalScale);
|
||||
using var style = ImStyleBorder.Frame.Push(Colors.RegexWarningBorder, Im.Style.GlobalScale, border);
|
||||
if (ImUtf8.InputScalar(label, ref tmp))
|
||||
if (Im.Input.Scalar(label, ref tmp))
|
||||
tmp = Math.Clamp(tmp, minId, maxId);
|
||||
|
||||
newId = (ushort)tmp;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ using Dalamud.Interface;
|
|||
using ImSharp;
|
||||
using Lumina.Data.Parsing;
|
||||
using Luna;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Widgets;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.GameData.Files;
|
||||
|
|
@ -10,6 +9,7 @@ using Penumbra.Import.Models;
|
|||
using Penumbra.Import.Models.Import;
|
||||
using Penumbra.String.Classes;
|
||||
using Penumbra.UI.Classes;
|
||||
using TagButtons = Luna.TagButtons;
|
||||
|
||||
namespace Penumbra.UI.AdvancedWindow;
|
||||
|
||||
|
|
@ -29,7 +29,6 @@ public partial class ModEditWindow
|
|||
private class LoadedData
|
||||
{
|
||||
public MdlFile LastFile = null!;
|
||||
public readonly List<TagButtons> SubMeshAttributeTags = [];
|
||||
public long[] LodTriCount = [];
|
||||
}
|
||||
|
||||
|
|
@ -49,15 +48,6 @@ public partial class ModEditWindow
|
|||
return data;
|
||||
|
||||
data.LastFile = file;
|
||||
var subMeshTotal = file.Meshes.Aggregate(0, (count, mesh) => count + mesh.SubMeshCount);
|
||||
if (data.SubMeshAttributeTags.Count != subMeshTotal)
|
||||
{
|
||||
data.SubMeshAttributeTags.Clear();
|
||||
data.SubMeshAttributeTags.AddRange(
|
||||
Enumerable.Range(0, subMeshTotal).Select(_ => new TagButtons())
|
||||
);
|
||||
}
|
||||
|
||||
data.LodTriCount = Enumerable.Range(0, file.Lods.Length).Select(l => GetTriangleCountForLod(file, l)).ToArray();
|
||||
return data;
|
||||
}
|
||||
|
|
@ -122,7 +112,8 @@ public partial class ModEditWindow
|
|||
return true;
|
||||
});
|
||||
|
||||
using (ImRaii.FramedGroup("Import", size, headerPreIcon: FontAwesomeIcon.FileImport))
|
||||
using (ImEx.FramedGroup("Import"u8, LunaStyle.ImportIcon, default, StringU8.Empty, ColorParameter.Default, ColorParameter.Default,
|
||||
size))
|
||||
{
|
||||
Im.Checkbox("Keep current materials"u8, ref tab.ImportKeepMaterials);
|
||||
Im.Checkbox("Keep current attributes"u8, ref tab.ImportKeepAttributes);
|
||||
|
|
@ -144,8 +135,9 @@ public partial class ModEditWindow
|
|||
|
||||
private void DrawExport(MdlTab tab, Vector2 size, bool _)
|
||||
{
|
||||
using var id = Im.Id.Push("export"u8);
|
||||
using var frame = ImRaii.FramedGroup("Export", size, headerPreIcon: FontAwesomeIcon.FileExport);
|
||||
using var id = Im.Id.Push("export"u8);
|
||||
using var frame = ImEx.FramedGroup("Export"u8, LunaStyle.FileExportIcon, default, StringU8.Empty, ColorParameter.Default,
|
||||
ColorParameter.Default, size);
|
||||
|
||||
if (tab.GamePaths is null)
|
||||
{
|
||||
|
|
@ -190,8 +182,8 @@ public partial class ModEditWindow
|
|||
return;
|
||||
|
||||
var size = Im.ContentRegion.Available with { Y = 0 };
|
||||
using var frame = ImRaii.FramedGroup("Exceptions", size, headerPreIcon: FontAwesomeIcon.TimesCircle,
|
||||
borderColor: new Rgba32(Colors.RegexWarningBorder).Color);
|
||||
using var frame = ImEx.FramedGroup("Exceptions"u8, LunaStyle.ErrorIcon, default, StringU8.Empty, ColorParameter.Default,
|
||||
LunaStyle.ErrorBorderColor, size);
|
||||
|
||||
var spaceAvail = Im.ContentRegion.Available.X - Im.Style.ItemSpacing.X - 100;
|
||||
foreach (var (index, exception) in tab.IoExceptions.Index())
|
||||
|
|
@ -216,8 +208,9 @@ public partial class ModEditWindow
|
|||
if (tab.IoWarnings.Count is 0)
|
||||
return;
|
||||
|
||||
var size = Im.ContentRegion.Available with { Y = 0 };
|
||||
using var frame = ImRaii.FramedGroup("Warnings", size, headerPreIcon: FontAwesomeIcon.ExclamationCircle, borderColor: 0xFF40FFFF);
|
||||
var size = Im.ContentRegion.Available with { Y = 0 };
|
||||
using var frame = ImEx.FramedGroup("Warnings"u8, LunaStyle.WarningIcon, default, StringU8.Empty, ColorParameter.Default,
|
||||
LunaStyle.WarningBorderColor, size);
|
||||
|
||||
var spaceAvail = Im.ContentRegion.Available.X - Im.Style.ItemSpacing.X - 100;
|
||||
foreach (var (index, warning) in tab.IoWarnings.Index())
|
||||
|
|
@ -294,7 +287,7 @@ public partial class ModEditWindow
|
|||
Im.Tooltip.OnHover("Right-Click to copy to clipboard."u8, HoveredFlags.AllowWhenDisabled);
|
||||
}
|
||||
|
||||
private void DrawDocumentationLink(string address)
|
||||
private static void DrawDocumentationLink(string address)
|
||||
{
|
||||
var text = "Documentation →"u8;
|
||||
var width = Im.Font.CalculateButtonSize(text).X;
|
||||
|
|
@ -427,7 +420,7 @@ public partial class ModEditWindow
|
|||
+ "and must end in \".mtrl\"."u8);
|
||||
}
|
||||
|
||||
private bool DrawModelLodDetails(MdlTab tab, int lodIndex, bool disabled)
|
||||
private static bool DrawModelLodDetails(MdlTab tab, int lodIndex, bool disabled)
|
||||
{
|
||||
using var lodNode = Im.Tree.Node($"Level of Detail #{lodIndex + 1}", TreeNodeFlags.DefaultOpen);
|
||||
if (!lodNode)
|
||||
|
|
@ -442,7 +435,7 @@ public partial class ModEditWindow
|
|||
return ret;
|
||||
}
|
||||
|
||||
private bool DrawModelMeshDetails(MdlTab tab, int meshIndex, bool disabled)
|
||||
private static bool DrawModelMeshDetails(MdlTab tab, int meshIndex, bool disabled)
|
||||
{
|
||||
using var meshNode = Im.Tree.Node($"Mesh #{meshIndex + 1}", TreeNodeFlags.DefaultOpen);
|
||||
if (!meshNode)
|
||||
|
|
@ -530,7 +523,7 @@ public partial class ModEditWindow
|
|||
return ret;
|
||||
}
|
||||
|
||||
private bool DrawSubMeshAttributes(in Im.TableDisposable table, MdlTab tab, int meshIndex, int subMeshOffset, bool disabled)
|
||||
private static bool DrawSubMeshAttributes(in Im.TableDisposable table, MdlTab tab, int meshIndex, int subMeshOffset, bool disabled)
|
||||
{
|
||||
using var _ = Im.Id.Push(subMeshOffset);
|
||||
|
||||
|
|
@ -540,8 +533,6 @@ public partial class ModEditWindow
|
|||
table.DrawFrameColumn($"Attributes #{subMeshOffset + 1}");
|
||||
|
||||
table.NextColumn();
|
||||
var data = disabled ? _preview : _main;
|
||||
var widget = data.SubMeshAttributeTags[subMeshIndex];
|
||||
var attributes = tab.GetSubMeshAttributes(subMeshIndex);
|
||||
|
||||
if (attributes is null)
|
||||
|
|
@ -550,8 +541,7 @@ public partial class ModEditWindow
|
|||
disabled = true;
|
||||
}
|
||||
|
||||
var tagIndex = widget.Draw(string.Empty, string.Empty, attributes,
|
||||
out var editedAttribute, !disabled);
|
||||
var tagIndex = TagButtons.Draw(StringU8.Empty, StringU8.Empty, attributes, out var editedAttribute, !disabled);
|
||||
if (tagIndex < 0)
|
||||
return false;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using Dalamud.Interface;
|
||||
using ImSharp;
|
||||
using OtterGui.Text;
|
||||
using Luna;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.Interop.ResourceTree;
|
||||
|
|
@ -17,7 +16,7 @@ public partial class ModEditWindow
|
|||
private readonly FileDialogService _fileDialog;
|
||||
private readonly ResourceTreeFactory _resourceTreeFactory;
|
||||
private readonly ResourceTreeViewer _quickImportViewer;
|
||||
private readonly Dictionary<(Utf8GamePath, IWritable?), QuickImportAction> _quickImportActions = new();
|
||||
private readonly Dictionary<(Utf8GamePath, IWritable?), QuickImportAction> _quickImportActions = new();
|
||||
|
||||
public HashSet<string> GetPlayerResourcesOfType(ResourceType type)
|
||||
{
|
||||
|
|
@ -41,7 +40,7 @@ public partial class ModEditWindow
|
|||
|
||||
private void DrawQuickImportTab()
|
||||
{
|
||||
using var tab = ImUtf8.TabItem("Import from Screen"u8);
|
||||
using var tab = Im.TabBar.BeginItem("Import from Screen"u8);
|
||||
if (!tab)
|
||||
{
|
||||
_quickImportActions.Clear();
|
||||
|
|
@ -61,7 +60,7 @@ public partial class ModEditWindow
|
|||
private void DrawQuickImportActions(ResourceNode resourceNode, IWritable? writable, Vector2 buttonSize)
|
||||
{
|
||||
Im.Line.Same();
|
||||
if (!_quickImportActions!.TryGetValue((resourceNode.GamePath, writable), out var quickImport))
|
||||
if (!_quickImportActions.TryGetValue((resourceNode.GamePath, writable), out var quickImport))
|
||||
{
|
||||
quickImport = QuickImportAction.Prepare(this, resourceNode.GamePath, writable);
|
||||
_quickImportActions.Add((resourceNode.GamePath, writable), quickImport);
|
||||
|
|
@ -69,9 +68,8 @@ public partial class ModEditWindow
|
|||
|
||||
var canQuickImport = quickImport.CanExecute;
|
||||
var quickImportEnabled = canQuickImport && (!resourceNode.Protected || _config.DeleteModModifier.IsActive());
|
||||
if (ImUtf8.IconButton(FontAwesomeIcon.FileImport,
|
||||
if (ImEx.Icon.Button(LunaStyle.ImportIcon,
|
||||
$"Add a copy of this file to {quickImport.OptionName}.{(canQuickImport && !quickImportEnabled ? $"\nHold {_config.DeleteModModifier} while clicking to add." : string.Empty)}",
|
||||
buttonSize,
|
||||
!quickImportEnabled))
|
||||
{
|
||||
quickImport.Execute();
|
||||
|
|
@ -162,10 +160,7 @@ public partial class ModEditWindow
|
|||
_editor.Compactor.WriteAllBytes(_targetPath!, _file!.Write());
|
||||
_editor.FileEditor.Revert(_editor.Mod!, _editor.Option!);
|
||||
var fileRegistry = _editor.Files.Available.First(file => file.File.FullName == _targetPath);
|
||||
_editor.FileEditor.AddPathsToSelected(_editor.Option!, new[]
|
||||
{
|
||||
fileRegistry,
|
||||
}, _subDirs);
|
||||
_editor.FileEditor.AddPathsToSelected(_editor.Option!, [fileRegistry], _subDirs);
|
||||
_editor.FileEditor.Apply(_editor.Mod!, _editor.Option!);
|
||||
|
||||
return fileRegistry;
|
||||
|
|
|
|||
|
|
@ -79,13 +79,9 @@ public class ResourceTreeViewer(
|
|||
using (ImGuiColor.Text.Push(CategoryColor(category).Value()))
|
||||
{
|
||||
var isOpen = Im.Tree.Header($"{(incognito.IncognitoMode ? tree.AnonymizedName : tree.Name)}###{index}",
|
||||
index == 0 ? TreeNodeFlags.DefaultOpen : 0);
|
||||
index is 0 ? TreeNodeFlags.DefaultOpen : 0);
|
||||
if (debugMode)
|
||||
{
|
||||
using var _ = Im.Font.PushMono();
|
||||
Im.Tooltip.OnHover(
|
||||
$"Object Index: {tree.GameObjectIndex}\nObject Address: 0x{tree.GameObjectAddress:X16}\nDraw Object Address: 0x{tree.DrawObjectAddress:X16}");
|
||||
}
|
||||
HeaderInteraction(tree);
|
||||
|
||||
if (!isOpen)
|
||||
continue;
|
||||
|
|
@ -295,11 +291,7 @@ public class ResourceTreeViewer(
|
|||
}
|
||||
|
||||
if (debugMode)
|
||||
{
|
||||
using var _ = Im.Font.PushMono();
|
||||
Im.Tooltip.OnHover(
|
||||
$"Resource Type: {resourceNode.Type}\nObject Address: 0x{resourceNode.ObjectAddress:X16}\nResource Handle: 0x{resourceNode.ResourceHandle:X16}\nLength: 0x{resourceNode.Length:X16}");
|
||||
}
|
||||
ResourceInteraction(resourceNode);
|
||||
}
|
||||
|
||||
table.NextColumn();
|
||||
|
|
@ -316,8 +308,9 @@ public class ResourceTreeViewer(
|
|||
if (Im.Item.Clicked())
|
||||
Im.Clipboard.Set(allPaths);
|
||||
using var tt = Im.Tooltip.Begin();
|
||||
using var c = Im.Color.PushDefault(ImGuiColor.Text);
|
||||
Im.Text(allPaths);
|
||||
Im.Text("\n\nClick to copy to clipboard."u8);
|
||||
Im.Text("\nClick to copy to clipboard."u8);
|
||||
}
|
||||
|
||||
table.NextColumn();
|
||||
|
|
@ -360,14 +353,13 @@ public class ResourceTreeViewer(
|
|||
if (hasMod && Im.Item.RightClicked() && Im.Io.KeyControl)
|
||||
communicator.SelectTab.Invoke(new SelectTab.Arguments(TabType.Mods, mod));
|
||||
|
||||
Im.Tooltip.OnHover(
|
||||
$"{resourceNode.FullPath.ToPath()}\n\nClick to copy to clipboard.{(hasMod ? "\nControl + Right-Click to jump to mod." : string.Empty)}{GetAdditionalDataSuffix(resourceNode.AdditionalData)}");
|
||||
Im.Tooltip.OnHover(default, $"{resourceNode.FullPath.ToPath()}\n\nClick to copy to clipboard.{(hasMod ? "\nControl + Right-Click to jump to mod." : string.Empty)}{GetAdditionalDataSuffix(resourceNode.AdditionalData)}", true);
|
||||
}
|
||||
else
|
||||
{
|
||||
Im.Selectable(GetPathStatusLabel(resourceNode.FullPathStatus), false, SelectableFlags.Disabled,
|
||||
Im.ContentRegion.Available with { Y = frameHeight });
|
||||
Im.Tooltip.OnHover(
|
||||
Im.Tooltip.OnHover(default,
|
||||
$"{GetPathStatusDescription(resourceNode.FullPathStatus)}{GetAdditionalDataSuffix(resourceNode.AdditionalData)}");
|
||||
}
|
||||
|
||||
|
|
@ -384,7 +376,9 @@ public class ResourceTreeViewer(
|
|||
return;
|
||||
|
||||
string GetAdditionalDataSuffix(CiByteString data)
|
||||
=> !debugMode || data.IsEmpty ? string.Empty : $"\n\nAdditional Data: {data}";
|
||||
{
|
||||
return !debugMode || data.IsEmpty ? string.Empty : $"\n\nAdditional Data: {data}";
|
||||
}
|
||||
|
||||
NodeVisibility GetNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemIconFlag parentFilterIcon)
|
||||
{
|
||||
|
|
@ -447,7 +441,8 @@ public class ResourceTreeViewer(
|
|||
_writableCache.Add(resourceNode.FullPath, writable);
|
||||
}
|
||||
|
||||
if (ImEx.Icon.Button(LunaStyle.SaveIcon, "Export this file."u8, resourceNode.FullPath.FullName.Length is 0 || writable is null, buttonSize))
|
||||
if (ImEx.Icon.Button(LunaStyle.SaveIcon, "Export this file."u8, resourceNode.FullPath.FullName.Length is 0 || writable is null,
|
||||
buttonSize))
|
||||
{
|
||||
var fullPathStr = resourceNode.FullPath.FullName;
|
||||
var ext = resourceNode.PossibleGamePaths.Length == 1
|
||||
|
|
@ -492,6 +487,56 @@ public class ResourceTreeViewer(
|
|||
_ => "The actual path to this file is unavailable."u8,
|
||||
};
|
||||
|
||||
private static void HeaderInteraction(ResourceTree tree)
|
||||
{
|
||||
Im.Tooltip.OnHover(default, $"Object Index: {tree.GameObjectIndex}\nObject Address: 0x{tree.GameObjectAddress:X16}\nDraw Object Address: 0x{tree.DrawObjectAddress:X16}", true, Im.Font.Mono);
|
||||
if (tree.GameObjectAddress == nint.Zero)
|
||||
return;
|
||||
|
||||
using var context = Im.Popup.BeginContextItem();
|
||||
if (context)
|
||||
{
|
||||
using var text = Im.Color.PushDefault(ImGuiColor.Text);
|
||||
if (Im.Menu.Item("Copy Game Object Address"u8))
|
||||
Im.Clipboard.Set($"0x{tree.GameObjectAddress:X}");
|
||||
if (Penumbra.Dynamis.IsSubscribed && Im.Menu.Item("Inspect Game Object"u8))
|
||||
Penumbra.Dynamis.InspectObject(tree.GameObjectAddress, $"{tree.Name} Game Object");
|
||||
if (tree.DrawObjectAddress != nint.Zero)
|
||||
{
|
||||
if (Im.Menu.Item("Copy Draw Object Address"u8))
|
||||
Im.Clipboard.Set($"0x{tree.DrawObjectAddress:X}");
|
||||
if (Penumbra.Dynamis.IsSubscribed && Im.Menu.Item("Inspect Draw Object"u8))
|
||||
Penumbra.Dynamis.InspectObject(tree.DrawObjectAddress, $"{tree.Name} Draw Object");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ResourceInteraction(ResourceNode node)
|
||||
{
|
||||
Im.Tooltip.OnHover(default, $"Resource Type: {node.Type}\nObject Address: 0x{node.ObjectAddress:X16}\nResource Handle: 0x{node.ResourceHandle:X16}\nLength: 0x{node.Length:X16}",
|
||||
true, Im.Font.Mono);
|
||||
|
||||
if (node.ResourceHandle == nint.Zero)
|
||||
return;
|
||||
|
||||
using var context = Im.Popup.BeginContextItem();
|
||||
if (context)
|
||||
{
|
||||
using var text = Im.Color.PushDefault(ImGuiColor.Text);
|
||||
if (Im.Menu.Item("Copy Resource Handle Address"u8))
|
||||
Im.Clipboard.Set($"0x{node.ResourceHandle:X}");
|
||||
if (Penumbra.Dynamis.IsSubscribed && Im.Menu.Item("Inspect Resource Handle"u8))
|
||||
Penumbra.Dynamis.InspectObject(node.ResourceHandle, $"{node.Name} Resource Handle");
|
||||
if (node.ObjectAddress != nint.Zero)
|
||||
{
|
||||
if (Im.Menu.Item("Copy Object Address"u8))
|
||||
Im.Clipboard.Set($"0x{node.ObjectAddress:X}");
|
||||
if (Penumbra.Dynamis.IsSubscribed && Im.Menu.Item("Inspect Object"u8))
|
||||
Penumbra.Dynamis.InspectObject(node.ObjectAddress, $"{node.Name} Object");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Flags]
|
||||
private enum TreeCategory : uint
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,11 +1,18 @@
|
|||
using OtterGui.Widgets;
|
||||
using ImSharp;
|
||||
using Penumbra.GameData.Files.AtchStructs;
|
||||
using Penumbra.UI.AdvancedWindow.Meta;
|
||||
|
||||
namespace Penumbra.UI.Combos;
|
||||
|
||||
internal sealed class AtchPointCombo(Func<IReadOnlyList<AtchType>> generator)
|
||||
: FilterComboCache<AtchType>(generator, MouseWheelType.Control, Penumbra.Log)
|
||||
internal sealed class AtchPointCombo(AtchMetaDrawer parent)
|
||||
: SimpleFilterCombo<AtchType>(SimpleFilterType.Text)
|
||||
{
|
||||
protected override string ToString(AtchType obj)
|
||||
=> obj.ToName();
|
||||
public override StringU8 DisplayString(in AtchType value)
|
||||
=> new(value.ToNameU8());
|
||||
|
||||
public override string FilterString(in AtchType value)
|
||||
=> value.ToString();
|
||||
|
||||
public override IEnumerable<AtchType> GetBaseItems()
|
||||
=> parent.GetPoints();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,12 +141,11 @@ public sealed class ModGroupDrawer : IUiService
|
|||
private void DrawSingleGroupRadio(IModGroup group, int groupIdx, Setting setting)
|
||||
{
|
||||
using var id = Im.Id.Push(groupIdx);
|
||||
var selectedOption = setting.AsIndex;
|
||||
var minWidth = Widget.BeginFramedGroup(group.Name, group.Description);
|
||||
var options = group.Options;
|
||||
DrawCollapseHandling(options, minWidth, DrawOptions);
|
||||
var selectedOption = setting.AsIndex;
|
||||
using var g = ImEx.FramedGroup(group.Name, LunaStyle.HelpMarker, group.Description);
|
||||
DrawCollapseHandling(options, g.MinimumWidth, DrawOptions);
|
||||
|
||||
Widget.EndFramedGroup();
|
||||
return;
|
||||
|
||||
void DrawOptions()
|
||||
|
|
@ -174,12 +173,13 @@ public sealed class ModGroupDrawer : IUiService
|
|||
/// </summary>
|
||||
private void DrawMultiGroup(IModGroup group, int groupIdx, Setting setting)
|
||||
{
|
||||
using var id = Im.Id.Push(groupIdx);
|
||||
var minWidth = Widget.BeginFramedGroup(group.Name, group.Description);
|
||||
var options = group.Options;
|
||||
DrawCollapseHandling(options, minWidth, DrawOptions);
|
||||
using var id = Im.Id.Push(groupIdx);
|
||||
var options = group.Options;
|
||||
using (var g = ImEx.FramedGroup(group.Name, LunaStyle.HelpMarker, group.Description))
|
||||
{
|
||||
DrawCollapseHandling(options, g.MinimumWidth, DrawOptions);
|
||||
}
|
||||
|
||||
Widget.EndFramedGroup();
|
||||
var label = new StringU8($"##multi{groupIdx}");
|
||||
if (Im.Item.RightClicked())
|
||||
Im.Popup.Open(label);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ using Dalamud.Interface;
|
|||
using Dalamud.Interface.ImGuiNotification;
|
||||
using ImSharp;
|
||||
using Luna;
|
||||
using OtterGui.Raii;
|
||||
using Penumbra.Meta;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Mods.Groups;
|
||||
|
|
@ -68,7 +67,7 @@ public sealed class ModGroupEditDrawer(
|
|||
private void DrawGroup(IModGroup group, int idx)
|
||||
{
|
||||
using var id = Im.Id.Push(idx);
|
||||
using var frame = ImRaii.FramedGroup($"Group #{idx + 1}");
|
||||
using var frame = ImEx.FramedGroup($"Group #{idx + 1}");
|
||||
DrawGroupNameRow(group, idx);
|
||||
group.EditDrawer(this).Draw();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
using Dalamud.Interface;
|
||||
using ImSharp;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Text;
|
||||
using Luna;
|
||||
using Penumbra.Mods.Groups;
|
||||
|
||||
namespace Penumbra.UI.ModsTab.Groups;
|
||||
|
|
@ -12,7 +10,7 @@ public readonly struct MultiModGroupEditDrawer(ModGroupEditDrawer editor, MultiM
|
|||
{
|
||||
foreach (var (optionIdx, option) in group.OptionData.Index())
|
||||
{
|
||||
using var id = ImRaii.PushId(optionIdx);
|
||||
using var id = Im.Id.Push(optionIdx);
|
||||
editor.DrawOptionPosition(group, option, optionIdx);
|
||||
|
||||
Im.Line.SameInner();
|
||||
|
|
@ -39,7 +37,7 @@ public readonly struct MultiModGroupEditDrawer(ModGroupEditDrawer editor, MultiM
|
|||
{
|
||||
var g = group;
|
||||
var e = editor.ModManager.OptionEditor.MultiEditor;
|
||||
if (ImUtf8.Button("Convert to Single Group"u8, editor.AvailableWidth))
|
||||
if (Im.Button("Convert to Single Group"u8, editor.AvailableWidth))
|
||||
editor.ActionQueue.Enqueue(() => e.ChangeToSingle(g));
|
||||
}
|
||||
|
||||
|
|
@ -52,9 +50,9 @@ public readonly struct MultiModGroupEditDrawer(ModGroupEditDrawer editor, MultiM
|
|||
var name = editor.DrawNewOptionBase(group, count);
|
||||
|
||||
var validName = name.Length > 0;
|
||||
if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName
|
||||
if (ImEx.Icon.Button(LunaStyle.AddObjectIcon, validName
|
||||
? "Add a new option to this group."u8
|
||||
: "Please enter a name for the new option."u8, default, !validName))
|
||||
: "Please enter a name for the new option."u8, !validName))
|
||||
{
|
||||
editor.ModManager.OptionEditor.MultiEditor.AddOption(group, name);
|
||||
editor.NewOptionName = null;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ using Penumbra.Mods.Manager;
|
|||
using Penumbra.Mods.Settings;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.UI.Classes;
|
||||
using Penumbra.UI.ModsTab.Selector;
|
||||
using MessageService = Penumbra.Services.MessageService;
|
||||
|
||||
namespace Penumbra.UI.ModsTab;
|
||||
|
|
@ -380,7 +381,8 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
: null;
|
||||
|
||||
_fileDialog.OpenFilePicker("Import Mod Pack",
|
||||
"Mod Packs{.ttmp,.ttmp2,.pmp,.pcp},TexTools Mod Packs{.ttmp,.ttmp2},Penumbra Mod Packs{.pmp,.pcp},Archives{.zip,.7z,.rar},Penumbra Character Packs{.pcp}", (s, f) =>
|
||||
"Mod Packs{.ttmp,.ttmp2,.pmp,.pcp},TexTools Mod Packs{.ttmp,.ttmp2},Penumbra Mod Packs{.pmp,.pcp},Archives{.zip,.7z,.rar},Penumbra Character Packs{.pcp}",
|
||||
(s, f) =>
|
||||
{
|
||||
if (!s)
|
||||
return;
|
||||
|
|
@ -412,7 +414,8 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
}
|
||||
|
||||
private void DeleteModButton(Vector2 size)
|
||||
=> DeleteSelectionButton(size, Unsafe.BitCast<DoubleModifier, OtterGui.Classes.DoubleModifier>(_config.DeleteModModifier), "mod", "mods", _modManager.DeleteMod);
|
||||
=> DeleteSelectionButton(size, Unsafe.BitCast<DoubleModifier, OtterGui.Classes.DoubleModifier>(_config.DeleteModModifier), "mod",
|
||||
"mods", _modManager.DeleteMod);
|
||||
|
||||
private void AddHelpButton(Vector2 size)
|
||||
{
|
||||
|
|
@ -573,7 +576,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
public ModPriority Priority;
|
||||
}
|
||||
|
||||
private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods;
|
||||
private ModTypeFilter _stateTypeFilter = ModTypeFilterExtensions.UnfilteredStateMods;
|
||||
|
||||
private void SetFilterTooltip()
|
||||
{
|
||||
|
|
@ -608,13 +611,13 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
/// Uses count == 0 to check for has-not and count != 0 for has.
|
||||
/// Returns true if it should be filtered and false if not.
|
||||
/// </summary>
|
||||
private bool CheckFlags(int count, ModFilter hasNoFlag, ModFilter hasFlag)
|
||||
private bool CheckFlags(int count, ModTypeFilter hasNoFlag, ModTypeFilter hasFlag)
|
||||
=> count switch
|
||||
{
|
||||
0 when _stateFilter.HasFlag(hasNoFlag) => false,
|
||||
0 => true,
|
||||
_ when _stateFilter.HasFlag(hasFlag) => false,
|
||||
_ => true,
|
||||
0 when _stateTypeFilter.HasFlag(hasNoFlag) => false,
|
||||
0 => true,
|
||||
_ when _stateTypeFilter.HasFlag(hasFlag) => false,
|
||||
_ => true,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -628,7 +631,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
if (path is ModFileSystem.Folder f)
|
||||
{
|
||||
state = default;
|
||||
return ModFilterExtensions.UnfilteredStateMods != _stateFilter
|
||||
return ModTypeFilterExtensions.UnfilteredStateMods != _stateTypeFilter
|
||||
|| !_filter.IsVisible(f);
|
||||
}
|
||||
|
||||
|
|
@ -671,38 +674,38 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
{
|
||||
var isNew = _modManager.IsNew(mod);
|
||||
// Handle mod details.
|
||||
if (CheckFlags(mod.TotalFileCount, ModFilter.HasNoFiles, ModFilter.HasFiles)
|
||||
|| CheckFlags(mod.TotalSwapCount, ModFilter.HasNoFileSwaps, ModFilter.HasFileSwaps)
|
||||
|| CheckFlags(mod.TotalManipulations, ModFilter.HasNoMetaManipulations, ModFilter.HasMetaManipulations)
|
||||
|| CheckFlags(mod.HasOptions ? 1 : 0, ModFilter.HasNoConfig, ModFilter.HasConfig)
|
||||
|| CheckFlags(isNew ? 1 : 0, ModFilter.NotNew, ModFilter.IsNew))
|
||||
if (CheckFlags(mod.TotalFileCount, ModTypeFilter.HasNoFiles, ModTypeFilter.HasFiles)
|
||||
|| CheckFlags(mod.TotalSwapCount, ModTypeFilter.HasNoFileSwaps, ModTypeFilter.HasFileSwaps)
|
||||
|| CheckFlags(mod.TotalManipulations, ModTypeFilter.HasNoMetaManipulations, ModTypeFilter.HasMetaManipulations)
|
||||
|| CheckFlags(mod.HasOptions ? 1 : 0, ModTypeFilter.HasNoConfig, ModTypeFilter.HasConfig)
|
||||
|| CheckFlags(isNew ? 1 : 0, ModTypeFilter.NotNew, ModTypeFilter.IsNew))
|
||||
return true;
|
||||
|
||||
// Handle Favoritism
|
||||
if (!_stateFilter.HasFlag(ModFilter.Favorite) && mod.Favorite
|
||||
|| !_stateFilter.HasFlag(ModFilter.NotFavorite) && !mod.Favorite)
|
||||
if (!_stateTypeFilter.HasFlag(ModTypeFilter.Favorite) && mod.Favorite
|
||||
|| !_stateTypeFilter.HasFlag(ModTypeFilter.NotFavorite) && !mod.Favorite)
|
||||
return true;
|
||||
|
||||
// Handle Temporary
|
||||
if (!_stateFilter.HasFlag(ModFilter.Temporary) || !_stateFilter.HasFlag(ModFilter.NotTemporary))
|
||||
if (!_stateTypeFilter.HasFlag(ModTypeFilter.Temporary) || !_stateTypeFilter.HasFlag(ModTypeFilter.NotTemporary))
|
||||
{
|
||||
if (settings == null && _stateFilter.HasFlag(ModFilter.Temporary))
|
||||
if (settings == null && _stateTypeFilter.HasFlag(ModTypeFilter.Temporary))
|
||||
return true;
|
||||
|
||||
if (settings != null && settings.IsTemporary() != _stateFilter.HasFlag(ModFilter.Temporary))
|
||||
if (settings != null && settings.IsTemporary() != _stateTypeFilter.HasFlag(ModTypeFilter.Temporary))
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle Inheritance
|
||||
if (collection == _collectionManager.Active.Current)
|
||||
{
|
||||
if (!_stateFilter.HasFlag(ModFilter.Uninherited))
|
||||
if (!_stateTypeFilter.HasFlag(ModTypeFilter.Uninherited))
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
state.Color = ColorId.InheritedMod;
|
||||
if (!_stateFilter.HasFlag(ModFilter.Inherited))
|
||||
if (!_stateTypeFilter.HasFlag(ModTypeFilter.Inherited))
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -719,9 +722,9 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
if (settings == null)
|
||||
{
|
||||
state.Color = ColorId.UndefinedMod;
|
||||
if (!_stateFilter.HasFlag(ModFilter.Undefined)
|
||||
|| !_stateFilter.HasFlag(ModFilter.Disabled)
|
||||
|| !_stateFilter.HasFlag(ModFilter.NoConflict))
|
||||
if (!_stateTypeFilter.HasFlag(ModTypeFilter.Undefined)
|
||||
|| !_stateTypeFilter.HasFlag(ModTypeFilter.Disabled)
|
||||
|| !_stateTypeFilter.HasFlag(ModTypeFilter.NoConflict))
|
||||
return true;
|
||||
}
|
||||
else if (!settings.Enabled)
|
||||
|
|
@ -729,13 +732,13 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
state.Color = collection != _collectionManager.Active.Current
|
||||
? ColorId.InheritedDisabledMod
|
||||
: ColorId.DisabledMod;
|
||||
if (!_stateFilter.HasFlag(ModFilter.Disabled)
|
||||
|| !_stateFilter.HasFlag(ModFilter.NoConflict))
|
||||
if (!_stateTypeFilter.HasFlag(ModTypeFilter.Disabled)
|
||||
|| !_stateTypeFilter.HasFlag(ModTypeFilter.NoConflict))
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_stateFilter.HasFlag(ModFilter.Enabled))
|
||||
if (!_stateTypeFilter.HasFlag(ModTypeFilter.Enabled))
|
||||
return true;
|
||||
|
||||
// Conflicts can only be relevant if the mod is enabled.
|
||||
|
|
@ -744,20 +747,20 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
{
|
||||
if (conflicts.Any(c => !c.Solved))
|
||||
{
|
||||
if (!_stateFilter.HasFlag(ModFilter.UnsolvedConflict))
|
||||
if (!_stateTypeFilter.HasFlag(ModTypeFilter.UnsolvedConflict))
|
||||
return true;
|
||||
|
||||
state.Color = ColorId.ConflictingMod;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_stateFilter.HasFlag(ModFilter.SolvedConflict))
|
||||
if (!_stateTypeFilter.HasFlag(ModTypeFilter.SolvedConflict))
|
||||
return true;
|
||||
|
||||
state.Color = ColorId.HandledConflictMod;
|
||||
}
|
||||
}
|
||||
else if (!_stateFilter.HasFlag(ModFilter.NoConflict))
|
||||
else if (!_stateTypeFilter.HasFlag(ModTypeFilter.NoConflict))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
|
@ -781,7 +784,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
if (ApplyStringFilters(leaf, mod))
|
||||
return true;
|
||||
|
||||
if (_stateFilter != ModFilterExtensions.UnfilteredStateMods)
|
||||
if (_stateTypeFilter != ModTypeFilterExtensions.UnfilteredStateMods)
|
||||
return CheckStateFilters(mod, settings, collection, ref state);
|
||||
|
||||
(state.Color, state.Tint) = GetTextColor(mod, settings, collection);
|
||||
|
|
@ -800,23 +803,23 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
|
||||
if (ImUtf8.Checkbox("Everything"u8, ref everything))
|
||||
{
|
||||
_stateFilter = everything ? ModFilterExtensions.UnfilteredStateMods : 0;
|
||||
_stateTypeFilter = everything ? ModTypeFilterExtensions.UnfilteredStateMods : 0;
|
||||
SetFilterDirty();
|
||||
}
|
||||
|
||||
ImGui.Dummy(new Vector2(0, 5 * Im.Style.GlobalScale));
|
||||
foreach (var (onFlag, offFlag, name) in ModFilterExtensions.TriStatePairs)
|
||||
foreach (var (onFlag, offFlag, name) in ModTypeFilterExtensions.TriStatePairs)
|
||||
{
|
||||
if (TriStateCheckbox.Instance.Draw(name, ref _stateFilter, onFlag, offFlag))
|
||||
if (TriStateCheckbox.Instance.Draw(name, ref _stateTypeFilter, onFlag, offFlag))
|
||||
SetFilterDirty();
|
||||
}
|
||||
|
||||
foreach (var group in ModFilterExtensions.Groups)
|
||||
foreach (var group in ModTypeFilterExtensions.Groups)
|
||||
{
|
||||
Im.Separator();
|
||||
foreach (var (flag, name) in group)
|
||||
{
|
||||
if (ImUtf8.Checkbox(name, ref _stateFilter, flag))
|
||||
if (ImUtf8.Checkbox(name, ref _stateTypeFilter, flag))
|
||||
SetFilterDirty();
|
||||
}
|
||||
}
|
||||
|
|
@ -831,7 +834,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
var remainingWidth = width - Im.Style.FrameHeight;
|
||||
var comboPos = new Vector2(pos.X + remainingWidth, pos.Y);
|
||||
|
||||
var everything = _stateFilter == ModFilterExtensions.UnfilteredStateMods;
|
||||
var everything = _stateTypeFilter == ModTypeFilterExtensions.UnfilteredStateMods;
|
||||
|
||||
ImGui.SetCursorPos(comboPos);
|
||||
// Draw combo button
|
||||
|
|
@ -840,7 +843,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
_tutorial.OpenTutorial(BasicTutorialSteps.ModFilters);
|
||||
if (rightClick)
|
||||
{
|
||||
_stateFilter = ModFilterExtensions.UnfilteredStateMods;
|
||||
_stateTypeFilter = ModTypeFilterExtensions.UnfilteredStateMods;
|
||||
SetFilterDirty();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
namespace Penumbra.UI.ModsTab;
|
||||
|
||||
[Flags]
|
||||
public enum ModFilter
|
||||
{
|
||||
Enabled = 1 << 0,
|
||||
Disabled = 1 << 1,
|
||||
Favorite = 1 << 2,
|
||||
NotFavorite = 1 << 3,
|
||||
NoConflict = 1 << 4,
|
||||
SolvedConflict = 1 << 5,
|
||||
UnsolvedConflict = 1 << 6,
|
||||
HasNoMetaManipulations = 1 << 7,
|
||||
HasMetaManipulations = 1 << 8,
|
||||
HasNoFileSwaps = 1 << 9,
|
||||
HasFileSwaps = 1 << 10,
|
||||
HasConfig = 1 << 11,
|
||||
HasNoConfig = 1 << 12,
|
||||
HasNoFiles = 1 << 13,
|
||||
HasFiles = 1 << 14,
|
||||
IsNew = 1 << 15,
|
||||
NotNew = 1 << 16,
|
||||
Inherited = 1 << 17,
|
||||
Uninherited = 1 << 18,
|
||||
Temporary = 1 << 19,
|
||||
NotTemporary = 1 << 20,
|
||||
Undefined = 1 << 21,
|
||||
};
|
||||
|
||||
public static class ModFilterExtensions
|
||||
{
|
||||
public const ModFilter UnfilteredStateMods = (ModFilter)((1 << 22) - 1);
|
||||
|
||||
public static readonly IReadOnlyList<(ModFilter On, ModFilter Off, string Name)> TriStatePairs =
|
||||
[
|
||||
(ModFilter.Enabled, ModFilter.Disabled, "Enabled"),
|
||||
(ModFilter.IsNew, ModFilter.NotNew, "Newly Imported"),
|
||||
(ModFilter.Favorite, ModFilter.NotFavorite, "Favorite"),
|
||||
(ModFilter.HasConfig, ModFilter.HasNoConfig, "Has Options"),
|
||||
(ModFilter.HasFiles, ModFilter.HasNoFiles, "Has Redirections"),
|
||||
(ModFilter.HasMetaManipulations, ModFilter.HasNoMetaManipulations, "Has Meta Manipulations"),
|
||||
(ModFilter.HasFileSwaps, ModFilter.HasNoFileSwaps, "Has File Swaps"),
|
||||
(ModFilter.Temporary, ModFilter.NotTemporary, "Temporary"),
|
||||
];
|
||||
|
||||
public static readonly IReadOnlyList<IReadOnlyList<(ModFilter Filter, string Name)>> Groups =
|
||||
[
|
||||
[
|
||||
(ModFilter.NoConflict, "Has No Conflicts"),
|
||||
(ModFilter.SolvedConflict, "Has Solved Conflicts"),
|
||||
(ModFilter.UnsolvedConflict, "Has Unsolved Conflicts"),
|
||||
],
|
||||
[
|
||||
(ModFilter.Undefined, "Not Configured"),
|
||||
(ModFilter.Inherited, "Inherited Configuration"),
|
||||
(ModFilter.Uninherited, "Own Configuration"),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
using ImSharp;
|
||||
using Luna;
|
||||
using OtterGui.Widgets;
|
||||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.UI.Classes;
|
||||
|
||||
|
|
@ -13,8 +12,6 @@ public class ModPanelDescriptionTab(
|
|||
PredefinedTagManager predefinedTagsConfig)
|
||||
: ITab<ModPanelTab>
|
||||
{
|
||||
private readonly TagButtons _localTags = new();
|
||||
private readonly TagButtons _modTags = new();
|
||||
|
||||
public ReadOnlySpan<byte> Label
|
||||
=> "Description"u8;
|
||||
|
|
@ -33,9 +30,9 @@ public class ModPanelDescriptionTab(
|
|||
var (predefinedTagsEnabled, predefinedTagButtonOffset) = predefinedTagsConfig.Enabled
|
||||
? (true, Im.Style.FrameHeight + Im.Style.WindowPadding.X + (Im.Scroll.MaximumY > 0 ? Im.Style.ScrollbarSize : 0))
|
||||
: (false, 0);
|
||||
var tagIdx = _localTags.Draw("Local Tags: ",
|
||||
"Custom tags you can set personally that will not be exported to the mod data but only set for you.\n"
|
||||
+ "If the mod already contains a local tag in its own tags, the local tag will be ignored.", selector.Selected!.LocalTags,
|
||||
var tagIdx = TagButtons.Draw("Local Tags: "u8,
|
||||
"Custom tags you can set personally that will not be exported to the mod data but only set for you.\n"u8
|
||||
+ "If the mod already contains a local tag in its own tags, the local tag will be ignored."u8, selector.Selected!.LocalTags,
|
||||
out var editedTag, rightEndOffset: predefinedTagButtonOffset);
|
||||
tutorial.OpenTutorial(BasicTutorialSteps.Tags);
|
||||
if (tagIdx >= 0)
|
||||
|
|
@ -46,7 +43,7 @@ public class ModPanelDescriptionTab(
|
|||
selector.Selected!);
|
||||
|
||||
if (selector.Selected!.ModTags.Count > 0)
|
||||
_modTags.Draw("Mod Tags: ", "Tags assigned by the mod creator and saved with the mod data. To edit these, look at Edit Mod.",
|
||||
TagButtons.Draw("Mod Tags: "u8, "Tags assigned by the mod creator and saved with the mod data. To edit these, look at Edit Mod."u8,
|
||||
selector.Selected!.ModTags, out _, false,
|
||||
Im.Font.CalculateSize("Local "u8).X - Im.Font.CalculateSize("Mod "u8).X);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ using Dalamud.Interface;
|
|||
using Dalamud.Interface.ImGuiNotification;
|
||||
using ImSharp;
|
||||
using Luna;
|
||||
using OtterGui.Widgets;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Mods.Editor;
|
||||
using Penumbra.Mods.Manager;
|
||||
|
|
@ -25,8 +24,6 @@ public class ModPanelEditTab(
|
|||
AddGroupDrawer addGroupDrawer)
|
||||
: ITab<ModPanelTab>
|
||||
{
|
||||
private readonly TagButtons _modTags = new();
|
||||
|
||||
private ModFileSystem.Leaf _leaf = null!;
|
||||
private Mod _mod = null!;
|
||||
|
||||
|
|
@ -68,7 +65,7 @@ public class ModPanelEditTab(
|
|||
UiHelpers.DefaultLineSpace();
|
||||
var sharedTagsEnabled = predefinedTagManager.Enabled;
|
||||
var sharedTagButtonOffset = sharedTagsEnabled ? Im.Style.FrameHeight + Im.Style.FramePadding.X : 0;
|
||||
var tagIdx = _modTags.Draw("Mod Tags: ", "Edit tags by clicking them, or add new tags. Empty tags are removed.", _mod.ModTags,
|
||||
var tagIdx = TagButtons.Draw("Mod Tags: "u8, "Edit tags by clicking them, or add new tags. Empty tags are removed."u8, _mod.ModTags,
|
||||
out var editedTag, rightEndOffset: sharedTagButtonOffset);
|
||||
if (tagIdx >= 0)
|
||||
modManager.DataEditor.ChangeModTag(_mod, tagIdx, editedTag);
|
||||
|
|
|
|||
|
|
@ -110,9 +110,8 @@ public sealed class ModSearchStringSplitter : SearchStringSplitter<ModSearchType
|
|||
ModSearchType.Tag => leaf.Value.AllTagsLower.AsSpan().Contains(entry.Needle, StringComparison.Ordinal),
|
||||
ModSearchType.Name => leaf.Value.Name.AsSpan().Contains(entry.Needle, StringComparison.OrdinalIgnoreCase),
|
||||
ModSearchType.Author => leaf.Value.Author.AsSpan().Contains(entry.Needle, StringComparison.OrdinalIgnoreCase),
|
||||
ModSearchType.Category => leaf.Value.ChangedItems.Any(p
|
||||
=> ((p.Value?.Icon.ToFlag() ?? ChangedItemIconFlag.Unknown) & entry.IconFlagFilter) != 0),
|
||||
_ => true,
|
||||
ModSearchType.Category => leaf.Value.ChangedItems.Any(p => (p.Value.Icon.ToFlag() & entry.IconFlagFilter) is not 0),
|
||||
_ => true,
|
||||
};
|
||||
|
||||
protected override bool MatchesNone(ModSearchType type, bool negated, ModFileSystem.Leaf haystack)
|
||||
|
|
|
|||
119
Penumbra/UI/ModsTab/RedrawFooter.cs
Normal file
119
Penumbra/UI/ModsTab/RedrawFooter.cs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
using Dalamud.Game.ClientState.Objects;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using ImSharp;
|
||||
using Luna;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.GameData.Interop;
|
||||
using Penumbra.Interop.Services;
|
||||
using Penumbra.UI.Classes;
|
||||
|
||||
namespace Penumbra.UI.ModsTab;
|
||||
|
||||
public sealed class RedrawFooter(
|
||||
Configuration config,
|
||||
TutorialService tutorial,
|
||||
ObjectManager objects,
|
||||
ITargetManager targets,
|
||||
RedrawService redrawService) : IFooter
|
||||
{
|
||||
public bool Collapsed
|
||||
=> config.HideRedrawBar;
|
||||
|
||||
public void PostCollapsed()
|
||||
=> tutorial.SkipTutorial(BasicTutorialSteps.Redrawing);
|
||||
|
||||
private void DrawTooltip()
|
||||
{
|
||||
var hovered = Im.Item.Hovered();
|
||||
tutorial.OpenTutorial(BasicTutorialSteps.Redrawing);
|
||||
if (!hovered)
|
||||
return;
|
||||
|
||||
using var _ = Im.Tooltip.Begin();
|
||||
Im.Text("The supported modifiers for '/penumbra redraw' are:"u8);
|
||||
Im.BulletText("nothing, to redraw all characters\n"u8);
|
||||
Im.BulletText("'self' or '<me>': your own character\n"u8);
|
||||
Im.BulletText("'target' or '<t>': your target\n"u8);
|
||||
Im.BulletText("'focus' or '<f>: your focus target\n"u8);
|
||||
Im.BulletText("'mouseover' or '<mo>': the actor you are currently hovering over\n"u8);
|
||||
Im.BulletText("'furniture': most indoor furniture, does not currently work outdoors\n"u8);
|
||||
Im.BulletText("any specific actor name to redraw all actors of that exactly matching name."u8);
|
||||
}
|
||||
|
||||
private static void DrawInfo(Vector2 height)
|
||||
{
|
||||
var frameColor = Im.Style[ImGuiColor.FrameBackground];
|
||||
using var group = Im.Group();
|
||||
using (AwesomeIcon.Font.Push())
|
||||
{
|
||||
ImEx.TextFramed(LunaStyle.HelpMarker.Span, height, frameColor);
|
||||
}
|
||||
|
||||
Im.Line.NoSpacing();
|
||||
ImEx.TextFramed("Redraw: "u8, height, frameColor);
|
||||
}
|
||||
|
||||
public void Draw(Vector2 size)
|
||||
{
|
||||
using var style = Im.Style.PushDefault(ImStyleDouble.FramePadding);
|
||||
DrawInfo(size with { X = 0 });
|
||||
DrawTooltip();
|
||||
|
||||
using var id = Im.Id.Push("Redraw"u8);
|
||||
using var disabled = Im.Disabled(!objects[0].Valid);
|
||||
Im.Line.NoSpacing();
|
||||
var buttonWidth = size with { X = Im.ContentRegion.Available.X / 5 };
|
||||
var tt = !objects[0].Valid
|
||||
? "Can only be used when you are logged in and your character is available."u8
|
||||
: StringU8.Empty;
|
||||
DrawButton(buttonWidth, "All"u8, string.Empty, tt);
|
||||
Im.Line.NoSpacing();
|
||||
DrawButton(buttonWidth, "Self"u8, "self", tt);
|
||||
Im.Line.NoSpacing();
|
||||
|
||||
tt = targets.Target is null && targets.GPoseTarget is null
|
||||
? "Can only be used when you have a target."u8
|
||||
: StringU8.Empty;
|
||||
DrawButton(buttonWidth, "Target"u8, "target", tt);
|
||||
Im.Line.NoSpacing();
|
||||
|
||||
tt = targets.FocusTarget is null
|
||||
? "Can only be used when you have a focus target."u8
|
||||
: StringU8.Empty;
|
||||
DrawButton(buttonWidth, "Focus"u8, "focus", tt);
|
||||
Im.Line.NoSpacing();
|
||||
|
||||
tt = !IsIndoors()
|
||||
? "Can currently only be used for indoor furniture."u8
|
||||
: StringU8.Empty;
|
||||
DrawButton(buttonWidth, "Furniture"u8, "furniture", tt);
|
||||
}
|
||||
|
||||
private void DrawButton(Vector2 width, ReadOnlySpan<byte> label, string lower, ReadOnlySpan<byte> additionalTooltip)
|
||||
{
|
||||
using (Im.Disabled(additionalTooltip.Length > 0))
|
||||
{
|
||||
if (Im.Button(label, width))
|
||||
{
|
||||
if (lower.Length > 0)
|
||||
redrawService.RedrawObject(lower, RedrawType.Redraw);
|
||||
else
|
||||
redrawService.RedrawAll(RedrawType.Redraw);
|
||||
}
|
||||
}
|
||||
|
||||
if (!Im.Item.Hovered(HoveredFlags.AllowWhenDisabled))
|
||||
return;
|
||||
|
||||
using var _ = Im.Tooltip.Begin();
|
||||
if (lower.Length > 0)
|
||||
Im.Text($"Execute '/penumbra redraw {lower}'.");
|
||||
else
|
||||
Im.Text("Execute '/penumbra redraw'."u8);
|
||||
if (additionalTooltip.Length > 0)
|
||||
Im.Text(additionalTooltip);
|
||||
}
|
||||
|
||||
private static unsafe bool IsIndoors()
|
||||
=> HousingManager.Instance()->IsInside();
|
||||
}
|
||||
|
|
@ -1,26 +1,28 @@
|
|||
using Luna.Generators;
|
||||
|
||||
namespace Penumbra.UI.ModsTab;
|
||||
|
||||
[NamedEnum(Utf16: false)]
|
||||
[TooltipEnum]
|
||||
public enum RenameField
|
||||
{
|
||||
[Name("None")]
|
||||
[Tooltip("Show no rename fields in the context menu for mods.")]
|
||||
None,
|
||||
|
||||
[Name("Search Path")]
|
||||
[Tooltip("Show only the search path / move field in the context menu for mods.")]
|
||||
RenameSearchPath,
|
||||
|
||||
[Name("Mod Name")]
|
||||
[Tooltip("Show only the mod name field in the context menu for mods.")]
|
||||
RenameData,
|
||||
|
||||
[Name("Both (Focus Search Path)")]
|
||||
[Tooltip("Show both rename fields in the context menu for mods, but put the keyboard cursor on the search path field.")]
|
||||
BothSearchPathPrio,
|
||||
|
||||
[Name("Both (Focus Mod Name)")]
|
||||
[Tooltip("Show both rename fields in the context menu for mods, but put the keyboard cursor on the mod name field")]
|
||||
BothDataPrio,
|
||||
}
|
||||
|
||||
public static class RenameFieldExtensions
|
||||
{
|
||||
public static (string Name, string Desc) GetData(this RenameField value)
|
||||
=> value switch
|
||||
{
|
||||
RenameField.None => ("None", "Show no rename fields in the context menu for mods."),
|
||||
RenameField.RenameSearchPath => ("Search Path", "Show only the search path / move field in the context menu for mods."),
|
||||
RenameField.RenameData => ("Mod Name", "Show only the mod name field in the context menu for mods."),
|
||||
RenameField.BothSearchPathPrio => ("Both (Focus Search Path)",
|
||||
"Show both rename fields in the context menu for mods, but put the keyboard cursor on the search path field."),
|
||||
RenameField.BothDataPrio => ("Both (Focus Mod Name)",
|
||||
"Show both rename fields in the context menu for mods, but put the keyboard cursor on the mod name field"),
|
||||
_ => (string.Empty, string.Empty),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
294
Penumbra/UI/ModsTab/Selector/Filter/ModFilter.cs
Normal file
294
Penumbra/UI/ModsTab/Selector/Filter/ModFilter.cs
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
using ImSharp;
|
||||
using Luna;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.Mods.Settings;
|
||||
using Penumbra.UI.Classes;
|
||||
using static ImSharp.Im;
|
||||
|
||||
namespace Penumbra.UI.ModsTab.Selector;
|
||||
|
||||
public sealed class ModFilter(ModManager modManager, ActiveCollections collections)
|
||||
: TokenizedFilter<ModFilterTokenType, ModFileSystemCache.ModData, ModFilterToken>,
|
||||
IFileSystemFilter<ModFileSystemCache.ModData>
|
||||
{
|
||||
private ModTypeFilter _stateFilter = ModTypeFilterExtensions.UnfilteredStateMods;
|
||||
|
||||
public ModTypeFilter StateFilter
|
||||
=> _stateFilter;
|
||||
|
||||
protected override void DrawTooltip()
|
||||
{
|
||||
if (!Item.Hovered())
|
||||
return;
|
||||
|
||||
using var tt = Tooltip.Begin();
|
||||
var highlightColor = ColorId.NewMod.Value().ToVector();
|
||||
Im.Text("Filter mods for those where their full paths or names contain the given strings, split by spaces."u8);
|
||||
ImEx.TextMultiColored("Enter "u8).Then("c:[string]"u8, highlightColor).Then(" to filter for mods changing specific items."u8).End();
|
||||
ImEx.TextMultiColored("Enter "u8).Then("t:[string]"u8, highlightColor).Then(" to filter for mods set to specific tags."u8).End();
|
||||
ImEx.TextMultiColored("Enter "u8).Then("n:[string]"u8, highlightColor)
|
||||
.Then(" to filter for mods names without considering the paths."u8).End();
|
||||
ImEx.TextMultiColored("Enter "u8).Then("a:[string]"u8, highlightColor).Then(" to filter for mods by specific authors."u8).End();
|
||||
ImEx.TextMultiColored("Enter "u8).Then("s:[string]"u8, highlightColor).Then(
|
||||
$" to filter for mods by the categories of the items they change (use 1-{ChangedItemFlagExtensions.NumCategories + 1} or a partial category name).")
|
||||
.End();
|
||||
Line.New();
|
||||
ImEx.TextMultiColored("Use "u8).Then("None"u8, highlightColor).Then(" as a placeholder value that only matches empty lists or names."u8)
|
||||
.End();
|
||||
Im.Text("Regularly, a mod has to match all supplied criteria separately."u8);
|
||||
ImEx.TextMultiColored("Put a "u8).Then("'-'"u8, highlightColor)
|
||||
.Then(" in front of a search token to search only for mods not matching the criterion."u8).End();
|
||||
ImEx.TextMultiColored("Put a "u8).Then("'?'"u8, highlightColor)
|
||||
.Then(" in front of a search token to search for mods matching at least one of the '?'-criteria."u8).End();
|
||||
ImEx.TextMultiColored("Wrap spaces in "u8).Then("\"[string with space]\""u8, highlightColor)
|
||||
.Then(" to match this exact combination of words."u8).End();
|
||||
Line.New();
|
||||
Im.Text("Example: 't:Tag1 t:\"Tag 2\" -t:Tag3 -a:None s:Body -c:Hempen ?c:Camise ?n:Top' will match any mod that"u8);
|
||||
BulletText("contains the tags 'tag1' and 'tag2',"u8);
|
||||
BulletText("does not contain the tag 'tag3',"u8);
|
||||
BulletText("has any author set (negating None means Any),"u8);
|
||||
BulletText("changes an item of the 'Body' category,"u8);
|
||||
BulletText("and either contains a changed item with 'camise' in it's name, or has 'top' in the mod's name."u8);
|
||||
}
|
||||
|
||||
public override bool DrawFilter(ReadOnlySpan<byte> label, Vector2 availableRegion)
|
||||
{
|
||||
var ret = base.DrawFilter(label, availableRegion with { X = availableRegion.X - Style.FrameHeight });
|
||||
Line.NoSpacing();
|
||||
ret |= DrawFilterCombo();
|
||||
return ret;
|
||||
}
|
||||
|
||||
private bool DrawFilterCombo()
|
||||
{
|
||||
var everything = _stateFilter is not ModTypeFilterExtensions.UnfilteredStateMods;
|
||||
using var color = ImGuiColor.Button.Push(Colors.FilterActive, everything);
|
||||
using var combo = Combo.Begin("##combo"u8, StringU8.Empty,
|
||||
ComboFlags.NoPreview | ComboFlags.HeightLargest | ComboFlags.PopupAlignLeft);
|
||||
|
||||
if (Item.RightClicked())
|
||||
{
|
||||
// Ensure that a right-click clears the text filter if it is currently being edited.
|
||||
Id.ClearActive();
|
||||
Clear();
|
||||
}
|
||||
|
||||
Tooltip.OnHover("Filter mods for their activation status.\nRight-Click to clear all filters, including the text-filter."u8);
|
||||
|
||||
var changes = false;
|
||||
if (combo)
|
||||
{
|
||||
using var style = ImStyleDouble.ItemSpacing.PushY(3 * Style.GlobalScale);
|
||||
changes |= Checkbox("Everything"u8, ref _stateFilter, ModTypeFilterExtensions.UnfilteredStateMods);
|
||||
Dummy(new Vector2(0, 5 * Style.GlobalScale));
|
||||
foreach (var (onFlag, offFlag, name) in ModTypeFilterExtensions.TriStatePairs)
|
||||
changes |= ImEx.TriStateCheckbox(name, ref _stateFilter, onFlag, offFlag);
|
||||
|
||||
foreach (var group in ModTypeFilterExtensions.Groups)
|
||||
{
|
||||
Separator();
|
||||
foreach (var (flag, name) in group)
|
||||
changes |= Checkbox(name, ref _stateFilter, flag);
|
||||
}
|
||||
}
|
||||
|
||||
if (changes)
|
||||
InvokeEvent();
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
public override void Clear()
|
||||
{
|
||||
var changes = _stateFilter is not ModTypeFilterExtensions.UnfilteredStateMods;
|
||||
_stateFilter = ModTypeFilterExtensions.UnfilteredStateMods;
|
||||
if (!Set(string.Empty) && changes)
|
||||
InvokeEvent();
|
||||
}
|
||||
|
||||
protected override bool Matches(in ModFilterToken token, in ModFileSystemCache.ModData cacheItem)
|
||||
=> token.Type switch
|
||||
{
|
||||
ModFilterTokenType.Default => cacheItem.Node.FullPath.Contains(token.Needle, StringComparison.OrdinalIgnoreCase)
|
||||
|| cacheItem.Node.Value.Name.Contains(token.Needle, StringComparison.OrdinalIgnoreCase),
|
||||
ModFilterTokenType.ChangedItem => cacheItem.Node.Value.LowerChangedItemsString.Contains(token.Needle, StringComparison.Ordinal),
|
||||
ModFilterTokenType.Tag => cacheItem.Node.Value.AllTagsLower.Contains(token.Needle, StringComparison.Ordinal),
|
||||
ModFilterTokenType.Name => cacheItem.Node.Value.Name.Contains(token.Needle, StringComparison.OrdinalIgnoreCase),
|
||||
ModFilterTokenType.Author => cacheItem.Node.Value.Author.Contains(token.Needle, StringComparison.OrdinalIgnoreCase),
|
||||
ModFilterTokenType.Category => CheckCategory(token.IconFlagFilter, cacheItem),
|
||||
_ => true,
|
||||
};
|
||||
|
||||
private static bool CheckCategory(ChangedItemIconFlag flag, ModFileSystemCache.ModData cacheItem)
|
||||
=> cacheItem.Node.Value.ChangedItems.Any(p => (p.Value.Icon.ToFlag() & flag) is not 0);
|
||||
|
||||
protected override bool MatchesNone(ModFilterTokenType type, bool negated, in ModFileSystemCache.ModData cacheItem)
|
||||
=> type switch
|
||||
{
|
||||
ModFilterTokenType.Author when negated => cacheItem.Node.Value.Author.Length > 0,
|
||||
ModFilterTokenType.Author => cacheItem.Node.Value.Author.Length is 0,
|
||||
ModFilterTokenType.ChangedItem when negated => cacheItem.Node.Value.LowerChangedItemsString.Length > 0,
|
||||
ModFilterTokenType.ChangedItem => cacheItem.Node.Value.LowerChangedItemsString.Length is 0,
|
||||
ModFilterTokenType.Tag when negated => cacheItem.Node.Value.AllTagsLower.Length > 0,
|
||||
ModFilterTokenType.Tag => cacheItem.Node.Value.AllTagsLower.Length is 0,
|
||||
ModFilterTokenType.Category when negated => cacheItem.Node.Value.ChangedItems.Count > 0,
|
||||
ModFilterTokenType.Category => cacheItem.Node.Value.ChangedItems.Count is 0,
|
||||
_ => true,
|
||||
};
|
||||
|
||||
public override bool WouldBeVisible(in ModFileSystemCache.ModData cacheItem, int globalIndex)
|
||||
{
|
||||
if (!base.WouldBeVisible(in cacheItem, globalIndex))
|
||||
return false;
|
||||
|
||||
if (_stateFilter is ModTypeFilterExtensions.UnfilteredStateMods)
|
||||
return true;
|
||||
|
||||
return CheckStateFilters(cacheItem.Node.Value);
|
||||
}
|
||||
|
||||
private bool CheckStateFilters(Mod mod)
|
||||
{
|
||||
var (settings, collection) = collections.Current.GetActualSettings(mod.Index);
|
||||
var isNew = modManager.IsNew(mod);
|
||||
// Handle mod details.
|
||||
if (CheckFlags(mod.TotalFileCount, ModTypeFilter.HasNoFiles, ModTypeFilter.HasFiles)
|
||||
|| CheckFlags(mod.TotalSwapCount, ModTypeFilter.HasNoFileSwaps, ModTypeFilter.HasFileSwaps)
|
||||
|| CheckFlags(mod.TotalManipulations, ModTypeFilter.HasNoMetaManipulations, ModTypeFilter.HasMetaManipulations)
|
||||
|| CheckFlags(mod.HasOptions ? 1 : 0, ModTypeFilter.HasNoConfig, ModTypeFilter.HasConfig)
|
||||
|| CheckFlags(isNew ? 1 : 0, ModTypeFilter.NotNew, ModTypeFilter.IsNew))
|
||||
return false;
|
||||
|
||||
// Handle Favoritism
|
||||
if (!_stateFilter.HasFlag(ModTypeFilter.Favorite) && mod.Favorite
|
||||
|| !_stateFilter.HasFlag(ModTypeFilter.NotFavorite) && !mod.Favorite)
|
||||
return false;
|
||||
|
||||
// Handle Temporary
|
||||
if (!_stateFilter.HasFlag(ModTypeFilter.Temporary) || !_stateFilter.HasFlag(ModTypeFilter.NotTemporary))
|
||||
{
|
||||
if (settings is null && _stateFilter.HasFlag(ModTypeFilter.Temporary))
|
||||
return false;
|
||||
|
||||
if (settings is not null && settings.IsTemporary() != _stateFilter.HasFlag(ModTypeFilter.Temporary))
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle Inheritance
|
||||
if (collection == collections.Current)
|
||||
{
|
||||
if (!_stateFilter.HasFlag(ModTypeFilter.Uninherited))
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_stateFilter.HasFlag(ModTypeFilter.Inherited))
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle settings.
|
||||
if (settings is null)
|
||||
{
|
||||
if (!_stateFilter.HasFlag(ModTypeFilter.Undefined)
|
||||
|| !_stateFilter.HasFlag(ModTypeFilter.Disabled)
|
||||
|| !_stateFilter.HasFlag(ModTypeFilter.NoConflict))
|
||||
return false;
|
||||
}
|
||||
else if (!settings.Enabled)
|
||||
{
|
||||
if (!_stateFilter.HasFlag(ModTypeFilter.Disabled)
|
||||
|| !_stateFilter.HasFlag(ModTypeFilter.NoConflict))
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_stateFilter.HasFlag(ModTypeFilter.Enabled))
|
||||
return false;
|
||||
|
||||
// Conflicts can only be relevant if the mod is enabled.
|
||||
var conflicts = collections.Current.Conflicts(mod);
|
||||
if (conflicts.Count > 0)
|
||||
{
|
||||
if (conflicts.Any(c => !c.Solved))
|
||||
{
|
||||
if (!_stateFilter.HasFlag(ModTypeFilter.UnsolvedConflict))
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_stateFilter.HasFlag(ModTypeFilter.SolvedConflict))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (!_stateFilter.HasFlag(ModTypeFilter.NoConflict))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check the state filter for a specific pair of has/has-not flags.
|
||||
/// Uses count == 0 to check for has-not and count != 0 for has.
|
||||
/// Returns true if it should be filtered and false if not.
|
||||
/// </summary>
|
||||
private bool CheckFlags(int count, ModTypeFilter hasNoFlag, ModTypeFilter hasFlag)
|
||||
=> count switch
|
||||
{
|
||||
0 when _stateFilter.HasFlag(hasNoFlag) => false,
|
||||
0 => true,
|
||||
_ when _stateFilter.HasFlag(hasFlag) => false,
|
||||
_ => true,
|
||||
};
|
||||
|
||||
public bool WouldBeVisible(in FileSystemFolderCache folder)
|
||||
{
|
||||
if (_stateFilter is not ModTypeFilterExtensions.UnfilteredStateMods)
|
||||
return false;
|
||||
|
||||
switch (State)
|
||||
{
|
||||
case FilterState.NoFilters: return true;
|
||||
case FilterState.NoMatches: return false;
|
||||
}
|
||||
|
||||
foreach (var token in Forced)
|
||||
{
|
||||
if (token.Type switch
|
||||
{
|
||||
ModFilterTokenType.Name => folder.Name.Contains(token.Needle, StringComparison.OrdinalIgnoreCase),
|
||||
ModFilterTokenType.Default => folder.FullPath.Contains(token.Needle, StringComparison.OrdinalIgnoreCase),
|
||||
_ => true,
|
||||
})
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var token in Negated)
|
||||
{
|
||||
if (token.Type switch
|
||||
{
|
||||
ModFilterTokenType.Name => folder.Name.Contains(token.Needle, StringComparison.OrdinalIgnoreCase),
|
||||
ModFilterTokenType.Default => folder.FullPath.Contains(token.Needle, StringComparison.OrdinalIgnoreCase),
|
||||
_ => false,
|
||||
})
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var token in General)
|
||||
{
|
||||
if (token.Type switch
|
||||
{
|
||||
ModFilterTokenType.Name => folder.Name.Contains(token.Needle, StringComparison.OrdinalIgnoreCase),
|
||||
ModFilterTokenType.Default => folder.FullPath.Contains(token.Needle, StringComparison.OrdinalIgnoreCase),
|
||||
_ => false,
|
||||
})
|
||||
return true;
|
||||
}
|
||||
|
||||
return General.Count is 0;
|
||||
}
|
||||
}
|
||||
64
Penumbra/UI/ModsTab/Selector/Filter/ModFilterToken.cs
Normal file
64
Penumbra/UI/ModsTab/Selector/Filter/ModFilterToken.cs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
using ImSharp;
|
||||
using Penumbra.UI.Classes;
|
||||
|
||||
namespace Penumbra.UI.ModsTab.Selector;
|
||||
|
||||
public readonly struct ModFilterToken() : IFilterToken<ModFilterTokenType, ModFilterToken>
|
||||
{
|
||||
public string Needle { get; init; } = string.Empty;
|
||||
public ModFilterTokenType Type { get; init; }
|
||||
public ChangedItemIconFlag IconFlagFilter { get; init; }
|
||||
|
||||
public bool Contains(ModFilterToken other)
|
||||
{
|
||||
if (Type != other.Type)
|
||||
return false;
|
||||
if (Type is ModFilterTokenType.Category)
|
||||
return IconFlagFilter == other.IconFlagFilter;
|
||||
|
||||
return Needle.Contains(other.Needle);
|
||||
}
|
||||
|
||||
public static bool ConvertToken(char tokenCharacter, out ModFilterTokenType type)
|
||||
{
|
||||
type = tokenCharacter switch
|
||||
{
|
||||
'c' or 'C' => ModFilterTokenType.ChangedItem,
|
||||
't' or 'T' => ModFilterTokenType.Tag,
|
||||
'n' or 'N' => ModFilterTokenType.Name,
|
||||
'a' or 'A' => ModFilterTokenType.Author,
|
||||
's' or 'S' => ModFilterTokenType.Category,
|
||||
_ => ModFilterTokenType.Default,
|
||||
};
|
||||
return type is not ModFilterTokenType.Default;
|
||||
}
|
||||
|
||||
public static bool AllowsNone(ModFilterTokenType type)
|
||||
=> type switch
|
||||
{
|
||||
ModFilterTokenType.Author => true,
|
||||
ModFilterTokenType.ChangedItem => true,
|
||||
ModFilterTokenType.Tag => true,
|
||||
ModFilterTokenType.Category => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
public static void ProcessList(List<ModFilterToken> list)
|
||||
{
|
||||
for (var i = 0; i < list.Count; ++i)
|
||||
{
|
||||
var entry = list[i];
|
||||
if (entry.Type is not ModFilterTokenType.Category)
|
||||
continue;
|
||||
|
||||
if (ChangedItemDrawer.TryParsePartial(entry.Needle, out var icon))
|
||||
list[i] = entry with
|
||||
{
|
||||
IconFlagFilter = icon,
|
||||
Needle = string.Empty,
|
||||
};
|
||||
else
|
||||
list.RemoveAt(i--);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Penumbra/UI/ModsTab/Selector/Filter/ModFilterTokenType.cs
Normal file
11
Penumbra/UI/ModsTab/Selector/Filter/ModFilterTokenType.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
namespace Penumbra.UI.ModsTab.Selector;
|
||||
|
||||
public enum ModFilterTokenType
|
||||
{
|
||||
Default,
|
||||
ChangedItem,
|
||||
Tag,
|
||||
Name,
|
||||
Author,
|
||||
Category,
|
||||
}
|
||||
61
Penumbra/UI/ModsTab/Selector/Filter/ModTypeFilter.cs
Normal file
61
Penumbra/UI/ModsTab/Selector/Filter/ModTypeFilter.cs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
using ImSharp;
|
||||
|
||||
namespace Penumbra.UI.ModsTab.Selector;
|
||||
|
||||
[Flags]
|
||||
public enum ModTypeFilter
|
||||
{
|
||||
Enabled = 1 << 0,
|
||||
Disabled = 1 << 1,
|
||||
Favorite = 1 << 2,
|
||||
NotFavorite = 1 << 3,
|
||||
NoConflict = 1 << 4,
|
||||
SolvedConflict = 1 << 5,
|
||||
UnsolvedConflict = 1 << 6,
|
||||
HasNoMetaManipulations = 1 << 7,
|
||||
HasMetaManipulations = 1 << 8,
|
||||
HasNoFileSwaps = 1 << 9,
|
||||
HasFileSwaps = 1 << 10,
|
||||
HasConfig = 1 << 11,
|
||||
HasNoConfig = 1 << 12,
|
||||
HasNoFiles = 1 << 13,
|
||||
HasFiles = 1 << 14,
|
||||
IsNew = 1 << 15,
|
||||
NotNew = 1 << 16,
|
||||
Inherited = 1 << 17,
|
||||
Uninherited = 1 << 18,
|
||||
Temporary = 1 << 19,
|
||||
NotTemporary = 1 << 20,
|
||||
Undefined = 1 << 21,
|
||||
};
|
||||
|
||||
public static class ModTypeFilterExtensions
|
||||
{
|
||||
public const ModTypeFilter UnfilteredStateMods = (ModTypeFilter)((1 << 22) - 1);
|
||||
|
||||
public static readonly IReadOnlyList<(ModTypeFilter On, ModTypeFilter Off, StringU8 Name)> TriStatePairs =
|
||||
[
|
||||
(ModTypeFilter.Enabled, ModTypeFilter.Disabled, new StringU8("Enabled"u8)),
|
||||
(ModTypeFilter.IsNew, ModTypeFilter.NotNew, new StringU8("Newly Imported"u8)),
|
||||
(ModTypeFilter.Favorite, ModTypeFilter.NotFavorite, new StringU8("Favorite"u8)),
|
||||
(ModTypeFilter.HasConfig, ModTypeFilter.HasNoConfig, new StringU8("Has Options"u8)),
|
||||
(ModTypeFilter.HasFiles, ModTypeFilter.HasNoFiles, new StringU8("Has Redirections"u8)),
|
||||
(ModTypeFilter.HasMetaManipulations, ModTypeFilter.HasNoMetaManipulations, new StringU8("Has Meta Manipulations"u8)),
|
||||
(ModTypeFilter.HasFileSwaps, ModTypeFilter.HasNoFileSwaps, new StringU8("Has File Swaps"u8)),
|
||||
(ModTypeFilter.Temporary, ModTypeFilter.NotTemporary, new StringU8("Temporary"u8)),
|
||||
];
|
||||
|
||||
public static readonly IReadOnlyList<IReadOnlyList<(ModTypeFilter Filter, StringU8 Name)>> Groups =
|
||||
[
|
||||
[
|
||||
(ModTypeFilter.NoConflict, new StringU8("Has No Conflicts"u8)),
|
||||
(ModTypeFilter.SolvedConflict, new StringU8("Has Solved Conflicts"u8)),
|
||||
(ModTypeFilter.UnsolvedConflict, new StringU8("Has Unsolved Conflicts"u8)),
|
||||
],
|
||||
[
|
||||
(ModTypeFilter.Undefined, new StringU8("Not Configured"u8)),
|
||||
(ModTypeFilter.Inherited, new StringU8("Inherited Configuration"u8)),
|
||||
(ModTypeFilter.Uninherited, new StringU8("Own Configuration"u8)),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
@ -1,15 +1,127 @@
|
|||
using ImSharp;
|
||||
using Luna;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Mods.Settings;
|
||||
using Penumbra.UI.Classes;
|
||||
|
||||
namespace Penumbra.UI.ModsTab.Selector;
|
||||
|
||||
public sealed class ModFileSystemCache(ModFileSystemDrawer parent)
|
||||
: FileSystemCache<ModFileSystemCache.ModData>(parent), IService
|
||||
{
|
||||
public sealed class ModData : BaseFileSystemNodeCache<ModData>;
|
||||
public sealed class ModData(IFileSystemData<Mod> node) : BaseFileSystemNodeCache<ModData>
|
||||
{
|
||||
public readonly IFileSystemData<Mod> Node = node;
|
||||
public Vector4 TextColor;
|
||||
public ModPriority Priority;
|
||||
public SizedString PriorityText = SizedString.Empty;
|
||||
public ModSettings? Settings;
|
||||
public ModCollection Collection = ModCollection.Empty;
|
||||
|
||||
public override void Update(FileSystemCache cache, IFileSystemNode node)
|
||||
{
|
||||
base.Update(cache, node);
|
||||
var currentCollection = ((ModFileSystemDrawer)cache.Parent).CollectionManager.Active.Current;
|
||||
(Settings, Collection) = currentCollection.GetActualSettings(Node.Value.Index);
|
||||
TextColor = UpdateColor(cache, currentCollection);
|
||||
var priority = Settings?.Priority ?? ModPriority.Default;
|
||||
if (priority != Priority)
|
||||
{
|
||||
Priority = priority;
|
||||
PriorityText = priority.IsDefault ? SizedString.Empty : new SizedString($"[{priority}]");
|
||||
}
|
||||
}
|
||||
|
||||
private Vector4 UpdateColor(FileSystemCache cache, ModCollection current)
|
||||
{
|
||||
var modManager = ((ModFileSystemDrawer)cache.Parent).ModManager;
|
||||
var tint = (Settings.IsTemporary() ? ColorId.TemporaryModSettingsTint :
|
||||
modManager.IsNew(Node.Value) ? ColorId.NewModTint : ColorId.NoTint).Value().ToVector();
|
||||
if (Settings is null)
|
||||
return Rgba32.TintColor(ColorId.UndefinedMod.Value().ToVector(), tint);
|
||||
|
||||
if (!Settings.Enabled)
|
||||
return Rgba32.TintColor((Collection != current ? ColorId.InheritedDisabledMod : ColorId.DisabledMod).Value().ToVector(), tint);
|
||||
|
||||
var conflicts = current.Conflicts(Node.Value);
|
||||
if (conflicts.Count is 0)
|
||||
return Rgba32.TintColor((Collection != current ? ColorId.InheritedMod : ColorId.EnabledMod).Value().ToVector(), tint);
|
||||
|
||||
return Rgba32.TintColor((conflicts.Any(c => !c.Solved) ? ColorId.ConflictingMod : ColorId.HandledConflictMod).Value().ToVector(),
|
||||
tint);
|
||||
}
|
||||
|
||||
protected override void DrawInternal(FileSystemCache<ModData> cache, IFileSystemNode node)
|
||||
{
|
||||
using var color = ImGuiColor.Text.Push(TextColor)
|
||||
.Push(ImGuiColor.HeaderHovered, 0x4000FFFF, Node.Value.Favorite);
|
||||
using var id = Im.Id.Push(Node.Value.Index);
|
||||
base.DrawInternal(cache, node);
|
||||
if (Im.Item.MiddleClicked())
|
||||
OnMiddleClick(cache);
|
||||
DrawPriority(cache);
|
||||
}
|
||||
|
||||
private void OnMiddleClick(FileSystemCache<ModData> cache)
|
||||
{
|
||||
var modManager = ((ModFileSystemDrawer)cache.Parent).ModManager;
|
||||
var collectionManager = ((ModFileSystemDrawer)cache.Parent).CollectionManager;
|
||||
var config = ((ModFileSystemDrawer)cache.Parent).Config;
|
||||
|
||||
modManager.SetKnown(Node.Value);
|
||||
var (setting, collection) = collectionManager.Active.Current.GetActualSettings(Node.Value.Index);
|
||||
if (config.DeleteModModifier.ForcedModifier(new DoubleModifier(ModifierHotkey.Control, ModifierHotkey.Shift)).IsActive())
|
||||
{
|
||||
// Delete temporary settings if they exist, regardless of mode, or set to inheriting if none exist.
|
||||
if (collectionManager.Active.Current.GetTempSettings(Node.Value.Index) is not null)
|
||||
collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, Node.Value, null);
|
||||
else
|
||||
collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, Node.Value, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (config.DefaultTemporaryMode)
|
||||
{
|
||||
var settings = new TemporaryModSettings(Node.Value, setting) { ForceInherit = false };
|
||||
settings.Enabled = !settings.Enabled;
|
||||
collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, Node.Value, settings);
|
||||
}
|
||||
else
|
||||
{
|
||||
var inherited = collection != collectionManager.Active.Current;
|
||||
if (inherited)
|
||||
collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, Node.Value, false);
|
||||
collectionManager.Editor.SetModState(collectionManager.Active.Current, Node.Value, setting is not { Enabled: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawPriority(FileSystemCache<ModData> cache)
|
||||
{
|
||||
if (Priority.IsDefault)
|
||||
return;
|
||||
|
||||
var config = ((ModFileSystemDrawer)cache.Parent).Config;
|
||||
if (config.HidePrioritiesInSelector)
|
||||
return;
|
||||
|
||||
var line = Im.Item.UpperLeftCorner.Y;
|
||||
var itemPos = Im.Item.LowerRightCorner.X;
|
||||
var maxWidth = Im.Window.Position.X + Im.Window.MaximumContentRegion.X;
|
||||
var remainingSpace = maxWidth - itemPos;
|
||||
var offset = remainingSpace - PriorityText.Size.X;
|
||||
if (Im.Scroll.MaximumY is 0)
|
||||
offset -= Im.Style.ItemInnerSpacing.X;
|
||||
|
||||
if (offset > Im.Style.ItemSpacing.X)
|
||||
Im.Window.DrawList.Text(new Vector2(itemPos + offset, line), ColorId.SelectorPriority.Value().Color, PriorityText.Text);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Update()
|
||||
{ }
|
||||
|
||||
protected override ModData ConvertNode(in IFileSystemNode node)
|
||||
=> new();
|
||||
=> new((IFileSystemData<Mod>)node);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ public sealed class ModFileSystemDrawer : FileSystemDrawer<ModFileSystemCache.Mo
|
|||
public readonly Configuration Config;
|
||||
|
||||
public ModFileSystemDrawer(ModFileSystem2 fileSystem, ModManager modManager, CollectionManager collectionManager, Configuration config)
|
||||
: base(fileSystem, null)
|
||||
: base(fileSystem, new ModFilter(modManager, collectionManager.Active))
|
||||
{
|
||||
ModManager = modManager;
|
||||
CollectionManager = collectionManager;
|
||||
|
|
@ -28,9 +28,7 @@ public sealed class ModFileSystemDrawer : FileSystemDrawer<ModFileSystemCache.Mo
|
|||
FolderContext.AddButton(new SetDefaultImportFolderButton(this), -100);
|
||||
|
||||
DataContext.AddButton(new ToggleFavoriteButton(this), 10);
|
||||
|
||||
Footer.Buttons.AddButton(new AddNewModButton(this), 1000);
|
||||
|
||||
}
|
||||
|
||||
public override ReadOnlySpan<byte> Id
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using OtterGui.Text;
|
||||
using ImSharp;
|
||||
|
||||
namespace Penumbra.UI.Tabs.Debug;
|
||||
|
||||
|
|
@ -6,11 +6,11 @@ public static class DebugConfigurationDrawer
|
|||
{
|
||||
public static void Draw()
|
||||
{
|
||||
using var id = ImUtf8.CollapsingHeaderId("Debugging Options"u8);
|
||||
using var id = Im.Tree.HeaderId("Debugging Options"u8);
|
||||
if (!id)
|
||||
return;
|
||||
|
||||
ImUtf8.Checkbox("Log IMC File Replacements"u8, ref DebugConfiguration.WriteImcBytesToLog);
|
||||
ImUtf8.Checkbox("Scan for Skin Material Attributes"u8, ref DebugConfiguration.UseSkinMaterialProcessing);
|
||||
Im.Checkbox("Log IMC File Replacements"u8, ref DebugConfiguration.WriteImcBytesToLog);
|
||||
Im.Checkbox("Scan for Skin Material Attributes"u8, ref DebugConfiguration.UseSkinMaterialProcessing);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
|
|
@ -11,8 +10,6 @@ using Dalamud.Interface.Colors;
|
|||
using ImSharp;
|
||||
using Luna;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OtterGui;
|
||||
using OtterGui.Text;
|
||||
using Penumbra.Api;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections.Manager;
|
||||
|
|
@ -57,10 +54,10 @@ public class Diagnostics(ServiceManager provider) : IUiService
|
|||
.Where(t => t is { IsAbstract: false, IsInterface: false } && t.IsAssignableTo(typeof(IAsyncDataContainer))))
|
||||
{
|
||||
var container = (IAsyncDataContainer)provider.Provider!.GetRequiredService(type);
|
||||
ImGuiUtil.DrawTableColumn(container.Name);
|
||||
ImGuiUtil.DrawTableColumn(container.Time.ToString());
|
||||
ImGuiUtil.DrawTableColumn(Functions.HumanReadableSize(container.Memory));
|
||||
ImGuiUtil.DrawTableColumn(container.TotalCount.ToString());
|
||||
table.DrawColumn(container.Name);
|
||||
table.DrawColumn($"{container.Time}");
|
||||
table.DrawColumn(FormattingFunctions.HumanReadableSize(container.Memory));
|
||||
table.DrawColumn($"{container.TotalCount}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -207,7 +204,8 @@ public sealed class DebugTab : Window, ITab<TabType>
|
|||
_globalVariablesDrawer.Draw();
|
||||
DrawCloudApi();
|
||||
DrawDebugTabIpc();
|
||||
_dragDropManager.DrawDebugInfo();
|
||||
if(Im.Tree.Header("Drag & Drop Manager"u8))
|
||||
_dragDropManager.DrawDebugInfo();
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -228,7 +226,7 @@ public sealed class DebugTab : Window, ITab<TabType>
|
|||
continue;
|
||||
|
||||
color.Pop();
|
||||
using (var inheritanceNode = ImUtf8.TreeNode("Inheritance"u8))
|
||||
using (var inheritanceNode = Im.Tree.Node("Inheritance"u8))
|
||||
{
|
||||
if (inheritanceNode)
|
||||
{
|
||||
|
|
@ -243,17 +241,17 @@ public sealed class DebugTab : Window, ITab<TabType>
|
|||
{
|
||||
table.NextColumn();
|
||||
if (i < collection.Inheritance.DirectlyInheritsFrom.Count)
|
||||
ImUtf8.Text(collection.Inheritance.DirectlyInheritsFrom[i].Identity.Name);
|
||||
Im.Text(collection.Inheritance.DirectlyInheritsFrom[i].Identity.Name);
|
||||
else
|
||||
Im.Dummy(new Vector2(200 * Im.Style.GlobalScale, Im.Style.TextHeight));
|
||||
table.NextColumn();
|
||||
if (i < collection.Inheritance.DirectlyInheritedBy.Count)
|
||||
ImUtf8.Text(collection.Inheritance.DirectlyInheritedBy[i].Identity.Name);
|
||||
Im.Text(collection.Inheritance.DirectlyInheritedBy[i].Identity.Name);
|
||||
else
|
||||
Im.Dummy(new Vector2(200 * Im.Style.GlobalScale, Im.Style.TextHeight));
|
||||
table.NextColumn();
|
||||
if (i < collection.Inheritance.FlatHierarchy.Count)
|
||||
ImUtf8.Text(collection.Inheritance.FlatHierarchy[i].Identity.Name);
|
||||
Im.Text(collection.Inheritance.FlatHierarchy[i].Identity.Name);
|
||||
else
|
||||
Im.Dummy(new Vector2(200 * Im.Style.GlobalScale, Im.Style.TextHeight));
|
||||
}
|
||||
|
|
@ -261,14 +259,14 @@ public sealed class DebugTab : Window, ITab<TabType>
|
|||
}
|
||||
}
|
||||
|
||||
using (var resourceNode = ImUtf8.TreeNode("Custom Resources"u8))
|
||||
using (var resourceNode = Im.Tree.Node("Custom Resources"u8))
|
||||
{
|
||||
if (resourceNode)
|
||||
foreach (var (path, resource) in collection.Cache!.CustomResources)
|
||||
Im.Tree.Leaf($"{path} -> 0x{(ulong)resource.ResourceHandle:X}");
|
||||
}
|
||||
|
||||
using var modNode = ImUtf8.TreeNode("Enabled Mods"u8);
|
||||
using var modNode = Im.Tree.Node("Enabled Mods"u8);
|
||||
if (modNode)
|
||||
foreach (var (mod, paths, manips) in collection.Cache!.ModData.Data.OrderBy(t => t.Item1.Name))
|
||||
{
|
||||
|
|
@ -468,7 +466,7 @@ public sealed class DebugTab : Window, ITab<TabType>
|
|||
}
|
||||
|
||||
Im.Line.Same();
|
||||
using (ImUtf8.Group())
|
||||
using (Im.Group())
|
||||
{
|
||||
Im.Text($"{PenumbraStringMemory.CurrentStrings}");
|
||||
Im.Text($"{PenumbraStringMemory.AllocatedStrings}");
|
||||
|
|
@ -485,7 +483,7 @@ public sealed class DebugTab : Window, ITab<TabType>
|
|||
|
||||
private void DrawPerformanceTab()
|
||||
{
|
||||
if (!Im.Tree.Node("Performance"u8))
|
||||
if (!Im.Tree.Header("Performance"u8))
|
||||
return;
|
||||
|
||||
using (var start = Im.Tree.Node("Startup Performance"u8, TreeNodeFlags.DefaultOpen))
|
||||
|
|
@ -497,7 +495,7 @@ public sealed class DebugTab : Window, ITab<TabType>
|
|||
|
||||
private unsafe void DrawActorsDebug()
|
||||
{
|
||||
if (!Im.Tree.Node("Actors"u8))
|
||||
if (!Im.Tree.Header("Actors"u8))
|
||||
return;
|
||||
|
||||
using (var objectTree = Im.Tree.Node("Object Manager"u8))
|
||||
|
|
@ -570,7 +568,7 @@ public sealed class DebugTab : Window, ITab<TabType>
|
|||
/// </summary>
|
||||
private unsafe void DrawPathResolverDebug()
|
||||
{
|
||||
if (!Im.Tree.Node("Path Resolver"u8))
|
||||
if (!Im.Tree.Header("Path Resolver"u8))
|
||||
return;
|
||||
|
||||
Im.Text(
|
||||
|
|
@ -712,22 +710,22 @@ public sealed class DebugTab : Window, ITab<TabType>
|
|||
if (bannerTree)
|
||||
{
|
||||
var agent = &AgentBannerParty.Instance()->AgentBannerInterface;
|
||||
if (agent->Data == null)
|
||||
if (agent->Data is null)
|
||||
agent = &AgentBannerMIP.Instance()->AgentBannerInterface;
|
||||
|
||||
Im.Text("Agent: "u8);
|
||||
Im.Line.NoSpacing();
|
||||
Penumbra.Dynamis.DrawPointer((nint)agent);
|
||||
if (agent->Data != null)
|
||||
if (agent->Data is not null)
|
||||
{
|
||||
using var table = Im.Table.Begin("###PBannerTable"u8, 2, TableFlags.SizingFixedFit);
|
||||
if (table)
|
||||
for (var i = 0; i < 8; ++i)
|
||||
{
|
||||
ref var c = ref agent->Data->Characters[i];
|
||||
ImGuiUtil.DrawTableColumn($"Character {i}");
|
||||
table.DrawColumn($"Character {i}");
|
||||
var name = c.Name1.ToString();
|
||||
ImGuiUtil.DrawTableColumn(name.Length == 0 ? "NULL" : $"{name} ({c.WorldId})");
|
||||
table.DrawColumn(name.Length is 0 ? "NULL" : $"{name} ({c.WorldId})");
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
@ -744,17 +742,14 @@ public sealed class DebugTab : Window, ITab<TabType>
|
|||
using var table = Im.Table.Begin("###TmbTable"u8, 2, TableFlags.SizingFixedFit);
|
||||
if (table)
|
||||
foreach (var (id, name) in _schedulerService.ListedTmbs.OrderBy(kvp => kvp.Key))
|
||||
{
|
||||
ImUtf8.DrawTableColumn($"{id:D6}");
|
||||
ImUtf8.DrawTableColumn(name.Span);
|
||||
}
|
||||
table.DrawDataPair($"{id:D6}", name.Span);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawData()
|
||||
{
|
||||
if (!Im.Tree.Node("Game Data"u8))
|
||||
if (!Im.Tree.Header("Game Data"u8))
|
||||
return;
|
||||
|
||||
DrawEmotes();
|
||||
|
|
@ -800,7 +795,7 @@ public sealed class DebugTab : Window, ITab<TabType>
|
|||
if (!node)
|
||||
return;
|
||||
|
||||
if (ImUtf8.InputText("##ChangedItemTest"u8, ref _changedItemPath, "Changed Item File Path..."u8))
|
||||
if (Im.Input.Text("##ChangedItemTest"u8, ref _changedItemPath, "Changed Item File Path..."u8))
|
||||
{
|
||||
_changedItems.Clear();
|
||||
_objectIdentification.Identify(_changedItems, _changedItemPath);
|
||||
|
|
@ -809,13 +804,13 @@ public sealed class DebugTab : Window, ITab<TabType>
|
|||
if (_changedItems.Count == 0)
|
||||
return;
|
||||
|
||||
using var list = ImUtf8.ListBox("##ChangedItemList"u8,
|
||||
new Vector2(Im.ContentRegion.Available.X, 8 * Im.Style.TextHeightWithSpacing));
|
||||
using var list = Im.ListBox.Begin("##ChangedItemList"u8,
|
||||
Im.ContentRegion.Available with { Y = 8 * Im.Style.TextHeightWithSpacing });
|
||||
if (!list)
|
||||
return;
|
||||
|
||||
foreach (var item in _changedItems)
|
||||
ImUtf8.Selectable(item.Key);
|
||||
Im.Selectable(item.Key);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -839,7 +834,7 @@ public sealed class DebugTab : Window, ITab<TabType>
|
|||
if (_atchFile == null)
|
||||
return;
|
||||
|
||||
using var mainTree = ImUtf8.TreeNode("Atch File C0101"u8);
|
||||
using var mainTree = Im.Tree.Node("Atch File C0101"u8);
|
||||
if (!mainTree)
|
||||
return;
|
||||
|
||||
|
|
@ -893,8 +888,8 @@ public sealed class DebugTab : Window, ITab<TabType>
|
|||
kvp => kvp.Key.Contains(_tmbKeyFilterU8),
|
||||
p =>
|
||||
{
|
||||
ImUtf8.DrawTableColumn($"{p.Value}");
|
||||
ImUtf8.DrawTableColumn(p.Key.Span);
|
||||
Im.Table.DrawColumn($"{p.Value}");
|
||||
Im.Table.DrawColumn(p.Key.Span);
|
||||
});
|
||||
ImGuiClip.DrawEndDummy(dummy, Im.Style.TextHeightWithSpacing);
|
||||
}
|
||||
|
|
@ -946,7 +941,7 @@ public sealed class DebugTab : Window, ITab<TabType>
|
|||
foreach (var list in data.Scalars)
|
||||
{
|
||||
var scalar = list[i];
|
||||
ImGuiUtil.DrawTableColumn($"{scalar:F6}");
|
||||
table.DrawColumn($"{scalar:F6}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1034,12 +1029,12 @@ public sealed class DebugTab : Window, ITab<TabType>
|
|||
{
|
||||
if (t1)
|
||||
{
|
||||
ImGuiUtil.DrawTableColumn("Flags");
|
||||
ImGuiUtil.DrawTableColumn($"{model.AsCharacterBase->StateFlags}");
|
||||
ImGuiUtil.DrawTableColumn("Has Model In Slot Loaded");
|
||||
ImGuiUtil.DrawTableColumn($"{model.AsCharacterBase->HasModelInSlotLoaded:X8}");
|
||||
ImGuiUtil.DrawTableColumn("Has Model Files In Slot Loaded");
|
||||
ImGuiUtil.DrawTableColumn($"{model.AsCharacterBase->HasModelFilesInSlotLoaded:X8}");
|
||||
t1.DrawColumn("Flags"u8);
|
||||
t1.DrawColumn($"{model.AsCharacterBase->StateFlags}");
|
||||
t1.DrawColumn("Has Model In Slot Loaded"u8);
|
||||
t1.DrawColumn($"{model.AsCharacterBase->HasModelInSlotLoaded:X8}");
|
||||
t1.DrawColumn("Has Model Files In Slot Loaded"u8);
|
||||
t1.DrawColumn($"{model.AsCharacterBase->HasModelFilesInSlotLoaded:X8}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1091,7 +1086,7 @@ public sealed class DebugTab : Window, ITab<TabType>
|
|||
if (Im.Input.Text("##crcInput"u8, ref _crcInput, "Input path for CRC..."u8))
|
||||
_crcPath = new FullPath(_crcInput);
|
||||
|
||||
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
|
||||
using var font = Im.Font.PushMono();
|
||||
Im.Text($" CRC32: {_crcPath.InternalName.CiCrc32:X8}");
|
||||
Im.Text($"CI CRC32: {_crcPath.InternalName.Crc32:X8}");
|
||||
Im.Text($" CRC64: {_crcPath.Crc64:X16}");
|
||||
|
|
@ -1113,12 +1108,12 @@ public sealed class DebugTab : Window, ITab<TabType>
|
|||
|
||||
private unsafe void DrawResourceLoader()
|
||||
{
|
||||
if (!ImUtf8.CollapsingHeader("Resource Loader"u8))
|
||||
if (!Im.Tree.Header("Resource Loader"u8))
|
||||
return;
|
||||
|
||||
var ongoingLoads = _resourceLoader.OngoingLoads;
|
||||
var ongoingLoadCount = ongoingLoads.Count;
|
||||
ImUtf8.Text($"Ongoing Loads: {ongoingLoadCount}");
|
||||
Im.Text($"Ongoing Loads: {ongoingLoadCount}");
|
||||
|
||||
if (ongoingLoadCount == 0)
|
||||
return;
|
||||
|
|
@ -1134,9 +1129,9 @@ public sealed class DebugTab : Window, ITab<TabType>
|
|||
|
||||
foreach (var (handle, original) in ongoingLoads)
|
||||
{
|
||||
ImUtf8.DrawTableColumn($"0x{handle:X}");
|
||||
ImUtf8.DrawTableColumn(((ResourceHandle*)handle)->CsHandle.FileName);
|
||||
ImUtf8.DrawTableColumn(original.Path.Span);
|
||||
table.DrawColumn($"0x{handle:X}");
|
||||
table.DrawColumn(((ResourceHandle*)handle)->CsHandle.FileName.AsSpan());
|
||||
table.DrawColumn(original.Path.Span);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1173,11 +1168,10 @@ public sealed class DebugTab : Window, ITab<TabType>
|
|||
|
||||
private void DrawCloudApi()
|
||||
{
|
||||
if (!ImUtf8.CollapsingHeader("Cloud API"u8))
|
||||
using var header = Im.Tree.HeaderId("Cloud API"u8);
|
||||
if (!header)
|
||||
return;
|
||||
|
||||
using var id = ImRaii.PushId("CloudApiTester"u8);
|
||||
|
||||
if (Im.Input.Text("Path"u8, ref _cloudTesterPath, flags: InputTextFlags.EnterReturnsTrue))
|
||||
try
|
||||
{
|
||||
|
|
@ -1191,7 +1185,7 @@ public sealed class DebugTab : Window, ITab<TabType>
|
|||
}
|
||||
|
||||
if (_cloudTesterReturn.HasValue)
|
||||
ImUtf8.Text($"Is Cloud Synced? {_cloudTesterReturn}");
|
||||
Im.Text($"Is Cloud Synced? {_cloudTesterReturn}");
|
||||
|
||||
if (_cloudTesterError is not null)
|
||||
Im.Text($"{_cloudTesterError}", ImGuiColors.DalamudRed);
|
||||
|
|
@ -1201,10 +1195,10 @@ public sealed class DebugTab : Window, ITab<TabType>
|
|||
/// <summary> Draw information about IPC options and availability. </summary>
|
||||
private void DrawDebugTabIpc()
|
||||
{
|
||||
if (!ImUtf8.CollapsingHeader("IPC"u8))
|
||||
if (!Im.Tree.Header("IPC"u8))
|
||||
return;
|
||||
|
||||
using (var tree = ImUtf8.TreeNode("Dynamis"u8))
|
||||
using (var tree = Im.Tree.Node("Dynamis"u8))
|
||||
{
|
||||
if (tree)
|
||||
Penumbra.Dynamis.DrawDebugInfo();
|
||||
|
|
@ -1217,7 +1211,7 @@ public sealed class DebugTab : Window, ITab<TabType>
|
|||
=> DrawContent();
|
||||
|
||||
public override bool DrawConditions()
|
||||
=> _config.DebugMode && _config.Ephemeral.DebugSeparateWindow;
|
||||
=> _config is { DebugMode: true, Ephemeral.DebugSeparateWindow: true };
|
||||
|
||||
public override void OnClose()
|
||||
{
|
||||
|
|
@ -1229,6 +1223,6 @@ public sealed class DebugTab : Window, ITab<TabType>
|
|||
{
|
||||
Penumbra.Dynamis.DrawPointer(address);
|
||||
Im.Line.SameInner();
|
||||
ImUtf8.Text(label);
|
||||
Im.Text(label);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
using Dalamud.Game.ClientState.Objects;
|
||||
using OtterGui;
|
||||
using Penumbra.UI.Classes;
|
||||
using Dalamud.Interface;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using ImSharp;
|
||||
using Luna;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,8 @@
|
|||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Components;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
using ImSharp;
|
||||
using Luna;
|
||||
using OtterGui;
|
||||
using OtterGui.Text;
|
||||
using OtterGui.Widgets;
|
||||
using Penumbra.Api;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections;
|
||||
|
|
@ -57,8 +52,6 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
private readonly AttributeHook _attributeHook;
|
||||
private readonly PcpService _pcpService;
|
||||
|
||||
private readonly TagButtons _sharedTags = new();
|
||||
|
||||
private string _lastCloudSyncTestedPath = string.Empty;
|
||||
private bool _lastCloudSyncTestResult;
|
||||
|
||||
|
|
@ -113,7 +106,7 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
return;
|
||||
|
||||
DrawEnabledBox();
|
||||
EphemeralCheckbox("Lock Main Window", "Prevent the main window from being resized or moved.", _config.Ephemeral.FixMainWindow,
|
||||
EphemeralCheckbox("Lock Main Window"u8, "Prevent the main window from being resized or moved."u8, _config.Ephemeral.FixMainWindow,
|
||||
v => _config.Ephemeral.FixMainWindow = v);
|
||||
|
||||
Im.Line.New();
|
||||
|
|
@ -131,7 +124,7 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private void Checkbox(string label, string tooltip, bool current, Action<bool> setter)
|
||||
private void Checkbox(ReadOnlySpan<byte> label, ReadOnlySpan<byte> tooltip, bool current, Action<bool> setter)
|
||||
{
|
||||
using var id = Im.Id.Push(label);
|
||||
var tmp = current;
|
||||
|
|
@ -141,12 +134,11 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
_config.Save();
|
||||
}
|
||||
|
||||
Im.Line.Same();
|
||||
ImGuiUtil.LabeledHelpMarker(label, tooltip);
|
||||
LunaStyle.DrawAlignedHelpMarkerLabel(label, tooltip);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private void EphemeralCheckbox(string label, string tooltip, bool current, Action<bool> setter)
|
||||
private void EphemeralCheckbox(ReadOnlySpan<byte> label, ReadOnlySpan<byte> tooltip, bool current, Action<bool> setter)
|
||||
{
|
||||
using var id = Im.Id.Push(label);
|
||||
var tmp = current;
|
||||
|
|
@ -156,8 +148,7 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
_config.Ephemeral.Save();
|
||||
}
|
||||
|
||||
Im.Line.Same();
|
||||
ImGuiUtil.LabeledHelpMarker(label, tooltip);
|
||||
LunaStyle.DrawAlignedHelpMarkerLabel(label, tooltip);
|
||||
}
|
||||
|
||||
#region Main Settings
|
||||
|
|
@ -197,7 +188,7 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
if (IsSubPathOf(dalamud.FullName, newName))
|
||||
return ("Path is not allowed to be inside your Dalamud directories.", false);
|
||||
|
||||
if (Functions.GetDownloadsFolder(out var downloads) && IsSubPathOf(downloads, newName))
|
||||
if (WindowsFunctions.GetDownloadsFolder(out var downloads) && IsSubPathOf(downloads, newName))
|
||||
return ("Path is not allowed to be inside your Downloads folder.", false);
|
||||
|
||||
var gameDir = _gameData.GameData.DataPath.Parent!.Parent!.FullName;
|
||||
|
|
@ -219,7 +210,7 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
|
||||
static bool IsSubPathOf(string basePath, string subPath)
|
||||
{
|
||||
if (basePath.Length == 0)
|
||||
if (basePath.Length is 0)
|
||||
return false;
|
||||
|
||||
var rel = Path.GetRelativePath(basePath, subPath);
|
||||
|
|
@ -276,19 +267,19 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
Im.Line.Same();
|
||||
DrawDirectoryPickerButton();
|
||||
style.Pop();
|
||||
Im.Line.Same();
|
||||
|
||||
const string tt = "This is where Penumbra will store your extracted mod files.\n"
|
||||
+ "TTMP files are not copied, just extracted.\n"
|
||||
+ "This directory needs to be accessible and you need write access here.\n"
|
||||
+ "It is recommended that this directory is placed on a fast hard drive, preferably an SSD.\n"
|
||||
+ "It should also be placed near the root of a logical drive - the shorter the total path to this folder, the better.\n"
|
||||
+ "Definitely do not place it in your Dalamud directory or any sub-directory thereof.";
|
||||
ImGuiComponents.HelpMarker(tt);
|
||||
var tt = "This is where Penumbra will store your extracted mod files.\n"u8
|
||||
+ "TTMP files are not copied, just extracted.\n"u8
|
||||
+ "This directory needs to be accessible and you need write access here.\n"u8
|
||||
+ "It is recommended that this directory is placed on a fast hard drive, preferably an SSD.\n"u8
|
||||
+ "It should also be placed near the root of a logical drive - the shorter the total path to this folder, the better.\n"u8
|
||||
+ "Definitely do not place it in your Dalamud directory or any sub-directory thereof."u8;
|
||||
|
||||
LunaStyle.DrawAlignedHelpMarker(tt);
|
||||
_tutorial.OpenTutorial(BasicTutorialSteps.GeneralTooltips);
|
||||
Im.Line.Same();
|
||||
Im.Line.SameInner();
|
||||
Im.Text("Root Directory"u8);
|
||||
ImGuiUtil.HoverTooltip(tt);
|
||||
Im.Tooltip.OnHover(tt);
|
||||
}
|
||||
|
||||
_tutorial.OpenTutorial(BasicTutorialSteps.ModDirectory);
|
||||
|
|
@ -308,9 +299,9 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
UiHelpers.DrawOpenDirectoryButton(0, _modManager.BasePath, _modManager.Valid);
|
||||
Im.Line.Same();
|
||||
var tt = _modManager.Valid
|
||||
? "Force Penumbra to completely re-scan your root directory as if it was restarted."
|
||||
: "The currently selected folder is not valid. Please select a different folder.";
|
||||
if (ImGuiUtil.DrawDisabledButton("Rediscover Mods", Vector2.Zero, tt, !_modManager.Valid))
|
||||
? "Force Penumbra to completely re-scan your root directory as if it was restarted."u8
|
||||
: "The currently selected folder is not valid. Please select a different folder."u8;
|
||||
if (ImEx.Button("Rediscover Mods"u8, Vector2.Zero, tt, !_modManager.Valid))
|
||||
_modManager.DiscoverMods();
|
||||
}
|
||||
|
||||
|
|
@ -369,9 +360,9 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
_config.Save();
|
||||
}
|
||||
|
||||
ImGuiUtil.LabeledHelpMarker("Upper Limit for Single-Selection Group Radio Buttons",
|
||||
"All Single-Selection Groups with more options than specified here will be displayed as Combo-Boxes at the top.\n"
|
||||
+ "All other Single-Selection Groups will be displayed as a set of Radio-Buttons.");
|
||||
LunaStyle.DrawAlignedHelpMarkerLabel("Upper Limit for Single-Selection Group Radio Buttons"u8,
|
||||
"All Single-Selection Groups with more options than specified here will be displayed as Combo-Boxes at the top.\n"u8
|
||||
+ "All other Single-Selection Groups will be displayed as a set of Radio-Buttons."u8);
|
||||
}
|
||||
|
||||
/// <summary> Draw a selection for the minimum number of options after which a group is drawn as collapsible. </summary>
|
||||
|
|
@ -385,33 +376,33 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
_config.Save();
|
||||
}
|
||||
|
||||
ImGuiUtil.LabeledHelpMarker("Collapsible Option Group Limit",
|
||||
"Lower Limit for option groups displaying the Collapse/Expand button at the top.");
|
||||
LunaStyle.DrawAlignedHelpMarkerLabel("Collapsible Option Group Limit"u8,
|
||||
"Lower Limit for option groups displaying the Collapse/Expand button at the top."u8);
|
||||
}
|
||||
|
||||
|
||||
/// <summary> Draw the window hiding state checkboxes. </summary>
|
||||
private void DrawHidingSettings()
|
||||
{
|
||||
Checkbox("Open Config Window at Game Start", "Whether the Penumbra main window should be open or closed after launching the game.",
|
||||
Checkbox("Open Config Window at Game Start"u8, "Whether the Penumbra main window should be open or closed after launching the game."u8,
|
||||
_config.OpenWindowAtStart, v => _config.OpenWindowAtStart = v);
|
||||
|
||||
Checkbox("Hide Config Window when UI is Hidden",
|
||||
"Hide the Penumbra main window when you manually hide the in-game user interface.", _config.HideUiWhenUiHidden,
|
||||
Checkbox("Hide Config Window when UI is Hidden"u8,
|
||||
"Hide the Penumbra main window when you manually hide the in-game user interface."u8, _config.HideUiWhenUiHidden,
|
||||
v =>
|
||||
{
|
||||
_config.HideUiWhenUiHidden = v;
|
||||
_pluginInterface.UiBuilder.DisableUserUiHide = !v;
|
||||
});
|
||||
Checkbox("Hide Config Window when in Cutscenes",
|
||||
"Hide the Penumbra main window when you are currently watching a cutscene.", _config.HideUiInCutscenes,
|
||||
Checkbox("Hide Config Window when in Cutscenes"u8,
|
||||
"Hide the Penumbra main window when you are currently watching a cutscene."u8, _config.HideUiInCutscenes,
|
||||
v =>
|
||||
{
|
||||
_config.HideUiInCutscenes = v;
|
||||
_pluginInterface.UiBuilder.DisableCutsceneUiHide = !v;
|
||||
});
|
||||
Checkbox("Hide Config Window when in GPose",
|
||||
"Hide the Penumbra main window when you are currently in GPose mode.", _config.HideUiInGPose,
|
||||
Checkbox("Hide Config Window when in GPose"u8,
|
||||
"Hide the Penumbra main window when you are currently in GPose mode."u8, _config.HideUiInGPose,
|
||||
v =>
|
||||
{
|
||||
_config.HideUiInGPose = v;
|
||||
|
|
@ -422,15 +413,15 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
/// <summary> Draw all settings that do not fit into other categories. </summary>
|
||||
private void DrawMiscSettings()
|
||||
{
|
||||
Checkbox("Automatically Select Character-Associated Collection",
|
||||
"On every login, automatically select the collection associated with the current character as the current collection for editing.",
|
||||
Checkbox("Automatically Select Character-Associated Collection"u8,
|
||||
"On every login, automatically select the collection associated with the current character as the current collection for editing."u8,
|
||||
_config.AutoSelectCollection, _autoSelector.SetAutomaticSelection);
|
||||
Checkbox("Print Chat Command Success Messages to Chat",
|
||||
"Chat Commands usually print messages on failure but also on success to confirm your action. You can disable this here.",
|
||||
Checkbox("Print Chat Command Success Messages to Chat"u8,
|
||||
"Chat Commands usually print messages on failure but also on success to confirm your action. You can disable this here."u8,
|
||||
_config.PrintSuccessfulCommandsToChat, v => _config.PrintSuccessfulCommandsToChat = v);
|
||||
Checkbox("Hide Redraw Bar in Mod Panel", "Hides the lower redraw buttons in the mod panel in your Mods tab.",
|
||||
Checkbox("Hide Redraw Bar in Mod Panel"u8, "Hides the lower redraw buttons in the mod panel in your Mods tab."u8,
|
||||
_config.HideRedrawBar, v => _config.HideRedrawBar = v);
|
||||
Checkbox("Hide Changed Item Filters", "Hides the category filter line in the Changed Items tab and the Changed Items mod panel.",
|
||||
Checkbox("Hide Changed Item Filters"u8, "Hides the category filter line in the Changed Items tab and the Changed Items mod panel."u8,
|
||||
_config.HideChangedItemFilters, v =>
|
||||
{
|
||||
_config.HideChangedItemFilters = v;
|
||||
|
|
@ -446,19 +437,19 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
_config.ChangedItemDisplay = v;
|
||||
_config.Save();
|
||||
});
|
||||
ImUtf8.LabeledHelpMarker("Mod Changed Item Display"u8,
|
||||
LunaStyle.DrawAlignedHelpMarkerLabel("Mod Changed Item Display"u8,
|
||||
"Configure how to display the changed items of a single mod in the mods info panel."u8);
|
||||
|
||||
Checkbox("Omit Machinist Offhands in Changed Items",
|
||||
"Omits all Aetherotransformers (machinist offhands) in the changed items tabs because any change on them changes all of them at the moment.\n\n"
|
||||
+ "Changing this triggers a rediscovery of your mods so all changed items can be updated.",
|
||||
Checkbox("Omit Machinist Offhands in Changed Items"u8,
|
||||
"Omits all Aetherotransformers (machinist offhands) in the changed items tabs because any change on them changes all of them at the moment.\n\n"u8
|
||||
+ "Changing this triggers a rediscovery of your mods so all changed items can be updated."u8,
|
||||
_config.HideMachinistOffhandFromChangedItems, v =>
|
||||
{
|
||||
_config.HideMachinistOffhandFromChangedItems = v;
|
||||
_modManager.DiscoverMods();
|
||||
});
|
||||
Checkbox("Hide Priority Numbers in Mod Selector",
|
||||
"Hides the bracketed non-zero priority numbers displayed in the mod selector when there is enough space for them.",
|
||||
Checkbox("Hide Priority Numbers in Mod Selector"u8,
|
||||
"Hides the bracketed non-zero priority numbers displayed in the mod selector when there is enough space for them."u8,
|
||||
_config.HidePrioritiesInSelector, v => _config.HidePrioritiesInSelector = v);
|
||||
DrawSingleSelectRadioMax();
|
||||
DrawCollapsibleGroupMin();
|
||||
|
|
@ -467,28 +458,28 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
/// <summary> Draw all settings pertaining to actor identification for collections. </summary>
|
||||
private void DrawIdentificationSettings()
|
||||
{
|
||||
Checkbox("Use Interface Collection for other Plugin UIs",
|
||||
"Use the collection assigned to your interface for other plugins requesting UI-textures and icons through Dalamud.",
|
||||
Checkbox("Use Interface Collection for other Plugin UIs"u8,
|
||||
"Use the collection assigned to your interface for other plugins requesting UI-textures and icons through Dalamud."u8,
|
||||
_dalamudSubstitutionProvider.Enabled, _dalamudSubstitutionProvider.Set);
|
||||
Checkbox($"Use {"Assigned Collections"} in Lobby",
|
||||
"If this is disabled, no mods are applied to characters in the lobby or at the aesthetician.",
|
||||
Checkbox("Use Assigned Collections in Lobby"u8,
|
||||
"If this is disabled, no mods are applied to characters in the lobby or at the aesthetician."u8,
|
||||
_config.ShowModsInLobby, v => _config.ShowModsInLobby = v);
|
||||
Checkbox($"Use {"Assigned Collections"} in Character Window",
|
||||
"Use the individual collection for your characters name or the Your Character collection in your main character window, if it is set.",
|
||||
Checkbox("Use Assigned Collections in Character Window"u8,
|
||||
"Use the individual collection for your characters name or the Your Character collection in your main character window, if it is set."u8,
|
||||
_config.UseCharacterCollectionInMainWindow, v => _config.UseCharacterCollectionInMainWindow = v);
|
||||
Checkbox($"Use {"Assigned Collections"} in Adventurer Cards",
|
||||
"Use the appropriate individual collection for the adventurer card you are currently looking at, based on the adventurer's name.",
|
||||
Checkbox("Use Assigned Collections in Adventurer Cards"u8,
|
||||
"Use the appropriate individual collection for the adventurer card you are currently looking at, based on the adventurer's name."u8,
|
||||
_config.UseCharacterCollectionsInCards, v => _config.UseCharacterCollectionsInCards = v);
|
||||
Checkbox($"Use {"Assigned Collections"} in Try-On Window",
|
||||
"Use the individual collection for your character's name in your try-on, dye preview or glamour plate window, if it is set.",
|
||||
Checkbox("Use Assigned Collections in Try-On Window"u8,
|
||||
"Use the individual collection for your character's name in your try-on, dye preview or glamour plate window, if it is set."u8,
|
||||
_config.UseCharacterCollectionInTryOn, v => _config.UseCharacterCollectionInTryOn = v);
|
||||
Checkbox("Use No Mods in Inspect Windows", "Use the empty collection for characters you are inspecting, regardless of the character.\n"
|
||||
+ "Takes precedence before the next option.", _config.UseNoModsInInspect, v => _config.UseNoModsInInspect = v);
|
||||
Checkbox($"Use {"Assigned Collections"} in Inspect Windows",
|
||||
"Use the appropriate individual collection for the character you are currently inspecting, based on their name.",
|
||||
Checkbox("Use No Mods in Inspect Windows"u8, "Use the empty collection for characters you are inspecting, regardless of the character.\n"u8
|
||||
+ "Takes precedence before the next option."u8, _config.UseNoModsInInspect, v => _config.UseNoModsInInspect = v);
|
||||
Checkbox("Use Assigned Collections in Inspect Windows"u8,
|
||||
"Use the appropriate individual collection for the character you are currently inspecting, based on their name."u8,
|
||||
_config.UseCharacterCollectionInInspect, v => _config.UseCharacterCollectionInInspect = v);
|
||||
Checkbox($"Use {"Assigned Collections"} based on Ownership",
|
||||
"Use the owner's name to determine the appropriate individual collection for mounts, companions, accessories and combat pets.",
|
||||
Checkbox("Use Assigned Collections based on Ownership"u8,
|
||||
"Use the owner's name to determine the appropriate individual collection for mounts, companions, accessories and combat pets."u8,
|
||||
_config.UseOwnerNameForCharacterCollection, v => _config.UseOwnerNameForCharacterCollection = v);
|
||||
}
|
||||
|
||||
|
|
@ -497,7 +488,7 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
{
|
||||
var sortMode = _config.SortMode;
|
||||
Im.Item.SetNextWidth(UiHelpers.InputTextWidth.X);
|
||||
using (var combo = ImUtf8.Combo("##sortMode", sortMode.Name))
|
||||
using (var combo = Im.Combo.Begin("##sortMode"u8, sortMode.Name))
|
||||
{
|
||||
if (combo)
|
||||
foreach (var val in ISortMode.Valid.Values)
|
||||
|
|
@ -513,36 +504,28 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
}
|
||||
}
|
||||
|
||||
ImGuiUtil.LabeledHelpMarker("Sort Mode", "Choose the sort mode for the mod selector in the mods tab.");
|
||||
LunaStyle.DrawAlignedHelpMarkerLabel("Sort Mode"u8, "Choose the sort mode for the mod selector in the mods tab."u8);
|
||||
}
|
||||
|
||||
private void DrawRenameSettings()
|
||||
{
|
||||
Im.Item.SetNextWidth(UiHelpers.InputTextWidth.X);
|
||||
using (var combo = Im.Combo.Begin("##renameSettings"u8, _config.ShowRename.GetData().Name))
|
||||
using (var combo = Im.Combo.Begin("##renameSettings"u8, _config.ShowRename.ToNameU8()))
|
||||
{
|
||||
if (combo)
|
||||
foreach (var value in Enum.GetValues<RenameField>())
|
||||
{
|
||||
var (name, desc) = value.GetData();
|
||||
if (Im.Selectable(name, _config.ShowRename == value))
|
||||
if (Im.Selectable(value.ToNameU8(), _config.ShowRename == value))
|
||||
{
|
||||
_config.ShowRename = value;
|
||||
_selector.SetRenameSearchPath(value);
|
||||
_config.Save();
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip(desc);
|
||||
Im.Tooltip.OnHover(value.Tooltip());
|
||||
}
|
||||
}
|
||||
|
||||
Im.Line.Same();
|
||||
const string tt =
|
||||
"Select which of the two renaming input fields are visible when opening the right-click context menu of a mod in the mod selector.";
|
||||
ImGuiComponents.HelpMarker(tt);
|
||||
Im.Line.Same();
|
||||
Im.Text("Rename Fields in Mod Context Menu"u8);
|
||||
ImGuiUtil.HoverTooltip(tt);
|
||||
LunaStyle.DrawAlignedHelpMarkerLabel("Rename Fields in Mod Context Menu"u8, "Select which of the two renaming input fields are visible when opening the right-click context menu of a mod in the mod selector."u8);
|
||||
}
|
||||
|
||||
/// <summary> Draw all settings pertaining to the mod selector. </summary>
|
||||
|
|
@ -550,7 +533,7 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
{
|
||||
DrawFolderSortType();
|
||||
DrawRenameSettings();
|
||||
Checkbox("Open Folders by Default", "Whether to start with all folders collapsed or expanded in the mod selector.",
|
||||
Checkbox("Open Folders by Default"u8, "Whether to start with all folders collapsed or expanded in the mod selector."u8,
|
||||
_config.OpenFoldersByDefault, v =>
|
||||
{
|
||||
_config.OpenFoldersByDefault = v;
|
||||
|
|
@ -579,43 +562,42 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
/// <summary> Draw all settings pertaining to import and export of mods. </summary>
|
||||
private void DrawModHandlingSettings()
|
||||
{
|
||||
Checkbox("Use Temporary Settings Per Default",
|
||||
"When you make any changes to your collection, apply them as temporary changes first and require a click to 'turn permanent' if you want to keep them.",
|
||||
Checkbox("Use Temporary Settings Per Default"u8,
|
||||
"When you make any changes to your collection, apply them as temporary changes first and require a click to 'turn permanent' if you want to keep them."u8,
|
||||
_config.DefaultTemporaryMode, v => _config.DefaultTemporaryMode = v);
|
||||
Checkbox("Replace Non-Standard Symbols On Import",
|
||||
"Replace all non-ASCII symbols in mod and option names with underscores when importing mods.", _config.ReplaceNonAsciiOnImport,
|
||||
Checkbox("Replace Non-Standard Symbols On Import"u8,
|
||||
"Replace all non-ASCII symbols in mod and option names with underscores when importing mods."u8, _config.ReplaceNonAsciiOnImport,
|
||||
v => _config.ReplaceNonAsciiOnImport = v);
|
||||
Checkbox("Always Open Import at Default Directory",
|
||||
"Open the import window at the location specified here every time, forgetting your previous path.",
|
||||
Checkbox("Always Open Import at Default Directory"u8,
|
||||
"Open the import window at the location specified here every time, forgetting your previous path."u8,
|
||||
_config.AlwaysOpenDefaultImport, v => _config.AlwaysOpenDefaultImport = v);
|
||||
Checkbox("Handle PCP Files",
|
||||
"When encountering specific mods, usually but not necessarily denoted by a .pcp file ending, Penumbra will automatically try to create an associated collection and assign it to a specific character for this mod package. This can turn this behaviour off if unwanted.",
|
||||
Checkbox("Handle PCP Files"u8,
|
||||
"When encountering specific mods, usually but not necessarily denoted by a .pcp file ending, Penumbra will automatically try to create an associated collection and assign it to a specific character for this mod package. This can turn this behaviour off if unwanted."u8,
|
||||
!_config.PcpSettings.DisableHandling, v => _config.PcpSettings.DisableHandling = !v);
|
||||
|
||||
var active = _config.DeleteModModifier.IsActive();
|
||||
Im.Line.Same();
|
||||
if (ImUtf8.ButtonEx("Delete all PCP Mods"u8, "Deletes all mods tagged with 'PCP' from the mod list."u8, disabled: !active))
|
||||
if (ImEx.Button("Delete all PCP Mods"u8, default, "Deletes all mods tagged with 'PCP' from the mod list."u8, !active))
|
||||
_pcpService.CleanPcpMods();
|
||||
if (!active)
|
||||
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteModModifier} while clicking.");
|
||||
|
||||
Im.Line.Same();
|
||||
if (ImUtf8.ButtonEx("Delete all PCP Collections"u8, "Deletes all collections whose name starts with 'PCP/' from the collection list."u8,
|
||||
disabled: !active))
|
||||
if (ImEx.Button("Delete all PCP Collections"u8, default, "Deletes all collections whose name starts with 'PCP/' from the collection list."u8, !active))
|
||||
_pcpService.CleanPcpCollections();
|
||||
if (!active)
|
||||
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteModModifier} while clicking.");
|
||||
|
||||
Checkbox("Allow Other Plugins Access to PCP Handling",
|
||||
"When creating or importing PCP files, other plugins can add and interpret their own data to the character.json file.",
|
||||
Checkbox("Allow Other Plugins Access to PCP Handling"u8,
|
||||
"When creating or importing PCP files, other plugins can add and interpret their own data to the character.json file."u8,
|
||||
_config.PcpSettings.AllowIpc, v => _config.PcpSettings.AllowIpc = v);
|
||||
|
||||
Checkbox("Create PCP Collections",
|
||||
"When importing PCP files, create the associated collection.",
|
||||
Checkbox("Create PCP Collections"u8,
|
||||
"When importing PCP files, create the associated collection."u8,
|
||||
_config.PcpSettings.CreateCollection, v => _config.PcpSettings.CreateCollection = v);
|
||||
|
||||
Checkbox("Assign PCP Collections",
|
||||
"When importing PCP files and creating the associated collection, assign it to the associated character.",
|
||||
Checkbox("Assign PCP Collections"u8,
|
||||
"When importing PCP files and creating the associated collection, assign it to the associated character."u8,
|
||||
_config.PcpSettings.AssignCollection, v => _config.PcpSettings.AssignCollection = v);
|
||||
DrawDefaultModImportPath();
|
||||
DrawDefaultModAuthor();
|
||||
|
|
@ -623,11 +605,11 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
DrawPcpFolder();
|
||||
DrawPcpExtension();
|
||||
DrawDefaultModExportPath();
|
||||
Checkbox("Enable Directory Watcher",
|
||||
"Enables a File Watcher that automatically listens for Mod files that enter a specified directory, causing Penumbra to open a popup to import these mods.",
|
||||
Checkbox("Enable Directory Watcher"u8,
|
||||
"Enables a File Watcher that automatically listens for Mod files that enter a specified directory, causing Penumbra to open a popup to import these mods."u8,
|
||||
_config.EnableDirectoryWatch, _fileWatcher.Toggle);
|
||||
Checkbox("Enable Fully Automatic Import",
|
||||
"Uses the File Watcher in order to skip the query popup and automatically import any new mods.",
|
||||
Checkbox("Enable Fully Automatic Import"u8,
|
||||
"Uses the File Watcher in order to skip the query popup and automatically import any new mods."u8,
|
||||
_config.EnableAutomaticModImport, v => _config.EnableAutomaticModImport = v);
|
||||
DrawFileWatcherPath();
|
||||
}
|
||||
|
|
@ -636,19 +618,19 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
/// <summary> Draw input for the default import path for a mod. </summary>
|
||||
private void DrawDefaultModImportPath()
|
||||
{
|
||||
using var id = Im.Id.Push("##dmi"u8);
|
||||
var spacing = new Vector2(Im.Style.GlobalScale * 3);
|
||||
using var style = ImStyleDouble.ItemSpacing.Push(spacing);
|
||||
|
||||
Im.Item.SetNextWidth(UiHelpers.InputTextMinusButton3);
|
||||
if (ImEx.InputOnDeactivation.Text("##defaultModImport"u8, _config.DefaultModImportPath, out string newDirectory))
|
||||
if (ImEx.InputOnDeactivation.Text(StringU8.Empty, _config.DefaultModImportPath, out string newDirectory))
|
||||
{
|
||||
_config.DefaultModImportPath = newDirectory;
|
||||
_config.Save();
|
||||
}
|
||||
|
||||
Im.Line.Same();
|
||||
if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.Folder.ToIconString()}##import", UiHelpers.IconButtonSize,
|
||||
"Select a directory via dialog.", false, true))
|
||||
Im.Line.SameInner();
|
||||
if (ImEx.Icon.Button(LunaStyle.FolderIcon, "Select a directory via dialog."u8))
|
||||
{
|
||||
var startDir = _config.DefaultModImportPath.Length > 0 && Directory.Exists(_config.DefaultModImportPath)
|
||||
? _config.DefaultModImportPath
|
||||
|
|
@ -667,22 +649,22 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
}
|
||||
|
||||
style.Pop();
|
||||
ImGuiUtil.LabeledHelpMarker("Default Mod Import Directory",
|
||||
"Set the directory that gets opened when using the file picker to import mods for the first time.");
|
||||
LunaStyle.DrawAlignedHelpMarkerLabel("Default Mod Import Directory"u8,
|
||||
"Set the directory that gets opened when using the file picker to import mods for the first time."u8);
|
||||
}
|
||||
|
||||
/// <summary> Draw input for the default export/backup path for mods. </summary>
|
||||
private void DrawDefaultModExportPath()
|
||||
{
|
||||
using var id = Im.Id.Push("##dme"u8);
|
||||
var spacing = new Vector2(Im.Style.GlobalScale * 3);
|
||||
using var style = ImStyleDouble.ItemSpacing.Push(spacing);
|
||||
Im.Item.SetNextWidth(UiHelpers.InputTextMinusButton3);
|
||||
if (ImEx.InputOnDeactivation.Text("##defaultModExport"u8, _config.ExportDirectory, out string newDirectory))
|
||||
if (ImEx.InputOnDeactivation.Text(StringU8.Empty, _config.ExportDirectory, out string newDirectory))
|
||||
_modExportManager.UpdateExportDirectory(newDirectory);
|
||||
|
||||
Im.Line.Same();
|
||||
if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.Folder.ToIconString()}##export", UiHelpers.IconButtonSize,
|
||||
"Select a directory via dialog.", false, true))
|
||||
if (ImEx.Icon.Button(LunaStyle.FolderIcon, "Select a directory via dialog."u8))
|
||||
{
|
||||
var startDir = _config.ExportDirectory.Length > 0 && Directory.Exists(_config.ExportDirectory)
|
||||
? _config.ExportDirectory
|
||||
|
|
@ -697,9 +679,9 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
}
|
||||
|
||||
style.Pop();
|
||||
ImGuiUtil.LabeledHelpMarker("Default Mod Export Directory",
|
||||
"Set the directory mods get saved to when using the export function or loaded from when reimporting backups.\n"
|
||||
+ "Keep this empty to use the root directory.");
|
||||
LunaStyle.DrawAlignedHelpMarkerLabel("Default Mod Export Directory"u8,
|
||||
"Set the directory mods get saved to when using the export function or loaded from when reimporting backups.\n"u8
|
||||
+ "Keep this empty to use the root directory."u8);
|
||||
}
|
||||
|
||||
/// <summary> Draw input for the Automatic Mod import path. </summary>
|
||||
|
|
@ -709,7 +691,7 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
var spacing = new Vector2(Im.Style.GlobalScale * 3);
|
||||
using var style = ImStyleDouble.ItemSpacing.Push(spacing);
|
||||
Im.Item.SetNextWidth(UiHelpers.InputTextMinusButton3);
|
||||
if (ImEx.InputOnDeactivation.Text("##path"u8, _config.WatchDirectory, out string newDirectory, maxLength: 256))
|
||||
if (ImEx.InputOnDeactivation.Text(StringU8.Empty, _config.WatchDirectory, out string newDirectory, maxLength: 256))
|
||||
_fileWatcher.UpdateDirectory(newDirectory);
|
||||
|
||||
Im.Line.Same();
|
||||
|
|
@ -728,8 +710,8 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
}
|
||||
|
||||
style.Pop();
|
||||
ImGuiUtil.LabeledHelpMarker("Automatic Import Director",
|
||||
"Choose the Directory the File Watcher listens to.");
|
||||
LunaStyle.DrawAlignedHelpMarkerLabel("Automatic Import Director"u8,
|
||||
"Choose the Directory the File Watcher listens to."u8);
|
||||
}
|
||||
|
||||
/// <summary> Draw input for the default name to input as author into newly generated mods. </summary>
|
||||
|
|
@ -742,7 +724,7 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
_config.Save();
|
||||
}
|
||||
|
||||
ImGuiUtil.LabeledHelpMarker("Default Mod Author", "Set the default author stored for newly created mods.");
|
||||
LunaStyle.DrawAlignedHelpMarkerLabel("Default Mod Author"u8, "Set the default author stored for newly created mods."u8);
|
||||
}
|
||||
|
||||
/// <summary> Draw input for the default folder to sort put newly imported mods into. </summary>
|
||||
|
|
@ -755,8 +737,8 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
_config.Save();
|
||||
}
|
||||
|
||||
ImGuiUtil.LabeledHelpMarker("Default Mod Import Organizational Folder",
|
||||
"Set the default Penumbra mod folder to place newly imported mods into.\nLeave blank to import into Root.");
|
||||
LunaStyle.DrawAlignedHelpMarkerLabel("Default Mod Import Organizational Folder"u8,
|
||||
"Set the default Penumbra mod folder to place newly imported mods into.\nLeave blank to import into Root."u8);
|
||||
}
|
||||
|
||||
/// <summary> Draw input for the default folder to sort put newly imported mods into. </summary>
|
||||
|
|
@ -769,8 +751,9 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
_config.Save();
|
||||
}
|
||||
|
||||
ImGuiUtil.LabeledHelpMarker("Default PCP Organizational Folder",
|
||||
"The folder any penumbra character packs are moved to on import.\nLeave blank to import into Root.");
|
||||
LunaStyle.DrawAlignedHelpMarkerLabel("Default PCP Organizational Folder"u8,
|
||||
"The folder any penumbra character packs are moved to on import.\nLeave blank to import into Root."u8);
|
||||
|
||||
}
|
||||
|
||||
private void DrawPcpExtension()
|
||||
|
|
@ -790,16 +773,16 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
_config.Save();
|
||||
}
|
||||
|
||||
ImGuiUtil.LabeledHelpMarker("PCP Extension",
|
||||
"The extension used when exporting PCP files. Should generally be either \".pcp\" or \".pmp\".");
|
||||
LunaStyle.DrawAlignedHelpMarkerLabel("PCP Extension"u8,
|
||||
"The extension used when exporting PCP files. Should generally be either \".pcp\" or \".pmp\"."u8);
|
||||
}
|
||||
|
||||
|
||||
/// <summary> Draw all settings pertaining to advanced editing of mods. </summary>
|
||||
private void DrawModEditorSettings()
|
||||
{
|
||||
Checkbox("Advanced Editing: Edit Raw Tile UV Transforms",
|
||||
"Edit the raw matrix components of tile UV transforms, instead of having them decomposed into scale, rotation and shear.",
|
||||
Checkbox("Advanced Editing: Edit Raw Tile UV Transforms"u8,
|
||||
"Edit the raw matrix components of tile UV transforms, instead of having them decomposed into scale, rotation and shear."u8,
|
||||
_config.EditRawTileTransforms, v => _config.EditRawTileTransforms = v);
|
||||
|
||||
Checkbox("Advanced Editing: Always Highlight Color Row Pair when Hovering Selection Button",
|
||||
|
|
@ -819,8 +802,11 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
{
|
||||
var (defaultColor, name, description) = color.Data();
|
||||
var currentColor = _config.Colors.GetValueOrDefault(color, defaultColor);
|
||||
if (Widget.ColorPicker(name, description, currentColor, c => _config.Colors[color] = c, defaultColor))
|
||||
if (ImEx.ColorPicker(name, description, currentColor, out var newColor, defaultColor))
|
||||
{
|
||||
_config.Colors[color] = newColor.Color;
|
||||
_config.Save();
|
||||
}
|
||||
}
|
||||
|
||||
Im.Line.New();
|
||||
|
|
@ -839,19 +825,19 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
DrawCrashHandler();
|
||||
DrawMinimumDimensionConfig();
|
||||
DrawHdrRenderTargets();
|
||||
Checkbox("Auto Deduplicate on Import",
|
||||
"Automatically deduplicate mod files on import. This will make mod file sizes smaller, but deletes (binary identical) files.",
|
||||
Checkbox("Auto Deduplicate on Import"u8,
|
||||
"Automatically deduplicate mod files on import. This will make mod file sizes smaller, but deletes (binary identical) files."u8,
|
||||
_config.AutoDeduplicateOnImport, v => _config.AutoDeduplicateOnImport = v);
|
||||
Checkbox("Auto Reduplicate UI Files on PMP Import",
|
||||
"Automatically reduplicate and normalize UI-specific files on import from PMP files. This is STRONGLY recommended because deduplicated UI files crash the game.",
|
||||
Checkbox("Auto Reduplicate UI Files on PMP Import"u8,
|
||||
"Automatically reduplicate and normalize UI-specific files on import from PMP files. This is STRONGLY recommended because deduplicated UI files crash the game."u8,
|
||||
_config.AutoReduplicateUiOnImport, v => _config.AutoReduplicateUiOnImport = v);
|
||||
DrawCompressionBox();
|
||||
Checkbox("Keep Default Metadata Changes on Import",
|
||||
"Normally, metadata changes that equal their default values, which are sometimes exported by TexTools, are discarded. "
|
||||
+ "Toggle this to keep them, for example if an option in a mod is supposed to disable a metadata change from a prior option.",
|
||||
Checkbox("Keep Default Metadata Changes on Import"u8,
|
||||
"Normally, metadata changes that equal their default values, which are sometimes exported by TexTools, are discarded. "u8
|
||||
+ "Toggle this to keep them, for example if an option in a mod is supposed to disable a metadata change from a prior option."u8,
|
||||
_config.KeepDefaultMetaChanges, v => _config.KeepDefaultMetaChanges = v);
|
||||
Checkbox("Enable Custom Shape and Attribute Support",
|
||||
"Penumbra will allow for custom shape keys and attributes for modded models to be considered and combined.",
|
||||
Checkbox("Enable Custom Shape and Attribute Support"u8,
|
||||
"Penumbra will allow for custom shape keys and attributes for modded models to be considered and combined."u8,
|
||||
_config.EnableCustomShapes, _attributeHook.SetState);
|
||||
DrawWaitForPluginsReflection();
|
||||
DrawEnableHttpApiBox();
|
||||
|
|
@ -866,8 +852,8 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
|
||||
private void DrawCrashHandler()
|
||||
{
|
||||
Checkbox("Enable Penumbra Crash Logging (Experimental)",
|
||||
"Enables Penumbra to launch a secondary process that records some game activity which may or may not help diagnosing Penumbra-related game crashes.",
|
||||
Checkbox("Enable Penumbra Crash Logging (Experimental)"u8,
|
||||
"Enables Penumbra to launch a secondary process that records some game activity which may or may not help diagnosing Penumbra-related game crashes."u8,
|
||||
_config.UseCrashHandler ?? false,
|
||||
v =>
|
||||
{
|
||||
|
|
@ -883,8 +869,8 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
if (!_compactor.CanCompact)
|
||||
return;
|
||||
|
||||
Checkbox("Use Filesystem Compression",
|
||||
"Use Windows functionality to transparently reduce storage size of mod files on your computer. This might cost performance, but seems to generally be beneficial to performance by shifting more responsibility to the underused CPU and away from the overused hard drives.",
|
||||
Checkbox("Use Filesystem Compression"u8,
|
||||
"Use Windows functionality to transparently reduce storage size of mod files on your computer. This might cost performance, but seems to generally be beneficial to performance by shifting more responsibility to the underused CPU and away from the overused hard drives."u8,
|
||||
_config.UseFileSystemCompression,
|
||||
v =>
|
||||
{
|
||||
|
|
@ -892,15 +878,15 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
_compactor.Enabled = v;
|
||||
});
|
||||
Im.Line.Same();
|
||||
if (ImGuiUtil.DrawDisabledButton("Compress Existing Files", Vector2.Zero,
|
||||
"Try to compress all files in your root directory. This will take a while.",
|
||||
if (ImEx.Button("Compress Existing Files"u8, Vector2.Zero,
|
||||
"Try to compress all files in your root directory. This will take a while."u8,
|
||||
_compactor.MassCompactRunning || !_modManager.Valid))
|
||||
_compactor.StartMassCompact(_modManager.BasePath.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.Xpress8K,
|
||||
true);
|
||||
|
||||
Im.Line.Same();
|
||||
if (ImGuiUtil.DrawDisabledButton("Decompress Existing Files", Vector2.Zero,
|
||||
"Try to decompress all files in your root directory. This will take a while.",
|
||||
if (ImEx.Button("Decompress Existing Files"u8, Vector2.Zero,
|
||||
"Try to decompress all files in your root directory. This will take a while."u8,
|
||||
_compactor.MassCompactRunning || !_modManager.Valid))
|
||||
_compactor.StartMassCompact(_modManager.BasePath.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.None,
|
||||
true);
|
||||
|
|
@ -912,8 +898,7 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
Im.Style.FrameHeight),
|
||||
_compactor.CurrentFile?.FullName[(_modManager.BasePath.FullName.Length + 1)..] ?? "Gathering Files...");
|
||||
Im.Line.Same();
|
||||
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Ban.ToIconString(), UiHelpers.IconButtonSize, "Cancel the mass action.",
|
||||
!_compactor.MassCompactRunning, true))
|
||||
if (ImEx.Icon.Button(LunaStyle.CancelIcon, "Cancel the mass action."u8, !_compactor.MassCompactRunning))
|
||||
_compactor.CancelMassCompact();
|
||||
}
|
||||
else
|
||||
|
|
@ -939,6 +924,7 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
_config.MinimumSize.X = newX;
|
||||
_config.Save();
|
||||
}
|
||||
|
||||
Im.Line.Same();
|
||||
Im.Item.SetNextWidth(buttonWidth);
|
||||
if (ImEx.InputOnDeactivation.Drag("##yMinSize"u8, (int)_config.MinimumSize.Y, out var newY, 300, 1500, 0.1f))
|
||||
|
|
@ -948,7 +934,7 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
}
|
||||
|
||||
Im.Line.Same();
|
||||
if (ImGuiUtil.DrawDisabledButton("Reset##resetMinSize", new Vector2(buttonWidth / 2 - Im.Style.ItemSpacing.X * 2, 0),
|
||||
if (ImEx.Button("Reset##resetMinSize"u8, new Vector2(buttonWidth / 2 - Im.Style.ItemSpacing.X * 2, 0),
|
||||
$"Reset minimum dimensions to ({Configuration.Constants.MinimumSizeX}, {Configuration.Constants.MinimumSizeY}).",
|
||||
_config.MinimumSize is { X: Configuration.Constants.MinimumSizeX, Y: Configuration.Constants.MinimumSizeY }))
|
||||
{
|
||||
|
|
@ -956,8 +942,8 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
_config.Save();
|
||||
}
|
||||
|
||||
ImGuiUtil.LabeledHelpMarker("Minimum Window Dimensions",
|
||||
"Set the minimum dimensions for resizing this window. Reducing these dimensions may cause the window to look bad or more confusing and is not recommended.");
|
||||
LunaStyle.DrawAlignedHelpMarkerLabel("Minimum Window Dimensions"u8,
|
||||
"Set the minimum dimensions for resizing this window. Reducing these dimensions may cause the window to look bad or more confusing and is not recommended."u8);
|
||||
|
||||
if (warning.Length > 0)
|
||||
ImEx.TextFramed(warning, UiHelpers.InputTextWidth, Colors.PressEnterWarningBg);
|
||||
|
|
@ -967,18 +953,18 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
|
||||
private void DrawHdrRenderTargets()
|
||||
{
|
||||
Im.Item.SetNextWidth(ImUtf8.CalcTextSize("M"u8).X * 5.0f + Im.Style.FrameHeight);
|
||||
using (var combo = ImUtf8.Combo("##hdrRenderTarget"u8, _config.HdrRenderTargets ? "HDR"u8 : "SDR"u8))
|
||||
Im.Item.SetNextWidth(Im.Font.CalculateSize("M"u8).X * 5.0f + Im.Style.FrameHeight);
|
||||
using (var combo = Im.Combo.Begin("##hdrRenderTarget"u8, _config.HdrRenderTargets ? "HDR"u8 : "SDR"u8))
|
||||
{
|
||||
if (combo)
|
||||
{
|
||||
if (ImUtf8.Selectable("HDR"u8, _config.HdrRenderTargets) && !_config.HdrRenderTargets)
|
||||
if (Im.Selectable("HDR"u8, _config.HdrRenderTargets) && !_config.HdrRenderTargets)
|
||||
{
|
||||
_config.HdrRenderTargets = true;
|
||||
_config.Save();
|
||||
}
|
||||
|
||||
if (ImUtf8.Selectable("SDR"u8, !_config.HdrRenderTargets) && _config.HdrRenderTargets)
|
||||
if (Im.Selectable("SDR"u8, !_config.HdrRenderTargets) && _config.HdrRenderTargets)
|
||||
{
|
||||
_config.HdrRenderTargets = false;
|
||||
_config.Save();
|
||||
|
|
@ -986,8 +972,7 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
}
|
||||
}
|
||||
|
||||
Im.Line.Same();
|
||||
ImUtf8.LabeledHelpMarker("Diffuse Dynamic Range"u8,
|
||||
LunaStyle.DrawAlignedHelpMarkerLabel("Diffuse Dynamic Range"u8,
|
||||
"Set the dynamic range that can be used for diffuse colors in materials without causing visual artifacts.\n"u8
|
||||
+ "Changing this setting requires a game restart. It also only works if Wait for Plugins on Startup is enabled."u8);
|
||||
}
|
||||
|
|
@ -1007,9 +992,8 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
_config.Save();
|
||||
}
|
||||
|
||||
Im.Line.Same();
|
||||
ImGuiUtil.LabeledHelpMarker("Enable HTTP API",
|
||||
"Enables other applications, e.g. Anamnesis, to use some Penumbra functions, like requesting redraws.");
|
||||
LunaStyle.DrawAlignedHelpMarkerLabel("Enable HTTP API"u8,
|
||||
"Enables other applications, e.g. Anamnesis, to use some Penumbra functions, like requesting redraws."u8);
|
||||
}
|
||||
|
||||
/// <summary> Draw a checkbox to toggle Debug mode. </summary>
|
||||
|
|
@ -1022,16 +1006,15 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
_config.Save();
|
||||
}
|
||||
|
||||
Im.Line.Same();
|
||||
ImGuiUtil.LabeledHelpMarker("Enable Debug Mode",
|
||||
"[DEBUG] Enable the Debug Tab and Resource Manager Tab as well as some additional data collection. Also open the config window on plugin load.");
|
||||
LunaStyle.DrawAlignedHelpMarkerLabel("Enable Debug Mode"u8,
|
||||
"[DEBUG] Enable the Debug Tab and Resource Manager Tab as well as some additional data collection. Also open the config window on plugin load."u8);
|
||||
}
|
||||
|
||||
/// <summary> Draw a button that reloads resident resources. </summary>
|
||||
private void DrawReloadResourceButton()
|
||||
{
|
||||
if (ImGuiUtil.DrawDisabledButton("Reload Resident Resources", Vector2.Zero,
|
||||
"Reload some specific files that the game keeps in memory at all times.\nYou usually should not need to do this.",
|
||||
if (ImEx.Button("Reload Resident Resources"u8, Vector2.Zero,
|
||||
"Reload some specific files that the game keeps in memory at all times.\nYou usually should not need to do this."u8,
|
||||
!_characterUtility.Ready))
|
||||
_residentResources.Reload();
|
||||
}
|
||||
|
|
@ -1039,7 +1022,7 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
/// <summary> Draw a button that reloads fonts. </summary>
|
||||
private void DrawReloadFontsButton()
|
||||
{
|
||||
if (ImGuiUtil.DrawDisabledButton("Reload Fonts", Vector2.Zero, "Force the game to reload its font files.", !_fontReloader.Valid))
|
||||
if (ImEx.Button("Reload Fonts"u8, Vector2.Zero, "Force the game to reload its font files."u8, !_fontReloader.Valid))
|
||||
_fontReloader.Reload();
|
||||
}
|
||||
|
||||
|
|
@ -1048,10 +1031,10 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
var enabled = _config.DeleteModModifier.IsActive();
|
||||
if (_cleanupService.Progress is not 0.0 and not 1.0)
|
||||
{
|
||||
ImUtf8.ProgressBar((float)_cleanupService.Progress, new Vector2(200 * Im.Style.GlobalScale, Im.Style.FrameHeight),
|
||||
Im.ProgressBar((float)_cleanupService.Progress, new Vector2(200 * Im.Style.GlobalScale, Im.Style.FrameHeight),
|
||||
$"{_cleanupService.Progress * 100}%");
|
||||
Im.Line.Same();
|
||||
if (ImUtf8.Button("Cancel##FileCleanup"u8))
|
||||
if (Im.Button("Cancel##FileCleanup"u8))
|
||||
_cleanupService.Cancel();
|
||||
}
|
||||
else
|
||||
|
|
@ -1059,22 +1042,22 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
Im.Line.New();
|
||||
}
|
||||
|
||||
if (ImUtf8.ButtonEx("Clear Unused Local Mod Data Files"u8,
|
||||
"Delete all local mod data files that do not correspond to currently installed mods."u8, default,
|
||||
if (ImEx.Button("Clear Unused Local Mod Data Files"u8, default,
|
||||
"Delete all local mod data files that do not correspond to currently installed mods."u8,
|
||||
!enabled || _cleanupService.IsRunning))
|
||||
_cleanupService.CleanUnusedLocalData();
|
||||
if (!enabled)
|
||||
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteModModifier} while clicking to delete files.");
|
||||
|
||||
if (ImUtf8.ButtonEx("Clear Backup Files"u8,
|
||||
if (ImEx.Button("Clear Backup Files"u8, default,
|
||||
"Delete all backups of .json configuration files in your configuration folder and all backups of mod group files in your mod directory."u8,
|
||||
default, !enabled || _cleanupService.IsRunning))
|
||||
!enabled || _cleanupService.IsRunning))
|
||||
_cleanupService.CleanBackupFiles();
|
||||
if (!enabled)
|
||||
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteModModifier} while clicking to delete files.");
|
||||
|
||||
if (ImUtf8.ButtonEx("Clear All Unused Settings"u8,
|
||||
"Remove all mod settings in all of your collections that do not correspond to currently installed mods."u8, default,
|
||||
if (ImEx.Button("Clear All Unused Settings"u8, default,
|
||||
"Remove all mod settings in all of your collections that do not correspond to currently installed mods."u8,
|
||||
!enabled || _cleanupService.IsRunning))
|
||||
_cleanupService.CleanupAllUnusedSettings();
|
||||
if (!enabled)
|
||||
|
|
@ -1087,15 +1070,15 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
if (!_dalamudConfig.GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool value))
|
||||
{
|
||||
using var disabled = Im.Disabled();
|
||||
Checkbox("Wait for Plugins on Startup (Disabled, can not access Dalamud Configuration)", string.Empty, false, _ => { });
|
||||
Checkbox("Wait for Plugins on Startup (Disabled, can not access Dalamud Configuration)"u8, StringU8.Empty, false, _ => { });
|
||||
}
|
||||
else
|
||||
{
|
||||
Checkbox("Wait for Plugins on Startup",
|
||||
"Some mods need to change files that are loaded once when the game starts and never afterwards.\n"
|
||||
+ "This can cause issues with Penumbra loading after the files are already loaded.\n"
|
||||
+ "This setting causes the game to wait until certain plugins have finished loading, making those mods work (in the base collection).\n\n"
|
||||
+ "This changes a setting in the Dalamud Configuration found at /xlsettings -> General.",
|
||||
Checkbox("Wait for Plugins on Startup"u8,
|
||||
"Some mods need to change files that are loaded once when the game starts and never afterwards.\n"u8
|
||||
+ "This can cause issues with Penumbra loading after the files are already loaded.\n"u8
|
||||
+ "This setting causes the game to wait until certain plugins have finished loading, making those mods work (in the base collection).\n\n"u8
|
||||
+ "This changes a setting in the Dalamud Configuration found at /xlsettings -> General."u8,
|
||||
value,
|
||||
v => _dalamudConfig.SetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, v, "doWaitForPluginsOnStartup"));
|
||||
}
|
||||
|
|
@ -1109,7 +1092,7 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
var width = Im.Font.CalculateSize(UiHelpers.SupportInfoButtonText).X + Im.Style.FramePadding.X * 2;
|
||||
var xPos = Im.Window.Width - width;
|
||||
// Respect the scroll bar width.
|
||||
if (Im.Scroll.MaximumY> 0)
|
||||
if (Im.Scroll.MaximumY > 0)
|
||||
xPos -= Im.Style.ScrollbarSize + Im.Style.FramePadding.X;
|
||||
|
||||
Im.Cursor.Position = new Vector2(xPos, Im.Style.FrameHeightWithSpacing);
|
||||
|
|
@ -1141,8 +1124,8 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
if (!Im.Tree.Header("Tags"u8))
|
||||
return;
|
||||
|
||||
var tagIdx = _sharedTags.Draw("Predefined Tags: ",
|
||||
"Predefined tags that can be added or removed from mods with a single click.", _predefinedTagManager,
|
||||
var tagIdx = Luna.TagButtons.Draw("Predefined Tags: "u8,
|
||||
"Predefined tags that can be added or removed from mods with a single click."u8, _predefinedTagManager,
|
||||
out var editedTag);
|
||||
|
||||
if (tagIdx >= 0)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue