Compare commits

..

No commits in common. "master" and "testing_1.5.0.10" have entirely different histories.

39 changed files with 160 additions and 702 deletions

@ -1 +1 @@
Subproject commit a63f6735cf4bed4f7502a022a10378607082b770
Subproject commit 4a9b71a93e76aa5eed818542288329e34ec0dd89

@ -1 +1 @@
Subproject commit 3d6cee1a11922ccd426f36060fd026bc1a698adf
Subproject commit af41b1787acef9df7dc83619fe81e63a36443ee5

View file

@ -1,4 +1,4 @@
<Project Sdk="Dalamud.NET.Sdk/13.1.0">
<Project Sdk="Dalamud.NET.Sdk/13.0.0">
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>

@ -1 +1 @@
Subproject commit d889f9ef918514a46049725052d378b441915b00
Subproject commit 73010350338ecd7b98ad85d127bed08d7d8718d4

@ -1 +1 @@
Subproject commit c8611a0c546b6b2ec29214ab319fc2c38fe74793
Subproject commit 878acce46e286867d6ef1f8ecedb390f7bac34fd

View file

@ -17,7 +17,7 @@ public class PenumbraApi(
UiApi ui) : IDisposable, IApiService, IPenumbraApi
{
public const int BreakingVersion = 5;
public const int FeatureVersion = 13;
public const int FeatureVersion = 11;
public void Dispose()
{

View file

@ -2,14 +2,11 @@ using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Interop;
using Penumbra.Interop.Services;
namespace Penumbra.Api.Api;
public class RedrawApi(RedrawService redrawService, IFramework framework, CollectionManager collections, ObjectManager objects, ApiHelpers helpers) : IPenumbraApiRedraw, IApiService
public class RedrawApi(RedrawService redrawService, IFramework framework) : IPenumbraApiRedraw, IApiService
{
public void RedrawObject(int gameObjectIndex, RedrawType setting)
{
@ -31,24 +28,6 @@ public class RedrawApi(RedrawService redrawService, IFramework framework, Collec
framework.RunOnFrameworkThread(() => redrawService.RedrawAll(setting));
}
public void RedrawCollectionMembers(Guid collectionId, RedrawType setting)
{
if (!collections.Storage.ById(collectionId, out var collection))
collection = ModCollection.Empty;
framework.RunOnFrameworkThread(() =>
{
foreach (var actor in objects.Objects)
{
helpers.AssociatedCollection(actor.ObjectIndex, out var modCollection);
if (collection == modCollection)
{
redrawService.RedrawObject(actor.ObjectIndex, setting);
}
}
});
}
public event GameObjectRedrawnDelegate? GameObjectRedrawn
{
add => redrawService.GameObjectRedrawn += value;

View file

@ -5,7 +5,6 @@ using EmbedIO.WebApi;
using OtterGui.Services;
using Penumbra.Api.Api;
using Penumbra.Api.Enums;
using Penumbra.Mods.Settings;
namespace Penumbra.Api;
@ -14,7 +13,6 @@ public class HttpApi : IDisposable, IApiService
private partial class Controller : WebApiController
{
// @formatter:off
[Route( HttpVerbs.Get, "/moddirectory" )] public partial string GetModDirectory();
[Route( HttpVerbs.Get, "/mods" )] public partial object? GetMods();
[Route( HttpVerbs.Post, "/redraw" )] public partial Task Redraw();
[Route( HttpVerbs.Post, "/redrawAll" )] public partial Task RedrawAll();
@ -22,7 +20,6 @@ public class HttpApi : IDisposable, IApiService
[Route( HttpVerbs.Post, "/installmod" )] public partial Task InstallMod();
[Route( HttpVerbs.Post, "/openwindow" )] public partial void OpenWindow();
[Route( HttpVerbs.Post, "/focusmod" )] public partial Task FocusMod();
[Route( HttpVerbs.Post, "/setmodsettings")] public partial Task SetModSettings();
// @formatter:on
}
@ -68,12 +65,6 @@ public class HttpApi : IDisposable, IApiService
private partial class Controller(IPenumbraApi api, IFramework framework)
{
public partial string GetModDirectory()
{
Penumbra.Log.Debug($"[HTTP] {nameof(GetModDirectory)} triggered.");
return api.PluginState.GetModDirectory();
}
public partial object? GetMods()
{
Penumbra.Log.Debug($"[HTTP] {nameof(GetMods)} triggered.");
@ -125,7 +116,6 @@ public class HttpApi : IDisposable, IApiService
Penumbra.Log.Debug($"[HTTP] {nameof(OpenWindow)} triggered.");
api.Ui.OpenMainWindow(TabType.Mods, string.Empty, string.Empty);
}
public async partial Task FocusMod()
{
var data = await HttpContext.GetRequestDataAsync<ModFocusData>().ConfigureAwait(false);
@ -134,30 +124,6 @@ public class HttpApi : IDisposable, IApiService
api.Ui.OpenMainWindow(TabType.Mods, data.Path, data.Name);
}
public async partial Task SetModSettings()
{
var data = await HttpContext.GetRequestDataAsync<SetModSettingsData>().ConfigureAwait(false);
Penumbra.Log.Debug($"[HTTP] {nameof(SetModSettings)} triggered.");
await framework.RunOnFrameworkThread(() =>
{
var collection = data.CollectionId ?? api.Collection.GetCollection(ApiCollectionType.Current)!.Value.Id;
if (data.Inherit.HasValue)
{
api.ModSettings.TryInheritMod(collection, data.ModPath, data.ModName, data.Inherit.Value);
if (data.Inherit.Value)
return;
}
if (data.State.HasValue)
api.ModSettings.TrySetMod(collection, data.ModPath, data.ModName, data.State.Value);
if (data.Priority.HasValue)
api.ModSettings.TrySetModPriority(collection, data.ModPath, data.ModName, data.Priority.Value);
foreach (var (group, settings) in data.Settings ?? [])
api.ModSettings.TrySetModSettings(collection, data.ModPath, data.ModName, group, settings);
}
).ConfigureAwait(false);
}
private record ModReloadData(string Path, string Name)
{
public ModReloadData()
@ -185,19 +151,5 @@ public class HttpApi : IDisposable, IApiService
: this(string.Empty, RedrawType.Redraw, -1)
{ }
}
private record SetModSettingsData(
Guid? CollectionId,
string ModPath,
string ModName,
bool? Inherit,
bool? State,
int? Priority,
Dictionary<string, List<string>>? Settings)
{
public SetModSettingsData()
: this(null, string.Empty, string.Empty, null, null, null, null)
{}
}
}
}

View file

@ -88,7 +88,6 @@ public sealed class IpcProviders : IDisposable, IApiService
IpcSubscribers.RedrawObject.Provider(pi, api.Redraw),
IpcSubscribers.RedrawAll.Provider(pi, api.Redraw),
IpcSubscribers.GameObjectRedrawn.Provider(pi, api.Redraw),
IpcSubscribers.RedrawCollectionMembers.Provider(pi, api.Redraw),
IpcSubscribers.ResolveDefaultPath.Provider(pi, api.Resolve),
IpcSubscribers.ResolveInterfacePath.Provider(pi, api.Resolve),

View file

@ -121,10 +121,6 @@ public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService
}).ToArray();
ImGui.OpenPopup("Changed Item List");
}
IpcTester.DrawIntro(RedrawCollectionMembers.Label, "Redraw Collection Members");
if (ImGui.Button("Redraw##ObjectCollection"))
new RedrawCollectionMembers(pi).Invoke(collectionList[0].Id, RedrawType.Redraw);
}
private void DrawChangedItemPopup()

View file

@ -53,7 +53,6 @@ public class Configuration : IPluginConfiguration, ISavable, IService
public string ModDirectory { get; set; } = string.Empty;
public string ExportDirectory { get; set; } = string.Empty;
public string WatchDirectory { get; set; } = string.Empty;
public bool? UseCrashHandler { get; set; } = null;
public bool OpenWindowAtStart { get; set; } = false;
@ -77,8 +76,6 @@ public class Configuration : IPluginConfiguration, ISavable, IService
public bool HideRedrawBar { get; set; } = false;
public bool HideMachinistOffhandFromChangedItems { get; set; } = true;
public bool DefaultTemporaryMode { get; set; } = false;
public bool EnableDirectoryWatch { get; set; } = false;
public bool EnableAutomaticModImport { get; set; } = false;
public bool EnableCustomShapes { get; set; } = true;
public PcpSettings PcpSettings = new();
public RenameField ShowRename { get; set; } = RenameField.BothDataPrio;

View file

@ -1,47 +0,0 @@
namespace Penumbra.Interop;
public static unsafe partial class CloudApi
{
private const int CfSyncRootInfoBasic = 0;
/// <summary> Determines whether a file or directory is cloud-synced using OneDrive or other providers that use the Cloud API. </summary>
/// <remarks> Can be expensive. Callers should cache the result when relevant. </remarks>
public static bool IsCloudSynced(string path)
{
var buffer = stackalloc long[1];
int hr;
uint length;
try
{
hr = CfGetSyncRootInfoByPath(path, CfSyncRootInfoBasic, buffer, sizeof(long), out length);
}
catch (DllNotFoundException)
{
Penumbra.Log.Debug($"{nameof(CfGetSyncRootInfoByPath)} threw DllNotFoundException");
return false;
}
catch (EntryPointNotFoundException)
{
Penumbra.Log.Debug($"{nameof(CfGetSyncRootInfoByPath)} threw EntryPointNotFoundException");
return false;
}
Penumbra.Log.Debug($"{nameof(CfGetSyncRootInfoByPath)} returned HRESULT 0x{hr:X8}");
if (hr < 0)
return false;
if (length != sizeof(long))
{
Penumbra.Log.Debug($"Expected {nameof(CfGetSyncRootInfoByPath)} to return {sizeof(long)} bytes, got {length} bytes");
return false;
}
Penumbra.Log.Debug($"{nameof(CfGetSyncRootInfoByPath)} returned {{ SyncRootFileId = 0x{*buffer:X16} }}");
return true;
}
[LibraryImport("cldapi.dll", StringMarshalling = StringMarshalling.Utf16)]
private static partial int CfGetSyncRootInfoByPath(string filePath, int infoClass, void* infoBuffer, uint infoBufferLength,
out uint returnedLength);
}

View file

@ -63,7 +63,8 @@ public sealed unsafe class LoadTimelineResources : FastHook<LoadTimelineResource
{
if (timeline != null)
{
var idx = timeline->GetOwningGameObjectIndex();
// TODO: Clientstructify
var idx = ((delegate* unmanaged<SchedulerTimeline*, int>**)timeline)[0][29](timeline);
if (idx >= 0 && idx < objects.TotalCount)
{
var obj = objects[idx];

View file

@ -137,7 +137,7 @@ public sealed unsafe class CollectionResolver(
{
var item = charaEntry.Value;
var identifier = actors.CreatePlayer(new ByteString(item->Name), item->HomeWorldId);
Penumbra.Log.Excessive(
Penumbra.Log.Verbose(
$"Identified {identifier.Incognito(null)} in cutscene for actor {idx + 200} at 0x{(ulong)gameObject:X} of race {(gameObject->IsCharacter() ? ((Character*)gameObject)->DrawData.CustomizeData.Race.ToString() : "Unknown")}.");
if (identifier.IsValid && CollectionByIdentifier(identifier) is { } coll)
{

View file

@ -75,7 +75,6 @@ public sealed class CutsceneService : IRequiredService, IDisposable
return false;
_copiedCharacters[copyIdx - CutsceneStartIdx] = (short)parentIdx;
_objects.InvokeRequiredUpdates();
return true;
}

View file

@ -38,9 +38,10 @@ public static unsafe class SkinMtrlPathEarlyProcessing
if (character is null)
return null;
if (character->PerSlotStagingArea is not null)
if (character->TempSlotData is not null)
{
var handle = character->PerSlotStagingArea[slotIndex].ModelResourceHandle;
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1564)
var handle = *(ModelResourceHandle**)((nint)character->TempSlotData + 0xE0 * slotIndex + 0x8);
if (handle != null)
return handle;
}

View file

@ -242,10 +242,10 @@ public class ResourceTree(
}
private unsafe void AddSkeleton(List<ResourceNode> nodes, ResolveContext context, CharacterBase* model, string prefix = "")
=> AddSkeleton(nodes, context, model->EID, model->Skeleton, model->BonePhysicsModule, model->BoneKineDriverModule, prefix);
=> AddSkeleton(nodes, context, model->EID, model->Skeleton, model->BonePhysicsModule, *(void**)((nint)model + 0x160), prefix);
private unsafe void AddSkeleton(List<ResourceNode> nodes, ResolveContext context, void* eid, Skeleton* skeleton, BonePhysicsModule* physics,
BoneKineDriverModule* kineDriver, string prefix = "")
void* kineDriver, string prefix = "")
{
var eidNode = context.CreateNodeFromEid((ResourceHandle*)eid);
if (eidNode != null)
@ -261,7 +261,8 @@ public class ResourceTree(
for (var i = 0; i < skeleton->PartialSkeletonCount; ++i)
{
var phybHandle = physics != null ? physics->BonePhysicsResourceHandles[i] : null;
var kdbHandle = kineDriver != null ? kineDriver->PartialSkeletonEntries[i].KineDriverResourceHandle : null;
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1562)
var kdbHandle = kineDriver != null ? *(ResourceHandle**)((nint)kineDriver + 0x20 + 0x18 * i) : null;
if (context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, kdbHandle, (uint)i) is { } sklbNode)
{
if (context.Global.WithUiData)

View file

@ -421,9 +421,9 @@ public sealed unsafe partial class RedrawService : IDisposable
return;
foreach (ref var f in currentTerritory->FurnitureManager.FurnitureMemory)
foreach (ref var f in currentTerritory->Furniture)
{
var gameObject = f.Index >= 0 ? currentTerritory->FurnitureManager.ObjectManager.ObjectArray.Objects[f.Index].Value : null;
var gameObject = f.Index >= 0 ? currentTerritory->HousingObjectManager.Objects[f.Index].Value : null;
if (gameObject == null)
continue;

View file

@ -66,8 +66,11 @@ internal static class StructExtensions
public static unsafe CiByteString ResolveKdbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex)
{
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1561)
var vf80 = (delegate* unmanaged<CharacterBase*, byte*, nuint, uint, byte*>)((nint*)character.VirtualTable)[80];
var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
return ToOwnedByteString(character.ResolveKdbPath(pathBuffer, CharacterBase.PathBufferSize, partialSkeletonIndex));
return ToOwnedByteString(vf80((CharacterBase*)Unsafe.AsPointer(ref character), pathBuffer, CharacterBase.PathBufferSize,
partialSkeletonIndex));
}
private static unsafe CiByteString ToOwnedByteString(CStringPointer str)

View file

@ -372,6 +372,7 @@ public class ModMerger : IDisposable, IService
}
else
{
// TODO DataContainer <> Option.
var (group, _, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name);
var (option, _, _) = _editor.FindOrAddOption(group!, originalOption.GetName());
var folder = Path.Combine(dir.FullName, group!.Name, option!.Name);

View file

@ -1,6 +1,5 @@
using OtterGui.Services;
using Penumbra.Communication;
using Penumbra.Interop;
using Penumbra.Mods.Editor;
using Penumbra.Mods.Manager.OptionEditor;
using Penumbra.Services;
@ -304,9 +303,6 @@ public sealed class ModManager : ModStorage, IDisposable, IService
if (!firstTime && _config.ModDirectory != BasePath.FullName)
TriggerModDirectoryChange(BasePath.FullName, Valid);
}
if (CloudApi.IsCloudSynced(BasePath.FullName))
Penumbra.Log.Warning($"Mod base directory {BasePath.FullName} is cloud-synced. This may cause issues.");
}
private void TriggerModDirectoryChange(string newPath, bool valid)

View file

@ -21,8 +21,8 @@ using Penumbra.UI;
using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager;
using Dalamud.Plugin.Services;
using Lumina.Excel.Sheets;
using Penumbra.GameData;
using Penumbra.GameData.Data;
using Penumbra.Interop;
using Penumbra.Interop.Hooks;
using Penumbra.Interop.Hooks.PostProcessing;
using Penumbra.Interop.Hooks.ResourceLoading;
@ -213,7 +213,6 @@ public class Penumbra : IDalamudPlugin
{
var sb = new StringBuilder(10240);
var exists = _config.ModDirectory.Length > 0 && Directory.Exists(_config.ModDirectory);
var cloudSynced = exists && CloudApi.IsCloudSynced(_config.ModDirectory);
var hdrEnabler = _services.GetService<RenderTargetHdrEnabler>();
var drive = exists ? new DriveInfo(new DirectoryInfo(_config.ModDirectory).Root.FullName) : null;
sb.AppendLine("**Settings**");
@ -224,8 +223,7 @@ public class Penumbra : IDalamudPlugin
sb.Append($"> **`Operating System: `** {(Dalamud.Utility.Util.IsWine() ? "Mac/Linux (Wine)" : "Windows")}\n");
if (Dalamud.Utility.Util.IsWine())
sb.Append($"> **`Locale Environment Variables:`** {CollectLocaleEnvironmentVariables()}\n");
sb.Append(
$"> **`Root Directory: `** `{_config.ModDirectory}`, {(exists ? "Exists" : "Not Existing")}{(cloudSynced ? ", Cloud-Synced" : "")}\n");
sb.Append($"> **`Root Directory: `** `{_config.ModDirectory}`, {(exists ? "Exists" : "Not Existing")}\n");
sb.Append(
$"> **`Free Drive Space: `** {(drive != null ? Functions.HumanReadableSize(drive.AvailableFreeSpace) : "Unknown")}\n");
sb.Append($"> **`Game Data Files: `** {(_gameData.HasModifiedGameDataFiles ? "Modified" : "Pristine")}\n");

View file

@ -1,4 +1,4 @@
<Project Sdk="Dalamud.NET.Sdk/13.1.0">
<Project Sdk="Dalamud.NET.Sdk/13.0.0">
<PropertyGroup>
<AssemblyTitle>Penumbra</AssemblyTitle>
<Company>absolute gangstas</Company>

View file

@ -1,5 +1,5 @@
{
"Author": "Ottermandias, Nylfae, Adam, Wintermute",
"Author": "Ottermandias, Adam, Wintermute",
"Name": "Penumbra",
"Punchline": "Runtime mod loader and manager.",
"Description": "Runtime mod loader and manager.",

View file

@ -1,209 +0,0 @@
using OtterGui.Services;
using Penumbra.Mods.Manager;
namespace Penumbra.Services;
public class FileWatcher : IDisposable, IService
{
// TODO: use ConcurrentSet when it supports comparers in Luna.
private readonly ConcurrentDictionary<string, byte> _pending = new(StringComparer.OrdinalIgnoreCase);
private readonly ModImportManager _modImportManager;
private readonly MessageService _messageService;
private readonly Configuration _config;
private bool _pausedConsumer;
private FileSystemWatcher? _fsw;
private CancellationTokenSource? _cts = new();
private Task? _consumer;
public FileWatcher(ModImportManager modImportManager, MessageService messageService, Configuration config)
{
_modImportManager = modImportManager;
_messageService = messageService;
_config = config;
if (_config.EnableDirectoryWatch)
{
SetupFileWatcher(_config.WatchDirectory);
SetupConsumerTask();
}
}
public void Toggle(bool value)
{
if (_config.EnableDirectoryWatch == value)
return;
_config.EnableDirectoryWatch = value;
_config.Save();
if (value)
{
SetupFileWatcher(_config.WatchDirectory);
SetupConsumerTask();
}
else
{
EndFileWatcher();
EndConsumerTask();
}
}
internal void PauseConsumer(bool pause)
=> _pausedConsumer = pause;
private void EndFileWatcher()
{
if (_fsw is null)
return;
_fsw.Dispose();
_fsw = null;
}
private void SetupFileWatcher(string directory)
{
EndFileWatcher();
_fsw = new FileSystemWatcher
{
IncludeSubdirectories = false,
NotifyFilter = NotifyFilters.FileName | NotifyFilters.CreationTime,
InternalBufferSize = 32 * 1024,
};
// Only wake us for the exact patterns we care about
_fsw.Filters.Add("*.pmp");
_fsw.Filters.Add("*.pcp");
_fsw.Filters.Add("*.ttmp");
_fsw.Filters.Add("*.ttmp2");
_fsw.Created += OnPath;
_fsw.Renamed += OnPath;
UpdateDirectory(directory);
}
private void EndConsumerTask()
{
if (_cts is not null)
{
_cts.Cancel();
_cts = null;
}
_consumer = null;
}
private void SetupConsumerTask()
{
EndConsumerTask();
_cts = new CancellationTokenSource();
_consumer = Task.Factory.StartNew(
() => ConsumerLoopAsync(_cts.Token),
_cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap();
}
public void UpdateDirectory(string newPath)
{
if (_config.WatchDirectory != newPath)
{
_config.WatchDirectory = newPath;
_config.Save();
}
if (_fsw is null)
return;
_fsw.EnableRaisingEvents = false;
if (!Directory.Exists(newPath) || newPath.Length is 0)
{
_fsw.Path = string.Empty;
}
else
{
_fsw.Path = newPath;
_fsw.EnableRaisingEvents = true;
}
}
private void OnPath(object? sender, FileSystemEventArgs e)
=> _pending.TryAdd(e.FullPath, 0);
private async Task ConsumerLoopAsync(CancellationToken token)
{
while (true)
{
var (path, _) = _pending.FirstOrDefault();
if (path is null || _pausedConsumer)
{
await Task.Delay(500, token).ConfigureAwait(false);
continue;
}
try
{
await ProcessOneAsync(path, token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
Penumbra.Log.Debug("[FileWatcher] Canceled via Token.");
}
catch (Exception ex)
{
Penumbra.Log.Warning($"[FileWatcher] Error during Processing: {ex}");
}
finally
{
_pending.TryRemove(path, out _);
}
}
}
private async Task ProcessOneAsync(string path, CancellationToken token)
{
// Downloads often finish via rename; file may be locked briefly.
// Wait until it exists and is readable; also require two stable size checks.
const int maxTries = 40;
long lastLen = -1;
for (var i = 0; i < maxTries && !token.IsCancellationRequested; i++)
{
if (!File.Exists(path))
{
await Task.Delay(100, token);
continue;
}
try
{
var fi = new FileInfo(path);
var len = fi.Length;
if (len > 0 && len == lastLen)
{
if (_config.EnableAutomaticModImport)
_modImportManager.AddUnpack(path);
else
_messageService.AddMessage(new InstallNotification(_modImportManager, path), false);
return;
}
lastLen = len;
}
catch (IOException)
{
Penumbra.Log.Debug($"[FileWatcher] File is still being written to.");
}
catch (UnauthorizedAccessException)
{
Penumbra.Log.Debug($"[FileWatcher] File is locked.");
}
await Task.Delay(150, token);
}
}
public void Dispose()
{
EndConsumerTask();
EndFileWatcher();
}
}

View file

@ -1,39 +0,0 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.ImGuiNotification.EventArgs;
using OtterGui.Text;
using Penumbra.Mods.Manager;
namespace Penumbra.Services;
public class InstallNotification(ModImportManager modImportManager, string filePath) : OtterGui.Classes.MessageService.IMessage
{
public string Message
=> "A new mod has been found!";
public NotificationType NotificationType
=> NotificationType.Info;
public uint NotificationDuration
=> uint.MaxValue;
public string NotificationTitle { get; } = Path.GetFileNameWithoutExtension(filePath);
public string LogMessage
=> $"A new mod has been found: {Path.GetFileName(filePath)}";
public void OnNotificationActions(INotificationDrawArgs args)
{
var region = ImGui.GetContentRegionAvail();
var buttonSize = new Vector2((region.X - ImGui.GetStyle().ItemSpacing.X) / 2, 0);
if (ImUtf8.ButtonEx("Install"u8, ""u8, buttonSize))
{
modImportManager.AddUnpack(filePath);
args.Notification.DismissNow();
}
ImGui.SameLine();
if (ImUtf8.ButtonEx("Ignore"u8, ""u8, buttonSize))
args.Notification.DismissNow();
}
}

View file

@ -82,9 +82,9 @@ public class PcpService : IApiService, IDisposable
public void CleanPcpCollections()
{
var collections = _collections.Storage.Where(c => c.Identity.Name.StartsWith("PCP/")).ToList();
Penumbra.Log.Information($"[PCPService] Deleting {collections.Count} collections starting with PCP/.");
Penumbra.Log.Information($"[PCPService] Deleting {collections.Count} mods containing the tag PCP.");
foreach (var collection in collections)
_collections.Storage.RemoveCollection(collection);
_collections.Storage.Delete(collection);
}
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, DirectoryInfo? newDirectory)

View file

@ -216,7 +216,7 @@ public sealed partial class MtrlTab : IWritable, IDisposable
}
public bool Valid
=> Mtrl.Valid; // FIXME This should be _shadersKnown && Mtrl.Valid but the algorithm for _shadersKnown is flawed as of 7.2.
=> _shadersKnown && Mtrl.Valid;
public byte[] Write()
{

View file

@ -287,17 +287,6 @@ public partial class ModEditWindow
using var font = ImRaii.PushFont(UiBuilder.IconFont);
ImGuiUtil.TextColored(0xFF0000FF, FontAwesomeIcon.TimesCircle.ToIconString());
}
else if (tmp.Length > 0 && Path.GetExtension(tmp) != registry.File.Extension)
{
ImGui.SameLine();
ImGui.SetCursorPosX(pos);
using (var font = ImRaii.PushFont(UiBuilder.IconFont))
{
ImGuiUtil.TextColored(0xFF00B0B0, FontAwesomeIcon.ExclamationCircle.ToIconString());
}
ImUtf8.HoverTooltip("The game path and the file do not have the same extension."u8);
}
}
private void PrintNewGamePath(int i, FileRegistry registry, IModDataContainer subMod)
@ -330,17 +319,6 @@ public partial class ModEditWindow
using var font = ImRaii.PushFont(UiBuilder.IconFont);
ImGuiUtil.TextColored(0xFF0000FF, FontAwesomeIcon.TimesCircle.ToIconString());
}
else if (tmp.Length > 0 && Path.GetExtension(tmp) != registry.File.Extension)
{
ImGui.SameLine();
ImGui.SetCursorPosX(pos);
using (var font = ImRaii.PushFont(UiBuilder.IconFont))
{
ImGuiUtil.TextColored(0xFF00B0B0, FontAwesomeIcon.ExclamationCircle.ToIconString());
}
ImUtf8.HoverTooltip("The game path and the file do not have the same extension."u8);
}
}
private void DrawButtonHeader()

View file

@ -17,6 +17,7 @@ public partial class ModEditWindow
private readonly FileDialogService _fileDialog;
private readonly ResourceTreeFactory _resourceTreeFactory;
private readonly ResourceTreeViewer _quickImportViewer;
private readonly Dictionary<FullPath, IWritable?> _quickImportWritables = new();
private readonly Dictionary<(Utf8GamePath, IWritable?), QuickImportAction> _quickImportActions = new();
private HashSet<string> GetPlayerResourcesOfType(ResourceType type)
@ -55,11 +56,52 @@ public partial class ModEditWindow
private void OnQuickImportRefresh()
{
_quickImportWritables.Clear();
_quickImportActions.Clear();
}
private void DrawQuickImportActions(ResourceNode resourceNode, IWritable? writable, Vector2 buttonSize)
private void DrawQuickImportActions(ResourceNode resourceNode, Vector2 buttonSize)
{
if (!_quickImportWritables!.TryGetValue(resourceNode.FullPath, out var writable))
{
var path = resourceNode.FullPath.ToPath();
if (resourceNode.FullPath.IsRooted)
{
writable = new RawFileWritable(path);
}
else
{
var file = _gameData.GetFile(path);
writable = file is null ? null : new RawGameFileWritable(file);
}
_quickImportWritables.Add(resourceNode.FullPath, writable);
}
if (ImUtf8.IconButton(FontAwesomeIcon.Save, "Export this file."u8, buttonSize,
resourceNode.FullPath.FullName.Length is 0 || writable is null))
{
var fullPathStr = resourceNode.FullPath.FullName;
var ext = resourceNode.PossibleGamePaths.Length == 1
? Path.GetExtension(resourceNode.GamePath.ToString())
: Path.GetExtension(fullPathStr);
_fileDialog.OpenSavePicker($"Export {Path.GetFileName(fullPathStr)} to...", ext, Path.GetFileNameWithoutExtension(fullPathStr), ext,
(success, name) =>
{
if (!success)
return;
try
{
_editor.Compactor.WriteAllBytes(name, writable!.Write());
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not export {fullPathStr}:\n{e}");
}
}, null, false);
}
ImGui.SameLine();
if (!_quickImportActions!.TryGetValue((resourceNode.GamePath, writable), out var quickImport))
{
@ -79,6 +121,24 @@ public partial class ModEditWindow
}
}
private record RawFileWritable(string Path) : IWritable
{
public bool Valid
=> true;
public byte[] Write()
=> File.ReadAllBytes(Path);
}
private record RawGameFileWritable(FileResource FileResource) : IWritable
{
public bool Valid
=> true;
public byte[] Write()
=> FileResource.Data;
}
public class QuickImportAction
{
public const string FallbackOptionName = "the current option";

View file

@ -667,7 +667,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
_center = new CombinedTexture(_left, _right);
_textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor, () => GetPlayerResourcesOfType(ResourceType.Tex));
_resourceTreeFactory = resourceTreeFactory;
_quickImportViewer = resourceTreeViewerFactory.Create(1, OnQuickImportRefresh, DrawQuickImportActions);
_quickImportViewer = resourceTreeViewerFactory.Create(2, OnQuickImportRefresh, DrawQuickImportActions);
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModEditWindow);
IsOpen = _config is { OpenWindowAtStart: true, Ephemeral.AdvancedEditingOpen: true };
if (IsOpen && selection.Mod != null)

View file

@ -4,20 +4,16 @@ using Dalamud.Interface.Colors;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.Utility;
using Dalamud.Plugin.Services;
using Lumina.Data;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Compression;
using OtterGui.Extensions;
using OtterGui.Raii;
using OtterGui.Text;
using Penumbra.Api.Enums;
using Penumbra.GameData.Files;
using Penumbra.GameData.Structs;
using Penumbra.Interop.ResourceTree;
using Penumbra.Services;
using Penumbra.String;
using Penumbra.String.Classes;
using Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow;
@ -29,20 +25,17 @@ public class ResourceTreeViewer(
IncognitoService incognito,
int actionCapacity,
Action onRefresh,
Action<ResourceNode, IWritable?, Vector2> drawActions,
Action<ResourceNode, Vector2> drawActions,
CommunicatorService communicator,
PcpService pcpService,
IDataManager gameData,
FileDialogService fileDialog,
FileCompactor compactor)
IDataManager gameData)
{
private const ResourceTreeFactory.Flags ResourceTreeFactoryFlags =
ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership;
ResourceTreeFactory.Flags.RedactExternalPaths | ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership;
private readonly HashSet<nint> _unfolded = [];
private readonly Dictionary<nint, NodeVisibility> _filterCache = [];
private readonly Dictionary<FullPath, IWritable?> _writableCache = [];
private TreeCategory _categoryFilter = AllCategories;
private ChangedItemIconFlag _typeFilter = ChangedItemFlagExtensions.AllFlags;
@ -122,7 +115,7 @@ public class ResourceTreeViewer(
ImUtf8.InputText("##note"u8, ref _note, "Export note..."u8);
using var table = ImRaii.Table("##ResourceTree", 4,
using var table = ImRaii.Table("##ResourceTree", actionCapacity > 0 ? 4 : 3,
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
if (!table)
continue;
@ -130,8 +123,9 @@ public class ResourceTreeViewer(
ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthStretch, 0.2f);
ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthStretch, 0.3f);
ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f);
if (actionCapacity > 0)
ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed,
actionCapacity * 3 * ImGuiHelpers.GlobalScale + (actionCapacity + 1) * ImGui.GetFrameHeight());
(actionCapacity - 1) * 3 * ImGuiHelpers.GlobalScale + actionCapacity * ImGui.GetFrameHeight());
ImGui.TableHeadersRow();
DrawNodes(tree.Nodes, 0, unchecked(tree.DrawObjectAddress * 31), 0);
@ -217,7 +211,6 @@ public class ResourceTreeViewer(
finally
{
_filterCache.Clear();
_writableCache.Clear();
_unfolded.Clear();
onRefresh();
}
@ -228,6 +221,7 @@ public class ResourceTreeViewer(
{
var debugMode = config.DebugMode;
var frameHeight = ImGui.GetFrameHeight();
var cellHeight = actionCapacity > 0 ? frameHeight : 0.0f;
foreach (var (resourceNode, index) in resourceNodes.WithIndex())
{
@ -297,7 +291,7 @@ public class ResourceTreeViewer(
0 => "(none)",
1 => resourceNode.GamePath.ToString(),
_ => "(multiple)",
}, false, hasGamePaths ? 0 : ImGuiSelectableFlags.Disabled, new Vector2(ImGui.GetContentRegionAvail().X, frameHeight));
}, false, hasGamePaths ? 0 : ImGuiSelectableFlags.Disabled, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight));
if (hasGamePaths)
{
var allPaths = string.Join('\n', resourceNode.PossibleGamePaths);
@ -318,29 +312,17 @@ public class ResourceTreeViewer(
using (var color = ImRaii.PushColor(ImGuiCol.Text, (hasMod ? ColorId.NewMod : ColorId.DisabledMod).Value()))
{
ImUtf8.Selectable(modName, false, ImGuiSelectableFlags.AllowItemOverlap,
new Vector2(ImGui.GetContentRegionAvail().X, frameHeight));
new Vector2(ImGui.GetContentRegionAvail().X, cellHeight));
}
ImGui.SameLine();
ImGui.SetCursorPosX(textPos);
ImUtf8.Text(resourceNode.ModRelativePath);
}
else if (resourceNode.FullPath.IsRooted)
{
var path = resourceNode.FullPath.FullName;
var lastDirectorySeparator = path.LastIndexOf('\\');
var secondLastDirectorySeparator = lastDirectorySeparator > 0
? path.LastIndexOf('\\', lastDirectorySeparator - 1)
: -1;
if (secondLastDirectorySeparator >= 0)
path = $"…{path.AsSpan(secondLastDirectorySeparator)}";
ImGui.Selectable(path.AsSpan(), false, ImGuiSelectableFlags.AllowItemOverlap,
new Vector2(ImGui.GetContentRegionAvail().X, frameHeight));
}
else
{
ImGui.Selectable(resourceNode.FullPath.ToPath(), false, ImGuiSelectableFlags.AllowItemOverlap,
new Vector2(ImGui.GetContentRegionAvail().X, frameHeight));
new Vector2(ImGui.GetContentRegionAvail().X, cellHeight));
}
if (ImGui.IsItemClicked())
@ -354,17 +336,20 @@ public class ResourceTreeViewer(
else
{
ImUtf8.Selectable(GetPathStatusLabel(resourceNode.FullPathStatus), false, ImGuiSelectableFlags.Disabled,
new Vector2(ImGui.GetContentRegionAvail().X, frameHeight));
new Vector2(ImGui.GetContentRegionAvail().X, cellHeight));
ImGuiUtil.HoverTooltip(
$"{GetPathStatusDescription(resourceNode.FullPathStatus)}{GetAdditionalDataSuffix(resourceNode.AdditionalData)}");
}
mutedColor.Dispose();
if (actionCapacity > 0)
{
ImGui.TableNextColumn();
using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing,
ImGui.GetStyle().ItemSpacing with { X = 3 * ImGuiHelpers.GlobalScale });
DrawActions(resourceNode, new Vector2(frameHeight));
drawActions(resourceNode, new Vector2(frameHeight));
}
if (unfolded)
DrawNodes(resourceNode.Children, level + 1, unchecked(nodePathHash * 31), filterIcon);
@ -417,51 +402,6 @@ public class ResourceTreeViewer(
|| node.FullPath.InternalName.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase)
|| Array.Exists(node.PossibleGamePaths, path => path.Path.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase));
}
void DrawActions(ResourceNode resourceNode, Vector2 buttonSize)
{
if (!_writableCache!.TryGetValue(resourceNode.FullPath, out var writable))
{
var path = resourceNode.FullPath.ToPath();
if (resourceNode.FullPath.IsRooted)
{
writable = new RawFileWritable(path);
}
else
{
var file = gameData.GetFile(path);
writable = file is null ? null : new RawGameFileWritable(file);
}
_writableCache.Add(resourceNode.FullPath, writable);
}
if (ImUtf8.IconButton(FontAwesomeIcon.Save, "Export this file."u8, buttonSize,
resourceNode.FullPath.FullName.Length is 0 || writable is null))
{
var fullPathStr = resourceNode.FullPath.FullName;
var ext = resourceNode.PossibleGamePaths.Length == 1
? Path.GetExtension(resourceNode.GamePath.ToString())
: Path.GetExtension(fullPathStr);
fileDialog.OpenSavePicker($"Export {Path.GetFileName(fullPathStr)} to...", ext, Path.GetFileNameWithoutExtension(fullPathStr), ext,
(success, name) =>
{
if (!success)
return;
try
{
compactor.WriteAllBytes(name, writable!.Write());
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not export {fullPathStr}:\n{e}");
}
}, null, false);
}
drawActions(resourceNode, writable, new Vector2(frameHeight));
}
}
private static ReadOnlySpan<byte> GetPathStatusLabel(ResourceNode.PathStatus status)
@ -525,22 +465,4 @@ public class ResourceTreeViewer(
Visible = 1,
DescendentsOnly = 2,
}
private record RawFileWritable(string Path) : IWritable
{
public bool Valid
=> true;
public byte[] Write()
=> File.ReadAllBytes(Path);
}
private record RawGameFileWritable(FileResource FileResource) : IWritable
{
public bool Valid
=> true;
public byte[] Write()
=> FileResource.Data;
}
}

View file

@ -1,7 +1,5 @@
using Dalamud.Plugin.Services;
using OtterGui.Compression;
using OtterGui.Services;
using Penumbra.GameData.Files;
using Penumbra.Interop.ResourceTree;
using Penumbra.Services;
@ -14,11 +12,8 @@ public class ResourceTreeViewerFactory(
IncognitoService incognito,
CommunicatorService communicator,
PcpService pcpService,
IDataManager gameData,
FileDialogService fileDialog,
FileCompactor compactor) : IService
IDataManager gameData) : IService
{
public ResourceTreeViewer Create(int actionCapacity, Action onRefresh, Action<ResourceNode, IWritable?, Vector2> drawActions)
=> new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions, communicator, pcpService, gameData,
fileDialog, compactor);
public ResourceTreeViewer Create(int actionCapacity, Action onRefresh, Action<ResourceNode, Vector2> drawActions)
=> new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions, communicator, pcpService, gameData);
}

View file

@ -63,28 +63,10 @@ public class PenumbraChangelog : IUiService
Add1_3_6_4(Changelog);
Add1_4_0_0(Changelog);
Add1_5_0_0(Changelog);
Add1_5_1_0(Changelog);
}
#region Changelogs
private static void Add1_5_1_0(Changelog log)
=> log.NextVersion("Version 1.5.1.0")
.RegisterHighlight("Added the option to export a characters current data as a .pcp modpack in the On-Screen tab.")
.RegisterEntry("Other plugins can attach to this functionality and package and interpret their own data.", 1)
.RegisterEntry("When a .pcp modpack is installed, it can create and assign collections for the corresponding character it was created for.", 1)
.RegisterEntry("This basically provides an easier way to manually synchronize other players, but does not contain any automation.", 1)
.RegisterEntry("The settings provide some fine control about what happens when a PCP is installed, as well as buttons to cleanup any PCP-created data.", 1)
.RegisterEntry("Added a warning message when the game's integrity is corrupted to the On-Screen tab.")
.RegisterEntry("Added .kdb files to the On-Screen tab and associated functionality (thanks Ny!).")
.RegisterEntry("Updated the creation of temporary collections to require a passed identity.")
.RegisterEntry("Added the option to change the skin material suffix in models using the stockings shader by adding specific attributes (thanks Ny!).")
.RegisterEntry("Added predefined tag utility to the multi-mod selection.")
.RegisterEntry("Fixed an issue with the automatic collection selection on character login when no mods are assigned.")
.RegisterImportant(
"Fixed issue with new deformer data that makes modded deformers not containing this data work implicitly. Updates are still recommended (1.5.0.5).")
.RegisterEntry("Fixed various issues after patch (1.5.0.1 - 1.5.0.4).");
private static void Add1_5_0_0(Changelog log)
=> log.NextVersion("Version 1.5.0.0")
.RegisterImportant("Updated for game version 7.30 and Dalamud API13, which uses a new GUI backend. Some things may not work as expected. Please let me know any issues you encounter.")

