mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Add initial PCP.
This commit is contained in:
parent
b7f326e29c
commit
e3b7f72893
11 changed files with 338 additions and 27 deletions
|
|
@ -1 +1 @@
|
||||||
Subproject commit c27a06004138f2ec80ccdb494bb6ddf6d39d2165
|
Subproject commit 2e26d9119249e67f03f415f8ebe1dcb7c28d5cf2
|
||||||
|
|
@ -3,6 +3,7 @@ using Penumbra.Api;
|
||||||
using Penumbra.Api.Api;
|
using Penumbra.Api.Api;
|
||||||
using Penumbra.Mods;
|
using Penumbra.Mods;
|
||||||
using Penumbra.Mods.Manager;
|
using Penumbra.Mods.Manager;
|
||||||
|
using Penumbra.Services;
|
||||||
|
|
||||||
namespace Penumbra.Communication;
|
namespace Penumbra.Communication;
|
||||||
|
|
||||||
|
|
@ -20,11 +21,14 @@ public sealed class ModPathChanged()
|
||||||
{
|
{
|
||||||
public enum Priority
|
public enum Priority
|
||||||
{
|
{
|
||||||
|
/// <seealso cref="PcpService.OnModPathChange"/>
|
||||||
|
PcpService = int.MinValue,
|
||||||
|
|
||||||
/// <seealso cref="ModsApi.OnModPathChange"/>
|
/// <seealso cref="ModsApi.OnModPathChange"/>
|
||||||
ApiMods = int.MinValue,
|
ApiMods = int.MinValue + 1,
|
||||||
|
|
||||||
/// <seealso cref="ModSettingsApi.OnModPathChange"/>
|
/// <seealso cref="ModSettingsApi.OnModPathChange"/>
|
||||||
ApiModSettings = int.MinValue,
|
ApiModSettings = int.MinValue + 1,
|
||||||
|
|
||||||
/// <seealso cref="EphemeralConfig.OnModPathChanged"/>
|
/// <seealso cref="EphemeralConfig.OnModPathChanged"/>
|
||||||
EphemeralConfig = -500,
|
EphemeralConfig = -500,
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService
|
||||||
public bool OpenFoldersByDefault { get; set; } = false;
|
public bool OpenFoldersByDefault { get; set; } = false;
|
||||||
public int SingleGroupRadioMax { get; set; } = 2;
|
public int SingleGroupRadioMax { get; set; } = 2;
|
||||||
public string DefaultImportFolder { get; set; } = string.Empty;
|
public string DefaultImportFolder { get; set; } = string.Empty;
|
||||||
|
public string PcpFolderName { get; set; } = "PCP";
|
||||||
public string QuickMoveFolder1 { get; set; } = string.Empty;
|
public string QuickMoveFolder1 { get; set; } = string.Empty;
|
||||||
public string QuickMoveFolder2 { get; set; } = string.Empty;
|
public string QuickMoveFolder2 { get; set; } = string.Empty;
|
||||||
public string QuickMoveFolder3 { get; set; } = string.Empty;
|
public string QuickMoveFolder3 { get; set; } = string.Empty;
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ public partial class TexToolsImporter : IDisposable
|
||||||
// Puts out warnings if extension does not correspond to data.
|
// Puts out warnings if extension does not correspond to data.
|
||||||
private DirectoryInfo VerifyVersionAndImport(FileInfo modPackFile)
|
private DirectoryInfo VerifyVersionAndImport(FileInfo modPackFile)
|
||||||
{
|
{
|
||||||
if (modPackFile.Extension.ToLowerInvariant() is ".pmp" or ".zip" or ".7z" or ".rar")
|
if (modPackFile.Extension.ToLowerInvariant() is ".pmp" or ".pcp" or ".zip" or ".7z" or ".rar")
|
||||||
return HandleRegularArchive(modPackFile);
|
return HandleRegularArchive(modPackFile);
|
||||||
|
|
||||||
using var zfs = modPackFile.OpenRead();
|
using var zfs = modPackFile.OpenRead();
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic
|
||||||
|
|
||||||
/// <summary> Create the file containing the meta information about a mod from scratch. </summary>
|
/// <summary> Create the file containing the meta information about a mod from scratch. </summary>
|
||||||
public void CreateMeta(DirectoryInfo directory, string? name, string? author, string? description, string? version,
|
public void CreateMeta(DirectoryInfo directory, string? name, string? author, string? description, string? version,
|
||||||
string? website)
|
string? website, params string[] tags)
|
||||||
{
|
{
|
||||||
var mod = new Mod(directory);
|
var mod = new Mod(directory);
|
||||||
mod.Name = name.IsNullOrEmpty() ? mod.Name : new LowerString(name);
|
mod.Name = name.IsNullOrEmpty() ? mod.Name : new LowerString(name);
|
||||||
|
|
@ -44,6 +44,7 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic
|
||||||
mod.Description = description ?? mod.Description;
|
mod.Description = description ?? mod.Description;
|
||||||
mod.Version = version ?? mod.Version;
|
mod.Version = version ?? mod.Version;
|
||||||
mod.Website = website ?? mod.Website;
|
mod.Website = website ?? mod.Website;
|
||||||
|
mod.ModTags = tags;
|
||||||
saveService.ImmediateSaveSync(new ModMeta(mod));
|
saveService.ImmediateSaveSync(new ModMeta(mod));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,12 +32,12 @@ public partial class ModCreator(
|
||||||
public readonly Configuration Config = config;
|
public readonly Configuration Config = config;
|
||||||
|
|
||||||
/// <summary> Creates directory and files necessary for a new mod without adding it to the manager. </summary>
|
/// <summary> Creates directory and files necessary for a new mod without adding it to the manager. </summary>
|
||||||
public DirectoryInfo? CreateEmptyMod(DirectoryInfo basePath, string newName, string description = "", string? author = null)
|
public DirectoryInfo? CreateEmptyMod(DirectoryInfo basePath, string newName, string description = "", string? author = null, params string[] tags)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var newDir = CreateModFolder(basePath, newName, Config.ReplaceNonAsciiOnImport, true);
|
var newDir = CreateModFolder(basePath, newName, Config.ReplaceNonAsciiOnImport, true);
|
||||||
dataEditor.CreateMeta(newDir, newName, author ?? Config.DefaultModAuthor, description, "1.0", string.Empty);
|
dataEditor.CreateMeta(newDir, newName, author ?? Config.DefaultModAuthor, description, "1.0", string.Empty, tags);
|
||||||
CreateDefaultFiles(newDir);
|
CreateDefaultFiles(newDir);
|
||||||
return newDir;
|
return newDir;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
259
Penumbra/Services/PcpService.cs
Normal file
259
Penumbra/Services/PcpService.cs
Normal file
|
|
@ -0,0 +1,259 @@
|
||||||
|
using System.Buffers.Text;
|
||||||
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using OtterGui.Classes;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.Collections;
|
||||||
|
using Penumbra.Collections.Manager;
|
||||||
|
using Penumbra.Communication;
|
||||||
|
using Penumbra.GameData.Actors;
|
||||||
|
using Penumbra.GameData.Interop;
|
||||||
|
using Penumbra.GameData.Structs;
|
||||||
|
using Penumbra.Interop.PathResolving;
|
||||||
|
using Penumbra.Interop.ResourceTree;
|
||||||
|
using Penumbra.Meta.Manipulations;
|
||||||
|
using Penumbra.Mods;
|
||||||
|
using Penumbra.Mods.Groups;
|
||||||
|
using Penumbra.Mods.Manager;
|
||||||
|
using Penumbra.Mods.SubMods;
|
||||||
|
using Penumbra.String.Classes;
|
||||||
|
|
||||||
|
namespace Penumbra.Services;
|
||||||
|
|
||||||
|
public class PcpService : IApiService, IDisposable
|
||||||
|
{
|
||||||
|
public const string Extension = ".pcp";
|
||||||
|
|
||||||
|
private readonly Configuration _config;
|
||||||
|
private readonly SaveService _files;
|
||||||
|
private readonly ResourceTreeFactory _treeFactory;
|
||||||
|
private readonly ObjectManager _objectManager;
|
||||||
|
private readonly ActorManager _actors;
|
||||||
|
private readonly FrameworkManager _framework;
|
||||||
|
private readonly CollectionResolver _collectionResolver;
|
||||||
|
private readonly CollectionManager _collections;
|
||||||
|
private readonly ModCreator _modCreator;
|
||||||
|
private readonly ModExportManager _modExport;
|
||||||
|
private readonly CommunicatorService _communicator;
|
||||||
|
private readonly SHA1 _sha1 = SHA1.Create();
|
||||||
|
private readonly ModFileSystem _fileSystem;
|
||||||
|
|
||||||
|
public PcpService(Configuration config,
|
||||||
|
SaveService files,
|
||||||
|
ResourceTreeFactory treeFactory,
|
||||||
|
ObjectManager objectManager,
|
||||||
|
ActorManager actors,
|
||||||
|
FrameworkManager framework,
|
||||||
|
CollectionManager collections,
|
||||||
|
CollectionResolver collectionResolver,
|
||||||
|
ModCreator modCreator,
|
||||||
|
ModExportManager modExport,
|
||||||
|
CommunicatorService communicator,
|
||||||
|
ModFileSystem fileSystem)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
_files = files;
|
||||||
|
_treeFactory = treeFactory;
|
||||||
|
_objectManager = objectManager;
|
||||||
|
_actors = actors;
|
||||||
|
_framework = framework;
|
||||||
|
_collectionResolver = collectionResolver;
|
||||||
|
_collections = collections;
|
||||||
|
_modCreator = modCreator;
|
||||||
|
_modExport = modExport;
|
||||||
|
_communicator = communicator;
|
||||||
|
_fileSystem = fileSystem;
|
||||||
|
|
||||||
|
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.PcpService);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, DirectoryInfo? newDirectory)
|
||||||
|
{
|
||||||
|
if (type is not ModPathChangeType.Added || newDirectory is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var file = Path.Combine(newDirectory.FullName, "collection.json");
|
||||||
|
if (!File.Exists(file))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var text = File.ReadAllText(file);
|
||||||
|
var jObj = JObject.Parse(text);
|
||||||
|
var identifier = _actors.FromJson(jObj["Actor"] as JObject);
|
||||||
|
if (!identifier.IsValid)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (jObj["Collection"]?.ToObject<string>() is not { } collectionName)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var name = $"PCP/{collectionName}";
|
||||||
|
if (!_collections.Storage.AddCollection(name, null))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var collection = _collections.Storage[^1];
|
||||||
|
_collections.Editor.SetModState(collection, mod, true);
|
||||||
|
|
||||||
|
var identifierGroup = _collections.Active.Individuals.GetGroup(identifier);
|
||||||
|
_collections.Active.SetCollection(collection, CollectionType.Individual, identifierGroup);
|
||||||
|
if (_fileSystem.TryGetValue(mod, out var leaf))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var folder = _fileSystem.FindOrCreateAllFolders(_config.PcpFolderName);
|
||||||
|
_fileSystem.Move(leaf, folder);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Error($"Error reading the collection.json file from {mod.Identifier}:\n{ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
=> _communicator.ModPathChanged.Unsubscribe(OnModPathChange);
|
||||||
|
|
||||||
|
public async Task<(bool, string)> CreatePcp(ObjectIndex objectIndex, string note = "", CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (identifier, tree, collection) = await _framework.Framework.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
var (actor, identifier) = CheckActor(objectIndex);
|
||||||
|
cancel.ThrowIfCancellationRequested();
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
var collection = _collectionResolver.IdentifyCollection((GameObject*)actor.Address, true);
|
||||||
|
if (!collection.Valid || !collection.ModCollection.HasCache)
|
||||||
|
throw new Exception($"Actor {identifier} has no mods applying, nothing to do.");
|
||||||
|
|
||||||
|
cancel.ThrowIfCancellationRequested();
|
||||||
|
if (_treeFactory.FromCharacter(actor, 0) is not { } tree)
|
||||||
|
throw new Exception($"Unable to fetch modded resources for {identifier}.");
|
||||||
|
|
||||||
|
return (identifier.CreatePermanent(), tree, collection);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cancel.ThrowIfCancellationRequested();
|
||||||
|
var time = DateTime.Now;
|
||||||
|
var modDirectory = CreateMod(identifier, note, time);
|
||||||
|
await CreateDefaultMod(modDirectory, collection.ModCollection, tree, cancel);
|
||||||
|
await CreateCollectionInfo(modDirectory, identifier, note, time, cancel);
|
||||||
|
var file = ZipUp(modDirectory);
|
||||||
|
return (true, file);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return (false, ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ZipUp(DirectoryInfo directory)
|
||||||
|
{
|
||||||
|
var fileName = directory.FullName + Extension;
|
||||||
|
ZipFile.CreateFromDirectory(directory.FullName, fileName, CompressionLevel.Optimal, false);
|
||||||
|
directory.Delete(true);
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task CreateCollectionInfo(DirectoryInfo directory, ActorIdentifier actor, string note, DateTime time,
|
||||||
|
CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
var jObj = new JObject
|
||||||
|
{
|
||||||
|
["Version"] = 1,
|
||||||
|
["Actor"] = actor.ToJson(),
|
||||||
|
["Collection"] = note.Length > 0 ? $"{actor.ToName()}: {note}" : actor.ToName(),
|
||||||
|
["Time"] = time,
|
||||||
|
["Note"] = note,
|
||||||
|
};
|
||||||
|
if (note.Length > 0)
|
||||||
|
cancel.ThrowIfCancellationRequested();
|
||||||
|
var filePath = Path.Combine(directory.FullName, "collection.json");
|
||||||
|
await using var file = File.Open(filePath, File.Exists(filePath) ? FileMode.Truncate : FileMode.CreateNew);
|
||||||
|
await using var stream = new StreamWriter(file);
|
||||||
|
await using var json = new JsonTextWriter(stream);
|
||||||
|
json.Formatting = Formatting.Indented;
|
||||||
|
await jObj.WriteToAsync(json, cancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DirectoryInfo CreateMod(ActorIdentifier actor, string note, DateTime time)
|
||||||
|
{
|
||||||
|
var directory = _modExport.ExportDirectory;
|
||||||
|
directory.Create();
|
||||||
|
var actorName = actor.ToName();
|
||||||
|
var authorName = _actors.GetCurrentPlayer().ToName();
|
||||||
|
var suffix = note.Length > 0
|
||||||
|
? note
|
||||||
|
: time.ToString("yyyy-MM-ddTHH\\:mm", CultureInfo.InvariantCulture);
|
||||||
|
var modName = $"{actorName} - {suffix}";
|
||||||
|
var description = $"On-Screen Data for {actorName} as snapshotted on {time}.";
|
||||||
|
return _modCreator.CreateEmptyMod(directory, modName, description, authorName, "PCP")
|
||||||
|
?? throw new Exception($"Unable to create mod {modName} in {directory.FullName}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateDefaultMod(DirectoryInfo modDirectory, ModCollection collection, ResourceTree tree,
|
||||||
|
CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
var subDirectory = modDirectory.CreateSubdirectory("files");
|
||||||
|
var subMod = new DefaultSubMod(null!);
|
||||||
|
foreach (var node in tree.FlatNodes)
|
||||||
|
{
|
||||||
|
cancel.ThrowIfCancellationRequested();
|
||||||
|
var gamePath = node.GamePath;
|
||||||
|
var fullPath = node.FullPath;
|
||||||
|
if (fullPath.IsRooted)
|
||||||
|
{
|
||||||
|
var hash = await _sha1.ComputeHashAsync(File.OpenRead(fullPath.FullName), cancel).ConfigureAwait(false);
|
||||||
|
cancel.ThrowIfCancellationRequested();
|
||||||
|
var name = Convert.ToHexString(hash) + fullPath.Extension;
|
||||||
|
var newFile = Path.Combine(subDirectory.FullName, name);
|
||||||
|
if (!File.Exists(newFile))
|
||||||
|
File.Copy(fullPath.FullName, newFile);
|
||||||
|
subMod.Files.TryAdd(gamePath, new FullPath(newFile));
|
||||||
|
}
|
||||||
|
else if (gamePath.Path != fullPath.InternalName)
|
||||||
|
{
|
||||||
|
subMod.FileSwaps.TryAdd(gamePath, fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel.ThrowIfCancellationRequested();
|
||||||
|
subMod.Manipulations = new MetaDictionary(collection.MetaCache);
|
||||||
|
|
||||||
|
var saveGroup = new ModSaveGroup(modDirectory, subMod, _config.ReplaceNonAsciiOnImport);
|
||||||
|
var filePath = _files.FileNames.OptionGroupFile(modDirectory.FullName, -1, string.Empty, _config.ReplaceNonAsciiOnImport);
|
||||||
|
cancel.ThrowIfCancellationRequested();
|
||||||
|
await using var fileStream = File.Open(filePath, File.Exists(filePath) ? FileMode.Truncate : FileMode.CreateNew);
|
||||||
|
await using var writer = new StreamWriter(fileStream);
|
||||||
|
saveGroup.Save(writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private (ICharacter Actor, ActorIdentifier Identifier) CheckActor(ObjectIndex objectIndex)
|
||||||
|
{
|
||||||
|
var actor = _objectManager[objectIndex];
|
||||||
|
if (!actor.Valid)
|
||||||
|
throw new Exception($"No Actor at index {objectIndex} found.");
|
||||||
|
|
||||||
|
if (!actor.Identifier(_actors, out var identifier))
|
||||||
|
throw new Exception($"Could not create valid identifier for actor at index {objectIndex}.");
|
||||||
|
|
||||||
|
if (!actor.IsCharacter)
|
||||||
|
throw new Exception($"Actor {identifier} at index {objectIndex} is not a valid character.");
|
||||||
|
|
||||||
|
if (!actor.Model.Valid)
|
||||||
|
throw new Exception($"Actor {identifier} at index {objectIndex} has no model.");
|
||||||
|
|
||||||
|
if (_objectManager.Objects.CreateObjectReference(actor.Address) is not ICharacter character)
|
||||||
|
throw new Exception($"Actor {identifier} at index {objectIndex} could not be converted to ICharacter");
|
||||||
|
|
||||||
|
return (character, identifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,19 @@
|
||||||
using Dalamud.Interface;
|
|
||||||
using Dalamud.Interface.Utility;
|
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using OtterGui.Raii;
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Interface.ImGuiNotification;
|
||||||
|
using Dalamud.Interface.Utility;
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
|
using OtterGui.Classes;
|
||||||
|
using OtterGui.Extensions;
|
||||||
|
using OtterGui.Raii;
|
||||||
using OtterGui.Text;
|
using OtterGui.Text;
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
|
using Penumbra.GameData.Structs;
|
||||||
using Penumbra.Interop.ResourceTree;
|
using Penumbra.Interop.ResourceTree;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
using Penumbra.UI.Classes;
|
|
||||||
using Penumbra.String;
|
using Penumbra.String;
|
||||||
using OtterGui.Extensions;
|
using Penumbra.UI.Classes;
|
||||||
|
using static System.Net.Mime.MediaTypeNames;
|
||||||
|
|
||||||
namespace Penumbra.UI.AdvancedWindow;
|
namespace Penumbra.UI.AdvancedWindow;
|
||||||
|
|
||||||
|
|
@ -21,12 +25,13 @@ public class ResourceTreeViewer(
|
||||||
int actionCapacity,
|
int actionCapacity,
|
||||||
Action onRefresh,
|
Action onRefresh,
|
||||||
Action<ResourceNode, Vector2> drawActions,
|
Action<ResourceNode, Vector2> drawActions,
|
||||||
CommunicatorService communicator)
|
CommunicatorService communicator,
|
||||||
|
PcpService pcpService)
|
||||||
{
|
{
|
||||||
private const ResourceTreeFactory.Flags ResourceTreeFactoryFlags =
|
private const ResourceTreeFactory.Flags ResourceTreeFactoryFlags =
|
||||||
ResourceTreeFactory.Flags.RedactExternalPaths | ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership;
|
ResourceTreeFactory.Flags.RedactExternalPaths | ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership;
|
||||||
|
|
||||||
private readonly HashSet<nint> _unfolded = [];
|
private readonly HashSet<nint> _unfolded = [];
|
||||||
|
|
||||||
private readonly Dictionary<nint, NodeVisibility> _filterCache = [];
|
private readonly Dictionary<nint, NodeVisibility> _filterCache = [];
|
||||||
|
|
||||||
|
|
@ -34,6 +39,7 @@ public class ResourceTreeViewer(
|
||||||
private ChangedItemIconFlag _typeFilter = ChangedItemFlagExtensions.AllFlags;
|
private ChangedItemIconFlag _typeFilter = ChangedItemFlagExtensions.AllFlags;
|
||||||
private string _nameFilter = string.Empty;
|
private string _nameFilter = string.Empty;
|
||||||
private string _nodeFilter = string.Empty;
|
private string _nodeFilter = string.Empty;
|
||||||
|
private string _note = string.Empty;
|
||||||
|
|
||||||
private Task<ResourceTree[]>? _task;
|
private Task<ResourceTree[]>? _task;
|
||||||
|
|
||||||
|
|
@ -83,7 +89,28 @@ public class ResourceTreeViewer(
|
||||||
|
|
||||||
using var id = ImRaii.PushId(index);
|
using var id = ImRaii.PushId(index);
|
||||||
|
|
||||||
ImGui.TextUnformatted($"Collection: {(incognito.IncognitoMode ? tree.AnonymizedCollectionName : tree.CollectionName)}");
|
ImUtf8.TextFrameAligned($"Collection: {(incognito.IncognitoMode ? tree.AnonymizedCollectionName : tree.CollectionName)}");
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImUtf8.ButtonEx("Export Character Pack"u8,
|
||||||
|
"Note that this recomputes the current data of the actor if it still exists, and does not use the cached data."u8))
|
||||||
|
{
|
||||||
|
pcpService.CreatePcp((ObjectIndex)tree.GameObjectIndex, _note).ContinueWith(t =>
|
||||||
|
{
|
||||||
|
|
||||||
|
var (success, text) = t.Result;
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
Penumbra.Messager.NotificationMessage($"Created {text}.", NotificationType.Success, false);
|
||||||
|
else
|
||||||
|
Penumbra.Messager.NotificationMessage(text, NotificationType.Error, false);
|
||||||
|
});
|
||||||
|
_note = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImUtf8.SameLineInner();
|
||||||
|
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
|
||||||
|
ImUtf8.InputText("##note"u8, ref _note, "Export note..."u8);
|
||||||
|
|
||||||
|
|
||||||
using var table = ImRaii.Table("##ResourceTree", actionCapacity > 0 ? 4 : 3,
|
using var table = ImRaii.Table("##ResourceTree", actionCapacity > 0 ? 4 : 3,
|
||||||
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
|
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
|
||||||
|
|
@ -263,7 +290,8 @@ public class ResourceTreeViewer(
|
||||||
using var group = ImUtf8.Group();
|
using var group = ImUtf8.Group();
|
||||||
using (var color = ImRaii.PushColor(ImGuiCol.Text, (hasMod ? ColorId.NewMod : ColorId.DisabledMod).Value()))
|
using (var color = ImRaii.PushColor(ImGuiCol.Text, (hasMod ? ColorId.NewMod : ColorId.DisabledMod).Value()))
|
||||||
{
|
{
|
||||||
ImUtf8.Selectable(modName, false, ImGuiSelectableFlags.AllowItemOverlap, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight));
|
ImUtf8.Selectable(modName, false, ImGuiSelectableFlags.AllowItemOverlap,
|
||||||
|
new Vector2(ImGui.GetContentRegionAvail().X, cellHeight));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
|
|
@ -272,7 +300,8 @@ public class ResourceTreeViewer(
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ImGui.Selectable(resourceNode.FullPath.ToPath(), false, ImGuiSelectableFlags.AllowItemOverlap, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight));
|
ImGui.Selectable(resourceNode.FullPath.ToPath(), false, ImGuiSelectableFlags.AllowItemOverlap,
|
||||||
|
new Vector2(ImGui.GetContentRegionAvail().X, cellHeight));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ImGui.IsItemClicked())
|
if (ImGui.IsItemClicked())
|
||||||
|
|
@ -365,9 +394,10 @@ public class ResourceTreeViewer(
|
||||||
private static string GetPathStatusDescription(ResourceNode.PathStatus status)
|
private static string GetPathStatusDescription(ResourceNode.PathStatus status)
|
||||||
=> status switch
|
=> status switch
|
||||||
{
|
{
|
||||||
ResourceNode.PathStatus.External => "The actual path to this file is unavailable, because it is managed by external tools.",
|
ResourceNode.PathStatus.External => "The actual path to this file is unavailable, because it is managed by external tools.",
|
||||||
ResourceNode.PathStatus.NonExistent => "The actual path to this file is unavailable, because it seems to have been moved or deleted since it was loaded.",
|
ResourceNode.PathStatus.NonExistent =>
|
||||||
_ => "The actual path to this file is unavailable.",
|
"The actual path to this file is unavailable, because it seems to have been moved or deleted since it was loaded.",
|
||||||
|
_ => "The actual path to this file is unavailable.",
|
||||||
};
|
};
|
||||||
|
|
||||||
[Flags]
|
[Flags]
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,9 @@ public class ResourceTreeViewerFactory(
|
||||||
ResourceTreeFactory treeFactory,
|
ResourceTreeFactory treeFactory,
|
||||||
ChangedItemDrawer changedItemDrawer,
|
ChangedItemDrawer changedItemDrawer,
|
||||||
IncognitoService incognito,
|
IncognitoService incognito,
|
||||||
CommunicatorService communicator) : IService
|
CommunicatorService communicator,
|
||||||
|
PcpService pcpService) : IService
|
||||||
{
|
{
|
||||||
public ResourceTreeViewer Create(int actionCapacity, Action onRefresh, Action<ResourceNode, Vector2> drawActions)
|
public ResourceTreeViewer Create(int actionCapacity, Action onRefresh, Action<ResourceNode, Vector2> drawActions)
|
||||||
=> new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions, communicator);
|
=> new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions, communicator, pcpService);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
||||||
".ttmp",
|
".ttmp",
|
||||||
".ttmp2",
|
".ttmp2",
|
||||||
".pmp",
|
".pmp",
|
||||||
|
".pcp",
|
||||||
".zip",
|
".zip",
|
||||||
".rar",
|
".rar",
|
||||||
".7z",
|
".7z",
|
||||||
|
|
@ -380,7 +381,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
_fileDialog.OpenFilePicker("Import Mod Pack",
|
_fileDialog.OpenFilePicker("Import Mod Pack",
|
||||||
"Mod Packs{.ttmp,.ttmp2,.pmp},TexTools Mod Packs{.ttmp,.ttmp2},Penumbra Mod Packs{.pmp},Archives{.zip,.7z,.rar}", (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)
|
if (!s)
|
||||||
return;
|
return;
|
||||||
|
|
@ -445,7 +446,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
||||||
ImUtf8.Text("Mod Management"u8);
|
ImUtf8.Text("Mod Management"u8);
|
||||||
ImUtf8.BulletText("You can create empty mods or import mods with the buttons in this row."u8);
|
ImUtf8.BulletText("You can create empty mods or import mods with the buttons in this row."u8);
|
||||||
using var indent = ImRaii.PushIndent();
|
using var indent = ImRaii.PushIndent();
|
||||||
ImUtf8.BulletText("Supported formats for import are: .ttmp, .ttmp2, .pmp."u8);
|
ImUtf8.BulletText("Supported formats for import are: .ttmp, .ttmp2, .pmp, .pcp."u8);
|
||||||
ImUtf8.BulletText(
|
ImUtf8.BulletText(
|
||||||
"You can also support .zip, .7z or .rar archives, but only if they already contain Penumbra-styled mods with appropriate metadata."u8);
|
"You can also support .zip, .7z or .rar archives, but only if they already contain Penumbra-styled mods with appropriate metadata."u8);
|
||||||
indent.Pop(1);
|
indent.Pop(1);
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ using OtterGui.Raii;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using OtterGui.Text;
|
using OtterGui.Text;
|
||||||
using OtterGui.Widgets;
|
using OtterGui.Widgets;
|
||||||
using OtterGuiInternal.Enums;
|
|
||||||
using Penumbra.Api;
|
using Penumbra.Api;
|
||||||
using Penumbra.Collections;
|
using Penumbra.Collections;
|
||||||
using Penumbra.Interop.Hooks.PostProcessing;
|
using Penumbra.Interop.Hooks.PostProcessing;
|
||||||
|
|
@ -21,7 +20,6 @@ using Penumbra.Mods.Manager;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
using Penumbra.UI.Classes;
|
using Penumbra.UI.Classes;
|
||||||
using Penumbra.UI.ModsTab;
|
using Penumbra.UI.ModsTab;
|
||||||
using ImGuiId = OtterGuiInternal.Enums.ImGuiId;
|
|
||||||
|
|
||||||
namespace Penumbra.UI.Tabs;
|
namespace Penumbra.UI.Tabs;
|
||||||
|
|
||||||
|
|
@ -603,6 +601,7 @@ public class SettingsTab : ITab, IUiService
|
||||||
DrawDefaultModImportPath();
|
DrawDefaultModImportPath();
|
||||||
DrawDefaultModAuthor();
|
DrawDefaultModAuthor();
|
||||||
DrawDefaultModImportFolder();
|
DrawDefaultModImportFolder();
|
||||||
|
DrawPcpFolder();
|
||||||
DrawDefaultModExportPath();
|
DrawDefaultModExportPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -712,6 +711,21 @@ public class SettingsTab : ITab, IUiService
|
||||||
"Set the default Penumbra mod folder to place newly imported mods into.\nLeave blank to import into Root.");
|
"Set the default Penumbra mod folder to place newly imported mods into.\nLeave blank to import into Root.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary> Draw input for the default folder to sort put newly imported mods into. </summary>
|
||||||
|
private void DrawPcpFolder()
|
||||||
|
{
|
||||||
|
var tmp = _config.PcpFolderName;
|
||||||
|
ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X);
|
||||||
|
if (ImUtf8.InputText("##pcpFolder"u8, ref tmp))
|
||||||
|
_config.PcpFolderName = tmp;
|
||||||
|
|
||||||
|
if (ImGui.IsItemDeactivatedAfterEdit())
|
||||||
|
_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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary> Draw all settings pertaining to advanced editing of mods. </summary>
|
/// <summary> Draw all settings pertaining to advanced editing of mods. </summary>
|
||||||
private void DrawModEditorSettings()
|
private void DrawModEditorSettings()
|
||||||
|
|
@ -1055,7 +1069,7 @@ public class SettingsTab : ITab, IUiService
|
||||||
if (ImGui.Button("Show Changelogs", new Vector2(width, 0)))
|
if (ImGui.Button("Show Changelogs", new Vector2(width, 0)))
|
||||||
_penumbra.ForceChangelogOpen();
|
_penumbra.ForceChangelogOpen();
|
||||||
|
|
||||||
ImGui.SetCursorPos(new Vector2(xPos, 5 * ImGui.GetFrameHeightWithSpacing()));
|
ImGui.SetCursorPos(new Vector2(xPos, 5 * ImGui.GetFrameHeightWithSpacing()));
|
||||||
CustomGui.DrawKofiPatreonButton(Penumbra.Messager, new Vector2(width, 0));
|
CustomGui.DrawKofiPatreonButton(Penumbra.Messager, new Vector2(width, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue