Merge remote-tracking branch 'upstream/master' into model-export

This commit is contained in:
Adam Moy 2025-02-12 12:47:05 -06:00
commit af0a0eb144
9 changed files with 244 additions and 97 deletions

@ -1 +1 @@
Subproject commit 35b25bef92e9b0be96c44c150a3df89d848d2658
Subproject commit c67809057fac73a0fd407e3ad567f0aa6bc0bc37

@ -1 +1 @@
Subproject commit 0bc2b0f66eee1a02c9575b2bb30f27ce166f8632
Subproject commit 4eb7c118cdac5873afb97cb04719602f061f03b7

View file

@ -1,3 +1,4 @@
using Dalamud.Interface.ImGuiNotification;
using OtterGui;
using OtterGui.Classes;
using Penumbra.Meta.Manipulations;
@ -274,6 +275,24 @@ public sealed class CollectionCache : IDisposable
_manager.ResolvedFileChanged.Invoke(collection, type, key, value, old, mod);
}
private static bool IsRedirectionSupported(Utf8GamePath path, IMod mod)
{
var ext = path.Extension().AsciiToLower().ToString();
switch (ext)
{
case ".atch" or ".eqp" or ".eqdp" or ".est" or ".gmp" or ".cmp" or ".imc":
Penumbra.Messager.NotificationMessage(
$"Redirection of {ext} files for {mod.Name} is unsupported. Please use the corresponding meta manipulations instead.",
NotificationType.Warning);
return false;
case ".lvb" or ".lgb" or ".sgb":
Penumbra.Messager.NotificationMessage($"Redirection of {ext} files for {mod.Name} is unsupported as this breaks the game.",
NotificationType.Warning);
return false;
default: return true;
}
}
// Add a specific file redirection, handling potential conflicts.
// For different mods, higher mod priority takes precedence before option group priority,
// which takes precedence before option priority, which takes precedence before ordering.
@ -283,6 +302,9 @@ public sealed class CollectionCache : IDisposable
if (!CheckFullPath(path, file))
return;
if (!IsRedirectionSupported(path, mod))
return;
try
{
if (ResolvedFiles.TryAdd(path, new ModPath(mod, file)))
@ -343,7 +365,8 @@ public sealed class CollectionCache : IDisposable
private bool AddConflict(object data, IMod addedMod, IMod existingMod)
{
var addedPriority = addedMod.Index >= 0 ? _collection.GetActualSettings(addedMod.Index).Settings!.Priority : addedMod.Priority;
var existingPriority = existingMod.Index >= 0 ? _collection.GetActualSettings(existingMod.Index).Settings!.Priority : existingMod.Priority;
var existingPriority =
existingMod.Index >= 0 ? _collection.GetActualSettings(existingMod.Index).Settings!.Priority : existingMod.Priority;
if (existingPriority < addedPriority)
{
@ -427,7 +450,8 @@ public sealed class CollectionCache : IDisposable
if (!_changedItems.TryGetValue(name, out var data))
_changedItems.Add(name, (new SingleArray<IMod>(mod), obj));
else if (!data.Item1.Contains(mod))
_changedItems[name] = (data.Item1.Append(mod), obj is IdentifiedCounter x && data.Item2 is IdentifiedCounter y ? x + y : obj);
_changedItems[name] = (data.Item1.Append(mod),
obj is IdentifiedCounter x && data.Item2 is IdentifiedCounter y ? x + y : obj);
else if (obj is IdentifiedCounter x && data.Item2 is IdentifiedCounter y)
_changedItems[name] = (data.Item1, x + y);
}

View file

@ -171,8 +171,7 @@ public class CollectionCacheManager : IDisposable, IService
try
{
ResolvedFileChanged.Invoke(collection, ResolvedFileChanged.Type.FullRecomputeStart, Utf8GamePath.Empty, FullPath.Empty,
FullPath.Empty,
null);
FullPath.Empty, null);
cache.ResolvedFiles.Clear();
cache.Meta.Reset();
cache.ConflictDict.Clear();

View file

@ -1,4 +1,3 @@
using System.Linq;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using OtterGui.Services;
using Penumbra.Api.Enums;
@ -49,15 +48,16 @@ public class PathResolver : IDisposable, IService
if (!_config.EnableMods)
return (null, ResolveData.Invalid);
return resourceType switch
{
// Do not allow manipulating layers to prevent very obvious cheating and softlocks.
if (resourceType is ResourceType.Lvb or ResourceType.Lgb or ResourceType.Sgb)
return (null, ResolveData.Invalid);
ResourceType.Lvb or ResourceType.Lgb or ResourceType.Sgb => (null, ResolveData.Invalid),
// Prevent .atch loading to prevent crashes on outdated .atch files.
ResourceType.Atch => ResolveAtch(path),
// These are manipulated through Meta Edits instead.
ResourceType.Eqp or ResourceType.Eqdp or ResourceType.Est or ResourceType.Gmp or ResourceType.Cmp => (null, ResolveData.Invalid),
// Prevent .atch loading to prevent crashes on outdated .atch files. TODO: handle atch modding differently.
if (resourceType is ResourceType.Atch)
return ResolveAtch(path);
return category switch
_ => category switch
{
// Only Interface collection.
ResourceCategory.Ui => ResolveUi(path),
@ -71,20 +71,17 @@ public class PathResolver : IDisposable, IService
ResourceCategory.Sound => Resolve(path, resourceType),
// EXD Modding in general should probably be prohibited but is currently used for fan translations.
// We prevent WebURL specifically because it technically allows launching arbitrary programs / to execute arbitrary code.
ResourceCategory.Exd => path.Path.StartsWith("exd/weburl"u8)
? (null, ResolveData.Invalid)
: DefaultResolver(path),
ResourceCategory.Exd => path.Path.StartsWith("exd/weburl"u8) ? (null, ResolveData.Invalid) : DefaultResolver(path),
// None of these files are ever associated with specific characters,
// always use the default resolver for now,
// except that common/font is conceptually more UI.
ResourceCategory.Common => path.Path.StartsWith("common/font"u8)
? ResolveUi(path)
: DefaultResolver(path),
ResourceCategory.Common => path.Path.StartsWith("common/font"u8) ? ResolveUi(path) : DefaultResolver(path),
ResourceCategory.BgCommon => DefaultResolver(path),
ResourceCategory.Bg => DefaultResolver(path),
ResourceCategory.Cut => DefaultResolver(path),
ResourceCategory.Music => DefaultResolver(path),
_ => DefaultResolver(path),
}
};
}

View file

@ -6,69 +6,160 @@ namespace Penumbra.Services;
public class CleanupService(SaveService saveService, ModManager mods, CollectionManager collections) : IService
{
private CancellationTokenSource _cancel = new();
private Task? _task;
public double Progress { get; private set; }
public bool IsRunning
=> _task is { IsCompleted: false };
public void Cancel()
=> _cancel.Cancel();
public void CleanUnusedLocalData()
{
if (IsRunning)
return;
var usedFiles = mods.Select(saveService.FileNames.LocalDataFile).ToHashSet();
foreach (var file in saveService.FileNames.LocalDataFiles.ToList())
Progress = 0;
var deleted = 0;
_cancel = new CancellationTokenSource();
_task = Task.Run(() =>
{
var localFiles = saveService.FileNames.LocalDataFiles.ToList();
var step = 0.9 / localFiles.Count;
Progress = 0.1;
foreach (var file in localFiles)
{
if (_cancel.IsCancellationRequested)
break;
try
{
if (!file.Exists || usedFiles.Contains(file.FullName))
continue;
file.Delete();
Penumbra.Log.Information($"[CleanupService] Deleted unused local data file {file.Name}.");
Penumbra.Log.Debug($"[CleanupService] Deleted unused local data file {file.Name}.");
++deleted;
}
catch (Exception ex)
{
Penumbra.Log.Error($"[CleanupService] Failed to delete unused local data file {file.Name}:\n{ex}");
}
Progress += step;
}
Penumbra.Log.Information($"[CleanupService] Deleted {deleted} unused local data files.");
Progress = 1;
});
}
public void CleanBackupFiles()
{
foreach (var file in mods.BasePath.EnumerateFiles("group_*.json.bak", SearchOption.AllDirectories))
if (IsRunning)
return;
Progress = 0;
var deleted = 0;
_cancel = new CancellationTokenSource();
_task = Task.Run(() =>
{
var configFiles = Directory.EnumerateFiles(saveService.FileNames.ConfigDirectory, "*.json.bak", SearchOption.AllDirectories)
.ToList();
Progress = 0.1;
if (_cancel.IsCancellationRequested)
return;
var groupFiles = mods.BasePath.EnumerateFiles("group_*.json.bak", SearchOption.AllDirectories).ToList();
Progress = 0.5;
var step = 0.4 / (groupFiles.Count + configFiles.Count);
foreach (var file in groupFiles)
{
if (_cancel.IsCancellationRequested)
break;
try
{
if (!file.Exists)
continue;
file.Delete();
Penumbra.Log.Information($"[CleanupService] Deleted group backup file {file.FullName}.");
++deleted;
Penumbra.Log.Debug($"[CleanupService] Deleted group backup file {file.FullName}.");
}
catch (Exception ex)
{
Penumbra.Log.Error($"[CleanupService] Failed to delete group backup file {file.FullName}:\n{ex}");
}
Progress += step;
}
foreach (var file in Directory.EnumerateFiles(saveService.FileNames.ConfigDirectory, "*.json.bak", SearchOption.AllDirectories))
Penumbra.Log.Information($"[CleanupService] Deleted {deleted} group backup files.");
deleted = 0;
foreach (var file in configFiles)
{
if (_cancel.IsCancellationRequested)
break;
try
{
if (!File.Exists(file))
continue;
File.Delete(file);
Penumbra.Log.Information($"[CleanupService] Deleted config backup file {file}.");
++deleted;
Penumbra.Log.Debug($"[CleanupService] Deleted config backup file {file}.");
}
catch (Exception ex)
{
Penumbra.Log.Error($"[CleanupService] Failed to delete config backup file {file}:\n{ex}");
}
Progress += step;
}
Penumbra.Log.Information($"[CleanupService] Deleted {deleted} config backup files.");
Progress = 1;
});
}
public void CleanupAllUnusedSettings()
{
if (IsRunning)
return;
Progress = 0;
var totalRemoved = 0;
var diffCollections = 0;
_cancel = new CancellationTokenSource();
_task = Task.Run(() =>
{
var step = 1.0 / collections.Storage.Count;
foreach (var collection in collections.Storage)
{
if (_cancel.IsCancellationRequested)
break;
var count = collections.Storage.CleanUnavailableSettings(collection);
if (count > 0)
Penumbra.Log.Information(
{
Penumbra.Log.Debug(
$"[CleanupService] Removed {count} unused settings from collection {collection.Identity.AnonymizedName}.");
}
totalRemoved += count;
++diffCollections;
}
Progress += step;
}
Penumbra.Log.Information($"[CleanupService] Removed {totalRemoved} unused settings from {diffCollections} separate collections.");
Progress = 1;
});
}
}

View file

@ -57,10 +57,32 @@ public class PenumbraChangelog : IUiService
Add1_3_1_0(Changelog);
Add1_3_2_0(Changelog);
Add1_3_3_0(Changelog);
Add1_3_4_0(Changelog);
}
#region Changelogs
private static void Add1_3_4_0(Changelog log)
=> log.NextVersion("Version 1.3.4.0")
.RegisterHighlight("Added HDR functionality to diffuse buffers. This allows more accurate representation of non-standard color values for e.g. skin or hair colors when used with advanced customizations in Glamourer.")
.RegisterEntry("This option requires Wait For Plugins On Load to be enabled in Dalamud and to be enabled on start to work. It is on by default but can be turned off.", 1)
.RegisterHighlight("Added a new option group type: Combining Groups.")
.RegisterEntry("A combining group behaves similarly to a multi group for the user, but instead of enabling the different options separately, it results in exactly one option per choice of settings.", 1)
.RegisterEntry("Example: The user sees 2 checkboxes [+25%, +50%], but the 4 different selection states result in +0%, +25%, +50% or +75% if both are toggled on. Every choice of settings can be configured separately by the mod creator.", 1)
.RegisterEntry("Added new functionality to better track copies of the player character in cutscenes if they get forced to specific clothing, like in the Margrat cutscene. Might improve tracking in wedding ceremonies, too, let me know.")
.RegisterEntry("Added a display of the number of selected files and folders to the multi mod selection.")
.RegisterEntry("Added cleaning functionality to remove outdated or unused files or backups from the config and mod folders via manual action.")
.RegisterEntry("Updated the Bone and Material limits in the Model Importer.")
.RegisterEntry("Improved handling of IMC and Material files loaded asynchronously.")
.RegisterEntry("Added IPC functionality to query temporary settings.")
.RegisterEntry("Improved some mod setting IPC functions.")
.RegisterEntry("Fixed some path detection issues in the OnScreen tab.")
.RegisterEntry("Fixed some issues with temporary mod settings.")
.RegisterEntry("Fixed issues with IPC calls before the game has finished loading.")
.RegisterEntry("Fixed using the wrong dye channel in the material editor previews.")
.RegisterEntry("Added some log warnings if outdated materials are loaded by the game.")
.RegisterEntry("Added Schemas for some of the json files generated and read by Penumbra to the solution.");
private static void Add1_3_3_0(Changelog log)
=> log.NextVersion("Version 1.3.3.0")
.RegisterHighlight("Added Temporary Settings to collections.")

View file

@ -797,7 +797,6 @@ public class SettingsTab : ITab, IUiService
ImGui.Separator();
DrawCleanupButtons();
ImGui.NewLine();
}
private void DrawCrashHandler()
@ -991,24 +990,39 @@ public class SettingsTab : ITab, IUiService
private void DrawCleanupButtons()
{
var enabled = _config.DeleteModModifier.IsActive();
if (_cleanupService.Progress is not 0.0 and not 1.0)
{
ImUtf8.ProgressBar((float)_cleanupService.Progress, new Vector2(200 * ImUtf8.GlobalScale, ImGui.GetFrameHeight()),
$"{_cleanupService.Progress * 100}%");
ImGui.SameLine();
if (ImUtf8.Button("Cancel##FileCleanup"u8))
_cleanupService.Cancel();
}
else
{
ImGui.NewLine();
}
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, !enabled))
"Delete all local mod data files that do not correspond to currently installed mods."u8, default,
!enabled || _cleanupService.IsRunning))
_cleanupService.CleanUnusedLocalData();
if (!enabled)
ImUtf8.HoverTooltip($"Hold {_config.DeleteModModifier} while clicking to delete files.");
ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteModModifier} while clicking to delete files.");
if (ImUtf8.ButtonEx("Clear Backup Files"u8,
"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))
default, !enabled || _cleanupService.IsRunning))
_cleanupService.CleanBackupFiles();
if (!enabled)
ImUtf8.HoverTooltip($"Hold {_config.DeleteModModifier} while clicking to delete files.");
ImUtf8.HoverTooltip(ImGuiHoveredFlags.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, !enabled))
"Remove all mod settings in all of your collections that do not correspond to currently installed mods."u8, default,
!enabled || _cleanupService.IsRunning))
_cleanupService.CleanupAllUnusedSettings();
if (!enabled)
ImUtf8.HoverTooltip($"Hold {_config.DeleteModModifier} while clicking to remove settings.");
ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteModModifier} while clicking to remove settings.");
}
/// <summary> Draw a checkbox that toggles the dalamud setting to wait for plugins on open. </summary>

View file

@ -5,8 +5,8 @@
"Punchline": "Runtime mod loader and manager.",
"Description": "Runtime mod loader and manager.",
"InternalName": "Penumbra",
"AssemblyVersion": "1.3.3.1",
"TestingAssemblyVersion": "1.3.3.10",
"AssemblyVersion": "1.3.4.0",
"TestingAssemblyVersion": "1.3.4.0",
"RepoUrl": "https://github.com/xivdev/Penumbra",
"ApplicableVersion": "any",
"DalamudApiLevel": 11,
@ -18,9 +18,9 @@
"LoadPriority": 69420,
"LoadRequiredState": 2,
"LoadSync": true,
"DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip",
"DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.10/Penumbra.zip",
"DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip",
"DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip",
"DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip",
"DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip",
"IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png"
}
]