View file

@ -11,7 +11,6 @@ using OtterGui;
using OtterGui.Classes;
using OtterGui.Extensions;
using OtterGui.Raii;
using OtterGui.Text;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors;
@ -223,11 +222,11 @@ public sealed class CollectionPanel(
ImGui.EndGroup();
ImGui.SameLine();
ImGui.BeginGroup();
var width = ImGui.GetContentRegionAvail().X;
using (ImRaii.Disabled(_collections.DefaultNamed == collection))
{
using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f));
var name = _newName ?? collection.Identity.Name;
var identifier = collection.Identity.Identifier;
var width = ImGui.GetContentRegionAvail().X;
var fileName = saveService.FileNames.CollectionFile(collection);
ImGui.SetNextItemWidth(width);
if (ImGui.InputText("##name", ref name, 128))
_newName = name;
@ -242,12 +241,7 @@ public sealed class CollectionPanel(
{
_newName = null;
}
}
if (_collections.DefaultNamed == collection)
ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "The Default collection can not be renamed."u8);
var identifier = collection.Identity.Identifier;
var fileName = saveService.FileNames.CollectionFile(collection);
using (ImRaii.PushFont(UiBuilder.MonoFont))
{
if (ImGui.Button(collection.Identity.Identifier, new Vector2(width, 0)))
@ -381,7 +375,9 @@ public sealed class CollectionPanel(
ImGuiUtil.TextWrapped(type.ToDescription());
switch (type)
{
case CollectionType.Default: ImGui.TextUnformatted("Overruled by any other Assignment."); break;
case CollectionType.Default:
ImGui.TextUnformatted("Overruled by any other Assignment.");
break;
case CollectionType.Yourself:
ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Individual ", ColorId.NewMod.Value()), ("Assignments.", 0));
break;

View file

@ -116,8 +116,7 @@ public sealed class CollectionSelector : ItemSelector<ModCollection>, IDisposabl
public void RestoreCollections()
{
Items.Clear();
Items.Add(_storage.DefaultNamed);
foreach (var c in _storage.OrderBy(c => c.Identity.Name).Where(c => c != _storage.DefaultNamed))
foreach (var c in _storage.OrderBy(c => c.Identity.Name))
Items.Add(c);
SetFilterDirty();
SetCurrent(_active.Current);

View file

@ -9,7 +9,6 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Colors;
using Microsoft.Extensions.DependencyInjection;
using OtterGui;
using OtterGui.Classes;
@ -42,7 +41,6 @@ using Penumbra.GameData.Data;
using Penumbra.Interop.Hooks.PostProcessing;
using Penumbra.Interop.Hooks.ResourceLoading;
using Penumbra.GameData.Files.StainMapStructs;
using Penumbra.Interop;
using Penumbra.String.Classes;
using Penumbra.UI.AdvancedWindow.Materials;
@ -208,7 +206,6 @@ public class DebugTab : Window, ITab, IUiService
_hookOverrides.Draw();
DrawPlayerModelInfo();
_globalVariablesDrawer.Draw();
DrawCloudApi();
DrawDebugTabIpc();
}
@ -1202,42 +1199,6 @@ public class DebugTab : Window, ITab, IUiService
}
private string _cloudTesterPath = string.Empty;
private bool? _cloudTesterReturn;
private Exception? _cloudTesterError;
private void DrawCloudApi()
{
if (!ImUtf8.CollapsingHeader("Cloud API"u8))
return;
using var id = ImRaii.PushId("CloudApiTester"u8);
if (ImUtf8.InputText("Path"u8, ref _cloudTesterPath, flags: ImGuiInputTextFlags.EnterReturnsTrue))
{
try
{
_cloudTesterReturn = CloudApi.IsCloudSynced(_cloudTesterPath);
_cloudTesterError = null;
}
catch (Exception e)
{
_cloudTesterReturn = null;
_cloudTesterError = e;
}
}
if (_cloudTesterReturn.HasValue)
ImUtf8.Text($"Is Cloud Synced? {_cloudTesterReturn}");
if (_cloudTesterError is not null)
{
using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
ImUtf8.Text($"{_cloudTesterError}");
}
}
/// <summary> Draw information about IPC options and availability. </summary>
private void DrawDebugTabIpc()
{

View file

@ -14,7 +14,6 @@ using OtterGui.Text;
using OtterGui.Widgets;
using Penumbra.Api;
using Penumbra.Collections;
using Penumbra.Interop;
using Penumbra.Interop.Hooks.PostProcessing;
using Penumbra.Interop.Services;
using Penumbra.Mods.Manager;
@ -37,7 +36,6 @@ public class SettingsTab : ITab, IUiService
private readonly Penumbra _penumbra;
private readonly FileDialogService _fileDialog;
private readonly ModManager _modManager;
private readonly FileWatcher _fileWatcher;
private readonly ModExportManager _modExportManager;
private readonly ModFileSystemSelector _selector;
private readonly CharacterUtility _characterUtility;
@ -61,13 +59,9 @@ public class SettingsTab : ITab, IUiService
private readonly TagButtons _sharedTags = new();
private string _lastCloudSyncTestedPath = string.Empty;
private bool _lastCloudSyncTestResult = false;
public SettingsTab(IDalamudPluginInterface pluginInterface, Configuration config, FontReloader fontReloader, TutorialService tutorial,
Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector,
CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager,
FileWatcher fileWatcher, HttpApi httpApi,
CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi,
DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig,
IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService,
MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService,
@ -84,7 +78,6 @@ public class SettingsTab : ITab, IUiService
_characterUtility = characterUtility;
_residentResources = residentResources;
_modExportManager = modExportManager;
_fileWatcher = fileWatcher;
_httpApi = httpApi;
_dalamudSubstitutionProvider = dalamudSubstitutionProvider;
_compactor = compactor;
@ -215,15 +208,6 @@ public class SettingsTab : ITab, IUiService
if (IsSubPathOf(gameDir, newName))
return ("Path is not allowed to be inside your game folder.", false);
if (_lastCloudSyncTestedPath != newName)
{
_lastCloudSyncTestResult = CloudApi.IsCloudSynced(newName);
_lastCloudSyncTestedPath = newName;
}
if (_lastCloudSyncTestResult)
return ("Path is not allowed to be cloud-synced.", false);
return selected
? ($"Press Enter or Click Here to Save (Current Directory: {old})", true)
: ($"Click Here to Save (Current Directory: {old})", true);
@ -650,13 +634,6 @@ public class SettingsTab : ITab, IUiService
DrawDefaultModImportFolder();
DrawPcpFolder();
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.",
_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.",
_config.EnableAutomaticModImport, v => _config.EnableAutomaticModImport = v);
DrawFileWatcherPath();
}
@ -736,46 +713,6 @@ public class SettingsTab : ITab, IUiService
+ "Keep this empty to use the root directory.");
}
private string? _tempWatchDirectory;
/// <summary> Draw input for the Automatic Mod import path. </summary>
private void DrawFileWatcherPath()
{
var tmp = _tempWatchDirectory ?? _config.WatchDirectory;
var spacing = new Vector2(UiHelpers.ScaleX3);
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing);
ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3);
if (ImGui.InputText("##fileWatchPath", ref tmp, 256))
_tempWatchDirectory = tmp;
if (ImGui.IsItemDeactivated() && _tempWatchDirectory is not null)
{
if (ImGui.IsItemDeactivatedAfterEdit())
_fileWatcher.UpdateDirectory(_tempWatchDirectory);
_tempWatchDirectory = null;
}
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.Folder.ToIconString()}##fileWatch", UiHelpers.IconButtonSize,
"Select a directory via dialog.", false, true))
{
var startDir = _config.WatchDirectory.Length > 0 && Directory.Exists(_config.WatchDirectory)
? _config.WatchDirectory
: Directory.Exists(_config.ModDirectory)
? _config.ModDirectory
: null;
_fileDialog.OpenFolderPicker("Choose Automatic Import Directory", (b, s) =>
{
if (b)
_fileWatcher.UpdateDirectory(s);
}, startDir, false);
}
style.Pop();
ImGuiUtil.LabeledHelpMarker("Automatic Import Director",
"Choose the Directory the File Watcher listens to.");
}
/// <summary> Draw input for the default name to input as author into newly generated mods. </summary>
private void DrawDefaultModAuthor()
{

View file

@ -1,12 +1,12 @@
[
{
"Author": "Ottermandias, Nylfae, Adam, Wintermute",
"Author": "Ottermandias, Adam, Wintermute",
"Name": "Penumbra",
"Punchline": "Runtime mod loader and manager.",
"Description": "Runtime mod loader and manager.",
"InternalName": "Penumbra",
"AssemblyVersion": "1.5.1.8",
"TestingAssemblyVersion": "1.5.1.8",
"AssemblyVersion": "1.5.0.6",
"TestingAssemblyVersion": "1.5.0.9",
"RepoUrl": "https://github.com/xivdev/Penumbra",
"ApplicableVersion": "any",
"DalamudApiLevel": 13,
@ -18,9 +18,9 @@
"LoadPriority": 69420,
"LoadRequiredState": 2,
"LoadSync": true,
"DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.8/Penumbra.zip",
"DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.8/Penumbra.zip",
"DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.8/Penumbra.zip",
"DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip",
"DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.0.9/Penumbra.zip",
"DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip",
"IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png"
}
]