mirror of
https://github.com/xivdev/Penumbra.git
synced 2026-01-03 06:13:45 +01:00
Merge remote-tracking branch 'upstream/master' into feature/dt-model-io
This commit is contained in:
commit
c1f58b16ba
66 changed files with 1196 additions and 515 deletions
2
OtterGui
2
OtterGui
|
|
@ -1 +1 @@
|
|||
Subproject commit 07a009134bf5eb7da9a54ba40e82c88fc613544a
|
||||
Subproject commit 3e6b085749741f35dd6732c33d0720c6a51ebb97
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 552246e595ffab2aaba2c75f578d564f8938fc9a
|
||||
Subproject commit 97e9f427406f82a59ddef764b44ecea654a51623
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit b7fdfe9d19f7e3229834480db446478b0bf6acee
|
||||
Subproject commit 66bc00dc8517204e58c6515af5aec0ba6d196716
|
||||
|
|
@ -10,6 +10,7 @@ public class EditingApi(TextureManager textureManager) : IPenumbraApiEditing, IA
|
|||
=> textureType switch
|
||||
{
|
||||
TextureType.Png => textureManager.SavePng(inputFile, outputFile),
|
||||
TextureType.Targa => textureManager.SaveTga(inputFile, outputFile),
|
||||
TextureType.AsIsTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, true, inputFile, outputFile),
|
||||
TextureType.AsIsDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, false, inputFile, outputFile),
|
||||
TextureType.RgbaTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, true, inputFile, outputFile),
|
||||
|
|
@ -26,6 +27,7 @@ public class EditingApi(TextureManager textureManager) : IPenumbraApiEditing, IA
|
|||
=> textureType switch
|
||||
{
|
||||
TextureType.Png => textureManager.SavePng(new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
|
||||
TextureType.Targa => textureManager.SaveTga(new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
|
||||
TextureType.AsIsTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
|
||||
TextureType.AsIsDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
|
||||
TextureType.RgbaTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
|
||||
|
|
|
|||
|
|
@ -1,16 +1,21 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Collections.Cache;
|
||||
using Penumbra.GameData.Files.Utility;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Interop.PathResolving;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
|
||||
namespace Penumbra.Api.Api;
|
||||
|
||||
public class MetaApi(CollectionResolver collectionResolver, ApiHelpers helpers) : IPenumbraApiMeta, IApiService
|
||||
public class MetaApi(IFramework framework, CollectionResolver collectionResolver, ApiHelpers helpers)
|
||||
: IPenumbraApiMeta, IApiService
|
||||
{
|
||||
public const int CurrentVersion = 0;
|
||||
public const int CurrentVersion = 1;
|
||||
|
||||
public string GetPlayerMetaManipulations()
|
||||
{
|
||||
|
|
@ -24,7 +29,32 @@ public class MetaApi(CollectionResolver collectionResolver, ApiHelpers helpers)
|
|||
return CompressMetaManipulations(collection);
|
||||
}
|
||||
|
||||
public Task<string> GetPlayerMetaManipulationsAsync()
|
||||
{
|
||||
return Task.Run(async () =>
|
||||
{
|
||||
var playerCollection = await framework.RunOnFrameworkThread(collectionResolver.PlayerCollection).ConfigureAwait(false);
|
||||
return CompressMetaManipulations(playerCollection);
|
||||
});
|
||||
}
|
||||
|
||||
public Task<string> GetMetaManipulationsAsync(int gameObjectIdx)
|
||||
{
|
||||
return Task.Run(async () =>
|
||||
{
|
||||
var playerCollection = await framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
helpers.AssociatedCollection(gameObjectIdx, out var collection);
|
||||
return collection;
|
||||
}).ConfigureAwait(false);
|
||||
return CompressMetaManipulations(playerCollection);
|
||||
});
|
||||
}
|
||||
|
||||
internal static string CompressMetaManipulations(ModCollection collection)
|
||||
=> CompressMetaManipulationsV0(collection);
|
||||
|
||||
private static string CompressMetaManipulationsV0(ModCollection collection)
|
||||
{
|
||||
var array = new JArray();
|
||||
if (collection.MetaCache is { } cache)
|
||||
|
|
@ -38,6 +68,228 @@ public class MetaApi(CollectionResolver collectionResolver, ApiHelpers helpers)
|
|||
MetaDictionary.SerializeTo(array, cache.Gmp.Select(kvp => new KeyValuePair<GmpIdentifier, GmpEntry>(kvp.Key, kvp.Value.Entry)));
|
||||
}
|
||||
|
||||
return Functions.ToCompressedBase64(array, CurrentVersion);
|
||||
return Functions.ToCompressedBase64(array, 0);
|
||||
}
|
||||
|
||||
private static unsafe string CompressMetaManipulationsV1(ModCollection? collection)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
ms.Capacity = 1024;
|
||||
using (var zipStream = new GZipStream(ms, CompressionMode.Compress, true))
|
||||
{
|
||||
zipStream.Write((byte)1);
|
||||
zipStream.Write("META0001"u8);
|
||||
if (collection?.MetaCache is not { } cache)
|
||||
{
|
||||
zipStream.Write(0);
|
||||
zipStream.Write(0);
|
||||
zipStream.Write(0);
|
||||
zipStream.Write(0);
|
||||
zipStream.Write(0);
|
||||
zipStream.Write(0);
|
||||
zipStream.Write(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteCache(zipStream, cache.Imc);
|
||||
WriteCache(zipStream, cache.Eqp);
|
||||
WriteCache(zipStream, cache.Eqdp);
|
||||
WriteCache(zipStream, cache.Est);
|
||||
WriteCache(zipStream, cache.Rsp);
|
||||
WriteCache(zipStream, cache.Gmp);
|
||||
cache.GlobalEqp.EnterReadLock();
|
||||
|
||||
try
|
||||
{
|
||||
zipStream.Write(cache.GlobalEqp.Count);
|
||||
foreach (var (globalEqp, _) in cache.GlobalEqp)
|
||||
zipStream.Write(new ReadOnlySpan<byte>(&globalEqp, sizeof(GlobalEqpManipulation)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
cache.GlobalEqp.ExitReadLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ms.Flush();
|
||||
ms.Position = 0;
|
||||
var data = ms.GetBuffer().AsSpan(0, (int)ms.Length);
|
||||
return Convert.ToBase64String(data);
|
||||
|
||||
void WriteCache<TKey, TValue>(Stream stream, MetaCacheBase<TKey, TValue> metaCache)
|
||||
where TKey : unmanaged, IMetaIdentifier
|
||||
where TValue : unmanaged
|
||||
{
|
||||
metaCache.EnterReadLock();
|
||||
try
|
||||
{
|
||||
stream.Write(metaCache.Count);
|
||||
foreach (var (identifier, (_, value)) in metaCache)
|
||||
{
|
||||
stream.Write(identifier);
|
||||
stream.Write(value);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
metaCache.ExitReadLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert manipulations from a transmitted base64 string to actual manipulations.
|
||||
/// The empty string is treated as an empty set.
|
||||
/// Only returns true if all conversions are successful and distinct.
|
||||
/// </summary>
|
||||
internal static bool ConvertManips(string manipString, [NotNullWhen(true)] out MetaDictionary? manips)
|
||||
{
|
||||
if (manipString.Length == 0)
|
||||
{
|
||||
manips = new MetaDictionary();
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var bytes = Convert.FromBase64String(manipString);
|
||||
using var compressedStream = new MemoryStream(bytes);
|
||||
using var zipStream = new GZipStream(compressedStream, CompressionMode.Decompress);
|
||||
using var resultStream = new MemoryStream();
|
||||
zipStream.CopyTo(resultStream);
|
||||
resultStream.Flush();
|
||||
resultStream.Position = 0;
|
||||
var data = resultStream.GetBuffer().AsSpan(0, (int)resultStream.Length);
|
||||
var version = data[0];
|
||||
data = data[1..];
|
||||
switch (version)
|
||||
{
|
||||
case 0: return ConvertManipsV0(data, out manips);
|
||||
case 1: return ConvertManipsV1(data, out manips);
|
||||
default:
|
||||
Penumbra.Log.Debug($"Invalid version for manipulations: {version}.");
|
||||
manips = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Penumbra.Log.Debug($"Error decompressing manipulations:\n{ex}");
|
||||
manips = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ConvertManipsV1(ReadOnlySpan<byte> data, [NotNullWhen(true)] out MetaDictionary? manips)
|
||||
{
|
||||
if (!data.StartsWith("META0001"u8))
|
||||
{
|
||||
Penumbra.Log.Debug($"Invalid manipulations of version 1, does not start with valid prefix.");
|
||||
manips = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
manips = new MetaDictionary();
|
||||
var r = new SpanBinaryReader(data[8..]);
|
||||
var imcCount = r.ReadInt32();
|
||||
for (var i = 0; i < imcCount; ++i)
|
||||
{
|
||||
var identifier = r.Read<ImcIdentifier>();
|
||||
var value = r.Read<ImcEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
var eqpCount = r.ReadInt32();
|
||||
for (var i = 0; i < eqpCount; ++i)
|
||||
{
|
||||
var identifier = r.Read<EqpIdentifier>();
|
||||
var value = r.Read<EqpEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
var eqdpCount = r.ReadInt32();
|
||||
for (var i = 0; i < eqdpCount; ++i)
|
||||
{
|
||||
var identifier = r.Read<EqdpIdentifier>();
|
||||
var value = r.Read<EqdpEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
var estCount = r.ReadInt32();
|
||||
for (var i = 0; i < estCount; ++i)
|
||||
{
|
||||
var identifier = r.Read<EstIdentifier>();
|
||||
var value = r.Read<EstEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
var rspCount = r.ReadInt32();
|
||||
for (var i = 0; i < rspCount; ++i)
|
||||
{
|
||||
var identifier = r.Read<RspIdentifier>();
|
||||
var value = r.Read<RspEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
var gmpCount = r.ReadInt32();
|
||||
for (var i = 0; i < gmpCount; ++i)
|
||||
{
|
||||
var identifier = r.Read<GmpIdentifier>();
|
||||
var value = r.Read<GmpEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
var globalEqpCount = r.ReadInt32();
|
||||
for (var i = 0; i < globalEqpCount; ++i)
|
||||
{
|
||||
var manip = r.Read<GlobalEqpManipulation>();
|
||||
if (!manip.Validate() || !manips.TryAdd(manip))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ConvertManipsV0(ReadOnlySpan<byte> data, [NotNullWhen(true)] out MetaDictionary? manips)
|
||||
{
|
||||
var json = Encoding.UTF8.GetString(data);
|
||||
manips = JsonConvert.DeserializeObject<MetaDictionary>(json);
|
||||
return manips != null;
|
||||
}
|
||||
|
||||
internal void TestMetaManipulations()
|
||||
{
|
||||
var collection = collectionResolver.PlayerCollection();
|
||||
var dict = new MetaDictionary(collection.MetaCache);
|
||||
var count = dict.Count;
|
||||
|
||||
var watch = Stopwatch.StartNew();
|
||||
var v0 = CompressMetaManipulationsV0(collection);
|
||||
var v0Time = watch.ElapsedMilliseconds;
|
||||
|
||||
watch.Restart();
|
||||
var v1 = CompressMetaManipulationsV1(collection);
|
||||
var v1Time = watch.ElapsedMilliseconds;
|
||||
|
||||
watch.Restart();
|
||||
var v1Success = ConvertManips(v1, out var v1Roundtrip);
|
||||
var v1RoundtripTime = watch.ElapsedMilliseconds;
|
||||
|
||||
watch.Restart();
|
||||
var v0Success = ConvertManips(v0, out var v0Roundtrip);
|
||||
var v0RoundtripTime = watch.ElapsedMilliseconds;
|
||||
|
||||
Penumbra.Log.Information($"Version | Count | Time | Length | Success | ReCount | ReTime | Equal");
|
||||
Penumbra.Log.Information(
|
||||
$"0 | {count} | {v0Time} | {v0.Length} | {v0Success} | {v0Roundtrip?.Count} | {v0RoundtripTime} | {v0Roundtrip?.Equals(dict)}");
|
||||
Penumbra.Log.Information(
|
||||
$"1 | {count} | {v1Time} | {v1.Length} | {v1Success} | {v1Roundtrip?.Count} | {v1RoundtripTime} | {v0Roundtrip?.Equals(dict)}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
|
|||
!= Path.TrimEndingDirectorySeparator(Path.GetFullPath(dir.Parent.FullName)))
|
||||
return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
|
||||
|
||||
_modManager.AddMod(dir);
|
||||
_modManager.AddMod(dir, true);
|
||||
if (_config.MigrateImportedModelsToV6)
|
||||
{
|
||||
_migrationManager.MigrateMdlDirectory(dir.FullName, false);
|
||||
|
|
@ -91,7 +91,7 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
|
|||
|
||||
if (_config.UseFileSystemCompression)
|
||||
new FileCompactor(Penumbra.Log).StartMassCompact(dir.EnumerateFiles("*.*", SearchOption.AllDirectories),
|
||||
CompressionAlgorithm.Xpress8K);
|
||||
CompressionAlgorithm.Xpress8K, false);
|
||||
|
||||
return ApiHelpers.Return(PenumbraApiEc.Success, args);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
using OtterGui;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.GameData.Actors;
|
||||
using Penumbra.GameData.Interop;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods.Settings;
|
||||
using Penumbra.String.Classes;
|
||||
|
||||
|
|
@ -62,7 +60,7 @@ public class TemporaryApi(
|
|||
if (!ConvertPaths(paths, out var p))
|
||||
return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args);
|
||||
|
||||
if (!ConvertManips(manipString, out var m))
|
||||
if (!MetaApi.ConvertManips(manipString, out var m))
|
||||
return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args);
|
||||
|
||||
var ret = tempMods.Register(tag, null, p, m, new ModPriority(priority)) switch
|
||||
|
|
@ -88,7 +86,7 @@ public class TemporaryApi(
|
|||
if (!ConvertPaths(paths, out var p))
|
||||
return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args);
|
||||
|
||||
if (!ConvertManips(manipString, out var m))
|
||||
if (!MetaApi.ConvertManips(manipString, out var m))
|
||||
return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args);
|
||||
|
||||
var ret = tempMods.Register(tag, collection, p, m, new ModPriority(priority)) switch
|
||||
|
|
@ -153,24 +151,4 @@ public class TemporaryApi(
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert manipulations from a transmitted base64 string to actual manipulations.
|
||||
/// The empty string is treated as an empty set.
|
||||
/// Only returns true if all conversions are successful and distinct.
|
||||
/// </summary>
|
||||
private static bool ConvertManips(string manipString, [NotNullWhen(true)] out MetaDictionary? manips)
|
||||
{
|
||||
if (manipString.Length == 0)
|
||||
{
|
||||
manips = new MetaDictionary();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Functions.FromCompressedBase64(manipString, out manips!) == MetaApi.CurrentVersion)
|
||||
return true;
|
||||
|
||||
manips = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using OtterGui.Classes;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
|
|
@ -5,7 +6,7 @@ using Penumbra.Mods.Editor;
|
|||
|
||||
namespace Penumbra.Collections.Cache;
|
||||
|
||||
public class GlobalEqpCache : Dictionary<GlobalEqpManipulation, IMod>, IService
|
||||
public class GlobalEqpCache : ReadWriteDictionary<GlobalEqpManipulation, IMod>, IService
|
||||
{
|
||||
private readonly HashSet<PrimaryId> _doNotHideEarrings = [];
|
||||
private readonly HashSet<PrimaryId> _doNotHideNecklace = [];
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ using Penumbra.GameData.Structs;
|
|||
using Penumbra.Meta;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods.Editor;
|
||||
using Penumbra.String.Classes;
|
||||
|
||||
namespace Penumbra.Collections.Cache;
|
||||
|
||||
|
|
@ -16,6 +15,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
public readonly RspCache Rsp = new(manager, collection);
|
||||
public readonly ImcCache Imc = new(manager, collection);
|
||||
public readonly GlobalEqpCache GlobalEqp = new();
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
public int Count
|
||||
=> Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + GlobalEqp.Count;
|
||||
|
|
@ -42,6 +42,10 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
|
||||
public void Dispose()
|
||||
{
|
||||
if (IsDisposed)
|
||||
return;
|
||||
|
||||
IsDisposed = true;
|
||||
Eqp.Dispose();
|
||||
Eqdp.Dispose();
|
||||
Est.Dispose();
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using OtterGui.Classes;
|
||||
using Penumbra.Meta;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods.Editor;
|
||||
|
|
@ -5,27 +6,19 @@ using Penumbra.Mods.Editor;
|
|||
namespace Penumbra.Collections.Cache;
|
||||
|
||||
public abstract class MetaCacheBase<TIdentifier, TEntry>(MetaFileManager manager, ModCollection collection)
|
||||
: Dictionary<TIdentifier, (IMod Source, TEntry Entry)>
|
||||
: ReadWriteDictionary<TIdentifier, (IMod Source, TEntry Entry)>
|
||||
where TIdentifier : unmanaged, IMetaIdentifier
|
||||
where TEntry : unmanaged
|
||||
{
|
||||
protected readonly MetaFileManager Manager = manager;
|
||||
protected readonly ModCollection Collection = collection;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
protected readonly MetaFileManager Manager = manager;
|
||||
protected readonly ModCollection Collection = collection;
|
||||
|
||||
public bool ApplyMod(IMod source, TIdentifier identifier, TEntry entry)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (TryGetValue(identifier, out var pair) && pair.Source == source && EqualityComparer<TEntry>.Default.Equals(pair.Entry, entry))
|
||||
return false;
|
||||
if (TryGetValue(identifier, out var pair) && pair.Source == source && EqualityComparer<TEntry>.Default.Equals(pair.Entry, entry))
|
||||
return false;
|
||||
|
||||
this[identifier] = (source, entry);
|
||||
}
|
||||
this[identifier] = (source, entry);
|
||||
|
||||
ApplyModInternal(identifier, entry);
|
||||
return true;
|
||||
|
|
@ -33,17 +26,14 @@ public abstract class MetaCacheBase<TIdentifier, TEntry>(MetaFileManager manager
|
|||
|
||||
public bool RevertMod(TIdentifier identifier, [NotNullWhen(true)] out IMod? mod)
|
||||
{
|
||||
lock (this)
|
||||
if (!Remove(identifier, out var pair))
|
||||
{
|
||||
if (!Remove(identifier, out var pair))
|
||||
{
|
||||
mod = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
mod = pair.Source;
|
||||
mod = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
mod = pair.Source;
|
||||
|
||||
RevertModInternal(identifier);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -54,7 +44,4 @@ public abstract class MetaCacheBase<TIdentifier, TEntry>(MetaFileManager manager
|
|||
|
||||
protected virtual void RevertModInternal(TIdentifier identifier)
|
||||
{ }
|
||||
|
||||
protected virtual void Dispose(bool _)
|
||||
{ }
|
||||
}
|
||||
|
|
@ -46,5 +46,8 @@ public sealed class CollectionChange()
|
|||
|
||||
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnCollectionChange"/>
|
||||
ModFileSystemSelector = 0,
|
||||
|
||||
/// <seealso cref="Mods.ModSelection.OnCollectionChange"/>
|
||||
ModSelection = 10,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,5 +23,8 @@ public sealed class CollectionInheritanceChanged()
|
|||
|
||||
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnInheritanceChange"/>
|
||||
ModFileSystemSelector = 0,
|
||||
|
||||
/// <seealso cref="Mods.ModSelection.OnInheritanceChange"/>
|
||||
ModSelection = 10,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using OtterGui.Classes;
|
||||
using Penumbra.Api;
|
||||
using Penumbra.Api.Api;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections;
|
||||
|
|
@ -35,5 +34,8 @@ public sealed class ModSettingChanged()
|
|||
|
||||
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnSettingChange"/>
|
||||
ModFileSystemSelector = 0,
|
||||
|
||||
/// <seealso cref="Mods.ModSelection.OnSettingChange"/>
|
||||
ModSelection = 10,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Lumina.Extensions;
|
||||
using OtterGui;
|
||||
using Penumbra.GameData.Files;
|
||||
|
|
@ -23,11 +25,11 @@ public class MeshExporter
|
|||
? scene.AddSkinnedMesh(data.Mesh, Matrix4x4.Identity, [.. skeleton.Value.Joints])
|
||||
: scene.AddRigidMesh(data.Mesh, Matrix4x4.Identity);
|
||||
|
||||
var extras = new Dictionary<string, object>(data.Attributes.Length);
|
||||
var node = new JsonObject();
|
||||
foreach (var attribute in data.Attributes)
|
||||
extras.Add(attribute, true);
|
||||
node[attribute] = true;
|
||||
|
||||
instance.WithExtras(JsonContent.CreateFrom(extras));
|
||||
instance.WithExtras(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -233,10 +235,7 @@ public class MeshExporter
|
|||
|
||||
// Named morph targets aren't part of the specification, however `MESH.extras.targetNames`
|
||||
// is a commonly-accepted means of providing the data.
|
||||
meshBuilder.Extras = JsonContent.CreateFrom(new Dictionary<string, object>()
|
||||
{
|
||||
{ "targetNames", shapeNames },
|
||||
});
|
||||
meshBuilder.Extras = new JsonObject { ["targetNames"] = JsonSerializer.SerializeToNode(shapeNames) };
|
||||
|
||||
string[] attributes = [];
|
||||
var maxAttribute = 31 - BitOperations.LeadingZeroCount(attributeMask);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using System;
|
||||
using SharpGLTF.Geometry.VertexTypes;
|
||||
using SharpGLTF.Memory;
|
||||
using SharpGLTF.Schema2;
|
||||
|
||||
namespace Penumbra.Import.Models.Export;
|
||||
|
|
@ -11,35 +13,40 @@ and there's reason to overhaul the export pipeline.
|
|||
|
||||
public struct VertexColorFfxiv : IVertexCustom
|
||||
{
|
||||
// NOTE: We only realistically require UNSIGNED_BYTE for this, however Blender 3.6 errors on that (fixed in 4.0).
|
||||
[VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, true)]
|
||||
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
|
||||
{
|
||||
// NOTE: We only realistically require UNSIGNED_BYTE for this, however Blender 3.6 errors on that (fixed in 4.0).
|
||||
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR",
|
||||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
}
|
||||
|
||||
public Vector4 FfxivColor;
|
||||
|
||||
public int MaxColors => 0;
|
||||
public int MaxColors
|
||||
=> 0;
|
||||
|
||||
public int MaxTextCoords => 0;
|
||||
public int MaxTextCoords
|
||||
=> 0;
|
||||
|
||||
private static readonly string[] CustomNames = ["_FFXIV_COLOR"];
|
||||
public IEnumerable<string> CustomAttributes => CustomNames;
|
||||
|
||||
public IEnumerable<string> CustomAttributes
|
||||
=> CustomNames;
|
||||
|
||||
public VertexColorFfxiv(Vector4 ffxivColor)
|
||||
{
|
||||
FfxivColor = ffxivColor;
|
||||
}
|
||||
=> FfxivColor = ffxivColor;
|
||||
|
||||
public void Add(in VertexMaterialDelta delta)
|
||||
{
|
||||
}
|
||||
{ }
|
||||
|
||||
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
|
||||
=> new VertexMaterialDelta(Vector4.Zero, Vector4.Zero, Vector2.Zero, Vector2.Zero);
|
||||
=> new(Vector4.Zero, Vector4.Zero, Vector2.Zero, Vector2.Zero);
|
||||
|
||||
public Vector2 GetTexCoord(int index)
|
||||
=> throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
public void SetTexCoord(int setIndex, Vector2 coord)
|
||||
{
|
||||
}
|
||||
{ }
|
||||
|
||||
public bool TryGetCustomAttribute(string attributeName, out object? value)
|
||||
{
|
||||
|
|
@ -65,12 +72,17 @@ public struct VertexColorFfxiv : IVertexCustom
|
|||
=> throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
public void SetColor(int setIndex, Vector4 color)
|
||||
{
|
||||
}
|
||||
{ }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
var components = new[] { FfxivColor.X, FfxivColor.Y, FfxivColor.Z, FfxivColor.W };
|
||||
var components = new[]
|
||||
{
|
||||
FfxivColor.X,
|
||||
FfxivColor.Y,
|
||||
FfxivColor.Z,
|
||||
FfxivColor.W,
|
||||
};
|
||||
if (components.Any(component => component < 0 || component > 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor));
|
||||
}
|
||||
|
|
@ -78,22 +90,32 @@ public struct VertexColorFfxiv : IVertexCustom
|
|||
|
||||
public struct VertexTexture1ColorFfxiv : IVertexCustom
|
||||
{
|
||||
[VertexAttribute("TEXCOORD_0")]
|
||||
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
|
||||
{
|
||||
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_0",
|
||||
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR",
|
||||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
}
|
||||
|
||||
public Vector2 TexCoord0;
|
||||
|
||||
[VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, true)]
|
||||
public Vector4 FfxivColor;
|
||||
|
||||
public int MaxColors => 0;
|
||||
public int MaxColors
|
||||
=> 0;
|
||||
|
||||
public int MaxTextCoords => 1;
|
||||
public int MaxTextCoords
|
||||
=> 1;
|
||||
|
||||
private static readonly string[] CustomNames = ["_FFXIV_COLOR"];
|
||||
public IEnumerable<string> CustomAttributes => CustomNames;
|
||||
|
||||
public IEnumerable<string> CustomAttributes
|
||||
=> CustomNames;
|
||||
|
||||
public VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor)
|
||||
{
|
||||
TexCoord0 = texCoord0;
|
||||
TexCoord0 = texCoord0;
|
||||
FfxivColor = ffxivColor;
|
||||
}
|
||||
|
||||
|
|
@ -103,9 +125,7 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom
|
|||
}
|
||||
|
||||
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
|
||||
{
|
||||
return new VertexMaterialDelta(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), Vector2.Zero);
|
||||
}
|
||||
=> new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), Vector2.Zero);
|
||||
|
||||
public Vector2 GetTexCoord(int index)
|
||||
=> index switch
|
||||
|
|
@ -116,8 +136,10 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom
|
|||
|
||||
public void SetTexCoord(int setIndex, Vector2 coord)
|
||||
{
|
||||
if (setIndex == 0) TexCoord0 = coord;
|
||||
if (setIndex >= 1) throw new ArgumentOutOfRangeException(nameof(setIndex));
|
||||
if (setIndex == 0)
|
||||
TexCoord0 = coord;
|
||||
if (setIndex >= 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(setIndex));
|
||||
}
|
||||
|
||||
public bool TryGetCustomAttribute(string attributeName, out object? value)
|
||||
|
|
@ -144,12 +166,17 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom
|
|||
=> throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
public void SetColor(int setIndex, Vector4 color)
|
||||
{
|
||||
}
|
||||
{ }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
var components = new[] { FfxivColor.X, FfxivColor.Y, FfxivColor.Z, FfxivColor.W };
|
||||
var components = new[]
|
||||
{
|
||||
FfxivColor.X,
|
||||
FfxivColor.Y,
|
||||
FfxivColor.Z,
|
||||
FfxivColor.W,
|
||||
};
|
||||
if (components.Any(component => component < 0 || component > 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor));
|
||||
}
|
||||
|
|
@ -157,26 +184,35 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom
|
|||
|
||||
public struct VertexTexture2ColorFfxiv : IVertexCustom
|
||||
{
|
||||
[VertexAttribute("TEXCOORD_0")]
|
||||
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
|
||||
{
|
||||
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_0",
|
||||
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_1",
|
||||
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR",
|
||||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
}
|
||||
|
||||
public Vector2 TexCoord0;
|
||||
|
||||
[VertexAttribute("TEXCOORD_1")]
|
||||
public Vector2 TexCoord1;
|
||||
|
||||
[VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, true)]
|
||||
public Vector4 FfxivColor;
|
||||
|
||||
public int MaxColors => 0;
|
||||
public int MaxColors
|
||||
=> 0;
|
||||
|
||||
public int MaxTextCoords => 2;
|
||||
public int MaxTextCoords
|
||||
=> 2;
|
||||
|
||||
private static readonly string[] CustomNames = ["_FFXIV_COLOR"];
|
||||
public IEnumerable<string> CustomAttributes => CustomNames;
|
||||
|
||||
public IEnumerable<string> CustomAttributes
|
||||
=> CustomNames;
|
||||
|
||||
public VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor)
|
||||
{
|
||||
TexCoord0 = texCoord0;
|
||||
TexCoord1 = texCoord1;
|
||||
TexCoord0 = texCoord0;
|
||||
TexCoord1 = texCoord1;
|
||||
FfxivColor = ffxivColor;
|
||||
}
|
||||
|
||||
|
|
@ -187,9 +223,7 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom
|
|||
}
|
||||
|
||||
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
|
||||
{
|
||||
return new VertexMaterialDelta(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1));
|
||||
}
|
||||
=> new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1));
|
||||
|
||||
public Vector2 GetTexCoord(int index)
|
||||
=> index switch
|
||||
|
|
@ -201,9 +235,12 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom
|
|||
|
||||
public void SetTexCoord(int setIndex, Vector2 coord)
|
||||
{
|
||||
if (setIndex == 0) TexCoord0 = coord;
|
||||
if (setIndex == 1) TexCoord1 = coord;
|
||||
if (setIndex >= 2) throw new ArgumentOutOfRangeException(nameof(setIndex));
|
||||
if (setIndex == 0)
|
||||
TexCoord0 = coord;
|
||||
if (setIndex == 1)
|
||||
TexCoord1 = coord;
|
||||
if (setIndex >= 2)
|
||||
throw new ArgumentOutOfRangeException(nameof(setIndex));
|
||||
}
|
||||
|
||||
public bool TryGetCustomAttribute(string attributeName, out object? value)
|
||||
|
|
@ -230,12 +267,17 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom
|
|||
=> throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
public void SetColor(int setIndex, Vector4 color)
|
||||
{
|
||||
}
|
||||
{ }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
var components = new[] { FfxivColor.X, FfxivColor.Y, FfxivColor.Z, FfxivColor.W };
|
||||
var components = new[]
|
||||
{
|
||||
FfxivColor.X,
|
||||
FfxivColor.Y,
|
||||
FfxivColor.Z,
|
||||
FfxivColor.W,
|
||||
};
|
||||
if (components.Any(component => component < 0 || component > 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ public class SubMeshImporter
|
|||
|
||||
try
|
||||
{
|
||||
_morphNames = node.Mesh.Extras.GetNode("targetNames").Deserialize<List<string>>();
|
||||
_morphNames = node.Mesh.Extras["targetNames"].Deserialize<List<string>>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ public partial class TexToolsImporter : IDisposable
|
|||
|
||||
// You can in no way rely on any file paths in TTMPs so we need to just do this, sorry
|
||||
private static ZipArchiveEntry? FindZipEntry(ZipArchive file, string fileName)
|
||||
=> file.Entries.FirstOrDefault(e => !e.IsDirectory && e.Key.Contains(fileName));
|
||||
=> file.Entries.FirstOrDefault(e => e is { IsDirectory: false, Key: not null } && e.Key.Contains(fileName));
|
||||
|
||||
private static string GetStringFromZipEntry(ZipArchiveEntry entry, Encoding encoding)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ public partial class TexToolsImporter
|
|||
if (name.Length == 0)
|
||||
throw new Exception("Invalid mod archive: mod meta has no name.");
|
||||
|
||||
using var f = File.OpenWrite(Path.Combine(_currentModDirectory.FullName, reader.Entry.Key));
|
||||
using var f = File.OpenWrite(Path.Combine(_currentModDirectory.FullName, reader.Entry.Key!));
|
||||
s.Seek(0, SeekOrigin.Begin);
|
||||
s.WriteTo(f);
|
||||
}
|
||||
|
|
@ -155,13 +155,9 @@ public partial class TexToolsImporter
|
|||
|
||||
ret = directory;
|
||||
// Check that all other files are also contained in the top-level directory.
|
||||
if (ret.IndexOfAny(new[]
|
||||
{
|
||||
'/',
|
||||
'\\',
|
||||
})
|
||||
>= 0
|
||||
|| !archive.Entries.All(e => e.Key.StartsWith(ret) && (e.Key.Length == ret.Length || e.Key[ret.Length] is '/' or '\\')))
|
||||
if (ret.IndexOfAny(['/', '\\']) >= 0
|
||||
|| !archive.Entries.All(e
|
||||
=> e.Key != null && e.Key.StartsWith(ret) && (e.Key.Length == ret.Length || e.Key[ret.Length] is '/' or '\\')))
|
||||
throw new Exception(
|
||||
"Invalid mod archive: meta.json in wrong location. It needs to be either at root or one directory deep, in which all other files must be nested too.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,14 @@ public partial class CombinedTexture : IDisposable
|
|||
SaveTask = textures.SavePng(_current.BaseImage, path, _current.RgbaPixels, _current.TextureWrap!.Width, _current.TextureWrap!.Height);
|
||||
}
|
||||
|
||||
public void SaveAsTarga(TextureManager textures, string path)
|
||||
{
|
||||
if (!IsLoaded || _current == null)
|
||||
return;
|
||||
|
||||
SaveTask = textures.SaveTga(_current.BaseImage, path, _current.RgbaPixels, _current.TextureWrap!.Width, _current.TextureWrap!.Height);
|
||||
}
|
||||
|
||||
private void SaveAs(TextureManager textures, string path, TextureSaveType type, bool mipMaps, bool writeTex)
|
||||
{
|
||||
if (!IsLoaded || _current == null)
|
||||
|
|
@ -72,6 +80,7 @@ public partial class CombinedTexture : IDisposable
|
|||
".tex" => TextureType.Tex,
|
||||
".dds" => TextureType.Dds,
|
||||
".png" => TextureType.Png,
|
||||
".tga" => TextureType.Targa,
|
||||
_ => TextureType.Unknown,
|
||||
};
|
||||
|
||||
|
|
@ -85,6 +94,9 @@ public partial class CombinedTexture : IDisposable
|
|||
break;
|
||||
case TextureType.Png:
|
||||
SaveAsPng(textures, path);
|
||||
break;
|
||||
case TextureType.Targa:
|
||||
SaveAsTarga(textures, path);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException(
|
||||
|
|
|
|||
|
|
@ -177,7 +177,9 @@ public static class TexFileParser
|
|||
DXGIFormat.BC1UNorm => TexFile.TextureFormat.BC1,
|
||||
DXGIFormat.BC2UNorm => TexFile.TextureFormat.BC2,
|
||||
DXGIFormat.BC3UNorm => TexFile.TextureFormat.BC3,
|
||||
DXGIFormat.BC4UNorm => (TexFile.TextureFormat)0x6120, // TODO: upstream to Lumina
|
||||
DXGIFormat.BC5UNorm => TexFile.TextureFormat.BC5,
|
||||
DXGIFormat.BC6HSF16 => (TexFile.TextureFormat)0x6330, // TODO: upstream to Lumina
|
||||
DXGIFormat.BC7UNorm => TexFile.TextureFormat.BC7,
|
||||
DXGIFormat.R16G16B16A16Typeless => TexFile.TextureFormat.D16,
|
||||
DXGIFormat.R24G8Typeless => TexFile.TextureFormat.D24S8,
|
||||
|
|
@ -202,7 +204,9 @@ public static class TexFileParser
|
|||
TexFile.TextureFormat.BC1 => DXGIFormat.BC1UNorm,
|
||||
TexFile.TextureFormat.BC2 => DXGIFormat.BC2UNorm,
|
||||
TexFile.TextureFormat.BC3 => DXGIFormat.BC3UNorm,
|
||||
(TexFile.TextureFormat)0x6120 => DXGIFormat.BC4UNorm, // TODO: upstream to Lumina
|
||||
TexFile.TextureFormat.BC5 => DXGIFormat.BC5UNorm,
|
||||
(TexFile.TextureFormat)0x6330 => DXGIFormat.BC6HSF16, // TODO: upstream to Lumina
|
||||
TexFile.TextureFormat.BC7 => DXGIFormat.BC7UNorm,
|
||||
TexFile.TextureFormat.D16 => DXGIFormat.R16G16B16A16Typeless,
|
||||
TexFile.TextureFormat.D24S8 => DXGIFormat.R24G8Typeless,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,21 @@ public enum TextureType
|
|||
Tex,
|
||||
Png,
|
||||
Bitmap,
|
||||
Targa,
|
||||
}
|
||||
|
||||
internal static class TextureTypeExtensions
|
||||
{
|
||||
public static TextureType ReduceToBehaviour(this TextureType type)
|
||||
=> type switch
|
||||
{
|
||||
TextureType.Dds => TextureType.Dds,
|
||||
TextureType.Tex => TextureType.Tex,
|
||||
TextureType.Png => TextureType.Png,
|
||||
TextureType.Bitmap => TextureType.Png,
|
||||
TextureType.Targa => TextureType.Png,
|
||||
_ => TextureType.Unknown,
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class Texture : IDisposable
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ public static class TextureDrawer
|
|||
current.Load(textures, paths[0]);
|
||||
}
|
||||
|
||||
fileDialog.OpenFilePicker("Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath, false);
|
||||
fileDialog.OpenFilePicker("Open Image...", "Textures{.png,.dds,.tex,.tga}", UpdatePath, 1, startPath, false);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using OtterGui.Tasks;
|
|||
using OtterTex;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Png;
|
||||
using SixLabors.ImageSharp.Formats.Tga;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using Image = SixLabors.ImageSharp.Image;
|
||||
|
||||
|
|
@ -33,10 +34,17 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
|
|||
}
|
||||
|
||||
public Task SavePng(string input, string output)
|
||||
=> Enqueue(new SavePngAction(this, input, output));
|
||||
=> Enqueue(new SaveImageSharpAction(this, input, output, TextureType.Png));
|
||||
|
||||
public Task SavePng(BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0)
|
||||
=> Enqueue(new SavePngAction(this, image, path, rgba, width, height));
|
||||
=> Enqueue(new SaveImageSharpAction(this, image, path, TextureType.Png, rgba, width, height));
|
||||
|
||||
public Task SaveTga(string input, string output)
|
||||
=> Enqueue(new SaveImageSharpAction(this, input, output, TextureType.Targa));
|
||||
|
||||
public Task SaveTga(BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0)
|
||||
=> Enqueue(new SaveImageSharpAction(this, image, path, TextureType.Targa, rgba, width, height));
|
||||
|
||||
|
||||
public Task SaveAs(CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, string input, string output)
|
||||
=> Enqueue(new SaveAsAction(this, type, mipMaps, asTex, input, output));
|
||||
|
|
@ -66,44 +74,65 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
|
|||
return t;
|
||||
}
|
||||
|
||||
private class SavePngAction : IAction
|
||||
private class SaveImageSharpAction : IAction
|
||||
{
|
||||
private readonly TextureManager _textures;
|
||||
private readonly string _outputPath;
|
||||
private readonly ImageInputData _input;
|
||||
private readonly TextureType _type;
|
||||
|
||||
public SavePngAction(TextureManager textures, string input, string output)
|
||||
public SaveImageSharpAction(TextureManager textures, string input, string output, TextureType type)
|
||||
{
|
||||
_textures = textures;
|
||||
_input = new ImageInputData(input);
|
||||
_outputPath = output;
|
||||
_type = type;
|
||||
if (_type.ReduceToBehaviour() is not TextureType.Png)
|
||||
throw new ArgumentOutOfRangeException(nameof(type), type, $"Can not save as {type} with ImageSharp.");
|
||||
}
|
||||
|
||||
public SavePngAction(TextureManager textures, BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0)
|
||||
public SaveImageSharpAction(TextureManager textures, BaseImage image, string path, TextureType type, byte[]? rgba = null, int width = 0,
|
||||
int height = 0)
|
||||
{
|
||||
_textures = textures;
|
||||
_input = new ImageInputData(image, rgba, width, height);
|
||||
_outputPath = path;
|
||||
_type = type;
|
||||
if (_type.ReduceToBehaviour() is not TextureType.Png)
|
||||
throw new ArgumentOutOfRangeException(nameof(type), type, $"Can not save as {type} with ImageSharp.");
|
||||
}
|
||||
|
||||
public void Execute(CancellationToken cancel)
|
||||
{
|
||||
_textures._logger.Information($"[{nameof(TextureManager)}] Saving {_input} as .png to {_outputPath}...");
|
||||
_textures._logger.Information($"[{nameof(TextureManager)}] Saving {_input} as {_type} to {_outputPath}...");
|
||||
var (image, rgba, width, height) = _input.GetData(_textures);
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
Image<Rgba32>? png = null;
|
||||
Image<Rgba32>? data = null;
|
||||
if (image.Type is TextureType.Unknown)
|
||||
{
|
||||
if (rgba != null && width > 0 && height > 0)
|
||||
png = ConvertToPng(rgba, width, height).AsPng!;
|
||||
data = ConvertToPng(rgba, width, height).AsPng!;
|
||||
}
|
||||
else
|
||||
{
|
||||
png = ConvertToPng(image, cancel, rgba).AsPng!;
|
||||
data = ConvertToPng(image, cancel, rgba).AsPng!;
|
||||
}
|
||||
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
png?.SaveAsync(_outputPath, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression }, cancel).Wait(cancel);
|
||||
switch (_type)
|
||||
{
|
||||
case TextureType.Png:
|
||||
data?.SaveAsync(_outputPath, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression }, cancel)
|
||||
.Wait(cancel);
|
||||
return;
|
||||
case TextureType.Targa:
|
||||
data?.SaveAsync(_outputPath, new TgaEncoder()
|
||||
{
|
||||
Compression = TgaCompression.None,
|
||||
BitsPerPixel = TgaBitsPerPixel.Pixel32,
|
||||
}, cancel).Wait(cancel);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
|
|
@ -111,7 +140,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
|
|||
|
||||
public bool Equals(IAction? other)
|
||||
{
|
||||
if (other is not SavePngAction rhs)
|
||||
if (other is not SaveImageSharpAction rhs)
|
||||
return false;
|
||||
|
||||
return string.Equals(_outputPath, rhs._outputPath, StringComparison.OrdinalIgnoreCase) && _input.Equals(rhs._input);
|
||||
|
|
@ -165,11 +194,12 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
|
|||
return;
|
||||
}
|
||||
|
||||
var imageTypeBehaviour = image.Type.ReduceToBehaviour();
|
||||
var dds = _type switch
|
||||
{
|
||||
CombinedTexture.TextureSaveType.AsIs when image.Type is TextureType.Png => ConvertToRgbaDds(image, _mipMaps, cancel, rgba,
|
||||
width, height),
|
||||
CombinedTexture.TextureSaveType.AsIs when image.Type is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps),
|
||||
CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Png => ConvertToRgbaDds(image, _mipMaps, cancel,
|
||||
rgba, width, height),
|
||||
CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps),
|
||||
CombinedTexture.TextureSaveType.Bitmap => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, width, height),
|
||||
CombinedTexture.TextureSaveType.BC3 => ConvertToCompressedDds(image, _mipMaps, false, cancel, rgba, width, height),
|
||||
CombinedTexture.TextureSaveType.BC7 => ConvertToCompressedDds(image, _mipMaps, true, cancel, rgba, width, height),
|
||||
|
|
@ -218,7 +248,9 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
|
|||
=> Path.GetExtension(path).ToLowerInvariant() switch
|
||||
{
|
||||
".dds" => (LoadDds(path), TextureType.Dds),
|
||||
".png" => (LoadPng(path), TextureType.Png),
|
||||
".png" => (LoadImageSharp(path), TextureType.Png),
|
||||
".tga" => (LoadImageSharp(path), TextureType.Targa),
|
||||
".bmp" => (LoadImageSharp(path), TextureType.Bitmap),
|
||||
".tex" => (LoadTex(path), TextureType.Tex),
|
||||
_ => throw new Exception($"Extension {Path.GetExtension(path)} unknown."),
|
||||
};
|
||||
|
|
@ -234,17 +266,17 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
|
|||
public BaseImage LoadDds(string path)
|
||||
=> ScratchImage.LoadDDS(path);
|
||||
|
||||
/// <summary> Load a .png file from drive using ImageSharp. </summary>
|
||||
public BaseImage LoadPng(string path)
|
||||
/// <summary> Load a supported file type from drive using ImageSharp. </summary>
|
||||
public BaseImage LoadImageSharp(string path)
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
return Image.Load<Rgba32>(stream);
|
||||
}
|
||||
|
||||
/// <summary> Convert an existing image to .png. Does not create a deep copy of an existing .png and just returns the existing one. </summary>
|
||||
/// <summary> Convert an existing image to ImageSharp. Does not create a deep copy of an existing ImageSharp file and just returns the existing one. </summary>
|
||||
public static BaseImage ConvertToPng(BaseImage input, CancellationToken cancel, byte[]? rgba = null, int width = 0, int height = 0)
|
||||
{
|
||||
switch (input.Type)
|
||||
switch (input.Type.ReduceToBehaviour())
|
||||
{
|
||||
case TextureType.Png: return input;
|
||||
case TextureType.Dds:
|
||||
|
|
@ -261,7 +293,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
|
|||
public static BaseImage ConvertToRgbaDds(BaseImage input, bool mipMaps, CancellationToken cancel, byte[]? rgba = null, int width = 0,
|
||||
int height = 0)
|
||||
{
|
||||
switch (input.Type)
|
||||
switch (input.Type.ReduceToBehaviour())
|
||||
{
|
||||
case TextureType.Png:
|
||||
{
|
||||
|
|
@ -291,7 +323,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
|
|||
public static BaseImage ConvertToCompressedDds(BaseImage input, bool mipMaps, bool bc7, CancellationToken cancel, byte[]? rgba = null,
|
||||
int width = 0, int height = 0)
|
||||
{
|
||||
switch (input.Type)
|
||||
switch (input.Type.ReduceToBehaviour())
|
||||
{
|
||||
case TextureType.Png:
|
||||
{
|
||||
|
|
@ -470,6 +502,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
|
|||
TextureType.Dds => $"Custom {_width} x {_height} {_image.Format} Image",
|
||||
TextureType.Tex => $"Custom {_width} x {_height} {_image.Format} Image",
|
||||
TextureType.Png => $"Custom {_width} x {_height} .png Image",
|
||||
TextureType.Targa => $"Custom {_width} x {_height} .tga Image",
|
||||
TextureType.Bitmap => $"Custom {_width} x {_height} RGBA Image",
|
||||
_ => "Unknown Image",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ public class MetaDictionary
|
|||
|
||||
public void Clear()
|
||||
{
|
||||
Count = 0;
|
||||
_imc.Clear();
|
||||
_eqp.Clear();
|
||||
_eqdp.Clear();
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co
|
|||
if (!useModManager || !modManager.TryGetMod(modDirectory.Name, string.Empty, out var mod))
|
||||
{
|
||||
mod = new Mod(modDirectory);
|
||||
modManager.Creator.ReloadMod(mod, true, out _);
|
||||
modManager.Creator.ReloadMod(mod, true, true, out _);
|
||||
}
|
||||
|
||||
Clear();
|
||||
|
|
|
|||
|
|
@ -25,6 +25,21 @@ public class ModEditor(
|
|||
public readonly MdlMaterialEditor MdlMaterialEditor = mdlMaterialEditor;
|
||||
public readonly FileCompactor Compactor = compactor;
|
||||
|
||||
|
||||
public bool IsLoading
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _loadingMod is { IsCompleted: false };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly object _lock = new();
|
||||
private Task? _loadingMod;
|
||||
|
||||
public Mod? Mod { get; private set; }
|
||||
public int GroupIdx { get; private set; }
|
||||
public int DataIdx { get; private set; }
|
||||
|
|
@ -32,28 +47,42 @@ public class ModEditor(
|
|||
public IModGroup? Group { get; private set; }
|
||||
public IModDataContainer? Option { get; private set; }
|
||||
|
||||
public void LoadMod(Mod mod)
|
||||
=> LoadMod(mod, -1, 0);
|
||||
|
||||
public void LoadMod(Mod mod, int groupIdx, int dataIdx)
|
||||
public async Task LoadMod(Mod mod, int groupIdx, int dataIdx)
|
||||
{
|
||||
Mod = mod;
|
||||
LoadOption(groupIdx, dataIdx, true);
|
||||
Files.UpdateAll(mod, Option!);
|
||||
SwapEditor.Revert(Option!);
|
||||
MetaEditor.Load(Mod!, Option!);
|
||||
Duplicates.Clear();
|
||||
MdlMaterialEditor.ScanModels(Mod!);
|
||||
await AppendTask(() =>
|
||||
{
|
||||
Mod = mod;
|
||||
LoadOption(groupIdx, dataIdx, true);
|
||||
Files.UpdateAll(mod, Option!);
|
||||
SwapEditor.Revert(Option!);
|
||||
MetaEditor.Load(Mod!, Option!);
|
||||
Duplicates.Clear();
|
||||
MdlMaterialEditor.ScanModels(Mod!);
|
||||
});
|
||||
}
|
||||
|
||||
public void LoadOption(int groupIdx, int dataIdx)
|
||||
private Task AppendTask(Action run)
|
||||
{
|
||||
LoadOption(groupIdx, dataIdx, true);
|
||||
SwapEditor.Revert(Option!);
|
||||
Files.UpdatePaths(Mod!, Option!);
|
||||
MetaEditor.Load(Mod!, Option!);
|
||||
FileEditor.Clear();
|
||||
Duplicates.Clear();
|
||||
lock (_lock)
|
||||
{
|
||||
if (_loadingMod == null || _loadingMod.IsCompleted)
|
||||
return _loadingMod = Task.Run(run);
|
||||
|
||||
return _loadingMod = _loadingMod.ContinueWith(_ => run());
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LoadOption(int groupIdx, int dataIdx)
|
||||
{
|
||||
await AppendTask(() =>
|
||||
{
|
||||
LoadOption(groupIdx, dataIdx, true);
|
||||
SwapEditor.Revert(Option!);
|
||||
Files.UpdatePaths(Mod!, Option!);
|
||||
MetaEditor.Load(Mod!, Option!);
|
||||
FileEditor.Clear();
|
||||
Duplicates.Clear();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary> Load the correct option by indices for the currently loaded mod if possible, unload if not. </summary>
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu
|
|||
if (deletions <= 0)
|
||||
return;
|
||||
|
||||
modManager.Creator.ReloadMod(mod, false, out _);
|
||||
modManager.Creator.ReloadMod(mod, false, false, out _);
|
||||
files.UpdateAll(mod, option);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,22 +10,21 @@ using Penumbra.Mods.Manager.OptionEditor;
|
|||
using Penumbra.Mods.SubMods;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.String.Classes;
|
||||
using Penumbra.UI.ModsTab;
|
||||
|
||||
namespace Penumbra.Mods.Editor;
|
||||
|
||||
public class ModMerger : IDisposable, IService
|
||||
{
|
||||
private readonly Configuration _config;
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly ModGroupEditor _editor;
|
||||
private readonly ModFileSystemSelector _selector;
|
||||
private readonly DuplicateManager _duplicates;
|
||||
private readonly ModManager _mods;
|
||||
private readonly ModCreator _creator;
|
||||
private readonly Configuration _config;
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly ModGroupEditor _editor;
|
||||
private readonly ModSelection _selection;
|
||||
private readonly DuplicateManager _duplicates;
|
||||
private readonly ModManager _mods;
|
||||
private readonly ModCreator _creator;
|
||||
|
||||
public Mod? MergeFromMod
|
||||
=> _selector.Selected;
|
||||
=> _selection.Mod;
|
||||
|
||||
public Mod? MergeToMod;
|
||||
public string OptionGroupName = "Merges";
|
||||
|
|
@ -41,23 +40,23 @@ public class ModMerger : IDisposable, IService
|
|||
public readonly IReadOnlyList<string> Warnings = new List<string>();
|
||||
public Exception? Error { get; private set; }
|
||||
|
||||
public ModMerger(ModManager mods, ModGroupEditor editor, ModFileSystemSelector selector, DuplicateManager duplicates,
|
||||
public ModMerger(ModManager mods, ModGroupEditor editor, ModSelection selection, DuplicateManager duplicates,
|
||||
CommunicatorService communicator, ModCreator creator, Configuration config)
|
||||
{
|
||||
_editor = editor;
|
||||
_selector = selector;
|
||||
_duplicates = duplicates;
|
||||
_communicator = communicator;
|
||||
_creator = creator;
|
||||
_config = config;
|
||||
_mods = mods;
|
||||
_selector.SelectionChanged += OnSelectionChange;
|
||||
_editor = editor;
|
||||
_selection = selection;
|
||||
_duplicates = duplicates;
|
||||
_communicator = communicator;
|
||||
_creator = creator;
|
||||
_config = config;
|
||||
_mods = mods;
|
||||
_selection.Subscribe(OnSelectionChange, ModSelection.Priority.ModMerger);
|
||||
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModMerger);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_selector.SelectionChanged -= OnSelectionChange;
|
||||
_selection.Unsubscribe(OnSelectionChange);
|
||||
_communicator.ModPathChanged.Unsubscribe(OnModPathChange);
|
||||
}
|
||||
|
||||
|
|
@ -257,7 +256,7 @@ public class ModMerger : IDisposable, IService
|
|||
if (dir == null)
|
||||
throw new Exception($"Could not split off mods, unable to create new mod with name {modName}.");
|
||||
|
||||
_mods.AddMod(dir);
|
||||
_mods.AddMod(dir, false);
|
||||
result = _mods[^1];
|
||||
if (mods.Count == 1)
|
||||
{
|
||||
|
|
@ -390,7 +389,7 @@ public class ModMerger : IDisposable, IService
|
|||
}
|
||||
}
|
||||
|
||||
private void OnSelectionChange(Mod? oldSelection, Mod? newSelection, in ModFileSystemSelector.ModState state)
|
||||
private void OnSelectionChange(Mod? oldSelection, Mod? newSelection)
|
||||
{
|
||||
if (OptionGroupName == "Merges" && OptionName.Length == 0 || OptionName == oldSelection?.Name.Text)
|
||||
OptionName = newSelection?.Name.Text ?? string.Empty;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
using System.Collections.Frozen;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Meta;
|
||||
using Penumbra.Meta.Files;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.Mods.Manager.OptionEditor;
|
||||
using Penumbra.Mods.SubMods;
|
||||
|
||||
namespace Penumbra.Mods.Editor;
|
||||
|
||||
public class ModMetaEditor(ModManager modManager) : MetaDictionary, IService
|
||||
public class ModMetaEditor(
|
||||
ModGroupEditor groupEditor,
|
||||
MetaFileManager metaFileManager,
|
||||
ImcChecker imcChecker) : MetaDictionary, IService
|
||||
{
|
||||
public sealed class OtherOptionData : HashSet<string>
|
||||
{
|
||||
|
|
@ -62,12 +67,111 @@ public class ModMetaEditor(ModManager modManager) : MetaDictionary, IService
|
|||
Changes = false;
|
||||
}
|
||||
|
||||
public static bool DeleteDefaultValues(MetaFileManager metaFileManager, ImcChecker imcChecker, MetaDictionary dict)
|
||||
{
|
||||
var clone = dict.Clone();
|
||||
dict.Clear();
|
||||
var count = 0;
|
||||
foreach (var (key, value) in clone.Imc)
|
||||
{
|
||||
var defaultEntry = imcChecker.GetDefaultEntry(key, false);
|
||||
if (!defaultEntry.Entry.Equals(value))
|
||||
{
|
||||
dict.TryAdd(key, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}.");
|
||||
++count;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (key, value) in clone.Eqp)
|
||||
{
|
||||
var defaultEntry = new EqpEntryInternal(ExpandedEqpFile.GetDefault(metaFileManager, key.SetId), key.Slot);
|
||||
if (!defaultEntry.Equals(value))
|
||||
{
|
||||
dict.TryAdd(key, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}.");
|
||||
++count;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (key, value) in clone.Eqdp)
|
||||
{
|
||||
var defaultEntry = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(metaFileManager, key), key.Slot);
|
||||
if (!defaultEntry.Equals(value))
|
||||
{
|
||||
dict.TryAdd(key, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}.");
|
||||
++count;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (key, value) in clone.Est)
|
||||
{
|
||||
var defaultEntry = EstFile.GetDefault(metaFileManager, key);
|
||||
if (!defaultEntry.Equals(value))
|
||||
{
|
||||
dict.TryAdd(key, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}.");
|
||||
++count;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (key, value) in clone.Gmp)
|
||||
{
|
||||
var defaultEntry = ExpandedGmpFile.GetDefault(metaFileManager, key);
|
||||
if (!defaultEntry.Equals(value))
|
||||
{
|
||||
dict.TryAdd(key, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}.");
|
||||
++count;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (key, value) in clone.Rsp)
|
||||
{
|
||||
var defaultEntry = CmpFile.GetDefault(metaFileManager, key.SubRace, key.Attribute);
|
||||
if (!defaultEntry.Equals(value))
|
||||
{
|
||||
dict.TryAdd(key, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}.");
|
||||
++count;
|
||||
}
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
return false;
|
||||
|
||||
Penumbra.Log.Debug($"Deleted {count} default-valued meta-entries from a mod option.");
|
||||
return true;
|
||||
}
|
||||
|
||||
public void DeleteDefaultValues()
|
||||
=> Changes = DeleteDefaultValues(metaFileManager, imcChecker, this);
|
||||
|
||||
public void Apply(IModDataContainer container)
|
||||
{
|
||||
if (!Changes)
|
||||
return;
|
||||
|
||||
modManager.OptionEditor.SetManipulations(container, this);
|
||||
groupEditor.SetManipulations(container, this);
|
||||
Changes = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ public class ModNormalizer(ModManager modManager, Configuration config, SaveServ
|
|||
if (!config.AutoReduplicateUiOnImport)
|
||||
return;
|
||||
|
||||
if (modManager.Creator.LoadMod(modDirectory, false) is not { } mod)
|
||||
if (modManager.Creator.LoadMod(modDirectory, false, false) is not { } mod)
|
||||
return;
|
||||
|
||||
Dictionary<FullPath, List<(IModDataContainer, Utf8GamePath)>> paths = [];
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ public class ModImportManager(ModManager modManager, Configuration config, ModEd
|
|||
return false;
|
||||
}
|
||||
|
||||
modManager.AddMod(directory);
|
||||
modManager.AddMod(directory, true);
|
||||
mod = modManager.LastOrDefault();
|
||||
return mod != null && mod.ModPath == directory;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,13 +81,13 @@ public sealed class ModManager : ModStorage, IDisposable, IService
|
|||
}
|
||||
|
||||
/// <summary> Load a new mod and add it to the manager if successful. </summary>
|
||||
public void AddMod(DirectoryInfo modFolder)
|
||||
public void AddMod(DirectoryInfo modFolder, bool deleteDefaultMeta)
|
||||
{
|
||||
if (this.Any(m => m.ModPath.Name == modFolder.Name))
|
||||
return;
|
||||
|
||||
Creator.SplitMultiGroups(modFolder);
|
||||
var mod = Creator.LoadMod(modFolder, true);
|
||||
var mod = Creator.LoadMod(modFolder, true, deleteDefaultMeta);
|
||||
if (mod == null)
|
||||
return;
|
||||
|
||||
|
|
@ -141,7 +141,7 @@ public sealed class ModManager : ModStorage, IDisposable, IService
|
|||
var oldName = mod.Name;
|
||||
|
||||
_communicator.ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath);
|
||||
if (!Creator.ReloadMod(mod, true, out var metaChange))
|
||||
if (!Creator.ReloadMod(mod, true, false, out var metaChange))
|
||||
{
|
||||
Penumbra.Log.Warning(mod.Name.Length == 0
|
||||
? $"Reloading mod {oldName} has failed, new name is empty. Removing from loaded mods instead."
|
||||
|
|
@ -206,7 +206,7 @@ public sealed class ModManager : ModStorage, IDisposable, IService
|
|||
|
||||
dir.Refresh();
|
||||
mod.ModPath = dir;
|
||||
if (!Creator.ReloadMod(mod, false, out var metaChange))
|
||||
if (!Creator.ReloadMod(mod, false, false, out var metaChange))
|
||||
{
|
||||
Penumbra.Log.Error($"Error reloading moved mod {mod.Name}.");
|
||||
return;
|
||||
|
|
@ -332,7 +332,7 @@ public sealed class ModManager : ModStorage, IDisposable, IService
|
|||
var queue = new ConcurrentQueue<Mod>();
|
||||
Parallel.ForEach(BasePath.EnumerateDirectories(), options, dir =>
|
||||
{
|
||||
var mod = Creator.LoadMod(dir, false);
|
||||
var mod = Creator.LoadMod(dir, false, false);
|
||||
if (mod != null)
|
||||
queue.Enqueue(mod);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ public static partial class ModMigration
|
|||
foreach (var (gamePath, swapPath) in swaps)
|
||||
mod.Default.FileSwaps.Add(gamePath, swapPath);
|
||||
|
||||
creator.IncorporateMetaChanges(mod.Default, mod.ModPath, true);
|
||||
creator.IncorporateMetaChanges(mod.Default, mod.ModPath, true, true);
|
||||
foreach (var group in mod.Groups)
|
||||
saveService.ImmediateSave(new ModSaveGroup(group, creator.Config.ReplaceNonAsciiOnImport));
|
||||
|
||||
|
|
@ -182,7 +182,7 @@ public static partial class ModMigration
|
|||
Description = option.OptionDesc,
|
||||
};
|
||||
AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles);
|
||||
creator.IncorporateMetaChanges(subMod, mod.ModPath, false);
|
||||
creator.IncorporateMetaChanges(subMod, mod.ModPath, false, true);
|
||||
return subMod;
|
||||
}
|
||||
|
||||
|
|
@ -196,7 +196,7 @@ public static partial class ModMigration
|
|||
Priority = priority,
|
||||
};
|
||||
AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles);
|
||||
creator.IncorporateMetaChanges(subMod, mod.ModPath, false);
|
||||
creator.IncorporateMetaChanges(subMod, mod.ModPath, false, true);
|
||||
return subMod;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ using OtterGui.Classes;
|
|||
using OtterGui.Filesystem;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Meta;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods.Groups;
|
||||
using Penumbra.Mods.Settings;
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ public class ModGroupEditor(
|
|||
ImcModGroupEditor imcEditor,
|
||||
CommunicatorService communicator,
|
||||
SaveService saveService,
|
||||
Configuration Config) : IService
|
||||
Configuration config) : IService
|
||||
{
|
||||
public SingleModGroupEditor SingleEditor
|
||||
=> singleEditor;
|
||||
|
|
@ -57,7 +57,7 @@ public class ModGroupEditor(
|
|||
return;
|
||||
|
||||
group.DefaultSettings = defaultOption;
|
||||
saveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
|
||||
saveService.QueueSave(new ModSaveGroup(group, config.ReplaceNonAsciiOnImport));
|
||||
communicator.ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, group.Mod, group, null, null, -1);
|
||||
}
|
||||
|
||||
|
|
@ -68,9 +68,9 @@ public class ModGroupEditor(
|
|||
if (oldName == newName || !VerifyFileName(group.Mod, group, newName, true))
|
||||
return;
|
||||
|
||||
saveService.ImmediateDelete(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
|
||||
saveService.ImmediateDelete(new ModSaveGroup(group, config.ReplaceNonAsciiOnImport));
|
||||
group.Name = newName;
|
||||
saveService.ImmediateSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
|
||||
saveService.ImmediateSave(new ModSaveGroup(group, config.ReplaceNonAsciiOnImport));
|
||||
communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, group.Mod, group, null, null, -1);
|
||||
}
|
||||
|
||||
|
|
@ -81,7 +81,7 @@ public class ModGroupEditor(
|
|||
var idx = group.GetIndex();
|
||||
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, group, null, null, -1);
|
||||
mod.Groups.RemoveAt(idx);
|
||||
saveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport);
|
||||
saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport);
|
||||
communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, null, null, null, idx);
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +93,7 @@ public class ModGroupEditor(
|
|||
if (!mod.Groups.Move(idxFrom, groupIdxTo))
|
||||
return;
|
||||
|
||||
saveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport);
|
||||
saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport);
|
||||
communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, group, null, null, idxFrom);
|
||||
}
|
||||
|
||||
|
|
@ -104,7 +104,7 @@ public class ModGroupEditor(
|
|||
return;
|
||||
|
||||
group.Priority = newPriority;
|
||||
saveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
|
||||
saveService.QueueSave(new ModSaveGroup(group, config.ReplaceNonAsciiOnImport));
|
||||
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, group.Mod, group, null, null, -1);
|
||||
}
|
||||
|
||||
|
|
@ -115,7 +115,7 @@ public class ModGroupEditor(
|
|||
return;
|
||||
|
||||
group.Description = newDescription;
|
||||
saveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
|
||||
saveService.QueueSave(new ModSaveGroup(group, config.ReplaceNonAsciiOnImport));
|
||||
communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, group.Mod, group, null, null, -1);
|
||||
}
|
||||
|
||||
|
|
@ -126,7 +126,7 @@ public class ModGroupEditor(
|
|||
return;
|
||||
|
||||
option.Name = newName;
|
||||
saveService.QueueSave(new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport));
|
||||
saveService.QueueSave(new ModSaveGroup(option.Group, config.ReplaceNonAsciiOnImport));
|
||||
communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, option.Mod, option.Group, option, null, -1);
|
||||
}
|
||||
|
||||
|
|
@ -137,7 +137,7 @@ public class ModGroupEditor(
|
|||
return;
|
||||
|
||||
option.Description = newDescription;
|
||||
saveService.QueueSave(new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport));
|
||||
saveService.QueueSave(new ModSaveGroup(option.Group, config.ReplaceNonAsciiOnImport));
|
||||
communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, option.Mod, option.Group, option, null, -1);
|
||||
}
|
||||
|
||||
|
|
@ -149,7 +149,7 @@ public class ModGroupEditor(
|
|||
|
||||
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1);
|
||||
subMod.Manipulations.SetTo(manipulations);
|
||||
saveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport));
|
||||
saveService.Save(saveType, new ModSaveGroup(subMod, config.ReplaceNonAsciiOnImport));
|
||||
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1);
|
||||
}
|
||||
|
||||
|
|
@ -161,13 +161,13 @@ public class ModGroupEditor(
|
|||
|
||||
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1);
|
||||
subMod.Files.SetTo(replacements);
|
||||
saveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport));
|
||||
saveService.Save(saveType, new ModSaveGroup(subMod, config.ReplaceNonAsciiOnImport));
|
||||
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1);
|
||||
}
|
||||
|
||||
/// <summary> Forces a file save of the given container's group. </summary>
|
||||
public void ForceSave(IModDataContainer subMod, SaveType saveType = SaveType.Queue)
|
||||
=> saveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport));
|
||||
=> saveService.Save(saveType, new ModSaveGroup(subMod, config.ReplaceNonAsciiOnImport));
|
||||
|
||||
/// <summary> Add additional file redirections to a given option, keeping already existing ones. Only fires an event if anything is actually added.</summary>
|
||||
public void AddFiles(IModDataContainer subMod, IReadOnlyDictionary<Utf8GamePath, FullPath> additions)
|
||||
|
|
@ -176,7 +176,7 @@ public class ModGroupEditor(
|
|||
subMod.Files.AddFrom(additions);
|
||||
if (oldCount != subMod.Files.Count)
|
||||
{
|
||||
saveService.QueueSave(new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport));
|
||||
saveService.QueueSave(new ModSaveGroup(subMod, config.ReplaceNonAsciiOnImport));
|
||||
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, (Mod)subMod.Mod, subMod.Group, null, subMod, -1);
|
||||
}
|
||||
}
|
||||
|
|
@ -189,7 +189,7 @@ public class ModGroupEditor(
|
|||
|
||||
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1);
|
||||
subMod.FileSwaps.SetTo(swaps);
|
||||
saveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport));
|
||||
saveService.Save(saveType, new ModSaveGroup(subMod, config.ReplaceNonAsciiOnImport));
|
||||
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ using Penumbra.GameData.Data;
|
|||
using Penumbra.Import;
|
||||
using Penumbra.Import.Structs;
|
||||
using Penumbra.Meta;
|
||||
using Penumbra.Mods.Editor;
|
||||
using Penumbra.Mods.Groups;
|
||||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.Mods.Settings;
|
||||
|
|
@ -20,11 +21,12 @@ using Penumbra.String.Classes;
|
|||
namespace Penumbra.Mods;
|
||||
|
||||
public partial class ModCreator(
|
||||
SaveService _saveService,
|
||||
SaveService saveService,
|
||||
Configuration config,
|
||||
ModDataEditor _dataEditor,
|
||||
MetaFileManager _metaFileManager,
|
||||
GamePathParser _gamePathParser) : IService
|
||||
ModDataEditor dataEditor,
|
||||
MetaFileManager metaFileManager,
|
||||
GamePathParser gamePathParser,
|
||||
ImcChecker imcChecker) : IService
|
||||
{
|
||||
public readonly Configuration Config = config;
|
||||
|
||||
|
|
@ -34,7 +36,7 @@ public partial class ModCreator(
|
|||
try
|
||||
{
|
||||
var newDir = CreateModFolder(basePath, newName, Config.ReplaceNonAsciiOnImport, true);
|
||||
_dataEditor.CreateMeta(newDir, newName, Config.DefaultModAuthor, description, "1.0", string.Empty);
|
||||
dataEditor.CreateMeta(newDir, newName, Config.DefaultModAuthor, description, "1.0", string.Empty);
|
||||
CreateDefaultFiles(newDir);
|
||||
return newDir;
|
||||
}
|
||||
|
|
@ -46,7 +48,7 @@ public partial class ModCreator(
|
|||
}
|
||||
|
||||
/// <summary> Load a mod by its directory. </summary>
|
||||
public Mod? LoadMod(DirectoryInfo modPath, bool incorporateMetaChanges)
|
||||
public Mod? LoadMod(DirectoryInfo modPath, bool incorporateMetaChanges, bool deleteDefaultMetaChanges)
|
||||
{
|
||||
modPath.Refresh();
|
||||
if (!modPath.Exists)
|
||||
|
|
@ -56,7 +58,7 @@ public partial class ModCreator(
|
|||
}
|
||||
|
||||
var mod = new Mod(modPath);
|
||||
if (ReloadMod(mod, incorporateMetaChanges, out _))
|
||||
if (ReloadMod(mod, incorporateMetaChanges, deleteDefaultMetaChanges, out _))
|
||||
return mod;
|
||||
|
||||
// Can not be base path not existing because that is checked before.
|
||||
|
|
@ -65,21 +67,29 @@ public partial class ModCreator(
|
|||
}
|
||||
|
||||
/// <summary> Reload a mod from its mod path. </summary>
|
||||
public bool ReloadMod(Mod mod, bool incorporateMetaChanges, out ModDataChangeType modDataChange)
|
||||
public bool ReloadMod(Mod mod, bool incorporateMetaChanges, bool deleteDefaultMetaChanges, out ModDataChangeType modDataChange)
|
||||
{
|
||||
modDataChange = ModDataChangeType.Deletion;
|
||||
if (!Directory.Exists(mod.ModPath.FullName))
|
||||
return false;
|
||||
|
||||
modDataChange = _dataEditor.LoadMeta(this, mod);
|
||||
modDataChange = dataEditor.LoadMeta(this, mod);
|
||||
if (modDataChange.HasFlag(ModDataChangeType.Deletion) || mod.Name.Length == 0)
|
||||
return false;
|
||||
|
||||
_dataEditor.LoadLocalData(mod);
|
||||
dataEditor.LoadLocalData(mod);
|
||||
LoadDefaultOption(mod);
|
||||
LoadAllGroups(mod);
|
||||
if (incorporateMetaChanges)
|
||||
IncorporateAllMetaChanges(mod, true);
|
||||
if (deleteDefaultMetaChanges && !Config.KeepDefaultMetaChanges)
|
||||
{
|
||||
foreach (var container in mod.AllDataContainers)
|
||||
{
|
||||
if (ModMetaEditor.DeleteDefaultValues(metaFileManager, imcChecker, container.Manipulations))
|
||||
saveService.ImmediateSaveSync(new ModSaveGroup(container, Config.ReplaceNonAsciiOnImport));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -89,13 +99,13 @@ public partial class ModCreator(
|
|||
{
|
||||
mod.Groups.Clear();
|
||||
var changes = false;
|
||||
foreach (var file in _saveService.FileNames.GetOptionGroupFiles(mod))
|
||||
foreach (var file in saveService.FileNames.GetOptionGroupFiles(mod))
|
||||
{
|
||||
var group = LoadModGroup(mod, file);
|
||||
if (group != null && mod.Groups.All(g => g.Name != group.Name))
|
||||
{
|
||||
changes = changes
|
||||
|| _saveService.FileNames.OptionGroupFile(mod.ModPath.FullName, mod.Groups.Count, group.Name, true)
|
||||
|| saveService.FileNames.OptionGroupFile(mod.ModPath.FullName, mod.Groups.Count, group.Name, true)
|
||||
!= Path.Combine(file.DirectoryName!, ReplaceBadXivSymbols(file.Name, true));
|
||||
mod.Groups.Add(group);
|
||||
}
|
||||
|
|
@ -106,13 +116,13 @@ public partial class ModCreator(
|
|||
}
|
||||
|
||||
if (changes)
|
||||
_saveService.SaveAllOptionGroups(mod, true, Config.ReplaceNonAsciiOnImport);
|
||||
saveService.SaveAllOptionGroups(mod, true, Config.ReplaceNonAsciiOnImport);
|
||||
}
|
||||
|
||||
/// <summary> Load the default option for a given mod.</summary>
|
||||
public void LoadDefaultOption(Mod mod)
|
||||
{
|
||||
var defaultFile = _saveService.FileNames.OptionGroupFile(mod, -1, Config.ReplaceNonAsciiOnImport);
|
||||
var defaultFile = saveService.FileNames.OptionGroupFile(mod, -1, Config.ReplaceNonAsciiOnImport);
|
||||
try
|
||||
{
|
||||
var jObject = File.Exists(defaultFile) ? JObject.Parse(File.ReadAllText(defaultFile)) : new JObject();
|
||||
|
|
@ -157,7 +167,7 @@ public partial class ModCreator(
|
|||
List<string> deleteList = new();
|
||||
foreach (var subMod in mod.AllDataContainers)
|
||||
{
|
||||
var (localChanges, localDeleteList) = IncorporateMetaChanges(subMod, mod.ModPath, false);
|
||||
var (localChanges, localDeleteList) = IncorporateMetaChanges(subMod, mod.ModPath, false, true);
|
||||
changes |= localChanges;
|
||||
if (delete)
|
||||
deleteList.AddRange(localDeleteList);
|
||||
|
|
@ -168,8 +178,8 @@ public partial class ModCreator(
|
|||
if (!changes)
|
||||
return;
|
||||
|
||||
_saveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport);
|
||||
_saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, mod.Default, Config.ReplaceNonAsciiOnImport));
|
||||
saveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport);
|
||||
saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, mod.Default, Config.ReplaceNonAsciiOnImport));
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -177,7 +187,7 @@ public partial class ModCreator(
|
|||
/// If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod.
|
||||
/// If delete is true, the files are deleted afterwards.
|
||||
/// </summary>
|
||||
public (bool Changes, List<string> DeleteList) IncorporateMetaChanges(IModDataContainer option, DirectoryInfo basePath, bool delete)
|
||||
public (bool Changes, List<string> DeleteList) IncorporateMetaChanges(IModDataContainer option, DirectoryInfo basePath, bool delete, bool deleteDefault)
|
||||
{
|
||||
var deleteList = new List<string>();
|
||||
var oldSize = option.Manipulations.Count;
|
||||
|
|
@ -194,7 +204,7 @@ public partial class ModCreator(
|
|||
if (!file.Exists)
|
||||
continue;
|
||||
|
||||
var meta = new TexToolsMeta(_metaFileManager, _gamePathParser, File.ReadAllBytes(file.FullName),
|
||||
var meta = new TexToolsMeta(metaFileManager, gamePathParser, File.ReadAllBytes(file.FullName),
|
||||
Config.KeepDefaultMetaChanges);
|
||||
Penumbra.Log.Verbose(
|
||||
$"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}");
|
||||
|
|
@ -207,7 +217,7 @@ public partial class ModCreator(
|
|||
if (!file.Exists)
|
||||
continue;
|
||||
|
||||
var rgsp = TexToolsMeta.FromRgspFile(_metaFileManager, file.FullName, File.ReadAllBytes(file.FullName),
|
||||
var rgsp = TexToolsMeta.FromRgspFile(metaFileManager, file.FullName, File.ReadAllBytes(file.FullName),
|
||||
Config.KeepDefaultMetaChanges);
|
||||
Penumbra.Log.Verbose(
|
||||
$"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}");
|
||||
|
|
@ -223,7 +233,11 @@ public partial class ModCreator(
|
|||
}
|
||||
|
||||
DeleteDeleteList(deleteList, delete);
|
||||
return (oldSize < option.Manipulations.Count, deleteList);
|
||||
var changes = oldSize < option.Manipulations.Count;
|
||||
if (deleteDefault && !Config.KeepDefaultMetaChanges)
|
||||
changes |= ModMetaEditor.DeleteDefaultValues(metaFileManager, imcChecker, option.Manipulations);
|
||||
|
||||
return (changes, deleteList);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -250,7 +264,7 @@ public partial class ModCreator(
|
|||
group.Priority = priority;
|
||||
group.DefaultSettings = defaultSettings;
|
||||
group.OptionData.AddRange(subMods.Select(s => s.Clone(group)));
|
||||
_saveService.ImmediateSaveSync(ModSaveGroup.WithoutMod(baseFolder, group, index, Config.ReplaceNonAsciiOnImport));
|
||||
saveService.ImmediateSaveSync(ModSaveGroup.WithoutMod(baseFolder, group, index, Config.ReplaceNonAsciiOnImport));
|
||||
break;
|
||||
}
|
||||
case GroupType.Single:
|
||||
|
|
@ -260,7 +274,7 @@ public partial class ModCreator(
|
|||
group.Priority = priority;
|
||||
group.DefaultSettings = defaultSettings;
|
||||
group.OptionData.AddRange(subMods.Select(s => s.ConvertToSingle(group)));
|
||||
_saveService.ImmediateSaveSync(ModSaveGroup.WithoutMod(baseFolder, group, index, Config.ReplaceNonAsciiOnImport));
|
||||
saveService.ImmediateSaveSync(ModSaveGroup.WithoutMod(baseFolder, group, index, Config.ReplaceNonAsciiOnImport));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -277,7 +291,8 @@ public partial class ModCreator(
|
|||
foreach (var (_, gamePath, file) in list)
|
||||
mod.Files.TryAdd(gamePath, file);
|
||||
|
||||
IncorporateMetaChanges(mod, baseFolder, true);
|
||||
IncorporateMetaChanges(mod, baseFolder, true, true);
|
||||
|
||||
return mod;
|
||||
}
|
||||
|
||||
|
|
@ -288,15 +303,15 @@ public partial class ModCreator(
|
|||
internal void CreateDefaultFiles(DirectoryInfo directory)
|
||||
{
|
||||
var mod = new Mod(directory);
|
||||
ReloadMod(mod, false, out _);
|
||||
ReloadMod(mod, false, false, out _);
|
||||
foreach (var file in mod.FindUnusedFiles())
|
||||
{
|
||||
if (Utf8GamePath.FromFile(new FileInfo(file.FullName), directory, out var gamePath))
|
||||
mod.Default.Files.TryAdd(gamePath, file);
|
||||
}
|
||||
|
||||
IncorporateMetaChanges(mod.Default, directory, true);
|
||||
_saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, mod.Default, Config.ReplaceNonAsciiOnImport));
|
||||
IncorporateMetaChanges(mod.Default, directory, true, true);
|
||||
saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, mod.Default, Config.ReplaceNonAsciiOnImport));
|
||||
}
|
||||
|
||||
/// <summary> Return the name of a new valid directory based on the base directory and the given name. </summary>
|
||||
|
|
@ -333,7 +348,7 @@ public partial class ModCreator(
|
|||
{
|
||||
var mod = new Mod(baseDir);
|
||||
|
||||
var files = _saveService.FileNames.GetOptionGroupFiles(mod).ToList();
|
||||
var files = saveService.FileNames.GetOptionGroupFiles(mod).ToList();
|
||||
var idx = 0;
|
||||
var reorder = false;
|
||||
foreach (var groupFile in files)
|
||||
|
|
|
|||
104
Penumbra/Mods/ModSelection.cs
Normal file
104
Penumbra/Mods/ModSelection.cs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
using OtterGui.Classes;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.Communication;
|
||||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.Mods.Settings;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
/// <summary>
|
||||
/// Triggered whenever the selected mod changes
|
||||
/// <list type="number">
|
||||
/// <item>Parameter is the old selected mod. </item>
|
||||
/// <item>Parameter is the new selected mod </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public class ModSelection : EventWrapper<Mod?, Mod?, ModSelection.Priority>
|
||||
{
|
||||
private readonly ActiveCollections _collections;
|
||||
private readonly EphemeralConfig _config;
|
||||
private readonly CommunicatorService _communicator;
|
||||
|
||||
public ModSelection(CommunicatorService communicator, ModManager mods, ActiveCollections collections, EphemeralConfig config)
|
||||
: base(nameof(ModSelection))
|
||||
{
|
||||
_communicator = communicator;
|
||||
_collections = collections;
|
||||
_config = config;
|
||||
if (_config.LastModPath.Length > 0)
|
||||
SelectMod(mods.FirstOrDefault(m => string.Equals(m.Identifier, config.LastModPath, StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
_communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.ModSelection);
|
||||
_communicator.CollectionInheritanceChanged.Subscribe(OnInheritanceChange, CollectionInheritanceChanged.Priority.ModSelection);
|
||||
_communicator.ModSettingChanged.Subscribe(OnSettingChange, ModSettingChanged.Priority.ModSelection);
|
||||
}
|
||||
|
||||
public ModSettings Settings { get; private set; } = ModSettings.Empty;
|
||||
public ModCollection Collection { get; private set; } = ModCollection.Empty;
|
||||
public Mod? Mod { get; private set; }
|
||||
|
||||
|
||||
public void SelectMod(Mod? mod)
|
||||
{
|
||||
if (mod == Mod)
|
||||
return;
|
||||
|
||||
var oldMod = Mod;
|
||||
Mod = mod;
|
||||
OnCollectionChange(CollectionType.Current, null, _collections.Current, string.Empty);
|
||||
Invoke(oldMod, Mod);
|
||||
_config.LastModPath = mod?.ModPath.Name ?? string.Empty;
|
||||
_config.Save();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool _)
|
||||
{
|
||||
_communicator.CollectionChange.Unsubscribe(OnCollectionChange);
|
||||
_communicator.CollectionInheritanceChanged.Unsubscribe(OnInheritanceChange);
|
||||
_communicator.ModSettingChanged.Unsubscribe(OnSettingChange);
|
||||
}
|
||||
|
||||
private void OnCollectionChange(CollectionType type, ModCollection? oldCollection, ModCollection? newCollection, string _2)
|
||||
{
|
||||
if (type is CollectionType.Current && oldCollection != newCollection)
|
||||
UpdateSettings();
|
||||
}
|
||||
|
||||
private void OnSettingChange(ModCollection collection, ModSettingChange _1, Mod? mod, Setting _2, int _3, bool _4)
|
||||
{
|
||||
if (collection == _collections.Current && mod == Mod)
|
||||
UpdateSettings();
|
||||
}
|
||||
|
||||
private void OnInheritanceChange(ModCollection collection, bool arg2)
|
||||
{
|
||||
if (collection == _collections.Current)
|
||||
UpdateSettings();
|
||||
}
|
||||
|
||||
private void UpdateSettings()
|
||||
{
|
||||
if (Mod == null)
|
||||
{
|
||||
Settings = ModSettings.Empty;
|
||||
Collection = ModCollection.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
(var settings, Collection) = _collections.Current[Mod.Index];
|
||||
Settings = settings ?? ModSettings.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="UI.ModsTab.ModPanel.OnSelectionChange"/>
|
||||
ModPanel = 0,
|
||||
|
||||
/// <seealso cref="Editor.ModMerger.OnSelectionChange"/>
|
||||
ModMerger = 0,
|
||||
}
|
||||
}
|
||||
|
|
@ -97,7 +97,7 @@ public class TemporaryMod : IMod
|
|||
defaultMod.Manipulations.UnionWith(manips);
|
||||
|
||||
saveService.ImmediateSaveSync(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport));
|
||||
modManager.AddMod(dir);
|
||||
modManager.AddMod(dir, false);
|
||||
Penumbra.Log.Information(
|
||||
$"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Identifier}.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ public class Penumbra : IDalamudPlugin
|
|||
var itemSheet = _services.GetService<IDataManager>().GetExcelSheet<Item>()!;
|
||||
_communicatorService.ChangedItemHover.Subscribe(it =>
|
||||
{
|
||||
if (it is IdentifiedItem)
|
||||
if (it is IdentifiedItem { Item.Id.IsItem: true })
|
||||
ImGui.TextUnformatted("Left Click to create an item link in chat.");
|
||||
}, ChangedItemHover.Priority.Link);
|
||||
|
||||
|
|
|
|||
|
|
@ -86,11 +86,13 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="EmbedIO" Version="3.4.3" />
|
||||
<!-- This reference is only there to silence a vulnerability warning caused by transitive inclusion of a lower version through PeNet and System.Security.Cryptography.Pkcs. -->
|
||||
<PackageReference Include="System.Formats.Asn1" Version="8.0.1" />
|
||||
<PackageReference Include="EmbedIO" Version="3.5.2" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
|
||||
<PackageReference Include="SharpCompress" Version="0.33.0" />
|
||||
<PackageReference Include="SharpGLTF.Core" Version="1.0.0-alpha0030" />
|
||||
<PackageReference Include="SharpGLTF.Toolkit" Version="1.0.0-alpha0030" />
|
||||
<PackageReference Include="SharpCompress" Version="0.37.2" />
|
||||
<PackageReference Include="SharpGLTF.Core" Version="1.0.1" />
|
||||
<PackageReference Include="SharpGLTF.Toolkit" Version="1.0.1" />
|
||||
<PackageReference Include="PeNet" Version="4.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ public class MigrationManager(Configuration config) : IService
|
|||
return;
|
||||
}
|
||||
|
||||
var path = Path.Combine(directory, reader.Entry.Key);
|
||||
var path = Path.Combine(directory, reader.Entry.Key!);
|
||||
using var s = new MemoryStream();
|
||||
using var e = reader.OpenEntryStream();
|
||||
e.CopyTo(s);
|
||||
|
|
|
|||
|
|
@ -281,7 +281,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService
|
|||
if (newDir == null)
|
||||
return;
|
||||
|
||||
_modManager.AddMod(newDir);
|
||||
_modManager.AddMod(newDir, false);
|
||||
var mod = _modManager[^1];
|
||||
if (!_swapData.WriteMod(_modManager, mod, mod.Default,
|
||||
_useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps))
|
||||
|
|
|
|||
|
|
@ -61,7 +61,13 @@ public sealed class EqdpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFil
|
|||
}
|
||||
|
||||
protected override IEnumerable<(EqdpIdentifier, EqdpEntryInternal)> Enumerate()
|
||||
=> Editor.Eqdp.Select(kvp => (kvp.Key, kvp.Value));
|
||||
=> Editor.Eqdp.OrderBy(kvp => kvp.Key.SetId.Id)
|
||||
.ThenBy(kvp => kvp.Key.GenderRace)
|
||||
.ThenBy(kvp => kvp.Key.Slot)
|
||||
.Select(kvp => (kvp.Key, kvp.Value));
|
||||
|
||||
protected override int Count
|
||||
=> Editor.Eqdp.Count;
|
||||
|
||||
private static bool DrawIdentifierInput(ref EqdpIdentifier identifier)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -59,7 +59,13 @@ public sealed class EqpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
|
|||
}
|
||||
|
||||
protected override IEnumerable<(EqpIdentifier, EqpEntryInternal)> Enumerate()
|
||||
=> Editor.Eqp.Select(kvp => (kvp.Key, kvp.Value));
|
||||
=> Editor.Eqp
|
||||
.OrderBy(kvp => kvp.Key.SetId.Id)
|
||||
.ThenBy(kvp => kvp.Key.Slot)
|
||||
.Select(kvp => (kvp.Key, kvp.Value));
|
||||
|
||||
protected override int Count
|
||||
=> Editor.Eqp.Count;
|
||||
|
||||
private static bool DrawIdentifierInput(ref EqpIdentifier identifier)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -58,7 +58,14 @@ public sealed class EstMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
|
|||
}
|
||||
|
||||
protected override IEnumerable<(EstIdentifier, EstEntry)> Enumerate()
|
||||
=> Editor.Est.Select(kvp => (kvp.Key, kvp.Value));
|
||||
=> Editor.Est
|
||||
.OrderBy(kvp => kvp.Key.SetId.Id)
|
||||
.ThenBy(kvp => kvp.Key.GenderRace)
|
||||
.ThenBy(kvp => kvp.Key.Slot)
|
||||
.Select(kvp => (kvp.Key, kvp.Value));
|
||||
|
||||
protected override int Count
|
||||
=> Editor.Est.Count;
|
||||
|
||||
private static bool DrawIdentifierInput(ref EstIdentifier identifier)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -47,7 +47,13 @@ public sealed class GlobalEqpMetaDrawer(ModMetaEditor editor, MetaFileManager me
|
|||
}
|
||||
|
||||
protected override IEnumerable<(GlobalEqpManipulation, byte)> Enumerate()
|
||||
=> Editor.GlobalEqp.Select(identifier => (identifier, (byte)0));
|
||||
=> Editor.GlobalEqp
|
||||
.OrderBy(identifier => identifier.Type)
|
||||
.ThenBy(identifier => identifier.Condition.Id)
|
||||
.Select(identifier => (identifier, (byte)0));
|
||||
|
||||
protected override int Count
|
||||
=> Editor.GlobalEqp.Count;
|
||||
|
||||
private static void DrawIdentifierInput(ref GlobalEqpManipulation identifier)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -57,7 +57,12 @@ public sealed class GmpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
|
|||
}
|
||||
|
||||
protected override IEnumerable<(GmpIdentifier, GmpEntry)> Enumerate()
|
||||
=> Editor.Gmp.Select(kvp => (kvp.Key, kvp.Value));
|
||||
=> Editor.Gmp
|
||||
.OrderBy(kvp => kvp.Key.SetId.Id)
|
||||
.Select(kvp => (kvp.Key, kvp.Value));
|
||||
|
||||
protected override int Count
|
||||
=> Editor.Gmp.Count;
|
||||
|
||||
private static bool DrawIdentifierInput(ref GmpIdentifier identifier)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -140,7 +140,17 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
|
|||
|
||||
|
||||
protected override IEnumerable<(ImcIdentifier, ImcEntry)> Enumerate()
|
||||
=> Editor.Imc.Select(kvp => (kvp.Key, kvp.Value));
|
||||
=> Editor.Imc
|
||||
.OrderBy(kvp => kvp.Key.ObjectType)
|
||||
.ThenBy(kvp => kvp.Key.PrimaryId.Id)
|
||||
.ThenBy(kvp => kvp.Key.EquipSlot)
|
||||
.ThenBy(kvp => kvp.Key.BodySlot)
|
||||
.ThenBy(kvp => kvp.Key.SecondaryId.Id)
|
||||
.ThenBy(kvp => kvp.Key.Variant.Id)
|
||||
.Select(kvp => (kvp.Key, kvp.Value));
|
||||
|
||||
protected override int Count
|
||||
=> Editor.Imc.Count;
|
||||
|
||||
public static bool DrawObjectType(ref ImcIdentifier identifier, float width = 110)
|
||||
{
|
||||
|
|
@ -149,18 +159,18 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
|
|||
|
||||
if (ret)
|
||||
{
|
||||
var equipSlot = type switch
|
||||
var (equipSlot, secondaryId) = type switch
|
||||
{
|
||||
ObjectType.Equipment => identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head,
|
||||
ObjectType.DemiHuman => identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head,
|
||||
ObjectType.Accessory => identifier.EquipSlot.IsAccessory() ? identifier.EquipSlot : EquipSlot.Ears,
|
||||
_ => EquipSlot.Unknown,
|
||||
ObjectType.Equipment => (identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, (SecondaryId) 0),
|
||||
ObjectType.DemiHuman => (identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId),
|
||||
ObjectType.Accessory => (identifier.EquipSlot.IsAccessory() ? identifier.EquipSlot : EquipSlot.Ears, (SecondaryId)0),
|
||||
_ => (EquipSlot.Unknown, identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId),
|
||||
};
|
||||
identifier = identifier with
|
||||
{
|
||||
ObjectType = type,
|
||||
EquipSlot = equipSlot,
|
||||
SecondaryId = identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId,
|
||||
SecondaryId = secondaryId,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,12 +41,14 @@ public abstract class MetaDrawer<TIdentifier, TEntry>(ModMetaEditor editor, Meta
|
|||
|
||||
using var id = ImUtf8.PushId((int)Identifier.Type);
|
||||
DrawNew();
|
||||
foreach (var ((identifier, entry), idx) in Enumerate().WithIndex())
|
||||
{
|
||||
id.Push(idx);
|
||||
DrawEntry(identifier, entry);
|
||||
id.Pop();
|
||||
}
|
||||
|
||||
var height = ImUtf8.FrameHeightSpacing;
|
||||
var skips = ImGuiClip.GetNecessarySkipsAtPos(height, ImGui.GetCursorPosY());
|
||||
var remainder = ImGuiClip.ClippedTableDraw(Enumerate(), skips, DrawLine, Count);
|
||||
ImGuiClip.DrawEndDummy(remainder, height);
|
||||
|
||||
void DrawLine((TIdentifier Identifier, TEntry Value) pair)
|
||||
=> DrawEntry(pair.Identifier, pair.Value);
|
||||
}
|
||||
|
||||
public abstract ReadOnlySpan<byte> Label { get; }
|
||||
|
|
@ -57,6 +59,7 @@ public abstract class MetaDrawer<TIdentifier, TEntry>(ModMetaEditor editor, Meta
|
|||
protected abstract void DrawEntry(TIdentifier identifier, TEntry entry);
|
||||
|
||||
protected abstract IEnumerable<(TIdentifier, TEntry)> Enumerate();
|
||||
protected abstract int Count { get; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -58,7 +58,13 @@ public sealed class RspMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
|
|||
}
|
||||
|
||||
protected override IEnumerable<(RspIdentifier, RspEntry)> Enumerate()
|
||||
=> Editor.Rsp.Select(kvp => (kvp.Key, kvp.Value));
|
||||
=> Editor.Rsp
|
||||
.OrderBy(kvp => kvp.Key.SubRace)
|
||||
.ThenBy(kvp => kvp.Key.Attribute)
|
||||
.Select(kvp => (kvp.Key, kvp.Value));
|
||||
|
||||
protected override int Count
|
||||
=> Editor.Rsp.Count;
|
||||
|
||||
private static bool DrawIdentifierInput(ref RspIdentifier identifier)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
using System.Linq;
|
||||
using Dalamud.Interface;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Text;
|
||||
using Penumbra.Mods.Editor;
|
||||
using Penumbra.Mods.SubMods;
|
||||
using Penumbra.String.Classes;
|
||||
|
|
@ -144,22 +146,20 @@ public partial class ModEditWindow
|
|||
|
||||
private static string DrawFileTooltip(FileRegistry registry, ColorId color)
|
||||
{
|
||||
(string, int) GetMulti()
|
||||
{
|
||||
var groups = registry.SubModUsage.GroupBy(s => s.Item1).ToArray();
|
||||
return (string.Join("\n", groups.Select(g => g.Key.GetName())), groups.Length);
|
||||
}
|
||||
|
||||
var (text, groupCount) = color switch
|
||||
{
|
||||
ColorId.ConflictingMod => (string.Empty, 0),
|
||||
ColorId.NewMod => (registry.SubModUsage[0].Item1.GetName(), 1),
|
||||
ColorId.ConflictingMod => (null, 0),
|
||||
ColorId.NewMod => ([registry.SubModUsage[0].Item1.GetName()], 1),
|
||||
ColorId.InheritedMod => GetMulti(),
|
||||
_ => (string.Empty, 0),
|
||||
_ => (null, 0),
|
||||
};
|
||||
|
||||
if (text.Length > 0 && ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip(text);
|
||||
if (text != null && ImGui.IsItemHovered())
|
||||
{
|
||||
using var tt = ImUtf8.Tooltip();
|
||||
using var c = ImRaii.DefaultColors();
|
||||
ImUtf8.Text(string.Join('\n', text));
|
||||
}
|
||||
|
||||
|
||||
return (groupCount, registry.SubModUsage.Count) switch
|
||||
|
|
@ -169,6 +169,12 @@ public partial class ModEditWindow
|
|||
(1, > 1) => $"(used {registry.SubModUsage.Count} times in 1 group)",
|
||||
_ => $"(used {registry.SubModUsage.Count} times over {groupCount} groups)",
|
||||
};
|
||||
|
||||
(IEnumerable<string>, int) GetMulti()
|
||||
{
|
||||
var groups = registry.SubModUsage.GroupBy(s => s.Item1).ToArray();
|
||||
return (groups.Select(g => g.Key.GetName()), groups.Length);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawSelectable(FileRegistry registry)
|
||||
|
|
|
|||
|
|
@ -16,21 +16,21 @@ public partial class ModEditWindow
|
|||
|
||||
private void DrawMetaTab()
|
||||
{
|
||||
using var tab = ImRaii.TabItem("Meta Manipulations");
|
||||
using var tab = ImUtf8.TabItem("Meta Manipulations"u8);
|
||||
if (!tab)
|
||||
return;
|
||||
|
||||
DrawOptionSelectHeader();
|
||||
|
||||
var setsEqual = !_editor.MetaEditor.Changes;
|
||||
var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option.";
|
||||
var tt = setsEqual ? "No changes staged."u8 : "Apply the currently staged changes to the option."u8;
|
||||
ImGui.NewLine();
|
||||
if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual))
|
||||
if (ImUtf8.ButtonEx("Apply Changes"u8, tt, Vector2.Zero, setsEqual))
|
||||
_editor.MetaEditor.Apply(_editor.Option!);
|
||||
|
||||
ImGui.SameLine();
|
||||
tt = setsEqual ? "No changes staged." : "Revert all currently staged changes.";
|
||||
if (ImGuiUtil.DrawDisabledButton("Revert Changes", Vector2.Zero, tt, setsEqual))
|
||||
tt = setsEqual ? "No changes staged."u8 : "Revert all currently staged changes."u8;
|
||||
if (ImUtf8.ButtonEx("Revert Changes"u8, tt, Vector2.Zero, setsEqual))
|
||||
_editor.MetaEditor.Load(_editor.Mod!, _editor.Option!);
|
||||
|
||||
ImGui.SameLine();
|
||||
|
|
@ -40,8 +40,11 @@ public partial class ModEditWindow
|
|||
ImGui.SameLine();
|
||||
CopyToClipboardButton("Copy all current manipulations to clipboard.", _iconSize, _editor.MetaEditor);
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Write as TexTools Files"))
|
||||
if (ImUtf8.Button("Write as TexTools Files"u8))
|
||||
_metaFileManager.WriteAllTexToolsMeta(Mod!);
|
||||
ImGui.SameLine();
|
||||
if (ImUtf8.ButtonEx("Remove All Default-Values", "Delete any entries from all lists that set the value to its default value."u8))
|
||||
_editor.MetaEditor.DeleteDefaultValues();
|
||||
|
||||
using var child = ImRaii.Child("##meta", -Vector2.One, true);
|
||||
if (!child)
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ public partial class ModEditWindow
|
|||
|
||||
ImGuiUtil.SelectableHelpMarker(newDesc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RedrawOnSaveBox()
|
||||
{
|
||||
|
|
@ -128,7 +128,8 @@ public partial class ModEditWindow
|
|||
? "This saves the texture in place. This is not revertible."
|
||||
: $"This saves the texture in place. This is not revertible. Hold {_config.DeleteModModifier} to save.";
|
||||
|
||||
var buttonSize2 = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0);
|
||||
var buttonSize2 = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0);
|
||||
var buttonSize3 = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X * 2) / 3, 0);
|
||||
if (ImGuiUtil.DrawDisabledButton("Save in place", buttonSize2,
|
||||
tt, !isActive || !canSaveInPlace || _center.IsLeftCopy && _currentSaveAs == (int)CombinedTexture.TextureSaveType.AsIs))
|
||||
{
|
||||
|
|
@ -141,17 +142,18 @@ public partial class ModEditWindow
|
|||
if (ImGui.Button("Save as TEX", buttonSize2))
|
||||
OpenSaveAsDialog(".tex");
|
||||
|
||||
if (ImGui.Button("Export as PNG", buttonSize2))
|
||||
if (ImGui.Button("Export as TGA", buttonSize3))
|
||||
OpenSaveAsDialog(".tga");
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Export as PNG", buttonSize3))
|
||||
OpenSaveAsDialog(".png");
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Export as DDS", buttonSize2))
|
||||
if (ImGui.Button("Export as DDS", buttonSize3))
|
||||
OpenSaveAsDialog(".dds");
|
||||
|
||||
ImGui.NewLine();
|
||||
|
||||
var canConvertInPlace = canSaveInPlace && _left.Type is TextureType.Tex && _center.IsLeftCopy;
|
||||
|
||||
var buttonSize3 = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X * 2) / 3, 0);
|
||||
if (ImGuiUtil.DrawDisabledButton("Convert to BC7", buttonSize3,
|
||||
"This converts the texture to BC7 format in place. This is not revertible.",
|
||||
!canConvertInPlace || _left.Format is DXGIFormat.BC7Typeless or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB))
|
||||
|
|
@ -226,7 +228,8 @@ public partial class ModEditWindow
|
|||
private void OpenSaveAsDialog(string defaultExtension)
|
||||
{
|
||||
var fileName = Path.GetFileNameWithoutExtension(_left.Path.Length > 0 ? _left.Path : _right.Path);
|
||||
_fileDialog.OpenSavePicker("Save Texture as TEX, DDS or PNG...", "Textures{.png,.dds,.tex},.tex,.dds,.png", fileName, defaultExtension,
|
||||
_fileDialog.OpenSavePicker("Save Texture as TEX, DDS, PNG or TGA...", "Textures{.png,.dds,.tex,.tga},.tex,.dds,.png,.tga", fileName,
|
||||
defaultExtension,
|
||||
(a, b) =>
|
||||
{
|
||||
if (a)
|
||||
|
|
@ -329,5 +332,6 @@ public partial class ModEditWindow
|
|||
".png",
|
||||
".dds",
|
||||
".tex",
|
||||
".tga",
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using ImGuiNET;
|
|||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Text;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.Communication;
|
||||
|
|
@ -36,8 +37,6 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
|
|||
{
|
||||
private const string WindowBaseLabel = "###SubModEdit";
|
||||
|
||||
public readonly MigrationManager MigrationManager;
|
||||
|
||||
private readonly PerformanceTracker _performance;
|
||||
private readonly ModEditor _editor;
|
||||
private readonly Configuration _config;
|
||||
|
|
@ -53,34 +52,68 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
|
|||
private Vector2 _iconSize = Vector2.Zero;
|
||||
private bool _allowReduplicate;
|
||||
|
||||
public Mod? Mod { get; private set; }
|
||||
public Mod? Mod { get; private set; }
|
||||
|
||||
|
||||
public bool IsLoading
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _editor.IsLoading || _loadingMod is { IsCompleted: false };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly object _lock = new();
|
||||
private Task? _loadingMod;
|
||||
|
||||
|
||||
private void AppendTask(Action run)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_loadingMod == null || _loadingMod.IsCompleted)
|
||||
_loadingMod = Task.Run(run);
|
||||
else
|
||||
_loadingMod = _loadingMod.ContinueWith(_ => run());
|
||||
}
|
||||
}
|
||||
|
||||
public void ChangeMod(Mod mod)
|
||||
{
|
||||
if (mod == Mod)
|
||||
return;
|
||||
|
||||
_editor.LoadMod(mod, -1, 0);
|
||||
Mod = mod;
|
||||
|
||||
SizeConstraints = new WindowSizeConstraints
|
||||
WindowName = $"{mod.Name} (LOADING){WindowBaseLabel}";
|
||||
AppendTask(() =>
|
||||
{
|
||||
MinimumSize = new Vector2(1240, 600),
|
||||
MaximumSize = 4000 * Vector2.One,
|
||||
};
|
||||
_selectedFiles.Clear();
|
||||
_modelTab.Reset();
|
||||
_materialTab.Reset();
|
||||
_shaderPackageTab.Reset();
|
||||
_itemSwapTab.UpdateMod(mod, _activeCollections.Current[mod.Index].Settings);
|
||||
UpdateModels();
|
||||
_forceTextureStartPath = true;
|
||||
_editor.LoadMod(mod, -1, 0).Wait();
|
||||
Mod = mod;
|
||||
|
||||
SizeConstraints = new WindowSizeConstraints
|
||||
{
|
||||
MinimumSize = new Vector2(1240, 600),
|
||||
MaximumSize = 4000 * Vector2.One,
|
||||
};
|
||||
_selectedFiles.Clear();
|
||||
_modelTab.Reset();
|
||||
_materialTab.Reset();
|
||||
_shaderPackageTab.Reset();
|
||||
_itemSwapTab.UpdateMod(mod, _activeCollections.Current[mod.Index].Settings);
|
||||
UpdateModels();
|
||||
_forceTextureStartPath = true;
|
||||
});
|
||||
}
|
||||
|
||||
public void ChangeOption(IModDataContainer? subMod)
|
||||
{
|
||||
var (groupIdx, dataIdx) = subMod?.GetDataIndices() ?? (-1, 0);
|
||||
_editor.LoadOption(groupIdx, dataIdx);
|
||||
AppendTask(() =>
|
||||
{
|
||||
var (groupIdx, dataIdx) = subMod?.GetDataIndices() ?? (-1, 0);
|
||||
_editor.LoadOption(groupIdx, dataIdx).Wait();
|
||||
});
|
||||
}
|
||||
|
||||
public void UpdateModels()
|
||||
|
|
@ -94,6 +127,9 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
|
|||
|
||||
public override void PreDraw()
|
||||
{
|
||||
if (IsLoading)
|
||||
return;
|
||||
|
||||
using var performance = _performance.Measure(PerformanceType.UiAdvancedWindow);
|
||||
|
||||
var sb = new StringBuilder(256);
|
||||
|
|
@ -146,13 +182,16 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
|
|||
|
||||
public override void OnClose()
|
||||
{
|
||||
_left.Dispose();
|
||||
_right.Dispose();
|
||||
_materialTab.Reset();
|
||||
_modelTab.Reset();
|
||||
_shaderPackageTab.Reset();
|
||||
_config.Ephemeral.AdvancedEditingOpen = false;
|
||||
_config.Ephemeral.Save();
|
||||
AppendTask(() =>
|
||||
{
|
||||
_left.Dispose();
|
||||
_right.Dispose();
|
||||
_materialTab.Reset();
|
||||
_modelTab.Reset();
|
||||
_shaderPackageTab.Reset();
|
||||
});
|
||||
}
|
||||
|
||||
public override void Draw()
|
||||
|
|
@ -165,6 +204,17 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
|
|||
_config.Ephemeral.Save();
|
||||
}
|
||||
|
||||
if (IsLoading)
|
||||
{
|
||||
var radius = 100 * ImUtf8.GlobalScale;
|
||||
var thickness = (int) (20 * ImUtf8.GlobalScale);
|
||||
var offsetX = ImGui.GetContentRegionAvail().X / 2 - radius;
|
||||
var offsetY = ImGui.GetContentRegionAvail().Y / 2 - radius;
|
||||
ImGui.SetCursorPos(ImGui.GetCursorPos() + new Vector2(offsetX, offsetY));
|
||||
ImUtf8.Spinner("##spinner"u8, radius, thickness, ImGui.GetColorU32(ImGuiCol.Text));
|
||||
return;
|
||||
}
|
||||
|
||||
using var tabBar = ImRaii.TabBar("##tabs");
|
||||
if (!tabBar)
|
||||
return;
|
||||
|
|
@ -407,14 +457,14 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
|
|||
if (ImGuiUtil.DrawDisabledButton(defaultOption, width, "Switch to the default option for the mod.\nThis resets unsaved changes.",
|
||||
_editor.Option is DefaultSubMod))
|
||||
{
|
||||
_editor.LoadOption(-1, 0);
|
||||
_editor.LoadOption(-1, 0).Wait();
|
||||
ret = true;
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGuiUtil.DrawDisabledButton("Refresh Data", width, "Refresh data for the current option.\nThis resets unsaved changes.", false))
|
||||
{
|
||||
_editor.LoadMod(_editor.Mod!, _editor.GroupIdx, _editor.DataIdx);
|
||||
_editor.LoadMod(_editor.Mod!, _editor.GroupIdx, _editor.DataIdx).Wait();
|
||||
ret = true;
|
||||
}
|
||||
|
||||
|
|
@ -432,7 +482,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
|
|||
if (ImGui.Selectable(option.GetFullName(), option == _editor.Option))
|
||||
{
|
||||
var (groupIdx, dataIdx) = option.GetDataIndices();
|
||||
_editor.LoadOption(groupIdx, dataIdx);
|
||||
_editor.LoadOption(groupIdx, dataIdx).Wait();
|
||||
ret = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -587,7 +637,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
|
|||
CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager,
|
||||
ResourceTreeViewerFactory resourceTreeViewerFactory, IFramework framework,
|
||||
MetaDrawers metaDrawers, MigrationManager migrationManager,
|
||||
MtrlTabFactory mtrlTabFactory)
|
||||
MtrlTabFactory mtrlTabFactory, ModSelection selection)
|
||||
: base(WindowBaseLabel)
|
||||
{
|
||||
_performance = performance;
|
||||
|
|
@ -604,7 +654,6 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
|
|||
_models = models;
|
||||
_fileDialog = fileDialog;
|
||||
_framework = framework;
|
||||
MigrationManager = migrationManager;
|
||||
_metaDrawers = metaDrawers;
|
||||
_materialTab = new FileEditor<MtrlTab>(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl",
|
||||
() => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => Mod?.ModPath.FullName ?? string.Empty,
|
||||
|
|
@ -622,6 +671,8 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
|
|||
_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)
|
||||
ChangeMod(selection.Mod);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
|
|
|||
|
|
@ -5,24 +5,24 @@ using OtterGui.Services;
|
|||
using Penumbra.Collections;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.Interop.PathResolving;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.UI.CollectionTab;
|
||||
using Penumbra.UI.ModsTab;
|
||||
|
||||
namespace Penumbra.UI.Classes;
|
||||
|
||||
public class CollectionSelectHeader : IUiService
|
||||
{
|
||||
private readonly CollectionCombo _collectionCombo;
|
||||
private readonly ActiveCollections _activeCollections;
|
||||
private readonly TutorialService _tutorial;
|
||||
private readonly ModFileSystemSelector _selector;
|
||||
private readonly CollectionResolver _resolver;
|
||||
private readonly CollectionCombo _collectionCombo;
|
||||
private readonly ActiveCollections _activeCollections;
|
||||
private readonly TutorialService _tutorial;
|
||||
private readonly ModSelection _selection;
|
||||
private readonly CollectionResolver _resolver;
|
||||
|
||||
public CollectionSelectHeader(CollectionManager collectionManager, TutorialService tutorial, ModFileSystemSelector selector,
|
||||
public CollectionSelectHeader(CollectionManager collectionManager, TutorialService tutorial, ModSelection selection,
|
||||
CollectionResolver resolver)
|
||||
{
|
||||
_tutorial = tutorial;
|
||||
_selector = selector;
|
||||
_selection = selection;
|
||||
_resolver = resolver;
|
||||
_activeCollections = collectionManager.Active;
|
||||
_collectionCombo = new CollectionCombo(collectionManager, () => collectionManager.Storage.OrderBy(c => c.Name).ToList());
|
||||
|
|
@ -115,7 +115,7 @@ public class CollectionSelectHeader : IUiService
|
|||
|
||||
private (ModCollection?, string, string, bool) GetInheritedCollectionInfo()
|
||||
{
|
||||
var collection = _selector.Selected == null ? null : _selector.SelectedSettingCollection;
|
||||
var collection = _selection.Mod == null ? null : _selection.Collection;
|
||||
return CheckCollection(collection, true) switch
|
||||
{
|
||||
CollectionState.Unavailable => (null, "Not Inherited",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ using OtterGui;
|
|||
using OtterGui.Raii;
|
||||
using OtterGui.Text;
|
||||
using OtterGui.Text.Widget;
|
||||
using OtterGui.Widgets;
|
||||
using OtterGuiInternal.Utility;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Mods.Groups;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ using OtterGui.Filesystem;
|
|||
using OtterGui.FileSystem.Selector;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Text;
|
||||
using OtterGui.Text.Widget;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Collections.Manager;
|
||||
|
|
@ -25,7 +27,6 @@ namespace Penumbra.UI.ModsTab;
|
|||
public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSystemSelector.ModState>, IUiService
|
||||
{
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly MessageService _messager;
|
||||
private readonly Configuration _config;
|
||||
private readonly FileDialogService _fileDialog;
|
||||
private readonly ModManager _modManager;
|
||||
|
|
@ -33,15 +34,12 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
private readonly TutorialService _tutorial;
|
||||
private readonly ModImportManager _modImportManager;
|
||||
private readonly IDragDropManager _dragDrop;
|
||||
private readonly ModSearchStringSplitter Filter = new();
|
||||
|
||||
public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty;
|
||||
public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty;
|
||||
|
||||
private readonly ModSearchStringSplitter _filter = new();
|
||||
private readonly ModSelection _selection;
|
||||
|
||||
public ModFileSystemSelector(IKeyState keyState, CommunicatorService communicator, ModFileSystem fileSystem, ModManager modManager,
|
||||
CollectionManager collectionManager, Configuration config, TutorialService tutorial, FileDialogService fileDialog,
|
||||
MessageService messager, ModImportManager modImportManager, IDragDropManager dragDrop)
|
||||
MessageService messager, ModImportManager modImportManager, IDragDropManager dragDrop, ModSelection selection)
|
||||
: base(fileSystem, keyState, Penumbra.Log, HandleException, allowMultipleSelection: true)
|
||||
{
|
||||
_communicator = communicator;
|
||||
|
|
@ -50,9 +48,9 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
_config = config;
|
||||
_tutorial = tutorial;
|
||||
_fileDialog = fileDialog;
|
||||
_messager = messager;
|
||||
_modImportManager = modImportManager;
|
||||
_dragDrop = dragDrop;
|
||||
_selection = selection;
|
||||
|
||||
// @formatter:off
|
||||
SubscribeRightClickFolder(EnableDescendants, 10);
|
||||
|
|
@ -78,22 +76,16 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
// @formatter:on
|
||||
SetFilterTooltip();
|
||||
|
||||
SelectionChanged += OnSelectionChange;
|
||||
if (_config.Ephemeral.LastModPath.Length > 0)
|
||||
{
|
||||
var mod = _modManager.FirstOrDefault(m
|
||||
=> string.Equals(m.Identifier, _config.Ephemeral.LastModPath, StringComparison.OrdinalIgnoreCase));
|
||||
if (mod != null)
|
||||
SelectByValue(mod);
|
||||
}
|
||||
|
||||
if (_selection.Mod != null)
|
||||
SelectByValue(_selection.Mod);
|
||||
_communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.ModFileSystemSelector);
|
||||
_communicator.ModSettingChanged.Subscribe(OnSettingChange, ModSettingChanged.Priority.ModFileSystemSelector);
|
||||
_communicator.CollectionInheritanceChanged.Subscribe(OnInheritanceChange, CollectionInheritanceChanged.Priority.ModFileSystemSelector);
|
||||
_communicator.ModDataChanged.Subscribe(OnModDataChange, ModDataChanged.Priority.ModFileSystemSelector);
|
||||
_communicator.ModDiscoveryStarted.Subscribe(StoreCurrentSelection, ModDiscoveryStarted.Priority.ModFileSystemSelector);
|
||||
_communicator.ModDiscoveryFinished.Subscribe(RestoreLastSelection, ModDiscoveryFinished.Priority.ModFileSystemSelector);
|
||||
OnCollectionChange(CollectionType.Current, null, _collectionManager.Active.Current, "");
|
||||
SetFilterDirty();
|
||||
SelectionChanged += OnSelectionChanged;
|
||||
}
|
||||
|
||||
public void SetRenameSearchPath(RenameField value)
|
||||
|
|
@ -190,7 +182,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
var newDir = _modManager.Creator.CreateEmptyMod(_modManager.BasePath, _newModName);
|
||||
if (newDir != null)
|
||||
{
|
||||
_modManager.AddMod(newDir);
|
||||
_modManager.AddMod(newDir, false);
|
||||
_newModName = string.Empty;
|
||||
}
|
||||
}
|
||||
|
|
@ -449,12 +441,8 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
|
||||
private void OnSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx, bool inherited)
|
||||
{
|
||||
if (collection != _collectionManager.Active.Current)
|
||||
return;
|
||||
|
||||
SetFilterDirty();
|
||||
if (mod == Selected)
|
||||
OnSelectionChange(Selected, Selected, default);
|
||||
if (collection == _collectionManager.Active.Current)
|
||||
SetFilterDirty();
|
||||
}
|
||||
|
||||
private void OnModDataChange(ModDataChangeType type, Mod mod, string? oldName)
|
||||
|
|
@ -473,41 +461,14 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
|
||||
private void OnInheritanceChange(ModCollection collection, bool _)
|
||||
{
|
||||
if (collection != _collectionManager.Active.Current)
|
||||
return;
|
||||
|
||||
SetFilterDirty();
|
||||
OnSelectionChange(Selected, Selected, default);
|
||||
if (collection == _collectionManager.Active.Current)
|
||||
SetFilterDirty();
|
||||
}
|
||||
|
||||
private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string _)
|
||||
{
|
||||
if (collectionType is not CollectionType.Current || oldCollection == newCollection)
|
||||
return;
|
||||
|
||||
SetFilterDirty();
|
||||
OnSelectionChange(Selected, Selected, default);
|
||||
}
|
||||
|
||||
private void OnSelectionChange(Mod? _1, Mod? newSelection, in ModState _2)
|
||||
{
|
||||
if (newSelection == null)
|
||||
{
|
||||
SelectedSettings = ModSettings.Empty;
|
||||
SelectedSettingCollection = ModCollection.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
(var settings, SelectedSettingCollection) = _collectionManager.Active.Current[newSelection.Index];
|
||||
SelectedSettings = settings ?? ModSettings.Empty;
|
||||
}
|
||||
|
||||
var name = newSelection?.Identifier ?? string.Empty;
|
||||
if (name != _config.Ephemeral.LastModPath)
|
||||
{
|
||||
_config.Ephemeral.LastModPath = name;
|
||||
_config.Ephemeral.Save();
|
||||
}
|
||||
if (collectionType is CollectionType.Current && oldCollection != newCollection)
|
||||
SetFilterDirty();
|
||||
}
|
||||
|
||||
// Keep selections across rediscoveries if possible.
|
||||
|
|
@ -530,6 +491,9 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
_lastSelectedDirectory = string.Empty;
|
||||
}
|
||||
|
||||
private void OnSelectionChanged(Mod? oldSelection, Mod? newSelection, in ModState state)
|
||||
=> _selection.SelectMod(newSelection);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Filters
|
||||
|
|
@ -567,7 +531,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
/// <summary> Appropriately identify and set the string filter and its type. </summary>
|
||||
protected override bool ChangeFilter(string filterValue)
|
||||
{
|
||||
Filter.Parse(filterValue);
|
||||
_filter.Parse(filterValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -597,7 +561,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
{
|
||||
state = default;
|
||||
return ModFilterExtensions.UnfilteredStateMods != _stateFilter
|
||||
|| !Filter.IsVisible(f);
|
||||
|| !_filter.IsVisible(f);
|
||||
}
|
||||
|
||||
return ApplyFiltersAndState((ModFileSystem.Leaf)path, out state);
|
||||
|
|
@ -605,7 +569,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
|
||||
/// <summary> Apply the string filters. </summary>
|
||||
private bool ApplyStringFilters(ModFileSystem.Leaf leaf, Mod mod)
|
||||
=> !Filter.IsVisible(leaf);
|
||||
=> !_filter.IsVisible(leaf);
|
||||
|
||||
/// <summary> Only get the text color for a mod if no filters are set. </summary>
|
||||
private ColorId GetTextColor(Mod mod, ModSettings? settings, ModCollection collection)
|
||||
|
|
@ -741,8 +705,6 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
|
||||
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing,
|
||||
ImGui.GetStyle().ItemSpacing with { Y = 3 * UiHelpers.Scale });
|
||||
var flags = (int)_stateFilter;
|
||||
|
||||
|
||||
if (ImGui.Checkbox("Everything", ref everything))
|
||||
{
|
||||
|
|
@ -751,12 +713,19 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
}
|
||||
|
||||
ImGui.Dummy(new Vector2(0, 5 * UiHelpers.Scale));
|
||||
foreach (ModFilter flag in Enum.GetValues(typeof(ModFilter)))
|
||||
foreach (var (onFlag, offFlag, name) in ModFilterExtensions.TriStatePairs)
|
||||
{
|
||||
if (ImGui.CheckboxFlags(flag.ToName(), ref flags, (int)flag))
|
||||
{
|
||||
_stateFilter = (ModFilter)flags;
|
||||
if (TriStateCheckbox.Instance.Draw(name, ref _stateFilter, onFlag, offFlag))
|
||||
SetFilterDirty();
|
||||
}
|
||||
|
||||
foreach (var group in ModFilterExtensions.Groups)
|
||||
{
|
||||
ImGui.Separator();
|
||||
foreach (var (flag, name) in group)
|
||||
{
|
||||
if (ImUtf8.Checkbox(name, ref _stateFilter, flag))
|
||||
SetFilterDirty();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,29 +29,28 @@ public static class ModFilterExtensions
|
|||
{
|
||||
public const ModFilter UnfilteredStateMods = (ModFilter)((1 << 20) - 1);
|
||||
|
||||
public static string ToName(this ModFilter filter)
|
||||
=> filter switch
|
||||
{
|
||||
ModFilter.Enabled => "Enabled",
|
||||
ModFilter.Disabled => "Disabled",
|
||||
ModFilter.Favorite => "Favorite",
|
||||
ModFilter.NotFavorite => "No Favorite",
|
||||
ModFilter.NoConflict => "No Conflicts",
|
||||
ModFilter.SolvedConflict => "Solved Conflicts",
|
||||
ModFilter.UnsolvedConflict => "Unsolved Conflicts",
|
||||
ModFilter.HasNoMetaManipulations => "No Meta Manipulations",
|
||||
ModFilter.HasMetaManipulations => "Meta Manipulations",
|
||||
ModFilter.HasNoFileSwaps => "No File Swaps",
|
||||
ModFilter.HasFileSwaps => "File Swaps",
|
||||
ModFilter.HasNoConfig => "No Configuration",
|
||||
ModFilter.HasConfig => "Configuration",
|
||||
ModFilter.HasNoFiles => "No Files",
|
||||
ModFilter.HasFiles => "Files",
|
||||
ModFilter.IsNew => "Newly Imported",
|
||||
ModFilter.NotNew => "Not Newly Imported",
|
||||
ModFilter.Inherited => "Inherited Configuration",
|
||||
ModFilter.Uninherited => "Own Configuration",
|
||||
ModFilter.Undefined => "Not Configured",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(filter), filter, null),
|
||||
};
|
||||
public static IReadOnlyList<(ModFilter On, ModFilter Off, string Name)> TriStatePairs =
|
||||
[
|
||||
(ModFilter.Enabled, ModFilter.Disabled, "Enabled"),
|
||||
(ModFilter.IsNew, ModFilter.NotNew, "Newly Imported"),
|
||||
(ModFilter.Favorite, ModFilter.NotFavorite, "Favorite"),
|
||||
(ModFilter.HasConfig, ModFilter.HasNoConfig, "Has Options"),
|
||||
(ModFilter.HasFiles, ModFilter.HasNoFiles, "Has Redirections"),
|
||||
(ModFilter.HasMetaManipulations, ModFilter.HasNoMetaManipulations, "Has Meta Manipulations"),
|
||||
(ModFilter.HasFileSwaps, ModFilter.HasNoFileSwaps, "Has File Swaps"),
|
||||
];
|
||||
|
||||
public static IReadOnlyList<IReadOnlyList<(ModFilter Filter, string Name)>> Groups =
|
||||
[
|
||||
[
|
||||
(ModFilter.NoConflict, "Has No Conflicts"),
|
||||
(ModFilter.SolvedConflict, "Has Solved Conflicts"),
|
||||
(ModFilter.UnsolvedConflict, "Has Unsolved Conflicts"),
|
||||
],
|
||||
[
|
||||
(ModFilter.Undefined, "Not Configured"),
|
||||
(ModFilter.Inherited, "Inherited Configuration"),
|
||||
(ModFilter.Uninherited, "Own Configuration"),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,22 +10,23 @@ namespace Penumbra.UI.ModsTab;
|
|||
|
||||
public class ModPanel : IDisposable, IUiService
|
||||
{
|
||||
private readonly MultiModPanel _multiModPanel;
|
||||
private readonly ModFileSystemSelector _selector;
|
||||
private readonly ModEditWindow _editWindow;
|
||||
private readonly ModPanelHeader _header;
|
||||
private readonly ModPanelTabBar _tabs;
|
||||
private bool _resetCursor;
|
||||
private readonly MultiModPanel _multiModPanel;
|
||||
private readonly ModSelection _selection;
|
||||
private readonly ModEditWindow _editWindow;
|
||||
private readonly ModPanelHeader _header;
|
||||
private readonly ModPanelTabBar _tabs;
|
||||
private bool _resetCursor;
|
||||
|
||||
public ModPanel(IDalamudPluginInterface pi, ModFileSystemSelector selector, ModEditWindow editWindow, ModPanelTabBar tabs,
|
||||
public ModPanel(IDalamudPluginInterface pi, ModSelection selection, ModEditWindow editWindow, ModPanelTabBar tabs,
|
||||
MultiModPanel multiModPanel, CommunicatorService communicator)
|
||||
{
|
||||
_selector = selector;
|
||||
_editWindow = editWindow;
|
||||
_tabs = tabs;
|
||||
_multiModPanel = multiModPanel;
|
||||
_header = new ModPanelHeader(pi, communicator);
|
||||
_selector.SelectionChanged += OnSelectionChange;
|
||||
_selection = selection;
|
||||
_editWindow = editWindow;
|
||||
_tabs = tabs;
|
||||
_multiModPanel = multiModPanel;
|
||||
_header = new ModPanelHeader(pi, communicator);
|
||||
_selection.Subscribe(OnSelectionChange, ModSelection.Priority.ModPanel);
|
||||
OnSelectionChange(null, _selection.Mod);
|
||||
}
|
||||
|
||||
public void Draw()
|
||||
|
|
@ -52,17 +53,17 @@ public class ModPanel : IDisposable, IUiService
|
|||
|
||||
public void Dispose()
|
||||
{
|
||||
_selector.SelectionChanged -= OnSelectionChange;
|
||||
_selection.Unsubscribe(OnSelectionChange);
|
||||
_header.Dispose();
|
||||
}
|
||||
|
||||
private bool _valid;
|
||||
private Mod _mod = null!;
|
||||
|
||||
private void OnSelectionChange(Mod? old, Mod? mod, in ModFileSystemSelector.ModState _)
|
||||
private void OnSelectionChange(Mod? old, Mod? mod)
|
||||
{
|
||||
_resetCursor = true;
|
||||
if (mod == null || _selector.Selected == null)
|
||||
if (mod == null || _selection.Mod == null)
|
||||
{
|
||||
_editWindow.IsOpen = false;
|
||||
_valid = false;
|
||||
|
|
@ -73,7 +74,7 @@ public class ModPanel : IDisposable, IUiService
|
|||
_editWindow.ChangeMod(mod);
|
||||
_valid = true;
|
||||
_mod = mod;
|
||||
_header.UpdateModData(_mod);
|
||||
_header.ChangeMod(_mod);
|
||||
_tabs.Settings.Reset();
|
||||
_tabs.Edit.Reset();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ public class ModPanelHeader : IDisposable
|
|||
private readonly IFontHandle _nameFont;
|
||||
|
||||
private readonly CommunicatorService _communicator;
|
||||
private float _lastPreSettingsHeight = 0;
|
||||
private float _lastPreSettingsHeight;
|
||||
private bool _dirty = true;
|
||||
|
||||
public ModPanelHeader(IDalamudPluginInterface pi, CommunicatorService communicator)
|
||||
{
|
||||
|
|
@ -33,6 +34,7 @@ public class ModPanelHeader : IDisposable
|
|||
/// </summary>
|
||||
public void Draw()
|
||||
{
|
||||
UpdateModData();
|
||||
var height = ImGui.GetContentRegionAvail().Y;
|
||||
var maxHeight = 3 * height / 4;
|
||||
using var child = _lastPreSettingsHeight > maxHeight && _communicator.PreSettingsTabBarDraw.HasSubscribers
|
||||
|
|
@ -49,16 +51,25 @@ public class ModPanelHeader : IDisposable
|
|||
_lastPreSettingsHeight = ImGui.GetCursorPosY();
|
||||
}
|
||||
|
||||
public void ChangeMod(Mod mod)
|
||||
{
|
||||
_mod = mod;
|
||||
_dirty = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update all mod header data. Should someone change frame padding or item spacing,
|
||||
/// or his default font, this will break, but he will just have to select a different mod to restore.
|
||||
/// </summary>
|
||||
public void UpdateModData(Mod mod)
|
||||
private void UpdateModData()
|
||||
{
|
||||
if (!_dirty)
|
||||
return;
|
||||
|
||||
_dirty = false;
|
||||
_lastPreSettingsHeight = 0;
|
||||
_mod = mod;
|
||||
// Name
|
||||
var name = $" {mod.Name} ";
|
||||
var name = $" {_mod.Name} ";
|
||||
if (name != _modName)
|
||||
{
|
||||
using var f = _nameFont.Push();
|
||||
|
|
@ -67,16 +78,16 @@ public class ModPanelHeader : IDisposable
|
|||
}
|
||||
|
||||
// Author
|
||||
if (mod.Author != _modAuthor)
|
||||
if (_mod.Author != _modAuthor)
|
||||
{
|
||||
var author = mod.Author.IsEmpty ? string.Empty : $"by {mod.Author}";
|
||||
_modAuthor = mod.Author.Text;
|
||||
var author = _mod.Author.IsEmpty ? string.Empty : $"by {_mod.Author}";
|
||||
_modAuthor = _mod.Author.Text;
|
||||
_modAuthorWidth = ImGui.CalcTextSize(author).X;
|
||||
_secondRowWidth = _modAuthorWidth + _modWebsiteButtonWidth + ImGui.GetStyle().ItemSpacing.X;
|
||||
}
|
||||
|
||||
// Version
|
||||
var version = mod.Version.Length > 0 ? $"({mod.Version})" : string.Empty;
|
||||
var version = _mod.Version.Length > 0 ? $"({_mod.Version})" : string.Empty;
|
||||
if (version != _modVersion)
|
||||
{
|
||||
_modVersion = version;
|
||||
|
|
@ -84,9 +95,9 @@ public class ModPanelHeader : IDisposable
|
|||
}
|
||||
|
||||
// Website
|
||||
if (_modWebsite != mod.Website)
|
||||
if (_modWebsite != _mod.Website)
|
||||
{
|
||||
_modWebsite = mod.Website;
|
||||
_modWebsite = _mod.Website;
|
||||
_websiteValid = Uri.TryCreate(_modWebsite, UriKind.Absolute, out var uriResult)
|
||||
&& (uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp);
|
||||
_modWebsiteButton = _websiteValid ? "Open Website" : _modWebsite.Length == 0 ? string.Empty : $"from {_modWebsite}";
|
||||
|
|
@ -253,7 +264,6 @@ public class ModPanelHeader : IDisposable
|
|||
{
|
||||
const ModDataChangeType relevantChanges =
|
||||
ModDataChangeType.Author | ModDataChangeType.Name | ModDataChangeType.Website | ModDataChangeType.Version;
|
||||
if ((changeType & relevantChanges) != 0)
|
||||
UpdateModData(mod);
|
||||
_dirty = (changeType & relevantChanges) != 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ using OtterGui.Raii;
|
|||
using OtterGui;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Widgets;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.UI.Classes;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.Mods.Settings;
|
||||
|
|
@ -16,16 +16,14 @@ namespace Penumbra.UI.ModsTab;
|
|||
public class ModPanelSettingsTab(
|
||||
CollectionManager collectionManager,
|
||||
ModManager modManager,
|
||||
ModFileSystemSelector selector,
|
||||
ModSelection selection,
|
||||
TutorialService tutorial,
|
||||
CommunicatorService communicator,
|
||||
ModGroupDrawer modGroupDrawer)
|
||||
: ITab, IUiService
|
||||
{
|
||||
private bool _inherited;
|
||||
private ModSettings _settings = null!;
|
||||
private ModCollection _collection = null!;
|
||||
private int? _currentPriority;
|
||||
private bool _inherited;
|
||||
private int? _currentPriority;
|
||||
|
||||
public ReadOnlySpan<byte> Label
|
||||
=> "Settings"u8;
|
||||
|
|
@ -42,12 +40,10 @@ public class ModPanelSettingsTab(
|
|||
if (!child)
|
||||
return;
|
||||
|
||||
_settings = selector.SelectedSettings;
|
||||
_collection = selector.SelectedSettingCollection;
|
||||
_inherited = _collection != collectionManager.Active.Current;
|
||||
_inherited = selection.Collection != collectionManager.Active.Current;
|
||||
DrawInheritedWarning();
|
||||
UiHelpers.DefaultLineSpace();
|
||||
communicator.PreSettingsPanelDraw.Invoke(selector.Selected!.Identifier);
|
||||
communicator.PreSettingsPanelDraw.Invoke(selection.Mod!.Identifier);
|
||||
DrawEnabledInput();
|
||||
tutorial.OpenTutorial(BasicTutorialSteps.EnablingMods);
|
||||
ImGui.SameLine();
|
||||
|
|
@ -55,11 +51,11 @@ public class ModPanelSettingsTab(
|
|||
tutorial.OpenTutorial(BasicTutorialSteps.Priority);
|
||||
DrawRemoveSettings();
|
||||
|
||||
communicator.PostEnabledDraw.Invoke(selector.Selected!.Identifier);
|
||||
communicator.PostEnabledDraw.Invoke(selection.Mod!.Identifier);
|
||||
|
||||
modGroupDrawer.Draw(selector.Selected!, _settings);
|
||||
modGroupDrawer.Draw(selection.Mod!, selection.Settings);
|
||||
UiHelpers.DefaultLineSpace();
|
||||
communicator.PostSettingsPanelDraw.Invoke(selector.Selected!.Identifier);
|
||||
communicator.PostSettingsPanelDraw.Invoke(selection.Mod!.Identifier);
|
||||
}
|
||||
|
||||
/// <summary> Draw a big red bar if the current setting is inherited. </summary>
|
||||
|
|
@ -70,8 +66,8 @@ public class ModPanelSettingsTab(
|
|||
|
||||
using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.PressEnterWarningBg);
|
||||
var width = new Vector2(ImGui.GetContentRegionAvail().X, 0);
|
||||
if (ImGui.Button($"These settings are inherited from {_collection.Name}.", width))
|
||||
collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selector.Selected!, false);
|
||||
if (ImGui.Button($"These settings are inherited from {selection.Collection.Name}.", width))
|
||||
collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, false);
|
||||
|
||||
ImGuiUtil.HoverTooltip("You can click this button to copy the current settings to the current selection.\n"
|
||||
+ "You can also just change any setting, which will copy the settings with the single setting changed to the current selection.");
|
||||
|
|
@ -80,12 +76,12 @@ public class ModPanelSettingsTab(
|
|||
/// <summary> Draw a checkbox for the enabled status of the mod. </summary>
|
||||
private void DrawEnabledInput()
|
||||
{
|
||||
var enabled = _settings.Enabled;
|
||||
var enabled = selection.Settings.Enabled;
|
||||
if (!ImGui.Checkbox("Enabled", ref enabled))
|
||||
return;
|
||||
|
||||
modManager.SetKnown(selector.Selected!);
|
||||
collectionManager.Editor.SetModState(collectionManager.Active.Current, selector.Selected!, enabled);
|
||||
modManager.SetKnown(selection.Mod!);
|
||||
collectionManager.Editor.SetModState(collectionManager.Active.Current, selection.Mod!, enabled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -95,15 +91,16 @@ public class ModPanelSettingsTab(
|
|||
private void DrawPriorityInput()
|
||||
{
|
||||
using var group = ImRaii.Group();
|
||||
var priority = _currentPriority ?? _settings.Priority.Value;
|
||||
var settings = selection.Settings;
|
||||
var priority = _currentPriority ?? settings.Priority.Value;
|
||||
ImGui.SetNextItemWidth(50 * UiHelpers.Scale);
|
||||
if (ImGui.InputInt("##Priority", ref priority, 0, 0))
|
||||
_currentPriority = priority;
|
||||
|
||||
if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue)
|
||||
{
|
||||
if (_currentPriority != _settings.Priority.Value)
|
||||
collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selector.Selected!,
|
||||
if (_currentPriority != settings.Priority.Value)
|
||||
collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selection.Mod!,
|
||||
new ModPriority(_currentPriority.Value));
|
||||
|
||||
_currentPriority = null;
|
||||
|
|
@ -120,13 +117,13 @@ public class ModPanelSettingsTab(
|
|||
private void DrawRemoveSettings()
|
||||
{
|
||||
const string text = "Inherit Settings";
|
||||
if (_inherited || _settings == ModSettings.Empty)
|
||||
if (_inherited || selection.Settings == ModSettings.Empty)
|
||||
return;
|
||||
|
||||
var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0;
|
||||
ImGui.SameLine(ImGui.GetWindowWidth() - ImGui.CalcTextSize(text).X - ImGui.GetStyle().FramePadding.X * 2 - scroll);
|
||||
if (ImGui.Button(text))
|
||||
collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selector.Selected!, true);
|
||||
collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, true);
|
||||
|
||||
ImGuiUtil.HoverTooltip("Remove current settings from this collection so that it can inherit them.\n"
|
||||
+ "If no inherited collection has settings for this mod, it will be disabled.");
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
|
@ -43,7 +42,6 @@ using Penumbra.Api.IpcTester;
|
|||
using Penumbra.Interop.Hooks.PostProcessing;
|
||||
using Penumbra.Interop.Hooks.ResourceLoading;
|
||||
using Penumbra.GameData.Files.StainMapStructs;
|
||||
using Penumbra.UI.AdvancedWindow;
|
||||
using Penumbra.UI.AdvancedWindow.Materials;
|
||||
|
||||
namespace Penumbra.UI.Tabs.Debug;
|
||||
|
|
@ -721,7 +719,8 @@ public class DebugTab : Window, ITab, IUiService
|
|||
if (!tree)
|
||||
continue;
|
||||
|
||||
using var table = Table("##table", data.Colors.Length + data.Scalars.Length, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
|
||||
using var table = Table("##table", data.Colors.Length + data.Scalars.Length,
|
||||
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
|
||||
if (!table)
|
||||
continue;
|
||||
|
||||
|
|
|
|||
|
|
@ -82,8 +82,7 @@ public class ModsTab(
|
|||
+ $"{selector.SortMode.Name} Sort Mode\n"
|
||||
+ $"{selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n"
|
||||
+ $"{selector.Selected?.Name ?? "NULL"} Selected Mod\n"
|
||||
+ $"{string.Join(", ", _activeCollections.Current.DirectlyInheritsFrom.Select(c => c.AnonymizedName))} Inheritances\n"
|
||||
+ $"{selector.SelectedSettingCollection.AnonymizedName} Collection\n");
|
||||
+ $"{string.Join(", ", _activeCollections.Current.DirectlyInheritsFrom.Select(c => c.AnonymizedName))} Inheritances\n");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -816,13 +816,13 @@ public class SettingsTab : ITab, IUiService
|
|||
if (ImGuiUtil.DrawDisabledButton("Compress Existing Files", Vector2.Zero,
|
||||
"Try to compress all files in your root directory. This will take a while.",
|
||||
_compactor.MassCompactRunning || !_modManager.Valid))
|
||||
_compactor.StartMassCompact(_modManager.BasePath.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.Xpress8K);
|
||||
_compactor.StartMassCompact(_modManager.BasePath.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.Xpress8K, true);
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGuiUtil.DrawDisabledButton("Decompress Existing Files", Vector2.Zero,
|
||||
"Try to decompress all files in your root directory. This will take a while.",
|
||||
_compactor.MassCompactRunning || !_modManager.Valid))
|
||||
_compactor.StartMassCompact(_modManager.BasePath.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.None);
|
||||
_compactor.StartMassCompact(_modManager.BasePath.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.None, true);
|
||||
|
||||
if (_compactor.MassCompactRunning)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@
|
|||
"net8.0-windows7.0": {
|
||||
"EmbedIO": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.4.3, )",
|
||||
"resolved": "3.4.3",
|
||||
"contentHash": "YM6hpZNAfvbbixfG9T4lWDGfF0D/TqutbTROL4ogVcHKwPF1hp+xS3ABwd3cxxTxvDFkj/zZl57QgWuFA8Igxw==",
|
||||
"requested": "[3.5.2, )",
|
||||
"resolved": "3.5.2",
|
||||
"contentHash": "YU4j+3XvuO8/VPkNf7KWOF1TpMhnyVhXnPsG1mvnDhTJ9D5BZOFXVDvCpE/SkQ1AJ0Aa+dXOVSW3ntgmLL7aJg==",
|
||||
"dependencies": {
|
||||
"Unosquare.Swan.Lite": "3.0.0"
|
||||
"Unosquare.Swan.Lite": "3.1.0"
|
||||
}
|
||||
},
|
||||
"PeNet": {
|
||||
|
|
@ -23,23 +23,26 @@
|
|||
},
|
||||
"SharpCompress": {
|
||||
"type": "Direct",
|
||||
"requested": "[0.33.0, )",
|
||||
"resolved": "0.33.0",
|
||||
"contentHash": "FlHfpTAADzaSlVCBF33iKJk9UhOr3Xj+r5LXbW2GzqYr0SrhiOf6shLX2LC2fqs7g7d+YlwKbBXqWFtb+e7icw=="
|
||||
"requested": "[0.37.2, )",
|
||||
"resolved": "0.37.2",
|
||||
"contentHash": "cFBpTct57aubLQXkdqMmgP8GGTFRh7fnRWP53lgE/EYUpDZJ27SSvTkdjB4OYQRZ20SJFpzczUquKLbt/9xkhw==",
|
||||
"dependencies": {
|
||||
"ZstdSharp.Port": "0.8.0"
|
||||
}
|
||||
},
|
||||
"SharpGLTF.Core": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.0.0-alpha0030, )",
|
||||
"resolved": "1.0.0-alpha0030",
|
||||
"contentHash": "HVL6PcrM0H/uEk96nRZfhtPeYvSFGHnni3g1aIckot2IWVp0jLMH5KWgaWfsatEz4Yds3XcdSLUWmJZivDBUPA=="
|
||||
"requested": "[1.0.1, )",
|
||||
"resolved": "1.0.1",
|
||||
"contentHash": "ykeV1oNHcJrEJE7s0pGAsf/nYGYY7wqF9nxCMxJUjp/WdW+UUgR1cGdbAa2lVZPkiXEwLzWenZ5wPz7yS0Gj9w=="
|
||||
},
|
||||
"SharpGLTF.Toolkit": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.0.0-alpha0030, )",
|
||||
"resolved": "1.0.0-alpha0030",
|
||||
"contentHash": "nsoJWAFhXgEky9bVCY0zLeZVDx+S88u7VjvuebvMb6dJiNyFOGF6FrrMHiJe+x5pcVBxxlc3VoXliBF7r/EqYA==",
|
||||
"requested": "[1.0.1, )",
|
||||
"resolved": "1.0.1",
|
||||
"contentHash": "LYBjHdHW5Z8R1oT1iI04si3559tWdZ3jTdHfDEu0jqhuyU8w3oJRLFUoDfVeCOI5zWXlVQPtlpjhH9XTfFFAcA==",
|
||||
"dependencies": {
|
||||
"SharpGLTF.Runtime": "1.0.0-alpha0030"
|
||||
"SharpGLTF.Runtime": "1.0.1"
|
||||
}
|
||||
},
|
||||
"SixLabors.ImageSharp": {
|
||||
|
|
@ -48,10 +51,16 @@
|
|||
"resolved": "3.1.5",
|
||||
"contentHash": "lNtlq7dSI/QEbYey+A0xn48z5w4XHSffF8222cC4F4YwTXfEImuiBavQcWjr49LThT/pRmtWJRcqA/PlL+eJ6g=="
|
||||
},
|
||||
"System.Formats.Asn1": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.1, )",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "XqKba7Mm/koKSjKMfW82olQdmfbI5yqeoLV/tidRp7fbh5rmHAQ5raDI/7SU0swTzv+jgqtUGkzmFxuUg0it1A=="
|
||||
},
|
||||
"JetBrains.Annotations": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2023.3.0",
|
||||
"contentHash": "PHfnvdBUdGaTVG9bR/GEfxgTwWM0Z97Y6X3710wiljELBISipSfF5okn/vz+C2gfO+ihoEyVPjaJwn8ZalVukA=="
|
||||
"resolved": "2024.2.0",
|
||||
"contentHash": "GNnqCFW/163p1fOehKx0CnAqjmpPrUSqrgfHM6qca+P+RN39C9rhlfZHQpJhxmQG/dkOYe/b3Z0P8b6Kv5m1qw=="
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection": {
|
||||
"type": "Transitive",
|
||||
|
|
@ -73,17 +82,12 @@
|
|||
},
|
||||
"SharpGLTF.Runtime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.0.0-alpha0030",
|
||||
"contentHash": "Ysn+fyj9EVXj6mfG0BmzSTBGNi/QvcnTrMd54dBMOlI/TsMRvnOY3JjTn0MpeH2CgHXX4qogzlDt4m+rb3n4Og==",
|
||||
"resolved": "1.0.1",
|
||||
"contentHash": "KsgEBKLfsEnu2IPeKaWp4Ih97+kby17IohrAB6Ev8gET18iS80nKMW/APytQWpenMmcWU06utInpANqyrwRlDg==",
|
||||
"dependencies": {
|
||||
"SharpGLTF.Core": "1.0.0-alpha0030"
|
||||
"SharpGLTF.Core": "1.0.1"
|
||||
}
|
||||
},
|
||||
"System.Formats.Asn1": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "AJukBuLoe3QeAF+mfaRKQb2dgyrvt340iMBHYv+VdBzCUM06IxGlvl0o/uPOS7lHnXPN6u8fFRHSHudx5aTi8w=="
|
||||
},
|
||||
"System.Security.Cryptography.Pkcs": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
|
|
@ -99,16 +103,21 @@
|
|||
},
|
||||
"Unosquare.Swan.Lite": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0",
|
||||
"contentHash": "noPwJJl1Q9uparXy1ogtkmyAPGNfSGb0BLT1292nFH1jdMKje6o2kvvrQUvF9Xklj+IoiAI0UzF6Aqxlvo10lw==",
|
||||
"resolved": "3.1.0",
|
||||
"contentHash": "X3s5QE/KMj3WAPFqFve7St+Ds10BB50u8kW8PmKIn7FVkn7yEXe9Yxr2htt1WV85DRqfFR0MN/BUNHkGHtL4OQ==",
|
||||
"dependencies": {
|
||||
"System.ValueTuple": "4.5.0"
|
||||
}
|
||||
},
|
||||
"ZstdSharp.Port": {
|
||||
"type": "Transitive",
|
||||
"resolved": "0.8.0",
|
||||
"contentHash": "Z62eNBIu8E8YtbqlMy57tK3dV1+m2b9NhPeaYovB5exmLKvrGCqOhJTzrEUH5VyUWU6vwX3c1XHJGhW5HVs8dA=="
|
||||
},
|
||||
"ottergui": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"JetBrains.Annotations": "[2023.3.0, )",
|
||||
"JetBrains.Annotations": "[2024.2.0, )",
|
||||
"Microsoft.Extensions.DependencyInjection": "[8.0.0, )"
|
||||
}
|
||||
},
|
||||
|
|
@ -122,7 +131,7 @@
|
|||
"type": "Project",
|
||||
"dependencies": {
|
||||
"OtterGui": "[1.0.0, )",
|
||||
"Penumbra.Api": "[5.2.0, )",
|
||||
"Penumbra.Api": "[5.3.0, )",
|
||||
"Penumbra.String": "[1.0.4, )"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
"Description": "Runtime mod loader and manager.",
|
||||
"InternalName": "Penumbra",
|
||||
"AssemblyVersion": "1.2.1.1",
|
||||
"TestingAssemblyVersion": "1.2.1.1",
|
||||
"TestingAssemblyVersion": "1.2.1.2",
|
||||
"RepoUrl": "https://github.com/xivdev/Penumbra",
|
||||
"ApplicableVersion": "any",
|
||||
"DalamudApiLevel": 10,
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
"LoadRequiredState": 2,
|
||||
"LoadSync": true,
|
||||
"DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip",
|
||||
"DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip",
|
||||
"DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.2/Penumbra.zip",
|
||||
"DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip",
|
||||
"IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue