mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Merge branch 'master' into mtrl-improvements
This commit is contained in:
commit
8182bb0cc3
102 changed files with 2457 additions and 1009 deletions
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
|
@ -29,7 +29,7 @@ jobs:
|
|||
- name: Archive
|
||||
run: Compress-Archive -Path Penumbra/bin/Release/* -DestinationPath Penumbra.zip
|
||||
- name: Upload a Build Artifact
|
||||
uses: actions/upload-artifact@v2.2.1
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: |
|
||||
./Penumbra/bin/Release/*
|
||||
|
|
|
|||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -37,7 +37,7 @@ jobs:
|
|||
- name: Archive
|
||||
run: Compress-Archive -Path Penumbra/bin/Release/* -DestinationPath Penumbra.zip
|
||||
- name: Upload a Build Artifact
|
||||
uses: actions/upload-artifact@v2.2.1
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: |
|
||||
./Penumbra/bin/Release/*
|
||||
|
|
|
|||
2
.github/workflows/test_release.yml
vendored
2
.github/workflows/test_release.yml
vendored
|
|
@ -37,7 +37,7 @@ jobs:
|
|||
- name: Archive
|
||||
run: Compress-Archive -Path Penumbra/bin/Debug/* -DestinationPath Penumbra.zip
|
||||
- name: Upload a Build Artifact
|
||||
uses: actions/upload-artifact@v2.2.1
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: |
|
||||
./Penumbra/bin/Debug/*
|
||||
|
|
|
|||
2
OtterGui
2
OtterGui
|
|
@ -1 +1 @@
|
|||
Subproject commit 07a009134bf5eb7da9a54ba40e82c88fc613544a
|
||||
Subproject commit 3e6b085749741f35dd6732c33d0720c6a51ebb97
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 552246e595ffab2aaba2c75f578d564f8938fc9a
|
||||
Subproject commit 97e9f427406f82a59ddef764b44ecea654a51623
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 6c02858d5a3d20100377dd127d1b85dbe82a4c44
|
||||
Subproject commit 27cef2c1b8ef8ce9b73bc658e03f543b5c7ef29d
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ using Penumbra.Api.Api;
|
|||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.Services;
|
||||
|
|
@ -28,6 +27,8 @@ public class TemporaryIpcTester(
|
|||
{
|
||||
public Guid LastCreatedCollectionId = Guid.Empty;
|
||||
|
||||
private readonly bool _debug = Assembly.GetAssembly(typeof(TemporaryIpcTester))?.GetName().Version?.Major >= 9;
|
||||
|
||||
private Guid? _tempGuid;
|
||||
private string _tempCollectionName = string.Empty;
|
||||
private string _tempCollectionGuidName = string.Empty;
|
||||
|
|
@ -48,9 +49,9 @@ public class TemporaryIpcTester(
|
|||
ImGui.InputTextWithHint("##tempCollection", "Collection Name...", ref _tempCollectionName, 128);
|
||||
ImGuiUtil.GuidInput("##guid", "Collection GUID...", string.Empty, ref _tempGuid, ref _tempCollectionGuidName);
|
||||
ImGui.InputInt("##tempActorIndex", ref _tempActorIndex, 0, 0);
|
||||
ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32);
|
||||
ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256);
|
||||
ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256);
|
||||
ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32);
|
||||
ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256);
|
||||
ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256);
|
||||
ImUtf8.InputText("##tempManip"u8, ref _tempManipulation, "Manipulation Base64 String..."u8);
|
||||
ImGui.Checkbox("Force Character Collection Overwrite", ref _forceOverwrite);
|
||||
|
||||
|
|
@ -102,7 +103,7 @@ public class TemporaryIpcTester(
|
|||
!collections.Storage.ByName(_tempModName, out var copyCollection))
|
||||
&& copyCollection is { HasCache: true })
|
||||
{
|
||||
var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString());
|
||||
var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString());
|
||||
var manips = MetaApi.CompressMetaManipulations(copyCollection);
|
||||
_lastTempError = new AddTemporaryMod(pi).Invoke(_tempModName, guid, files, manips, 999);
|
||||
}
|
||||
|
|
@ -124,11 +125,11 @@ public class TemporaryIpcTester(
|
|||
|
||||
public void DrawCollections()
|
||||
{
|
||||
using var collTree = ImRaii.TreeNode("Temporary Collections##TempCollections");
|
||||
using var collTree = ImUtf8.TreeNode("Temporary Collections##TempCollections"u8);
|
||||
if (!collTree)
|
||||
return;
|
||||
|
||||
using var table = ImRaii.Table("##collTree", 6, ImGuiTableFlags.SizingFixedFit);
|
||||
using var table = ImUtf8.Table("##collTree"u8, 6, ImGuiTableFlags.SizingFixedFit);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
|
|
@ -139,7 +140,7 @@ public class TemporaryIpcTester(
|
|||
var character = tempCollections.Collections.Where(p => p.Collection == collection).Select(p => p.DisplayName)
|
||||
.FirstOrDefault()
|
||||
?? "Unknown";
|
||||
if (ImGui.Button("Save##Collection"))
|
||||
if (_debug && ImUtf8.Button("Save##Collection"u8))
|
||||
TemporaryMod.SaveTempCollection(config, saveService, modManager, collection, character);
|
||||
|
||||
using (ImRaii.PushFont(UiBuilder.MonoFont))
|
||||
|
|
|
|||
|
|
@ -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 = [];
|
||||
|
|
@ -39,7 +40,7 @@ public class GlobalEqpCache : Dictionary<GlobalEqpManipulation, IMod>, IService
|
|||
original |= EqpEntry.HeadShowHrothgarHat;
|
||||
|
||||
if (_doNotHideEarrings.Contains(armor[5].Set))
|
||||
original |= EqpEntry.HeadShowEarrings | EqpEntry.HeadShowEarringsAura | EqpEntry.HeadShowEarringsHuman;
|
||||
original |= EqpEntry.HeadShowEarringsHyurRoe | EqpEntry.HeadShowEarringsLalaElezen | EqpEntry.HeadShowEarringsMiqoHrothViera | EqpEntry.HeadShowEarringsAura;
|
||||
|
||||
if (_doNotHideNecklace.Contains(armor[6].Set))
|
||||
original |= EqpEntry.BodyShowNecklace | EqpEntry.HeadShowNecklace;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,12 +36,14 @@ public class MaterialExporter
|
|||
return material.Mtrl.ShaderPackage.Name switch
|
||||
{
|
||||
// NOTE: this isn't particularly precise to game behavior (it has some fade around high opacity), but good enough for now.
|
||||
"character.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.MASK, 0.5f),
|
||||
"characterglass.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.BLEND),
|
||||
"hair.shpk" => BuildHair(material, name),
|
||||
"iris.shpk" => BuildIris(material, name),
|
||||
"skin.shpk" => BuildSkin(material, name),
|
||||
_ => BuildFallback(material, name, notifier),
|
||||
"character.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.MASK, 0.5f),
|
||||
"characterlegacy.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.MASK, 0.5f),
|
||||
"characterglass.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.BLEND),
|
||||
"charactertattoo.shpk" => BuildCharacterTattoo(material, name),
|
||||
"hair.shpk" => BuildHair(material, name),
|
||||
"iris.shpk" => BuildIris(material, name),
|
||||
"skin.shpk" => BuildSkin(material, name),
|
||||
_ => BuildFallback(material, name, notifier),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -49,70 +51,65 @@ public class MaterialExporter
|
|||
private static MaterialBuilder BuildCharacter(Material material, string name)
|
||||
{
|
||||
// Build the textures from the color table.
|
||||
var table = new LegacyColorTable(material.Mtrl.Table!);
|
||||
var table = new ColorTable(material.Mtrl.Table!);
|
||||
var indexTexture = material.Textures[(TextureUsage)1449103320];
|
||||
var indexOperation = new ProcessCharacterIndexOperation(indexTexture, table);
|
||||
ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, indexTexture.Bounds, in indexOperation);
|
||||
|
||||
var normal = material.Textures[TextureUsage.SamplerNormal];
|
||||
var normalTexture = material.Textures[TextureUsage.SamplerNormal];
|
||||
var normalOperation = new ProcessCharacterNormalOperation(normalTexture);
|
||||
ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normalTexture.Bounds, in normalOperation);
|
||||
|
||||
var operation = new ProcessCharacterNormalOperation(normal, table);
|
||||
ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds, in operation);
|
||||
// Merge in opacity from the normal.
|
||||
var baseColor = indexOperation.BaseColor;
|
||||
MultiplyOperation.Execute(baseColor, normalOperation.BaseColorOpacity);
|
||||
|
||||
// Check if full textures are provided, and merge in if available.
|
||||
var baseColor = operation.BaseColor;
|
||||
// Check if a full diffuse is provided, and merge in if available.
|
||||
if (material.Textures.TryGetValue(TextureUsage.SamplerDiffuse, out var diffuse))
|
||||
{
|
||||
MultiplyOperation.Execute(diffuse, operation.BaseColor);
|
||||
MultiplyOperation.Execute(diffuse, indexOperation.BaseColor);
|
||||
baseColor = diffuse;
|
||||
}
|
||||
|
||||
Image specular = operation.Specular;
|
||||
var specular = indexOperation.Specular;
|
||||
if (material.Textures.TryGetValue(TextureUsage.SamplerSpecular, out var specularTexture))
|
||||
{
|
||||
MultiplyOperation.Execute(specularTexture, operation.Specular);
|
||||
MultiplyOperation.Execute(specularTexture, indexOperation.Specular);
|
||||
specular = specularTexture;
|
||||
}
|
||||
|
||||
// Pull further information from the mask.
|
||||
if (material.Textures.TryGetValue(TextureUsage.SamplerMask, out var maskTexture))
|
||||
{
|
||||
// Extract the red channel for "ambient occlusion".
|
||||
maskTexture.Mutate(context => context.Resize(baseColor.Width, baseColor.Height));
|
||||
maskTexture.ProcessPixelRows(baseColor, (maskAccessor, baseColorAccessor) =>
|
||||
{
|
||||
for (var y = 0; y < maskAccessor.Height; y++)
|
||||
{
|
||||
var maskSpan = maskAccessor.GetRowSpan(y);
|
||||
var baseColorSpan = baseColorAccessor.GetRowSpan(y);
|
||||
var maskOperation = new ProcessCharacterMaskOperation(maskTexture);
|
||||
ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, maskTexture.Bounds, in maskOperation);
|
||||
|
||||
for (var x = 0; x < maskSpan.Length; x++)
|
||||
baseColorSpan[x].FromVector4(baseColorSpan[x].ToVector4() * new Vector4(maskSpan[x].R / 255f));
|
||||
}
|
||||
});
|
||||
// TODO: handle other textures stored in the mask?
|
||||
// TODO: consider using the occusion gltf material property.
|
||||
MultiplyOperation.Execute(baseColor, maskOperation.Occlusion);
|
||||
|
||||
// Similar to base color's alpha, this is a pretty wasteful operation for a single channel.
|
||||
MultiplyOperation.Execute(specular, maskOperation.SpecularFactor);
|
||||
}
|
||||
|
||||
// Specular extension puts colour on RGB and factor on A. We're already packing like that, so we can reuse the texture.
|
||||
var specularImage = BuildImage(specular, name, "specular");
|
||||
|
||||
return BuildSharedBase(material, name)
|
||||
.WithBaseColor(BuildImage(baseColor, name, "basecolor"))
|
||||
.WithNormal(BuildImage(operation.Normal, name, "normal"))
|
||||
.WithEmissive(BuildImage(operation.Emissive, name, "emissive"), Vector3.One, 1)
|
||||
.WithBaseColor(BuildImage(baseColor, name, "basecolor"))
|
||||
.WithNormal(BuildImage(normalOperation.Normal, name, "normal"))
|
||||
.WithEmissive(BuildImage(indexOperation.Emissive, name, "emissive"), Vector3.One, 1)
|
||||
.WithSpecularFactor(specularImage, 1)
|
||||
.WithSpecularColor(specularImage);
|
||||
}
|
||||
|
||||
// TODO: It feels a little silly to request the entire normal here when extracting the normal only needs some of the components.
|
||||
// As a future refactor, it would be neat to accept a single-channel field here, and then do composition of other stuff later.
|
||||
// TODO(Dawntrail): Use the dedicated index (_id) map, that is not embedded in the normal map's alpha channel anymore.
|
||||
private readonly struct ProcessCharacterNormalOperation(Image<Rgba32> normal, LegacyColorTable table) : IRowOperation
|
||||
private readonly struct ProcessCharacterIndexOperation(Image<Rgba32> index, ColorTable table) : IRowOperation
|
||||
{
|
||||
public Image<Rgba32> Normal { get; } = normal.Clone();
|
||||
public Image<Rgba32> BaseColor { get; } = new(normal.Width, normal.Height);
|
||||
public Image<Rgba32> Specular { get; } = new(normal.Width, normal.Height);
|
||||
public Image<Rgb24> Emissive { get; } = new(normal.Width, normal.Height);
|
||||
public Image<Rgba32> BaseColor { get; } = new(index.Width, index.Height);
|
||||
public Image<Rgba32> Specular { get; } = new(index.Width, index.Height);
|
||||
public Image<Rgb24> Emissive { get; } = new(index.Width, index.Height);
|
||||
|
||||
private Buffer2D<Rgba32> NormalBuffer
|
||||
=> Normal.Frames.RootFrame.PixelBuffer;
|
||||
private Buffer2D<Rgba32> IndexBuffer
|
||||
=> index.Frames.RootFrame.PixelBuffer;
|
||||
|
||||
private Buffer2D<Rgba32> BaseColorBuffer
|
||||
=> BaseColor.Frames.RootFrame.PixelBuffer;
|
||||
|
|
@ -125,66 +122,96 @@ public class MaterialExporter
|
|||
|
||||
public void Invoke(int y)
|
||||
{
|
||||
var normalSpan = NormalBuffer.DangerousGetRowSpan(y);
|
||||
var indexSpan = IndexBuffer.DangerousGetRowSpan(y);
|
||||
var baseColorSpan = BaseColorBuffer.DangerousGetRowSpan(y);
|
||||
var specularSpan = SpecularBuffer.DangerousGetRowSpan(y);
|
||||
var emissiveSpan = EmissiveBuffer.DangerousGetRowSpan(y);
|
||||
|
||||
for (var x = 0; x < indexSpan.Length; x++)
|
||||
{
|
||||
ref var indexPixel = ref indexSpan[x];
|
||||
|
||||
// Calculate and fetch the color table rows being used for this pixel.
|
||||
var tablePair = (int) Math.Round(indexPixel.R / 17f);
|
||||
var rowBlend = 1.0f - indexPixel.G / 255f;
|
||||
|
||||
var prevRow = table[tablePair * 2];
|
||||
var nextRow = table[Math.Min(tablePair * 2 + 1, ColorTable.NumRows)];
|
||||
|
||||
// Lerp between table row values to fetch final pixel values for each subtexture.
|
||||
var lerpedDiffuse = Vector3.Lerp((Vector3)prevRow.DiffuseColor, (Vector3)nextRow.DiffuseColor, rowBlend);
|
||||
baseColorSpan[x].FromVector4(new Vector4(lerpedDiffuse, 1));
|
||||
|
||||
var lerpedSpecularColor = Vector3.Lerp((Vector3)prevRow.SpecularColor, (Vector3)nextRow.SpecularColor, rowBlend);
|
||||
specularSpan[x].FromVector4(new Vector4(lerpedSpecularColor, 1));
|
||||
|
||||
var lerpedEmissive = Vector3.Lerp((Vector3)prevRow.EmissiveColor, (Vector3)nextRow.EmissiveColor, rowBlend);
|
||||
emissiveSpan[x].FromVector4(new Vector4(lerpedEmissive, 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct ProcessCharacterNormalOperation(Image<Rgba32> normal) : IRowOperation
|
||||
{
|
||||
// TODO: Consider omitting the alpha channel here.
|
||||
public Image<Rgba32> Normal { get; } = normal.Clone();
|
||||
// TODO: We only really need the alpha here, however using A8 will result in the multiply later zeroing out the RGB channels.
|
||||
public Image<Rgba32> BaseColorOpacity { get; } = new(normal.Width, normal.Height);
|
||||
|
||||
private Buffer2D<Rgba32> NormalBuffer
|
||||
=> Normal.Frames.RootFrame.PixelBuffer;
|
||||
|
||||
private Buffer2D<Rgba32> BaseColorOpacityBuffer
|
||||
=> BaseColorOpacity.Frames.RootFrame.PixelBuffer;
|
||||
|
||||
public void Invoke(int y)
|
||||
{
|
||||
var normalSpan = NormalBuffer.DangerousGetRowSpan(y);
|
||||
var baseColorOpacitySpan = BaseColorOpacityBuffer.DangerousGetRowSpan(y);
|
||||
|
||||
for (var x = 0; x < normalSpan.Length; x++)
|
||||
{
|
||||
ref var normalPixel = ref normalSpan[x];
|
||||
|
||||
// Table row data (.a)
|
||||
var tableRow = GetTableRowIndices(normalPixel.A / 255f);
|
||||
var prevRow = table[tableRow.Previous];
|
||||
var nextRow = table[tableRow.Next];
|
||||
baseColorOpacitySpan[x].FromVector4(Vector4.One);
|
||||
baseColorOpacitySpan[x].A = normalPixel.B;
|
||||
|
||||
// Base colour (table, .b)
|
||||
var lerpedDiffuse = Vector3.Lerp((Vector3)prevRow.DiffuseColor, (Vector3)nextRow.DiffuseColor, tableRow.Weight);
|
||||
baseColorSpan[x].FromVector4(new Vector4(lerpedDiffuse, 1));
|
||||
baseColorSpan[x].A = normalPixel.B;
|
||||
|
||||
// Specular (table)
|
||||
var lerpedSpecularColor = Vector3.Lerp((Vector3)prevRow.SpecularColor, (Vector3)nextRow.SpecularColor, tableRow.Weight);
|
||||
var lerpedSpecularFactor = float.Lerp((float)prevRow.SpecularMask, (float)nextRow.SpecularMask, tableRow.Weight);
|
||||
specularSpan[x].FromVector4(new Vector4(lerpedSpecularColor, lerpedSpecularFactor));
|
||||
|
||||
// Emissive (table)
|
||||
var lerpedEmissive = Vector3.Lerp((Vector3)prevRow.EmissiveColor, (Vector3)nextRow.EmissiveColor, tableRow.Weight);
|
||||
emissiveSpan[x].FromVector4(new Vector4(lerpedEmissive, 1));
|
||||
|
||||
// Normal (.rg)
|
||||
// TODO: we don't actually need alpha at all for normal, but _not_ using the existing rgba texture means I'll need a new one, with a new accessor. Think about it.
|
||||
normalPixel.B = byte.MaxValue;
|
||||
normalPixel.A = byte.MaxValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static TableRow GetTableRowIndices(float input)
|
||||
private readonly struct ProcessCharacterMaskOperation(Image<Rgba32> mask) : IRowOperation
|
||||
{
|
||||
// These calculations are ported from character.shpk.
|
||||
var smoothed = MathF.Floor(input * 7.5f % 1.0f * 2)
|
||||
* (-input * 15 + MathF.Floor(input * 15 + 0.5f))
|
||||
+ input * 15;
|
||||
public Image<Rgba32> Occlusion { get; } = new(mask.Width, mask.Height);
|
||||
public Image<Rgba32> SpecularFactor { get; } = new(mask.Width, mask.Height);
|
||||
|
||||
var stepped = MathF.Floor(smoothed + 0.5f);
|
||||
private Buffer2D<Rgba32> MaskBuffer
|
||||
=> mask.Frames.RootFrame.PixelBuffer;
|
||||
|
||||
return new TableRow
|
||||
private Buffer2D<Rgba32> OcclusionBuffer
|
||||
=> Occlusion.Frames.RootFrame.PixelBuffer;
|
||||
|
||||
private Buffer2D<Rgba32> SpecularFactorBuffer
|
||||
=> SpecularFactor.Frames.RootFrame.PixelBuffer;
|
||||
|
||||
public void Invoke(int y)
|
||||
{
|
||||
Stepped = (int)stepped,
|
||||
Previous = (int)MathF.Floor(smoothed),
|
||||
Next = (int)MathF.Ceiling(smoothed),
|
||||
Weight = smoothed % 1,
|
||||
};
|
||||
}
|
||||
var maskSpan = MaskBuffer.DangerousGetRowSpan(y);
|
||||
var occlusionSpan = OcclusionBuffer.DangerousGetRowSpan(y);
|
||||
var specularFactorSpan = SpecularFactorBuffer.DangerousGetRowSpan(y);
|
||||
|
||||
private ref struct TableRow
|
||||
{
|
||||
public int Stepped;
|
||||
public int Previous;
|
||||
public int Next;
|
||||
public float Weight;
|
||||
for (var x = 0; x < maskSpan.Length; x++)
|
||||
{
|
||||
ref var maskPixel = ref maskSpan[x];
|
||||
|
||||
occlusionSpan[x].FromL8(new L8(maskPixel.B));
|
||||
|
||||
specularFactorSpan[x].FromVector4(Vector4.One);
|
||||
specularFactorSpan[x].A = maskPixel.R;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct MultiplyOperation
|
||||
|
|
@ -218,6 +245,37 @@ public class MaterialExporter
|
|||
}
|
||||
}
|
||||
|
||||
private static readonly Vector4 DefaultTattooColor = new Vector4(38, 112, 102, 255) / new Vector4(255);
|
||||
|
||||
private static MaterialBuilder BuildCharacterTattoo(Material material, string name)
|
||||
{
|
||||
var normal = material.Textures[TextureUsage.SamplerNormal];
|
||||
var baseColor = new Image<Rgba32>(normal.Width, normal.Height);
|
||||
|
||||
normal.ProcessPixelRows(baseColor, (normalAccessor, baseColorAccessor) =>
|
||||
{
|
||||
for (var y = 0; y < normalAccessor.Height; y++)
|
||||
{
|
||||
var normalSpan = normalAccessor.GetRowSpan(y);
|
||||
var baseColorSpan = baseColorAccessor.GetRowSpan(y);
|
||||
|
||||
for (var x = 0; x < normalSpan.Length; x++)
|
||||
{
|
||||
baseColorSpan[x].FromVector4(DefaultTattooColor);
|
||||
baseColorSpan[x].A = normalSpan[x].A;
|
||||
|
||||
normalSpan[x].B = byte.MaxValue;
|
||||
normalSpan[x].A = byte.MaxValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return BuildSharedBase(material, name)
|
||||
.WithBaseColor(BuildImage(baseColor, name, "basecolor"))
|
||||
.WithNormal(BuildImage(normal, name, "normal"))
|
||||
.WithAlpha(AlphaMode.BLEND);
|
||||
}
|
||||
|
||||
// TODO: These are hardcoded colours - I'm not keen on supporting highly customizable exports, but there's possibly some more sensible values to use here.
|
||||
private static readonly Vector4 DefaultHairColor = new Vector4(130, 64, 13, 255) / new Vector4(255);
|
||||
private static readonly Vector4 DefaultHighlightColor = new Vector4(77, 126, 240, 255) / new Vector4(255);
|
||||
|
|
@ -248,10 +306,11 @@ public class MaterialExporter
|
|||
|
||||
for (var x = 0; x < normalSpan.Length; x++)
|
||||
{
|
||||
var color = Vector4.Lerp(DefaultHairColor, DefaultHighlightColor, maskSpan[x].A / 255f);
|
||||
baseColorSpan[x].FromVector4(color * new Vector4(maskSpan[x].R / 255f));
|
||||
var color = Vector4.Lerp(DefaultHairColor, DefaultHighlightColor, normalSpan[x].B / 255f);
|
||||
baseColorSpan[x].FromVector4(color * new Vector4(maskSpan[x].A / 255f));
|
||||
baseColorSpan[x].A = normalSpan[x].A;
|
||||
|
||||
normalSpan[x].B = byte.MaxValue;
|
||||
normalSpan[x].A = byte.MaxValue;
|
||||
}
|
||||
}
|
||||
|
|
@ -269,26 +328,23 @@ public class MaterialExporter
|
|||
// NOTE: This is largely the same as the hair material, but is also missing a few features that would cause it to diverge. Keeping separate for now.
|
||||
private static MaterialBuilder BuildIris(Material material, string name)
|
||||
{
|
||||
var normal = material.Textures[TextureUsage.SamplerNormal];
|
||||
var mask = material.Textures[TextureUsage.SamplerMask];
|
||||
var normal = material.Textures[TextureUsage.SamplerNormal];
|
||||
var mask = material.Textures[TextureUsage.SamplerMask];
|
||||
var baseColor = material.Textures[TextureUsage.SamplerDiffuse];
|
||||
|
||||
mask.Mutate(context => context.Resize(normal.Width, normal.Height));
|
||||
mask.Mutate(context => context.Resize(baseColor.Width, baseColor.Height));
|
||||
|
||||
var baseColor = new Image<Rgba32>(normal.Width, normal.Height);
|
||||
normal.ProcessPixelRows(mask, baseColor, (normalAccessor, maskAccessor, baseColorAccessor) =>
|
||||
baseColor.ProcessPixelRows(mask, (baseColorAccessor, maskAccessor) =>
|
||||
{
|
||||
for (var y = 0; y < normalAccessor.Height; y++)
|
||||
for (var y = 0; y < baseColor.Height; y++)
|
||||
{
|
||||
var normalSpan = normalAccessor.GetRowSpan(y);
|
||||
var maskSpan = maskAccessor.GetRowSpan(y);
|
||||
var baseColorSpan = baseColorAccessor.GetRowSpan(y);
|
||||
var maskSpan = maskAccessor.GetRowSpan(y);
|
||||
|
||||
for (var x = 0; x < normalSpan.Length; x++)
|
||||
for (var x = 0; x < baseColorSpan.Length; x++)
|
||||
{
|
||||
baseColorSpan[x].FromVector4(DefaultEyeColor * new Vector4(maskSpan[x].R / 255f));
|
||||
baseColorSpan[x].A = normalSpan[x].A;
|
||||
|
||||
normalSpan[x].A = byte.MaxValue;
|
||||
var eyeColor = Vector4.Lerp(Vector4.One, DefaultEyeColor, maskSpan[x].B / 255f);
|
||||
baseColorSpan[x].FromVector4(baseColorSpan[x].ToVector4() * eyeColor);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -314,21 +370,7 @@ public class MaterialExporter
|
|||
var diffuse = material.Textures[TextureUsage.SamplerDiffuse];
|
||||
var normal = material.Textures[TextureUsage.SamplerNormal];
|
||||
|
||||
// Create a copy of the normal that's the same size as the diffuse for purposes of copying the opacity across.
|
||||
var resizedNormal = normal.Clone(context => context.Resize(diffuse.Width, diffuse.Height));
|
||||
diffuse.ProcessPixelRows(resizedNormal, (diffuseAccessor, normalAccessor) =>
|
||||
{
|
||||
for (var y = 0; y < diffuseAccessor.Height; y++)
|
||||
{
|
||||
var diffuseSpan = diffuseAccessor.GetRowSpan(y);
|
||||
var normalSpan = normalAccessor.GetRowSpan(y);
|
||||
|
||||
for (var x = 0; x < diffuseSpan.Length; x++)
|
||||
diffuseSpan[x].A = normalSpan[x].B;
|
||||
}
|
||||
});
|
||||
|
||||
// Clear the blue channel out of the normal now that we're done with it.
|
||||
// The normal also stores the skin color influence (.b) and wetness mask (.a) - remove.
|
||||
normal.ProcessPixelRows(normalAccessor =>
|
||||
{
|
||||
for (var y = 0; y < normalAccessor.Height; y++)
|
||||
|
|
@ -336,7 +378,10 @@ public class MaterialExporter
|
|||
var normalSpan = normalAccessor.GetRowSpan(y);
|
||||
|
||||
for (var x = 0; x < normalSpan.Length; x++)
|
||||
{
|
||||
normalSpan[x].B = byte.MaxValue;
|
||||
normalSpan[x].A = byte.MaxValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -312,12 +311,10 @@ public class MeshExporter
|
|||
MdlFile.VertexType.Single3 => new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()),
|
||||
MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()),
|
||||
MdlFile.VertexType.UByte4 => reader.ReadBytes(4),
|
||||
MdlFile.VertexType.NByte4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f,
|
||||
reader.ReadByte() / 255f),
|
||||
MdlFile.VertexType.NByte4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f),
|
||||
MdlFile.VertexType.Half2 => new Vector2((float)reader.ReadHalf(), (float)reader.ReadHalf()),
|
||||
MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(),
|
||||
(float)reader.ReadHalf()),
|
||||
|
||||
MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf()),
|
||||
MdlFile.VertexType.UShort4 => reader.ReadBytes(8),
|
||||
var other => throw _notifier.Exception<ArgumentOutOfRangeException>($"Unhandled vertex type {other}"),
|
||||
};
|
||||
}
|
||||
|
|
@ -445,7 +442,16 @@ public class MeshExporter
|
|||
private static Type GetSkinningType(IReadOnlyDictionary<MdlFile.VertexUsage, MdlFile.VertexType> usages)
|
||||
{
|
||||
if (usages.ContainsKey(MdlFile.VertexUsage.BlendWeights) && usages.ContainsKey(MdlFile.VertexUsage.BlendIndices))
|
||||
return typeof(VertexJoints4);
|
||||
{
|
||||
if (usages[MdlFile.VertexUsage.BlendWeights] == MdlFile.VertexType.UShort4)
|
||||
{
|
||||
return typeof(VertexJoints8);
|
||||
}
|
||||
else
|
||||
{
|
||||
return typeof(VertexJoints4);
|
||||
}
|
||||
}
|
||||
|
||||
return typeof(VertexEmpty);
|
||||
}
|
||||
|
|
@ -456,15 +462,17 @@ public class MeshExporter
|
|||
if (_skinningType == typeof(VertexEmpty))
|
||||
return new VertexEmpty();
|
||||
|
||||
if (_skinningType == typeof(VertexJoints4))
|
||||
if (_skinningType == typeof(VertexJoints4) || _skinningType == typeof(VertexJoints8))
|
||||
{
|
||||
if (_boneIndexMap == null)
|
||||
throw _notifier.Exception("Tried to build skinned vertex but no bone mappings are available.");
|
||||
|
||||
var indices = ToByteArray(attributes[MdlFile.VertexUsage.BlendIndices]);
|
||||
var weights = ToVector4(attributes[MdlFile.VertexUsage.BlendWeights]);
|
||||
|
||||
var bindings = Enumerable.Range(0, 4)
|
||||
var indiciesData = attributes[MdlFile.VertexUsage.BlendIndices];
|
||||
var weightsData = attributes[MdlFile.VertexUsage.BlendWeights];
|
||||
var indices = ToByteArray(indiciesData);
|
||||
var weights = ToFloatArray(weightsData);
|
||||
|
||||
var bindings = Enumerable.Range(0, indices.Length)
|
||||
.Select(bindingIndex =>
|
||||
{
|
||||
// NOTE: I've not seen any files that throw this error that aren't completely broken.
|
||||
|
|
@ -475,7 +483,13 @@ public class MeshExporter
|
|||
return (jointIndex, weights[bindingIndex]);
|
||||
})
|
||||
.ToArray();
|
||||
return new VertexJoints4(bindings);
|
||||
|
||||
return bindings.Length switch
|
||||
{
|
||||
4 => new VertexJoints4(bindings),
|
||||
8 => new VertexJoints8(bindings),
|
||||
_ => throw _notifier.Exception($"Invalid number of bone bindings {bindings.Length}.")
|
||||
};
|
||||
}
|
||||
|
||||
throw _notifier.Exception($"Unknown skinning type {_skinningType}");
|
||||
|
|
@ -518,4 +532,13 @@ public class MeshExporter
|
|||
byte[] value => value,
|
||||
_ => throw new ArgumentOutOfRangeException($"Invalid byte[] input {data}"),
|
||||
};
|
||||
|
||||
private static float[] ToFloatArray(object data)
|
||||
=> data switch
|
||||
{
|
||||
byte[] value => value.Select(x => x / 255f).ToArray(),
|
||||
Vector4 v4 => new[] { v4.X, v4.Y, v4.Z, v4.W },
|
||||
_ => throw new ArgumentOutOfRangeException($"Invalid float[] input {data}"),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -194,24 +194,34 @@ public class MeshImporter(IEnumerable<Node> nodes, IoNotifier notifier)
|
|||
foreach (var (primitive, primitiveIndex) in node.Mesh.Primitives.WithIndex())
|
||||
{
|
||||
// Per glTF specification, an asset with a skin MUST contain skinning attributes on its meshes.
|
||||
var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0")?.AsVector4Array();
|
||||
var weightsAccessor = primitive.GetVertexAccessor("WEIGHTS_0")?.AsVector4Array();
|
||||
|
||||
if (jointsAccessor == null || weightsAccessor == null)
|
||||
var joints0Accessor = primitive.GetVertexAccessor("JOINTS_0")?.AsVector4Array();
|
||||
var weights0Accessor = primitive.GetVertexAccessor("WEIGHTS_0")?.AsVector4Array();
|
||||
var joints1Accessor = primitive.GetVertexAccessor("JOINTS_1")?.AsVector4Array();
|
||||
var weights1Accessor = primitive.GetVertexAccessor("WEIGHTS_1")?.AsVector4Array();
|
||||
|
||||
if (joints0Accessor == null || weights0Accessor == null)
|
||||
throw notifier.Exception($"Primitive {primitiveIndex} is skinned but does not contain skinning vertex attributes.");
|
||||
|
||||
// Build a set of joints that are referenced by this mesh.
|
||||
for (var i = 0; i < jointsAccessor.Count; i++)
|
||||
for (var i = 0; i < joints0Accessor.Count; i++)
|
||||
{
|
||||
var joints = jointsAccessor[i];
|
||||
var weights = weightsAccessor[i];
|
||||
var joints0 = joints0Accessor[i];
|
||||
var weights0 = weights0Accessor[i];
|
||||
var joints1 = joints1Accessor?[i];
|
||||
var weights1 = weights1Accessor?[i];
|
||||
for (var index = 0; index < 4; index++)
|
||||
{
|
||||
// If a joint has absolutely no weight, we omit the bone entirely.
|
||||
if (weights[index] == 0)
|
||||
continue;
|
||||
if (weights0[index] != 0)
|
||||
{
|
||||
usedJoints.Add((ushort)joints0[index]);
|
||||
}
|
||||
|
||||
usedJoints.Add((ushort)joints[index]);
|
||||
|
||||
if (joints1 != null && weights1 != null && weights1.Value[index] != 0)
|
||||
{
|
||||
usedJoints.Add((ushort)joints1.Value[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ public class VertexAttribute
|
|||
MdlFile.VertexType.NByte4 => 4,
|
||||
MdlFile.VertexType.Half2 => 4,
|
||||
MdlFile.VertexType.Half4 => 8,
|
||||
MdlFile.VertexType.UShort4 => 8,
|
||||
|
||||
_ => throw new Exception($"Unhandled vertex type {(MdlFile.VertexType)Element.Type}"),
|
||||
};
|
||||
|
|
@ -121,87 +122,120 @@ public class VertexAttribute
|
|||
|
||||
public static VertexAttribute? BlendWeight(Accessors accessors, IoNotifier notifier)
|
||||
{
|
||||
if (!accessors.TryGetValue("WEIGHTS_0", out var accessor))
|
||||
if (!accessors.TryGetValue("WEIGHTS_0", out var weights0Accessor))
|
||||
return null;
|
||||
|
||||
if (!accessors.ContainsKey("JOINTS_0"))
|
||||
throw notifier.Exception("Mesh contained WEIGHTS_0 attribute but no corresponding JOINTS_0 attribute.");
|
||||
|
||||
if (accessors.TryGetValue("WEIGHTS_1", out var weights1Accessor))
|
||||
{
|
||||
if (!accessors.ContainsKey("JOINTS_1"))
|
||||
throw notifier.Exception("Mesh contained WEIGHTS_1 attribute but no corresponding JOINTS_1 attribute.");
|
||||
}
|
||||
|
||||
var element = new MdlStructs.VertexElement()
|
||||
{
|
||||
Stream = 0,
|
||||
Type = (byte)MdlFile.VertexType.NByte4,
|
||||
Usage = (byte)MdlFile.VertexUsage.BlendWeights,
|
||||
Type = (byte)MdlFile.VertexType.UShort4,
|
||||
Usage = (byte)MdlFile.VertexUsage.BlendWeights,
|
||||
};
|
||||
|
||||
var values = accessor.AsVector4Array();
|
||||
var weights0 = weights0Accessor.AsVector4Array();
|
||||
var weights1 = weights1Accessor?.AsVector4Array();
|
||||
|
||||
return new VertexAttribute(
|
||||
element,
|
||||
index => {
|
||||
// Blend weights are _very_ sensitive to float imprecision - a vertex sum being off
|
||||
// by one, such as 256, is enough to cause a visible defect. To avoid this, we tweak
|
||||
// the converted values to have the expected sum, preferencing values with minimal differences.
|
||||
var originalValues = values[index];
|
||||
var byteValues = BuildNByte4(originalValues);
|
||||
|
||||
var adjustment = 255 - byteValues.Select(value => (int)value).Sum();
|
||||
while (adjustment != 0)
|
||||
{
|
||||
var convertedValues = byteValues.Select(value => value * (1f / 255f)).ToArray();
|
||||
var closestIndex = Enumerable.Range(0, 4)
|
||||
.Where(index => {
|
||||
var byteValue = byteValues[index];
|
||||
if (adjustment < 0) return byteValue > 0;
|
||||
if (adjustment > 0) return byteValue < 255;
|
||||
return true;
|
||||
})
|
||||
.Select(index => (index, delta: Math.Abs(originalValues[index] - convertedValues[index])))
|
||||
.MinBy(x => x.delta)
|
||||
.index;
|
||||
byteValues[closestIndex] = (byte)(byteValues[closestIndex] + Math.CopySign(1, adjustment));
|
||||
adjustment = 255 - byteValues.Select(value => (int)value).Sum();
|
||||
}
|
||||
|
||||
return byteValues;
|
||||
}
|
||||
index => BuildBlendWeights(weights0[index], weights1?[index] ?? Vector4.Zero)
|
||||
);
|
||||
}
|
||||
|
||||
private static byte[] BuildBlendWeights(Vector4 v1, Vector4 v2)
|
||||
{
|
||||
var originalData = BuildUshort4(v1, v2);
|
||||
var byteValues = new byte[originalData.Length];
|
||||
for (var i = 0; i < originalData.Length; i++)
|
||||
{
|
||||
byteValues[i] = (byte)Math.Round(originalData[i] * 255f);
|
||||
}
|
||||
|
||||
// Blend weights are _very_ sensitive to float imprecision - a vertex sum being off
|
||||
// by one, such as 256, is enough to cause a visible defect. To avoid this, we tweak
|
||||
// the converted values to have the expected sum, preferencing values with minimal differences.
|
||||
var adjustment = 255 - byteValues.Sum(value => value);
|
||||
while (adjustment != 0)
|
||||
{
|
||||
var closestIndex = Enumerable.Range(0, byteValues.Length)
|
||||
.Where(i => adjustment switch
|
||||
{
|
||||
< 0 => byteValues[i] > 0,
|
||||
> 0 => byteValues[i] < 255,
|
||||
_ => true,
|
||||
})
|
||||
.Select(index => (index, delta: Math.Abs(originalData[index] - (byteValues[index] * (1f / 255f)))))
|
||||
.MinBy(x => x.delta)
|
||||
.index;
|
||||
byteValues[closestIndex] += (byte)Math.CopySign(1, adjustment);
|
||||
adjustment = 255 - byteValues.Sum(value => value);
|
||||
}
|
||||
|
||||
return byteValues;
|
||||
}
|
||||
|
||||
public static VertexAttribute? BlendIndex(Accessors accessors, IDictionary<ushort, ushort>? boneMap, IoNotifier notifier)
|
||||
{
|
||||
if (!accessors.TryGetValue("JOINTS_0", out var jointsAccessor))
|
||||
if (!accessors.TryGetValue("JOINTS_0", out var joints0Accessor))
|
||||
return null;
|
||||
|
||||
if (!accessors.TryGetValue("WEIGHTS_0", out var weightsAccessor))
|
||||
if (!accessors.TryGetValue("WEIGHTS_0", out var weights0Accessor))
|
||||
throw notifier.Exception("Mesh contained JOINTS_0 attribute but no corresponding WEIGHTS_0 attribute.");
|
||||
|
||||
if (boneMap == null)
|
||||
throw notifier.Exception("Mesh contained JOINTS_0 attribute but no bone mapping was created.");
|
||||
|
||||
var element = new MdlStructs.VertexElement()
|
||||
var joints0 = joints0Accessor.AsVector4Array();
|
||||
var weights0 = weights0Accessor.AsVector4Array();
|
||||
accessors.TryGetValue("JOINTS_1", out var joints1Accessor);
|
||||
accessors.TryGetValue("WEIGHTS_1", out var weights1Accessor);
|
||||
var element = new MdlStructs.VertexElement
|
||||
{
|
||||
Stream = 0,
|
||||
Type = (byte)MdlFile.VertexType.UByte4,
|
||||
Type = (byte)MdlFile.VertexType.UShort4,
|
||||
Usage = (byte)MdlFile.VertexUsage.BlendIndices,
|
||||
};
|
||||
|
||||
var joints = jointsAccessor.AsVector4Array();
|
||||
var weights = weightsAccessor.AsVector4Array();
|
||||
var joints1 = joints1Accessor?.AsVector4Array();
|
||||
var weights1 = weights1Accessor?.AsVector4Array();
|
||||
|
||||
return new VertexAttribute(
|
||||
element,
|
||||
index =>
|
||||
{
|
||||
var gltfIndices = joints[index];
|
||||
var gltfWeights = weights[index];
|
||||
var gltfIndices0 = joints0[index];
|
||||
var gltfWeights0 = weights0[index];
|
||||
var gltfIndices1 = joints1?[index];
|
||||
var gltfWeights1 = weights1?[index];
|
||||
var v0 = new Vector4(
|
||||
gltfWeights0.X == 0 ? 0 : boneMap[(ushort)gltfIndices0.X],
|
||||
gltfWeights0.Y == 0 ? 0 : boneMap[(ushort)gltfIndices0.Y],
|
||||
gltfWeights0.Z == 0 ? 0 : boneMap[(ushort)gltfIndices0.Z],
|
||||
gltfWeights0.W == 0 ? 0 : boneMap[(ushort)gltfIndices0.W]
|
||||
);
|
||||
|
||||
var v1 = Vector4.Zero;
|
||||
if (gltfIndices1 != null && gltfWeights1 != null)
|
||||
{
|
||||
v1 = new Vector4(
|
||||
gltfWeights1.Value.X == 0 ? 0 : boneMap[(ushort)gltfIndices1.Value.X],
|
||||
gltfWeights1.Value.Y == 0 ? 0 : boneMap[(ushort)gltfIndices1.Value.Y],
|
||||
gltfWeights1.Value.Z == 0 ? 0 : boneMap[(ushort)gltfIndices1.Value.Z],
|
||||
gltfWeights1.Value.W == 0 ? 0 : boneMap[(ushort)gltfIndices1.Value.W]
|
||||
);
|
||||
}
|
||||
|
||||
return BuildUByte4(new Vector4(
|
||||
gltfWeights.X == 0 ? 0 : boneMap[(ushort)gltfIndices.X],
|
||||
gltfWeights.Y == 0 ? 0 : boneMap[(ushort)gltfIndices.Y],
|
||||
gltfWeights.Z == 0 ? 0 : boneMap[(ushort)gltfIndices.Z],
|
||||
gltfWeights.W == 0 ? 0 : boneMap[(ushort)gltfIndices.W]
|
||||
));
|
||||
var byteValues = BuildUshort4(v0, v1);
|
||||
|
||||
return byteValues.Select(x => (byte)x).ToArray();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -232,7 +266,7 @@ public class VertexAttribute
|
|||
var value = values[vertexIndex];
|
||||
|
||||
var delta = morphValues[morphIndex]?[vertexIndex];
|
||||
if (delta != null)
|
||||
if (delta != null)
|
||||
value += delta.Value;
|
||||
|
||||
return BuildSingle3(value);
|
||||
|
|
@ -489,4 +523,11 @@ public class VertexAttribute
|
|||
(byte)Math.Round(input.Z * 255f),
|
||||
(byte)Math.Round(input.W * 255f),
|
||||
];
|
||||
|
||||
private static float[] BuildUshort4(Vector4 v0, Vector4 v1) =>
|
||||
new[]
|
||||
{
|
||||
v0.X, v0.Y, v0.Z, v0.W,
|
||||
v1.X, v1.Y, v1.Z, v1.W,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,21 +46,28 @@ public partial class TexToolsImporter
|
|||
{
|
||||
ImGui.NewLine();
|
||||
ImGui.NewLine();
|
||||
percentage = _currentNumOptions == 0 ? 1f : _currentOptionIdx / (float)_currentNumOptions;
|
||||
ImGui.ProgressBar(percentage, size, $"Option {_currentOptionIdx + 1} / {_currentNumOptions}");
|
||||
if (_currentOptionIdx >= _currentNumOptions)
|
||||
ImGui.ProgressBar(1f, size, $"Extracted {_currentNumOptions} Options");
|
||||
else
|
||||
ImGui.ProgressBar(_currentOptionIdx / (float)_currentNumOptions, size,
|
||||
$"Extracting Option {_currentOptionIdx + 1} / {_currentNumOptions}...");
|
||||
|
||||
ImGui.NewLine();
|
||||
if (State != ImporterState.DeduplicatingFiles)
|
||||
ImGui.TextUnformatted(
|
||||
$"Extracting option {(_currentGroupName.Length == 0 ? string.Empty : $"{_currentGroupName} - ")}{_currentOptionName}...");
|
||||
$"Extracting Option {(_currentGroupName.Length == 0 ? string.Empty : $"{_currentGroupName} - ")}{_currentOptionName}...");
|
||||
}
|
||||
|
||||
ImGui.NewLine();
|
||||
ImGui.NewLine();
|
||||
percentage = _currentNumFiles == 0 ? 1f : _currentFileIdx / (float)_currentNumFiles;
|
||||
ImGui.ProgressBar(percentage, size, $"File {_currentFileIdx + 1} / {_currentNumFiles}");
|
||||
if (_currentFileIdx >= _currentNumFiles)
|
||||
ImGui.ProgressBar(1f, size, $"Extracted {_currentNumFiles} Files");
|
||||
else
|
||||
ImGui.ProgressBar(_currentFileIdx / (float)_currentNumFiles, size, $"Extracting File {_currentFileIdx + 1} / {_currentNumFiles}...");
|
||||
|
||||
ImGui.NewLine();
|
||||
if (State != ImporterState.DeduplicatingFiles)
|
||||
ImGui.TextUnformatted($"Extracting file {_currentFileName}...");
|
||||
ImGui.TextUnformatted($"Extracting File {_currentFileName}...");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ public partial class TexToolsImporter
|
|||
_currentGroupName = string.Empty;
|
||||
_currentOptionName = "Default";
|
||||
ExtractSimpleModList(_currentModDirectory, modList.SimpleModsList);
|
||||
++_currentOptionIdx;
|
||||
}
|
||||
|
||||
// Iterate through all pages
|
||||
|
|
@ -208,6 +209,7 @@ public partial class TexToolsImporter
|
|||
options.Insert(idx, MultiSubMod.WithoutGroup(option.Name, option.Description, ModPriority.Default));
|
||||
if (option.IsChecked)
|
||||
defaultSettings = Setting.Single(idx);
|
||||
++_currentOptionIdx;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ using Dalamud.Hooking;
|
|||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Interop.PathResolving;
|
||||
using static FFXIVClientStructs.FFXIV.Client.Game.Character.ActionEffectHandler;
|
||||
|
||||
namespace Penumbra.Interop.Hooks.Resources;
|
||||
|
||||
|
|
@ -212,30 +215,74 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
|
|||
return ret;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
private struct ChangedEquipData
|
||||
{
|
||||
[FieldOffset(0)]
|
||||
public PrimaryId Model;
|
||||
|
||||
[FieldOffset(2)]
|
||||
public Variant Variant;
|
||||
|
||||
[FieldOffset(8)]
|
||||
public PrimaryId BonusModel;
|
||||
|
||||
[FieldOffset(10)]
|
||||
public Variant BonusVariant;
|
||||
|
||||
[FieldOffset(20)]
|
||||
public ushort VfxId;
|
||||
|
||||
[FieldOffset(22)]
|
||||
public GenderRace GenderRace;
|
||||
}
|
||||
|
||||
private nint ResolveVfxHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint unkOutParam)
|
||||
{
|
||||
if (slotIndex is <= 4 or >= 10)
|
||||
return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam);
|
||||
switch (slotIndex)
|
||||
{
|
||||
case <= 4: return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam);
|
||||
case <= 10:
|
||||
{
|
||||
// Enable vfxs for accessories
|
||||
var changedEquipData = (ChangedEquipData*)((Human*)drawObject)->ChangedEquipData;
|
||||
if (changedEquipData == null)
|
||||
return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam);
|
||||
|
||||
var changedEquipData = ((Human*)drawObject)->ChangedEquipData;
|
||||
// Enable vfxs for accessories
|
||||
if (changedEquipData == null)
|
||||
return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam);
|
||||
ref var slot = ref changedEquipData[slotIndex];
|
||||
|
||||
var slot = (ushort*)(changedEquipData + 12 * (nint)slotIndex);
|
||||
var model = slot[0];
|
||||
var variant = slot[1];
|
||||
var vfxId = slot[4];
|
||||
if (slot.Model == 0 || slot.Variant == 0 || slot.VfxId == 0)
|
||||
return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam);
|
||||
|
||||
if (model == 0 || variant == 0 || vfxId == 0)
|
||||
return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam);
|
||||
if (!Utf8.TryWrite(new Span<byte>((void*)pathBuffer, (int)pathBufferSize),
|
||||
$"chara/accessory/a{slot.Model.Id:D4}/vfx/eff/va{slot.VfxId:D4}.avfx\0",
|
||||
out _))
|
||||
return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam);
|
||||
|
||||
if (!Utf8.TryWrite(new Span<byte>((void*)pathBuffer, (int)pathBufferSize), $"chara/accessory/a{model:D4}/vfx/eff/va{vfxId:D4}.avfx\0",
|
||||
out _))
|
||||
return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam);
|
||||
*(ulong*)unkOutParam = 4;
|
||||
return ResolvePath(drawObject, pathBuffer);
|
||||
}
|
||||
case 16:
|
||||
{
|
||||
// Enable vfxs for glasses
|
||||
var changedEquipData = (ChangedEquipData*)((Human*)drawObject)->ChangedEquipData;
|
||||
if (changedEquipData == null)
|
||||
return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam);
|
||||
|
||||
*(ulong*)unkOutParam = 4;
|
||||
return ResolvePath(drawObject, pathBuffer);
|
||||
ref var slot = ref changedEquipData[slotIndex - 6];
|
||||
|
||||
if (slot.BonusModel == 0 || slot.BonusVariant == 0 || slot.VfxId == 0)
|
||||
return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam);
|
||||
if (!Utf8.TryWrite(new Span<byte>((void*)pathBuffer, (int)pathBufferSize),
|
||||
$"chara/equipment/e{slot.BonusModel.Id:D4}/vfx/eff/ve{slot.VfxId:D4}.avfx\0",
|
||||
out _))
|
||||
return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam);
|
||||
|
||||
*(ulong*)unkOutParam = 4;
|
||||
return ResolvePath(drawObject, pathBuffer);
|
||||
}
|
||||
default: return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam);
|
||||
}
|
||||
}
|
||||
|
||||
private nint VFunc81(nint drawObject, int estType, nint unk)
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
|
|||
if (_originalColorTableTexture.Texture == null)
|
||||
throw new InvalidOperationException("Material doesn't have a color table");
|
||||
|
||||
Width = (int)_originalColorTableTexture.Texture->Width;
|
||||
Height = (int)_originalColorTableTexture.Texture->Height;
|
||||
Width = (int)_originalColorTableTexture.Texture->ActualWidth;
|
||||
Height = (int)_originalColorTableTexture.Texture->ActualHeight;
|
||||
ColorTable = new Half[Width * Height * 4];
|
||||
_updatePending = true;
|
||||
|
||||
|
|
|
|||
|
|
@ -36,13 +36,13 @@ internal partial record ResolveContext
|
|||
private Utf8GamePath ResolveEquipmentModelPath()
|
||||
{
|
||||
var path = IsEquipmentSlot(SlotIndex)
|
||||
? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot)
|
||||
: GamePaths.Accessory.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot);
|
||||
? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot.ToSlot())
|
||||
: GamePaths.Accessory.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), SlotIndex.ToEquipSlot());
|
||||
return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
|
||||
}
|
||||
|
||||
private GenderRace ResolveModelRaceCode()
|
||||
=> ResolveEqdpRaceCode(Slot, Equipment.Set);
|
||||
=> ResolveEqdpRaceCode(Slot.ToSlot(), Equipment.Set);
|
||||
|
||||
private unsafe GenderRace ResolveEqdpRaceCode(EquipSlot slot, PrimaryId primaryId)
|
||||
{
|
||||
|
|
@ -161,7 +161,7 @@ internal partial record ResolveContext
|
|||
return variant.Id;
|
||||
}
|
||||
|
||||
var entry = ImcFile.GetEntry(imcFileData, Slot, variant, out var exists);
|
||||
var entry = ImcFile.GetEntry(imcFileData, Slot.ToSlot(), variant, out var exists);
|
||||
if (!exists)
|
||||
return variant.Id;
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ internal record GlobalResolveContext(
|
|||
public readonly Dictionary<(Utf8GamePath, nint), ResourceNode> Nodes = new(128);
|
||||
|
||||
public unsafe ResolveContext CreateContext(CharaBase* characterBase, uint slotIndex = 0xFFFFFFFFu,
|
||||
EquipSlot slot = EquipSlot.Unknown, CharacterArmor equipment = default, SecondaryId secondaryId = default)
|
||||
FullEquipType slot = FullEquipType.Unknown, CharacterArmor equipment = default, SecondaryId secondaryId = default)
|
||||
=> new(this, characterBase, slotIndex, slot, equipment, secondaryId);
|
||||
}
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ internal unsafe partial record ResolveContext(
|
|||
GlobalResolveContext Global,
|
||||
Pointer<CharaBase> CharacterBasePointer,
|
||||
uint SlotIndex,
|
||||
EquipSlot Slot,
|
||||
FullEquipType Slot,
|
||||
CharacterArmor Equipment,
|
||||
SecondaryId SecondaryId)
|
||||
{
|
||||
|
|
@ -340,19 +340,20 @@ internal unsafe partial record ResolveContext(
|
|||
|
||||
internal ResourceNode.UiData GuessModelUiData(Utf8GamePath gamePath)
|
||||
{
|
||||
var path = gamePath.ToString().Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
var path = gamePath.Path.Split((byte)'/');
|
||||
// Weapons intentionally left out.
|
||||
var isEquipment = SafeGet(path, 0) == "chara" && SafeGet(path, 1) is "accessory" or "equipment";
|
||||
var isEquipment = path.Count >= 2 && path[0].Span.SequenceEqual("chara"u8) && (path[1].Span.SequenceEqual("accessory"u8) || path[1].Span.SequenceEqual("equipment"u8));
|
||||
if (isEquipment)
|
||||
foreach (var item in Global.Identifier.Identify(Equipment.Set, 0, Equipment.Variant, Slot.ToSlot()))
|
||||
{
|
||||
var name = Slot switch
|
||||
var name = item.Name;
|
||||
if (Slot is FullEquipType.Finger)
|
||||
name = SlotIndex switch
|
||||
{
|
||||
EquipSlot.RFinger => "R: ",
|
||||
EquipSlot.LFinger => "L: ",
|
||||
_ => string.Empty,
|
||||
}
|
||||
+ item.Name;
|
||||
8 => "R: " + name,
|
||||
9 => "L: " + name,
|
||||
_ => name,
|
||||
};
|
||||
return new ResourceNode.UiData(name, item.Type.GetCategoryIcon().ToFlag());
|
||||
}
|
||||
|
||||
|
|
@ -361,7 +362,7 @@ internal unsafe partial record ResolveContext(
|
|||
return dataFromPath;
|
||||
|
||||
return isEquipment
|
||||
? new ResourceNode.UiData(Slot.ToName(), Slot.ToEquipType().GetCategoryIcon().ToFlag())
|
||||
? new ResourceNode.UiData(Slot.ToName(), Slot.GetCategoryIcon().ToFlag())
|
||||
: new ResourceNode.UiData(null, ChangedItemIconFlag.Unknown);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,20 +7,20 @@ namespace Penumbra.Interop.ResourceTree;
|
|||
|
||||
public class ResourceNode : ICloneable
|
||||
{
|
||||
public string? Name;
|
||||
public string? FallbackName;
|
||||
public ChangedItemIconFlag IconFlag;
|
||||
public readonly ResourceType Type;
|
||||
public readonly nint ObjectAddress;
|
||||
public readonly nint ResourceHandle;
|
||||
public Utf8GamePath[] PossibleGamePaths;
|
||||
public FullPath FullPath;
|
||||
public string? ModName;
|
||||
public string? ModRelativePath;
|
||||
public CiByteString AdditionalData;
|
||||
public readonly ulong Length;
|
||||
public readonly List<ResourceNode> Children;
|
||||
internal ResolveContext? ResolveContext;
|
||||
public string? Name;
|
||||
public string? FallbackName;
|
||||
public ChangedItemIconFlag IconFlag;
|
||||
public readonly ResourceType Type;
|
||||
public readonly nint ObjectAddress;
|
||||
public readonly nint ResourceHandle;
|
||||
public Utf8GamePath[] PossibleGamePaths;
|
||||
public FullPath FullPath;
|
||||
public string? ModName;
|
||||
public string? ModRelativePath;
|
||||
public CiByteString AdditionalData;
|
||||
public readonly ulong Length;
|
||||
public readonly List<ResourceNode> Children;
|
||||
internal ResolveContext? ResolveContext;
|
||||
|
||||
public Utf8GamePath GamePath
|
||||
{
|
||||
|
|
@ -53,7 +53,7 @@ public class ResourceNode : ICloneable
|
|||
{
|
||||
Name = other.Name;
|
||||
FallbackName = other.FallbackName;
|
||||
IconFlag = other.IconFlag;
|
||||
IconFlag = other.IconFlag;
|
||||
Type = other.Type;
|
||||
ObjectAddress = other.ObjectAddress;
|
||||
ResourceHandle = other.ResourceHandle;
|
||||
|
|
@ -82,7 +82,7 @@ public class ResourceNode : ICloneable
|
|||
|
||||
public void SetUiData(UiData uiData)
|
||||
{
|
||||
Name = uiData.Name;
|
||||
Name = uiData.Name;
|
||||
IconFlag = uiData.IconFlag;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -80,12 +80,13 @@ public class ResourceTree
|
|||
{
|
||||
ModelType.Human => i switch
|
||||
{
|
||||
< 10 => globalContext.CreateContext(model, i, i.ToEquipSlot(), equipment[(int)i]),
|
||||
16 or 17 => globalContext.CreateContext(model, i, EquipSlot.Head, equipment[(int)(i - 6)]),
|
||||
_ => globalContext.CreateContext(model, i),
|
||||
< 10 => globalContext.CreateContext(model, i, i.ToEquipSlot().ToEquipType(), equipment[(int)i]),
|
||||
16 => globalContext.CreateContext(model, i, FullEquipType.Glasses, equipment[10]),
|
||||
17 => globalContext.CreateContext(model, i, FullEquipType.Unknown, equipment[11]),
|
||||
_ => globalContext.CreateContext(model, i),
|
||||
},
|
||||
_ => i < equipment.Length
|
||||
? globalContext.CreateContext(model, i, i.ToEquipSlot(), equipment[(int)i])
|
||||
? globalContext.CreateContext(model, i, i.ToEquipSlot().ToEquipType(), equipment[(int)i])
|
||||
: globalContext.CreateContext(model, i),
|
||||
};
|
||||
|
||||
|
|
@ -133,7 +134,7 @@ public class ResourceTree
|
|||
var weapon = (Weapon*)subObject;
|
||||
|
||||
// This way to tell apart MainHand and OffHand is not always accurate, but seems good enough for what we're doing with it.
|
||||
var slot = weaponIndex > 0 ? EquipSlot.OffHand : EquipSlot.MainHand;
|
||||
var slot = weaponIndex > 0 ? FullEquipType.UnknownOffhand : FullEquipType.UnknownMainhand;
|
||||
var equipment = new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, new StainIds(weapon->Stain0, weapon->Stain1));
|
||||
var weaponType = weapon->SecondaryId;
|
||||
|
||||
|
|
@ -184,7 +185,7 @@ public class ResourceTree
|
|||
{
|
||||
pbdNode = pbdNode.Clone();
|
||||
pbdNode.FallbackName = "Racial Deformer";
|
||||
pbdNode.IconFlag = ChangedItemIconFlag.Customization;
|
||||
pbdNode.IconFlag = ChangedItemIconFlag.Customization;
|
||||
}
|
||||
|
||||
Nodes.Add(pbdNode);
|
||||
|
|
@ -202,7 +203,7 @@ public class ResourceTree
|
|||
{
|
||||
decalNode = decalNode.Clone();
|
||||
decalNode.FallbackName = "Face Decal";
|
||||
decalNode.IconFlag = ChangedItemIconFlag.Customization;
|
||||
decalNode.IconFlag = ChangedItemIconFlag.Customization;
|
||||
}
|
||||
|
||||
Nodes.Add(decalNode);
|
||||
|
|
@ -219,7 +220,7 @@ public class ResourceTree
|
|||
{
|
||||
legacyDecalNode = legacyDecalNode.Clone();
|
||||
legacyDecalNode.FallbackName = "Legacy Body Decal";
|
||||
legacyDecalNode.IconFlag = ChangedItemIconFlag.Customization;
|
||||
legacyDecalNode.IconFlag = ChangedItemIconFlag.Customization;
|
||||
}
|
||||
|
||||
Nodes.Add(legacyDecalNode);
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ namespace Penumbra.Meta;
|
|||
|
||||
public class ImcChecker
|
||||
{
|
||||
private static readonly Dictionary<ImcIdentifier, int> VariantCounts = [];
|
||||
private static MetaFileManager? _dataManager;
|
||||
|
||||
private static readonly Dictionary<ImcIdentifier, int> VariantCounts = [];
|
||||
private static MetaFileManager? _dataManager;
|
||||
private static readonly ConcurrentDictionary<ImcIdentifier, CachedEntry> GlobalCachedDefaultEntries = [];
|
||||
|
||||
public static int GetVariantCount(ImcIdentifier identifier)
|
||||
{
|
||||
|
|
@ -26,23 +26,20 @@ public class ImcChecker
|
|||
|
||||
public readonly record struct CachedEntry(ImcEntry Entry, bool FileExists, bool VariantExists);
|
||||
|
||||
private readonly Dictionary<ImcIdentifier, CachedEntry> _cachedDefaultEntries = new();
|
||||
private readonly MetaFileManager _metaFileManager;
|
||||
|
||||
public ImcChecker(MetaFileManager metaFileManager)
|
||||
{
|
||||
_metaFileManager = metaFileManager;
|
||||
_dataManager = metaFileManager;
|
||||
}
|
||||
=> _dataManager = metaFileManager;
|
||||
|
||||
public CachedEntry GetDefaultEntry(ImcIdentifier identifier, bool storeCache)
|
||||
public static CachedEntry GetDefaultEntry(ImcIdentifier identifier, bool storeCache)
|
||||
{
|
||||
if (_cachedDefaultEntries.TryGetValue(identifier, out var entry))
|
||||
if (GlobalCachedDefaultEntries.TryGetValue(identifier, out var entry))
|
||||
return entry;
|
||||
|
||||
if (_dataManager == null)
|
||||
return new CachedEntry(default, false, false);
|
||||
|
||||
try
|
||||
{
|
||||
var e = ImcFile.GetDefault(_metaFileManager, identifier.GamePath(), identifier.EquipSlot, identifier.Variant, out var entryExists);
|
||||
var e = ImcFile.GetDefault(_dataManager, identifier.GamePath(), identifier.EquipSlot, identifier.Variant, out var entryExists);
|
||||
entry = new CachedEntry(e, true, entryExists);
|
||||
}
|
||||
catch (Exception)
|
||||
|
|
@ -51,7 +48,7 @@ public class ImcChecker
|
|||
}
|
||||
|
||||
if (storeCache)
|
||||
_cachedDefaultEntries.Add(identifier, entry);
|
||||
GlobalCachedDefaultEntries.TryAdd(identifier, entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -68,9 +68,13 @@ public readonly record struct ImcIdentifier(
|
|||
=> (MetaIndex)(-1);
|
||||
|
||||
public override string ToString()
|
||||
=> ObjectType is ObjectType.Equipment or ObjectType.Accessory
|
||||
? $"Imc - {PrimaryId} - {EquipSlot.ToName()} - {Variant}"
|
||||
: $"Imc - {PrimaryId} - {ObjectType.ToName()} - {SecondaryId} - {BodySlot} - {Variant}";
|
||||
=> ObjectType switch
|
||||
{
|
||||
ObjectType.Equipment or ObjectType.Accessory => $"Imc - {PrimaryId} - {EquipSlot.ToName()} - {Variant}",
|
||||
ObjectType.DemiHuman => $"Imc - {PrimaryId} - DemiHuman - {SecondaryId} - {EquipSlot.ToName()} - {Variant}",
|
||||
_ => $"Imc - {PrimaryId} - {ObjectType.ToName()} - {SecondaryId} - {BodySlot} - {Variant}",
|
||||
};
|
||||
|
||||
|
||||
public bool Validate()
|
||||
{
|
||||
|
|
@ -102,6 +106,7 @@ public readonly record struct ImcIdentifier(
|
|||
return false;
|
||||
if (ItemData.AdaptOffhandImc(PrimaryId, out _))
|
||||
return false;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -163,7 +168,7 @@ public readonly record struct ImcIdentifier(
|
|||
case ObjectType.DemiHuman:
|
||||
{
|
||||
var secondaryId = new SecondaryId(jObj["SecondaryId"]?.ToObject<ushort>() ?? 0);
|
||||
var slot = jObj["Slot"]?.ToObject<EquipSlot>() ?? EquipSlot.Unknown;
|
||||
var slot = jObj["EquipSlot"]?.ToObject<EquipSlot>() ?? EquipSlot.Unknown;
|
||||
ret = new ImcIdentifier(primaryId, (Variant)variant, objectType, secondaryId, slot, BodySlot.Unknown);
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ public class ModFileCollection : IDisposable, IService
|
|||
private readonly List<FileRegistry> _mdl = [];
|
||||
private readonly List<FileRegistry> _tex = [];
|
||||
private readonly List<FileRegistry> _shpk = [];
|
||||
private readonly List<FileRegistry> _pbd = [];
|
||||
|
||||
private readonly SortedSet<FullPath> _missing = [];
|
||||
private readonly HashSet<Utf8GamePath> _usedPaths = [];
|
||||
|
|
@ -23,19 +24,22 @@ public class ModFileCollection : IDisposable, IService
|
|||
=> Ready ? _usedPaths : [];
|
||||
|
||||
public IReadOnlyList<FileRegistry> Available
|
||||
=> Ready ? _available : Array.Empty<FileRegistry>();
|
||||
=> Ready ? _available : [];
|
||||
|
||||
public IReadOnlyList<FileRegistry> Mtrl
|
||||
=> Ready ? _mtrl : Array.Empty<FileRegistry>();
|
||||
=> Ready ? _mtrl : [];
|
||||
|
||||
public IReadOnlyList<FileRegistry> Mdl
|
||||
=> Ready ? _mdl : Array.Empty<FileRegistry>();
|
||||
=> Ready ? _mdl : [];
|
||||
|
||||
public IReadOnlyList<FileRegistry> Tex
|
||||
=> Ready ? _tex : Array.Empty<FileRegistry>();
|
||||
=> Ready ? _tex : [];
|
||||
|
||||
public IReadOnlyList<FileRegistry> Shpk
|
||||
=> Ready ? _shpk : Array.Empty<FileRegistry>();
|
||||
=> Ready ? _shpk : [];
|
||||
|
||||
public IReadOnlyList<FileRegistry> Pbd
|
||||
=> Ready ? _pbd : [];
|
||||
|
||||
public bool Ready { get; private set; } = true;
|
||||
|
||||
|
|
@ -128,6 +132,9 @@ public class ModFileCollection : IDisposable, IService
|
|||
case ".shpk":
|
||||
_shpk.Add(registry);
|
||||
break;
|
||||
case ".pbd":
|
||||
_pbd.Add(registry);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -139,6 +146,7 @@ public class ModFileCollection : IDisposable, IService
|
|||
_mdl.Clear();
|
||||
_tex.Clear();
|
||||
_shpk.Clear();
|
||||
_pbd.Clear();
|
||||
}
|
||||
|
||||
private void ClearPaths(bool clearRegistries, CancellationToken tok)
|
||||
|
|
|
|||
|
|
@ -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,16 @@
|
|||
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) : MetaDictionary, IService
|
||||
{
|
||||
public sealed class OtherOptionData : HashSet<string>
|
||||
{
|
||||
|
|
@ -62,12 +66,111 @@ public class ModMetaEditor(ModManager modManager) : MetaDictionary, IService
|
|||
Changes = false;
|
||||
}
|
||||
|
||||
public static bool DeleteDefaultValues(MetaFileManager metaFileManager, 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, 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 = [];
|
||||
|
|
|
|||
|
|
@ -24,14 +24,21 @@ public interface IModGroup
|
|||
{
|
||||
public const int MaxMultiOptions = 32;
|
||||
|
||||
public Mod Mod { get; }
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Image { get; set; }
|
||||
public GroupType Type { get; }
|
||||
public GroupDrawBehaviour Behaviour { get; }
|
||||
public ModPriority Priority { get; set; }
|
||||
public Setting DefaultSettings { get; set; }
|
||||
public Mod Mod { get; }
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary> Unused in Penumbra but for better TexTools interop. </summary>
|
||||
public string Image { get; set; }
|
||||
|
||||
public GroupType Type { get; }
|
||||
public GroupDrawBehaviour Behaviour { get; }
|
||||
public ModPriority Priority { get; set; }
|
||||
|
||||
/// <summary> Unused in Penumbra but for better TexTools interop. </summary>
|
||||
public int Page { get; set; }
|
||||
|
||||
public Setting DefaultSettings { get; set; }
|
||||
|
||||
public FullPath? FindBestMatch(Utf8GamePath gamePath);
|
||||
public IModOption? AddOption(string name, string description = "");
|
||||
|
|
|
|||
|
|
@ -29,11 +29,13 @@ public class ImcModGroup(Mod mod) : IModGroup
|
|||
=> GroupDrawBehaviour.MultiSelection;
|
||||
|
||||
public ModPriority Priority { get; set; } = ModPriority.Default;
|
||||
public int Page { get; set; }
|
||||
public Setting DefaultSettings { get; set; } = Setting.Zero;
|
||||
|
||||
public ImcIdentifier Identifier;
|
||||
public ImcEntry DefaultEntry;
|
||||
public bool AllVariants;
|
||||
public bool OnlyAttributes;
|
||||
|
||||
|
||||
public FullPath? FindBestMatch(Utf8GamePath gamePath)
|
||||
|
|
@ -96,28 +98,36 @@ public class ImcModGroup(Mod mod) : IModGroup
|
|||
public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer)
|
||||
=> new ImcModGroupEditDrawer(editDrawer, this);
|
||||
|
||||
public ImcEntry GetEntry(ushort mask)
|
||||
=> DefaultEntry with { AttributeMask = mask };
|
||||
private ImcEntry GetEntry(Variant variant, ushort mask)
|
||||
{
|
||||
if (!OnlyAttributes)
|
||||
return DefaultEntry with { AttributeMask = mask };
|
||||
|
||||
var defaultEntry = ImcChecker.GetDefaultEntry(Identifier with { Variant = variant }, true);
|
||||
if (defaultEntry.VariantExists)
|
||||
return defaultEntry.Entry with { AttributeMask = mask };
|
||||
|
||||
return DefaultEntry with { AttributeMask = mask };
|
||||
}
|
||||
|
||||
public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, MetaDictionary manipulations)
|
||||
{
|
||||
if (IsDisabled(setting))
|
||||
return;
|
||||
|
||||
var mask = GetCurrentMask(setting);
|
||||
var entry = GetEntry(mask);
|
||||
var mask = GetCurrentMask(setting);
|
||||
if (AllVariants)
|
||||
{
|
||||
var count = ImcChecker.GetVariantCount(Identifier);
|
||||
if (count == 0)
|
||||
manipulations.TryAdd(Identifier, entry);
|
||||
manipulations.TryAdd(Identifier, GetEntry(Identifier.Variant, mask));
|
||||
else
|
||||
for (var i = 0; i <= count; ++i)
|
||||
manipulations.TryAdd(Identifier with { Variant = (Variant)i }, entry);
|
||||
manipulations.TryAdd(Identifier with { Variant = (Variant)i }, GetEntry((Variant)i, mask));
|
||||
}
|
||||
else
|
||||
{
|
||||
manipulations.TryAdd(Identifier, entry);
|
||||
manipulations.TryAdd(Identifier, GetEntry(Identifier.Variant, mask));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -137,6 +147,8 @@ public class ImcModGroup(Mod mod) : IModGroup
|
|||
serializer.Serialize(jWriter, DefaultEntry);
|
||||
jWriter.WritePropertyName(nameof(AllVariants));
|
||||
jWriter.WriteValue(AllVariants);
|
||||
jWriter.WritePropertyName(nameof(OnlyAttributes));
|
||||
jWriter.WriteValue(OnlyAttributes);
|
||||
jWriter.WritePropertyName("Options");
|
||||
jWriter.WriteStartArray();
|
||||
foreach (var option in OptionData)
|
||||
|
|
@ -169,14 +181,11 @@ public class ImcModGroup(Mod mod) : IModGroup
|
|||
var identifier = ImcIdentifier.FromJson(json[nameof(Identifier)] as JObject);
|
||||
var ret = new ImcModGroup(mod)
|
||||
{
|
||||
Name = json[nameof(Name)]?.ToObject<string>() ?? string.Empty,
|
||||
Description = json[nameof(Description)]?.ToObject<string>() ?? string.Empty,
|
||||
Image = json[nameof(Image)]?.ToObject<string>() ?? string.Empty,
|
||||
Priority = json[nameof(Priority)]?.ToObject<ModPriority>() ?? ModPriority.Default,
|
||||
DefaultEntry = json[nameof(DefaultEntry)]?.ToObject<ImcEntry>() ?? new ImcEntry(),
|
||||
AllVariants = json[nameof(AllVariants)]?.ToObject<bool>() ?? false,
|
||||
DefaultEntry = json[nameof(DefaultEntry)]?.ToObject<ImcEntry>() ?? new ImcEntry(),
|
||||
AllVariants = json[nameof(AllVariants)]?.ToObject<bool>() ?? false,
|
||||
OnlyAttributes = json[nameof(OnlyAttributes)]?.ToObject<bool>() ?? false,
|
||||
};
|
||||
if (ret.Name.Length == 0)
|
||||
if (!ModSaveGroup.ReadJsonBase(json, ret))
|
||||
return null;
|
||||
|
||||
if (!identifier.HasValue || ret.DefaultEntry.MaterialId == 0)
|
||||
|
|
@ -215,7 +224,6 @@ public class ImcModGroup(Mod mod) : IModGroup
|
|||
}
|
||||
|
||||
ret.Identifier = identifier.Value;
|
||||
ret.DefaultSettings = json[nameof(DefaultSettings)]?.ToObject<Setting>() ?? Setting.Zero;
|
||||
ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings);
|
||||
return ret;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Penumbra.GameData.Files.ShaderStructs;
|
||||
using Penumbra.Mods.Settings;
|
||||
using Penumbra.Mods.SubMods;
|
||||
using Penumbra.Services;
|
||||
|
||||
|
|
@ -90,6 +93,8 @@ public readonly struct ModSaveGroup : ISavable
|
|||
jWriter.WriteValue(group.Description);
|
||||
jWriter.WritePropertyName(nameof(group.Image));
|
||||
jWriter.WriteValue(group.Image);
|
||||
jWriter.WritePropertyName(nameof(group.Page));
|
||||
jWriter.WriteValue(group.Page);
|
||||
jWriter.WritePropertyName(nameof(group.Priority));
|
||||
jWriter.WriteValue(group.Priority.Value);
|
||||
jWriter.WritePropertyName(nameof(group.Type));
|
||||
|
|
@ -97,4 +102,16 @@ public readonly struct ModSaveGroup : ISavable
|
|||
jWriter.WritePropertyName(nameof(group.DefaultSettings));
|
||||
jWriter.WriteValue(group.DefaultSettings.Value);
|
||||
}
|
||||
|
||||
public static bool ReadJsonBase(JObject json, IModGroup group)
|
||||
{
|
||||
group.Name = json[nameof(IModGroup.Name)]?.ToObject<string>() ?? string.Empty;
|
||||
group.Description = json[nameof(IModGroup.Description)]?.ToObject<string>() ?? string.Empty;
|
||||
group.Image = json[nameof(IModGroup.Image)]?.ToObject<string>() ?? string.Empty;
|
||||
group.Page = json[nameof(IModGroup.Page)]?.ToObject<int>() ?? 0;
|
||||
group.Priority = json[nameof(IModGroup.Priority)]?.ToObject<ModPriority>() ?? ModPriority.Default;
|
||||
group.DefaultSettings = json[nameof(IModGroup.DefaultSettings)]?.ToObject<Setting>() ?? Setting.Zero;
|
||||
|
||||
return group.Name.Length > 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup
|
|||
public string Description { get; set; } = string.Empty;
|
||||
public string Image { get; set; } = string.Empty;
|
||||
public ModPriority Priority { get; set; }
|
||||
public int Page { get; set; }
|
||||
public Setting DefaultSettings { get; set; }
|
||||
public readonly List<MultiSubMod> OptionData = [];
|
||||
|
||||
|
|
@ -66,15 +67,8 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup
|
|||
|
||||
public static MultiModGroup? Load(Mod mod, JObject json)
|
||||
{
|
||||
var ret = new MultiModGroup(mod)
|
||||
{
|
||||
Name = json[nameof(Name)]?.ToObject<string>() ?? string.Empty,
|
||||
Description = json[nameof(Description)]?.ToObject<string>() ?? string.Empty,
|
||||
Image = json[nameof(Image)]?.ToObject<string>() ?? string.Empty,
|
||||
Priority = json[nameof(Priority)]?.ToObject<ModPriority>() ?? ModPriority.Default,
|
||||
DefaultSettings = json[nameof(DefaultSettings)]?.ToObject<Setting>() ?? Setting.Zero,
|
||||
};
|
||||
if (ret.Name.Length == 0)
|
||||
var ret = new MultiModGroup(mod);
|
||||
if (!ModSaveGroup.ReadJsonBase(json, ret))
|
||||
return null;
|
||||
|
||||
var options = json["Options"];
|
||||
|
|
@ -105,6 +99,8 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup
|
|||
Name = Name,
|
||||
Description = Description,
|
||||
Priority = Priority,
|
||||
Image = Image,
|
||||
Page = Page,
|
||||
DefaultSettings = DefaultSettings.TurnMulti(OptionData.Count),
|
||||
};
|
||||
single.OptionData.AddRange(OptionData.Select(o => o.ConvertToSingle(single)));
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup
|
|||
public string Description { get; set; } = string.Empty;
|
||||
public string Image { get; set; } = string.Empty;
|
||||
public ModPriority Priority { get; set; }
|
||||
public int Page { get; set; }
|
||||
public Setting DefaultSettings { get; set; }
|
||||
|
||||
public readonly List<SingleSubMod> OptionData = [];
|
||||
|
|
@ -62,15 +63,8 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup
|
|||
public static SingleModGroup? Load(Mod mod, JObject json)
|
||||
{
|
||||
var options = json["Options"];
|
||||
var ret = new SingleModGroup(mod)
|
||||
{
|
||||
Name = json[nameof(Name)]?.ToObject<string>() ?? string.Empty,
|
||||
Description = json[nameof(Description)]?.ToObject<string>() ?? string.Empty,
|
||||
Image = json[nameof(Image)]?.ToObject<string>() ?? string.Empty,
|
||||
Priority = json[nameof(Priority)]?.ToObject<ModPriority>() ?? ModPriority.Default,
|
||||
DefaultSettings = json[nameof(DefaultSettings)]?.ToObject<Setting>() ?? Setting.Zero,
|
||||
};
|
||||
if (ret.Name.Length == 0)
|
||||
var ret = new SingleModGroup(mod);
|
||||
if (!ModSaveGroup.ReadJsonBase(json, ret))
|
||||
return null;
|
||||
|
||||
if (options != null)
|
||||
|
|
@ -91,6 +85,8 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup
|
|||
Name = Name,
|
||||
Description = Description,
|
||||
Priority = Priority,
|
||||
Image = Image,
|
||||
Page = Page,
|
||||
DefaultSettings = Setting.Multi((int)DefaultSettings.Value),
|
||||
};
|
||||
multi.OptionData.AddRange(OptionData.Select((o, i) => o.ConvertToMulti(multi, new ModPriority(i))));
|
||||
|
|
|
|||
|
|
@ -32,26 +32,26 @@ public static class EquipmentSwap
|
|||
EquipSlot slotFrom, EquipItem itemFrom, EquipSlot slotTo, EquipItem itemTo)
|
||||
{
|
||||
LookupItem(itemFrom, out var actualSlotFrom, out var idFrom, out var variantFrom);
|
||||
LookupItem(itemTo, out var actualSlotTo, out var idTo, out var variantTo);
|
||||
LookupItem(itemTo, out var actualSlotTo, out var idTo, out var variantTo);
|
||||
if (actualSlotFrom != slotFrom.ToSlot() || actualSlotTo != slotTo.ToSlot())
|
||||
throw new ItemSwap.InvalidItemTypeException();
|
||||
|
||||
var (imcFileFrom, variants, affectedItems) = GetVariants(manager, identifier, slotFrom, idFrom, idTo, variantFrom);
|
||||
var imcIdentifierTo = new ImcIdentifier(slotTo, idTo, variantTo);
|
||||
var imcFileTo = new ImcFile(manager, imcIdentifierTo);
|
||||
var imcFileTo = new ImcFile(manager, imcIdentifierTo);
|
||||
var imcEntry = manips.TryGetValue(imcIdentifierTo, out var entry)
|
||||
? entry
|
||||
: imcFileTo.GetEntry(imcIdentifierTo.EquipSlot, imcIdentifierTo.Variant);
|
||||
var mtrlVariantTo = imcEntry.MaterialId;
|
||||
var skipFemale = false;
|
||||
var skipMale = false;
|
||||
var skipFemale = false;
|
||||
var skipMale = false;
|
||||
foreach (var gr in Enum.GetValues<GenderRace>())
|
||||
{
|
||||
switch (gr.Split().Item1)
|
||||
{
|
||||
case Gender.Male when skipMale: continue;
|
||||
case Gender.Female when skipFemale: continue;
|
||||
case Gender.MaleNpc when skipMale: continue;
|
||||
case Gender.Male when skipMale: continue;
|
||||
case Gender.Female when skipFemale: continue;
|
||||
case Gender.MaleNpc when skipMale: continue;
|
||||
case Gender.FemaleNpc when skipFemale: continue;
|
||||
}
|
||||
|
||||
|
|
@ -94,7 +94,7 @@ public static class EquipmentSwap
|
|||
{
|
||||
// Check actual ids, variants and slots. We only support using the same slot.
|
||||
LookupItem(itemFrom, out var slotFrom, out var idFrom, out var variantFrom);
|
||||
LookupItem(itemTo, out var slotTo, out var idTo, out var variantTo);
|
||||
LookupItem(itemTo, out var slotTo, out var idTo, out var variantTo);
|
||||
if (slotFrom != slotTo)
|
||||
throw new ItemSwap.InvalidItemTypeException();
|
||||
|
||||
|
|
@ -111,7 +111,7 @@ public static class EquipmentSwap
|
|||
{
|
||||
(var imcFileFrom, var variants, affectedItems) = GetVariants(manager, identifier, slot, idFrom, idTo, variantFrom);
|
||||
var imcIdentifierTo = new ImcIdentifier(slotTo, idTo, variantTo);
|
||||
var imcFileTo = new ImcFile(manager, imcIdentifierTo);
|
||||
var imcFileTo = new ImcFile(manager, imcIdentifierTo);
|
||||
var imcEntry = manips.TryGetValue(imcIdentifierTo, out var entry)
|
||||
? entry
|
||||
: imcFileTo.GetEntry(imcIdentifierTo.EquipSlot, imcIdentifierTo.Variant);
|
||||
|
|
@ -122,18 +122,18 @@ public static class EquipmentSwap
|
|||
{
|
||||
EquipSlot.Head => EstType.Head,
|
||||
EquipSlot.Body => EstType.Body,
|
||||
_ => (EstType)0,
|
||||
_ => (EstType)0,
|
||||
};
|
||||
|
||||
var skipFemale = false;
|
||||
var skipMale = false;
|
||||
var skipMale = false;
|
||||
foreach (var gr in Enum.GetValues<GenderRace>())
|
||||
{
|
||||
switch (gr.Split().Item1)
|
||||
{
|
||||
case Gender.Male when skipMale: continue;
|
||||
case Gender.Female when skipFemale: continue;
|
||||
case Gender.MaleNpc when skipMale: continue;
|
||||
case Gender.Male when skipMale: continue;
|
||||
case Gender.Female when skipFemale: continue;
|
||||
case Gender.MaleNpc when skipMale: continue;
|
||||
case Gender.FemaleNpc when skipFemale: continue;
|
||||
}
|
||||
|
||||
|
|
@ -148,7 +148,7 @@ public static class EquipmentSwap
|
|||
swaps.Add(eqdp);
|
||||
|
||||
var ownMdl = eqdp?.SwapToModdedEntry.Model ?? false;
|
||||
var est = ItemSwap.CreateEst(manager, redirections, manips, estType, gr, idFrom, idTo, ownMdl);
|
||||
var est = ItemSwap.CreateEst(manager, redirections, manips, estType, gr, idFrom, idTo, ownMdl);
|
||||
if (est != null)
|
||||
swaps.Add(est);
|
||||
}
|
||||
|
|
@ -176,7 +176,6 @@ public static class EquipmentSwap
|
|||
|
||||
return affectedItems;
|
||||
}
|
||||
|
||||
public static MetaSwap<EqdpIdentifier, EqdpEntryInternal>? CreateEqdp(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections,
|
||||
MetaDictionary manips, EquipSlot slot, GenderRace gr, PrimaryId idFrom, PrimaryId idTo, byte mtrlTo)
|
||||
=> CreateEqdp(manager, redirections, manips, slot, slot, gr, idFrom, idTo, mtrlTo);
|
||||
|
|
@ -186,9 +185,9 @@ public static class EquipmentSwap
|
|||
PrimaryId idTo, byte mtrlTo)
|
||||
{
|
||||
var eqdpFromIdentifier = new EqdpIdentifier(idFrom, slotFrom, gr);
|
||||
var eqdpToIdentifier = new EqdpIdentifier(idTo, slotTo, gr);
|
||||
var eqdpFromDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpFromIdentifier), slotFrom);
|
||||
var eqdpToDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpToIdentifier), slotTo);
|
||||
var eqdpToIdentifier = new EqdpIdentifier(idTo, slotTo, gr);
|
||||
var eqdpFromDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpFromIdentifier), slotFrom);
|
||||
var eqdpToDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpToIdentifier), slotTo);
|
||||
var meta = new MetaSwap<EqdpIdentifier, EqdpEntryInternal>(i => manips.TryGetValue(i, out var e) ? e : null, eqdpFromIdentifier,
|
||||
eqdpFromDefault, eqdpToIdentifier,
|
||||
eqdpToDefault);
|
||||
|
|
@ -217,7 +216,7 @@ public static class EquipmentSwap
|
|||
? GamePaths.Accessory.Mdl.Path(idFrom, gr, slotFrom)
|
||||
: GamePaths.Equipment.Mdl.Path(idFrom, gr, slotFrom);
|
||||
var mdlPathTo = slotTo.IsAccessory() ? GamePaths.Accessory.Mdl.Path(idTo, gr, slotTo) : GamePaths.Equipment.Mdl.Path(idTo, gr, slotTo);
|
||||
var mdl = FileSwap.CreateSwap(manager, ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo);
|
||||
var mdl = FileSwap.CreateSwap(manager, ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo);
|
||||
|
||||
foreach (ref var fileName in mdl.AsMdl()!.Materials.AsSpan())
|
||||
{
|
||||
|
|
@ -242,13 +241,13 @@ public static class EquipmentSwap
|
|||
private static (ImcFile, Variant[], EquipItem[]) GetVariants(MetaFileManager manager, ObjectIdentification identifier, EquipSlot slotFrom,
|
||||
PrimaryId idFrom, PrimaryId idTo, Variant variantFrom)
|
||||
{
|
||||
var ident = new ImcIdentifier(slotFrom, idFrom, variantFrom);
|
||||
var imc = new ImcFile(manager, ident);
|
||||
var ident = new ImcIdentifier(slotFrom, idFrom, variantFrom);
|
||||
var imc = new ImcFile(manager, ident);
|
||||
EquipItem[] items;
|
||||
Variant[] variants;
|
||||
Variant[] variants;
|
||||
if (idFrom == idTo)
|
||||
{
|
||||
items = identifier.Identify(idFrom, 0, variantFrom, slotFrom).ToArray();
|
||||
items = identifier.Identify(idFrom, 0, variantFrom, slotFrom).ToArray();
|
||||
variants = [variantFrom];
|
||||
}
|
||||
else
|
||||
|
|
@ -271,9 +270,9 @@ public static class EquipmentSwap
|
|||
return null;
|
||||
|
||||
var manipFromIdentifier = new GmpIdentifier(idFrom);
|
||||
var manipToIdentifier = new GmpIdentifier(idTo);
|
||||
var manipFromDefault = ExpandedGmpFile.GetDefault(manager, manipFromIdentifier);
|
||||
var manipToDefault = ExpandedGmpFile.GetDefault(manager, manipToIdentifier);
|
||||
var manipToIdentifier = new GmpIdentifier(idTo);
|
||||
var manipFromDefault = ExpandedGmpFile.GetDefault(manager, manipFromIdentifier);
|
||||
var manipToDefault = ExpandedGmpFile.GetDefault(manager, manipToIdentifier);
|
||||
return new MetaSwap<GmpIdentifier, GmpEntry>(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault,
|
||||
manipToIdentifier, manipToDefault);
|
||||
}
|
||||
|
|
@ -288,9 +287,9 @@ public static class EquipmentSwap
|
|||
Variant variantFrom, Variant variantTo, ImcFile imcFileFrom, ImcFile imcFileTo)
|
||||
{
|
||||
var manipFromIdentifier = new ImcIdentifier(slotFrom, idFrom, variantFrom);
|
||||
var manipToIdentifier = new ImcIdentifier(slotTo, idTo, variantTo);
|
||||
var manipFromDefault = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom);
|
||||
var manipToDefault = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo);
|
||||
var manipToIdentifier = new ImcIdentifier(slotTo, idTo, variantTo);
|
||||
var manipFromDefault = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom);
|
||||
var manipToDefault = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo);
|
||||
var imc = new MetaSwap<ImcIdentifier, ImcEntry>(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault,
|
||||
manipToIdentifier, manipToDefault);
|
||||
|
||||
|
|
@ -329,7 +328,7 @@ public static class EquipmentSwap
|
|||
var vfxPathFrom = GamePaths.Equipment.Avfx.Path(idFrom, vfxId);
|
||||
vfxPathFrom = ItemSwap.ReplaceType(vfxPathFrom, slotFrom, slotTo, idFrom);
|
||||
var vfxPathTo = GamePaths.Equipment.Avfx.Path(idTo, vfxId);
|
||||
var avfx = FileSwap.CreateSwap(manager, ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo);
|
||||
var avfx = FileSwap.CreateSwap(manager, ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo);
|
||||
|
||||
foreach (ref var filePath in avfx.AsAvfx()!.Textures.AsSpan())
|
||||
{
|
||||
|
|
@ -347,9 +346,9 @@ public static class EquipmentSwap
|
|||
return null;
|
||||
|
||||
var manipFromIdentifier = new EqpIdentifier(idFrom, slot);
|
||||
var manipToIdentifier = new EqpIdentifier(idTo, slot);
|
||||
var manipFromDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idFrom), slot);
|
||||
var manipToDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idTo), slot);
|
||||
var manipToIdentifier = new EqpIdentifier(idTo, slot);
|
||||
var manipFromDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idFrom), slot);
|
||||
var manipToDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idTo), slot);
|
||||
return new MetaSwap<EqpIdentifier, EqpEntryInternal>(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier,
|
||||
manipFromDefault, manipToIdentifier, manipToDefault);
|
||||
}
|
||||
|
|
@ -381,7 +380,7 @@ public static class EquipmentSwap
|
|||
|
||||
if (newFileName != fileName)
|
||||
{
|
||||
fileName = newFileName;
|
||||
fileName = newFileName;
|
||||
dataWasChanged = true;
|
||||
}
|
||||
|
||||
|
|
@ -406,13 +405,13 @@ public static class EquipmentSwap
|
|||
EquipSlot slotTo, PrimaryId idFrom, PrimaryId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged)
|
||||
{
|
||||
var addedDashes = GamePaths.Tex.HandleDx11Path(texture, out var path);
|
||||
var newPath = ItemSwap.ReplaceAnyId(path, prefix, idFrom);
|
||||
var newPath = ItemSwap.ReplaceAnyId(path, prefix, idFrom);
|
||||
newPath = ItemSwap.ReplaceSlot(newPath, slotTo, slotFrom, slotTo != slotFrom);
|
||||
newPath = ItemSwap.ReplaceType(newPath, slotFrom, slotTo, idFrom);
|
||||
newPath = ItemSwap.AddSuffix(newPath, ".tex", $"_{Path.GetFileName(texture.Path).GetStableHashCode():x8}");
|
||||
if (newPath != path)
|
||||
{
|
||||
texture.Path = addedDashes ? newPath.Replace("--", string.Empty) : newPath;
|
||||
texture.Path = addedDashes ? newPath.Replace("--", string.Empty) : newPath;
|
||||
dataWasChanged = true;
|
||||
}
|
||||
|
||||
|
|
@ -430,8 +429,8 @@ public static class EquipmentSwap
|
|||
PrimaryId idFrom, ref string filePath, ref bool dataWasChanged)
|
||||
{
|
||||
var oldPath = filePath;
|
||||
filePath = ItemSwap.AddSuffix(filePath, ".atex", $"_{Path.GetFileName(filePath).GetStableHashCode():x8}");
|
||||
filePath = ItemSwap.ReplaceType(filePath, slotFrom, slotTo, idFrom);
|
||||
filePath = ItemSwap.AddSuffix(filePath, ".atex", $"_{Path.GetFileName(filePath).GetStableHashCode():x8}");
|
||||
filePath = ItemSwap.ReplaceType(filePath, slotFrom, slotTo, idFrom);
|
||||
dataWasChanged = true;
|
||||
|
||||
return FileSwap.CreateSwap(manager, ResourceType.Atex, redirections, filePath, oldPath, oldPath);
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic
|
|||
mod.Description = description ?? mod.Description;
|
||||
mod.Version = version ?? mod.Version;
|
||||
mod.Website = website ?? mod.Website;
|
||||
saveService.ImmediateSave(new ModMeta(mod));
|
||||
saveService.ImmediateSaveSync(new ModMeta(mod));
|
||||
}
|
||||
|
||||
public ModDataChangeType LoadLocalData(Mod mod)
|
||||
|
|
@ -249,6 +249,17 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic
|
|||
communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null);
|
||||
}
|
||||
|
||||
public void ResetModImportDate(Mod mod)
|
||||
{
|
||||
var newDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
if (mod.ImportDate == newDate)
|
||||
return;
|
||||
|
||||
mod.ImportDate = newDate;
|
||||
saveService.QueueSave(new ModLocalData(mod));
|
||||
communicatorService.ModDataChanged.Invoke(ModDataChangeType.ImportDate, mod, null);
|
||||
}
|
||||
|
||||
public void ChangeModNote(Mod mod, string newNote)
|
||||
{
|
||||
if (mod.Note == newNote)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -90,6 +89,16 @@ public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveServ
|
|||
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, group.Mod, group, null, null, -1);
|
||||
}
|
||||
|
||||
public void ChangeOnlyAttributes(ImcModGroup group, bool onlyAttributes, SaveType saveType = SaveType.Queue)
|
||||
{
|
||||
if (group.OnlyAttributes == onlyAttributes)
|
||||
return;
|
||||
|
||||
group.OnlyAttributes = onlyAttributes;
|
||||
SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
|
||||
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, group.Mod, group, null, null, -1);
|
||||
}
|
||||
|
||||
public void ChangeCanBeDisabled(ImcModGroup group, bool canBeDisabled, SaveType saveType = SaveType.Queue)
|
||||
{
|
||||
if (group.CanBeDisabled == canBeDisabled)
|
||||
|
|
|
|||
|
|
@ -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,11 @@ 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) : IService
|
||||
{
|
||||
public readonly Configuration Config = config;
|
||||
|
||||
|
|
@ -34,7 +35,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 +47,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 +57,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 +66,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);
|
||||
modDataChange |= 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, container.Manipulations))
|
||||
saveService.ImmediateSaveSync(new ModSaveGroup(container, Config.ReplaceNonAsciiOnImport));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -89,13 +98,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 +115,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 +166,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 +177,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 +186,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 +203,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 +216,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 +232,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, option.Manipulations);
|
||||
|
||||
return (changes, deleteList);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -250,7 +263,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 +273,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 +290,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 +302,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 +347,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,
|
||||
}
|
||||
}
|
||||
|
|
@ -66,4 +66,10 @@ public readonly record struct ModPriority(int Value) :
|
|||
|
||||
public int CompareTo(ModPriority other)
|
||||
=> Value.CompareTo(other.Value);
|
||||
|
||||
public const int HiddenMin = -84037;
|
||||
public const int HiddenMax = HiddenMin + 1000;
|
||||
|
||||
public bool IsHidden
|
||||
=> Value is > HiddenMin and < HiddenMax;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -48,15 +48,16 @@ public class ItemSwapTab : IDisposable, ITab, IUiService
|
|||
_selectors = new Dictionary<SwapType, (ItemSelector Source, ItemSelector Target, string TextFrom, string TextTo)>
|
||||
{
|
||||
// @formatter:off
|
||||
[SwapType.Hat] = (new ItemSelector(itemService, selector, FullEquipType.Head), new ItemSelector(itemService, null, FullEquipType.Head), "Take this Hat", "and put it on this one" ),
|
||||
[SwapType.Top] = (new ItemSelector(itemService, selector, FullEquipType.Body), new ItemSelector(itemService, null, FullEquipType.Body), "Take this Top", "and put it on this one" ),
|
||||
[SwapType.Gloves] = (new ItemSelector(itemService, selector, FullEquipType.Hands), new ItemSelector(itemService, null, FullEquipType.Hands), "Take these Gloves", "and put them on these" ),
|
||||
[SwapType.Pants] = (new ItemSelector(itemService, selector, FullEquipType.Legs), new ItemSelector(itemService, null, FullEquipType.Legs), "Take these Pants", "and put them on these" ),
|
||||
[SwapType.Shoes] = (new ItemSelector(itemService, selector, FullEquipType.Feet), new ItemSelector(itemService, null, FullEquipType.Feet), "Take these Shoes", "and put them on these" ),
|
||||
[SwapType.Earrings] = (new ItemSelector(itemService, selector, FullEquipType.Ears), new ItemSelector(itemService, null, FullEquipType.Ears), "Take these Earrings", "and put them on these" ),
|
||||
[SwapType.Necklace] = (new ItemSelector(itemService, selector, FullEquipType.Neck), new ItemSelector(itemService, null, FullEquipType.Neck), "Take this Necklace", "and put it on this one" ),
|
||||
[SwapType.Bracelet] = (new ItemSelector(itemService, selector, FullEquipType.Wrists), new ItemSelector(itemService, null, FullEquipType.Wrists), "Take these Bracelets", "and put them on these" ),
|
||||
[SwapType.Ring] = (new ItemSelector(itemService, selector, FullEquipType.Finger), new ItemSelector(itemService, null, FullEquipType.Finger), "Take this Ring", "and put it on this one" ),
|
||||
[SwapType.Hat] = (new ItemSelector(itemService, selector, FullEquipType.Head), new ItemSelector(itemService, null, FullEquipType.Head), "Take this Hat", "and put it on this one" ),
|
||||
[SwapType.Top] = (new ItemSelector(itemService, selector, FullEquipType.Body), new ItemSelector(itemService, null, FullEquipType.Body), "Take this Top", "and put it on this one" ),
|
||||
[SwapType.Gloves] = (new ItemSelector(itemService, selector, FullEquipType.Hands), new ItemSelector(itemService, null, FullEquipType.Hands), "Take these Gloves", "and put them on these" ),
|
||||
[SwapType.Pants] = (new ItemSelector(itemService, selector, FullEquipType.Legs), new ItemSelector(itemService, null, FullEquipType.Legs), "Take these Pants", "and put them on these" ),
|
||||
[SwapType.Shoes] = (new ItemSelector(itemService, selector, FullEquipType.Feet), new ItemSelector(itemService, null, FullEquipType.Feet), "Take these Shoes", "and put them on these" ),
|
||||
[SwapType.Earrings] = (new ItemSelector(itemService, selector, FullEquipType.Ears), new ItemSelector(itemService, null, FullEquipType.Ears), "Take these Earrings", "and put them on these" ),
|
||||
[SwapType.Necklace] = (new ItemSelector(itemService, selector, FullEquipType.Neck), new ItemSelector(itemService, null, FullEquipType.Neck), "Take this Necklace", "and put it on this one" ),
|
||||
[SwapType.Bracelet] = (new ItemSelector(itemService, selector, FullEquipType.Wrists), new ItemSelector(itemService, null, FullEquipType.Wrists), "Take these Bracelets", "and put them on these" ),
|
||||
[SwapType.Ring] = (new ItemSelector(itemService, selector, FullEquipType.Finger), new ItemSelector(itemService, null, FullEquipType.Finger), "Take this Ring", "and put it on this one" ),
|
||||
[SwapType.Glasses] = (new ItemSelector(itemService, selector, FullEquipType.Glasses), new ItemSelector(itemService, null, FullEquipType.Glasses), "Take these Glasses", "and put them on these" ),
|
||||
// @formatter:on
|
||||
};
|
||||
|
||||
|
|
@ -129,6 +130,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService
|
|||
Ears,
|
||||
Tail,
|
||||
Weapon,
|
||||
Glasses,
|
||||
}
|
||||
|
||||
private class ItemSelector(ItemData data, ModFileSystemSelector? selector, FullEquipType type)
|
||||
|
|
@ -158,14 +160,14 @@ public class ItemSwapTab : IDisposable, ITab, IUiService
|
|||
private ModSettings? _modSettings;
|
||||
private bool _dirty;
|
||||
|
||||
private SwapType _lastTab = SwapType.Hair;
|
||||
private Gender _currentGender = Gender.Male;
|
||||
private ModelRace _currentRace = ModelRace.Midlander;
|
||||
private int _targetId;
|
||||
private int _sourceId;
|
||||
private Exception? _loadException;
|
||||
private EquipSlot _slotFrom = EquipSlot.Head;
|
||||
private EquipSlot _slotTo = EquipSlot.Ears;
|
||||
private SwapType _lastTab = SwapType.Hair;
|
||||
private Gender _currentGender = Gender.Male;
|
||||
private ModelRace _currentRace = ModelRace.Midlander;
|
||||
private int _targetId;
|
||||
private int _sourceId;
|
||||
private Exception? _loadException;
|
||||
private BetweenSlotTypes _slotFrom = BetweenSlotTypes.Hat;
|
||||
private BetweenSlotTypes _slotTo = BetweenSlotTypes.Earrings;
|
||||
|
||||
private string _newModName = string.Empty;
|
||||
private string _newGroupName = "Swaps";
|
||||
|
|
@ -200,18 +202,19 @@ public class ItemSwapTab : IDisposable, ITab, IUiService
|
|||
case SwapType.Necklace:
|
||||
case SwapType.Bracelet:
|
||||
case SwapType.Ring:
|
||||
case SwapType.Glasses:
|
||||
var values = _selectors[_lastTab];
|
||||
if (values.Source.CurrentSelection.Item.Type != FullEquipType.Unknown
|
||||
&& values.Target.CurrentSelection.Item.Type != FullEquipType.Unknown)
|
||||
_affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection.Item, values.Source.CurrentSelection.Item,
|
||||
_useCurrentCollection ? _collectionManager.Active.Current : null, _useRightRing, _useLeftRing);
|
||||
|
||||
break;
|
||||
case SwapType.BetweenSlots:
|
||||
var (_, _, selectorFrom) = GetAccessorySelector(_slotFrom, true);
|
||||
var (_, _, selectorTo) = GetAccessorySelector(_slotTo, false);
|
||||
if (selectorFrom.CurrentSelection.Item.Valid && selectorTo.CurrentSelection.Item.Valid)
|
||||
_affectedItems = _swapData.LoadTypeSwap(_slotTo, selectorTo.CurrentSelection.Item, _slotFrom, selectorFrom.CurrentSelection.Item,
|
||||
_affectedItems = _swapData.LoadTypeSwap(ToEquipSlot(_slotTo), selectorTo.CurrentSelection.Item, ToEquipSlot(_slotFrom),
|
||||
selectorFrom.CurrentSelection.Item,
|
||||
_useCurrentCollection ? _collectionManager.Active.Current : null);
|
||||
break;
|
||||
case SwapType.Hair when _targetId > 0 && _sourceId > 0:
|
||||
|
|
@ -264,7 +267,23 @@ public class ItemSwapTab : IDisposable, ITab, IUiService
|
|||
}
|
||||
|
||||
private string CreateDescription()
|
||||
=> $"Created by swapping {_lastTab} {_sourceId} onto {_lastTab} {_targetId} for {_currentRace.ToName()} {_currentGender.ToName()}s in {_mod!.Name}.";
|
||||
{
|
||||
switch (_lastTab)
|
||||
{
|
||||
case SwapType.Ears:
|
||||
case SwapType.Face:
|
||||
case SwapType.Hair:
|
||||
case SwapType.Tail:
|
||||
return
|
||||
$"Created by swapping {_lastTab} {_sourceId} onto {_lastTab} {_targetId} for {_currentRace.ToName()} {_currentGender.ToName()}s in {_mod!.Name}.";
|
||||
case SwapType.BetweenSlots:
|
||||
return
|
||||
$"Created by swapping {GetAccessorySelector(_slotFrom, true).Item3.CurrentSelection.Item.Name} onto {GetAccessorySelector(_slotTo, false).Item3.CurrentSelection.Item.Name} in {_mod!.Name}.";
|
||||
default:
|
||||
return
|
||||
$"Created by swapping {_selectors[_lastTab].Source.CurrentSelection.Item.Name} onto {_selectors[_lastTab].Target.CurrentSelection.Item.Name} in {_mod!.Name}.";
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateOption()
|
||||
{
|
||||
|
|
@ -281,7 +300,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))
|
||||
|
|
@ -416,6 +435,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService
|
|||
DrawEquipmentSwap(SwapType.Necklace);
|
||||
DrawEquipmentSwap(SwapType.Bracelet);
|
||||
DrawEquipmentSwap(SwapType.Ring);
|
||||
DrawEquipmentSwap(SwapType.Glasses);
|
||||
DrawAccessorySwap();
|
||||
DrawHairSwap();
|
||||
//DrawFaceSwap();
|
||||
|
|
@ -454,23 +474,24 @@ public class ItemSwapTab : IDisposable, ITab, IUiService
|
|||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetNextItemWidth(100 * UiHelpers.Scale);
|
||||
using (var combo = ImRaii.Combo("##fromType", _slotFrom is EquipSlot.Head ? "Hat" : _slotFrom.ToName()))
|
||||
using (var combo = ImRaii.Combo("##fromType", ToName(_slotFrom)))
|
||||
{
|
||||
if (combo)
|
||||
foreach (var slot in EquipSlotExtensions.AccessorySlots.Prepend(EquipSlot.Head))
|
||||
foreach (var slot in Enum.GetValues<BetweenSlotTypes>())
|
||||
{
|
||||
if (!ImGui.Selectable(slot is EquipSlot.Head ? "Hat" : slot.ToName(), slot == _slotFrom) || slot == _slotFrom)
|
||||
if (!ImGui.Selectable(ToName(slot), slot == _slotFrom) || slot == _slotFrom)
|
||||
continue;
|
||||
|
||||
_dirty = true;
|
||||
_slotFrom = slot;
|
||||
if (slot == _slotTo)
|
||||
_slotTo = EquipSlotExtensions.AccessorySlots.First(s => slot != s);
|
||||
_slotTo = AvailableToTypes.First(s => slot != s);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
_dirty |= selector.Draw("##itemSource", selector.CurrentSelection.Item.Name ?? string.Empty, string.Empty, InputWidth * 2 * UiHelpers.Scale,
|
||||
_dirty |= selector.Draw("##itemSource", selector.CurrentSelection.Item.Name ?? string.Empty, string.Empty,
|
||||
InputWidth * 2 * UiHelpers.Scale,
|
||||
ImGui.GetTextLineHeightWithSpacing());
|
||||
|
||||
(article1, _, selector) = GetAccessorySelector(_slotTo, false);
|
||||
|
|
@ -480,12 +501,12 @@ public class ItemSwapTab : IDisposable, ITab, IUiService
|
|||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetNextItemWidth(100 * UiHelpers.Scale);
|
||||
using (var combo = ImRaii.Combo("##toType", _slotTo.ToName()))
|
||||
using (var combo = ImRaii.Combo("##toType", ToName(_slotTo)))
|
||||
{
|
||||
if (combo)
|
||||
foreach (var slot in EquipSlotExtensions.AccessorySlots.Where(s => s != _slotFrom))
|
||||
foreach (var slot in AvailableToTypes.Where(t => t != _slotFrom))
|
||||
{
|
||||
if (!ImGui.Selectable(slot.ToName(), slot == _slotTo) || slot == _slotTo)
|
||||
if (!ImGui.Selectable(ToName(slot), slot == _slotTo) || slot == _slotTo)
|
||||
continue;
|
||||
|
||||
_dirty = true;
|
||||
|
|
@ -508,17 +529,18 @@ public class ItemSwapTab : IDisposable, ITab, IUiService
|
|||
.Select(i => i.Name)));
|
||||
}
|
||||
|
||||
private (string, string, ItemSelector) GetAccessorySelector(EquipSlot slot, bool source)
|
||||
private (string, string, ItemSelector) GetAccessorySelector(BetweenSlotTypes slot, bool source)
|
||||
{
|
||||
var (type, article1, article2) = slot switch
|
||||
{
|
||||
EquipSlot.Head => (SwapType.Hat, "this", "it"),
|
||||
EquipSlot.Ears => (SwapType.Earrings, "these", "them"),
|
||||
EquipSlot.Neck => (SwapType.Necklace, "this", "it"),
|
||||
EquipSlot.Wrists => (SwapType.Bracelet, "these", "them"),
|
||||
EquipSlot.RFinger => (SwapType.Ring, "this", "it"),
|
||||
EquipSlot.LFinger => (SwapType.Ring, "this", "it"),
|
||||
_ => (SwapType.Ring, "this", "it"),
|
||||
BetweenSlotTypes.Hat => (SwapType.Hat, "this", "it"),
|
||||
BetweenSlotTypes.Earrings => (SwapType.Earrings, "these", "them"),
|
||||
BetweenSlotTypes.Necklace => (SwapType.Necklace, "this", "it"),
|
||||
BetweenSlotTypes.Bracelets => (SwapType.Bracelet, "these", "them"),
|
||||
BetweenSlotTypes.RightRing => (SwapType.Ring, "this", "it"),
|
||||
BetweenSlotTypes.LeftRing => (SwapType.Ring, "this", "it"),
|
||||
BetweenSlotTypes.Glasses => (SwapType.Glasses, "these", "them"),
|
||||
_ => (SwapType.Ring, "this", "it"),
|
||||
};
|
||||
var (itemSelector, target, _, _) = _selectors[type];
|
||||
return (article1, article2, source ? itemSelector : target);
|
||||
|
|
@ -689,6 +711,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService
|
|||
SwapType.Necklace => "One of the selected necklaces does not seem to exist.",
|
||||
SwapType.Bracelet => "One of the selected bracelets does not seem to exist.",
|
||||
SwapType.Ring => "One of the selected rings does not seem to exist.",
|
||||
SwapType.Glasses => "One of the selected glasses does not seem to exist.",
|
||||
SwapType.Hair => "One of the selected hairstyles does not seem to exist for this gender and race combo.",
|
||||
SwapType.Face => "One of the selected faces does not seem to exist for this gender and race combo.",
|
||||
SwapType.Ears => "One of the selected ear types does not seem to exist for this gender and race combo.",
|
||||
|
|
@ -746,4 +769,44 @@ public class ItemSwapTab : IDisposable, ITab, IUiService
|
|||
UpdateOption();
|
||||
_dirty = true;
|
||||
}
|
||||
|
||||
private enum BetweenSlotTypes
|
||||
{
|
||||
Hat,
|
||||
Earrings,
|
||||
Necklace,
|
||||
Bracelets,
|
||||
RightRing,
|
||||
LeftRing,
|
||||
Glasses,
|
||||
}
|
||||
|
||||
private static EquipSlot ToEquipSlot(BetweenSlotTypes type)
|
||||
=> type switch
|
||||
{
|
||||
BetweenSlotTypes.Hat => EquipSlot.Head,
|
||||
BetweenSlotTypes.Earrings => EquipSlot.Ears,
|
||||
BetweenSlotTypes.Necklace => EquipSlot.Neck,
|
||||
BetweenSlotTypes.Bracelets => EquipSlot.Wrists,
|
||||
BetweenSlotTypes.RightRing => EquipSlot.RFinger,
|
||||
BetweenSlotTypes.LeftRing => EquipSlot.LFinger,
|
||||
BetweenSlotTypes.Glasses => BonusItemFlag.Glasses.ToEquipSlot(),
|
||||
_ => EquipSlot.Unknown,
|
||||
};
|
||||
|
||||
private static string ToName(BetweenSlotTypes type)
|
||||
=> type switch
|
||||
{
|
||||
BetweenSlotTypes.Hat => "Hat",
|
||||
BetweenSlotTypes.Earrings => "Earrings",
|
||||
BetweenSlotTypes.Necklace => "Necklace",
|
||||
BetweenSlotTypes.Bracelets => "Bracelets",
|
||||
BetweenSlotTypes.RightRing => "Right Ring",
|
||||
BetweenSlotTypes.LeftRing => "Left Ring",
|
||||
BetweenSlotTypes.Glasses => "Glasses",
|
||||
_ => "Unknown",
|
||||
};
|
||||
|
||||
private static readonly IReadOnlyList<BetweenSlotTypes> AvailableToTypes =
|
||||
Enum.GetValues<BetweenSlotTypes>().Where(s => s is not BetweenSlotTypes.Hat).ToArray();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ public sealed unsafe class MaterialTemplatePickers : IUiService
|
|||
_textureSize = _firstNonNullTexture switch
|
||||
{
|
||||
null => Vector2.Zero,
|
||||
_ => new Vector2(_firstNonNullTexture->CsHandle.Texture->Width, _firstNonNullTexture->CsHandle.Texture->Height).Contain(new Vector2(MaximumTextureSize)),
|
||||
_ => new Vector2(_firstNonNullTexture->CsHandle.Texture->ActualWidth, _firstNonNullTexture->CsHandle.Texture->ActualHeight).Contain(new Vector2(MaximumTextureSize)),
|
||||
};
|
||||
|
||||
if (float.IsNaN(itemHeight))
|
||||
|
|
@ -192,10 +192,10 @@ public sealed unsafe class MaterialTemplatePickers : IUiService
|
|||
continue;
|
||||
|
||||
var position = regionStart with { X = regionStart.X + (itemSize.X + itemSpacing) * j };
|
||||
var size = new Vector2(texture->Width, texture->Height).Contain(itemSize);
|
||||
var size = new Vector2(texture->ActualWidth, texture->ActualHeight).Contain(itemSize);
|
||||
position += (itemSize - size) * 0.5f;
|
||||
ImGui.GetWindowDrawList().AddImage(handle, position, position + size, Vector2.Zero,
|
||||
new Vector2(texture->Width / (float)texture->Width2, texture->Height / (float)texture->Height2));
|
||||
new Vector2(texture->ActualWidth / (float)texture->AllocatedWidth, texture->ActualHeight / (float)texture->AllocatedHeight));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
|
|||
}
|
||||
|
||||
private void UpdateEntry()
|
||||
=> (Entry, _fileExists, _) = MetaFiles.ImcChecker.GetDefaultEntry(Identifier, true);
|
||||
=> (Entry, _fileExists, _) = ImcChecker.GetDefaultEntry(Identifier, true);
|
||||
|
||||
protected override void DrawNew()
|
||||
{
|
||||
|
|
@ -54,7 +54,7 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
|
|||
DrawMetaButtons(identifier, entry);
|
||||
DrawIdentifier(identifier);
|
||||
|
||||
var defaultEntry = MetaFiles.ImcChecker.GetDefaultEntry(identifier, true).Entry;
|
||||
var defaultEntry = ImcChecker.GetDefaultEntry(identifier, true).Entry;
|
||||
if (DrawEntry(defaultEntry, ref entry, true))
|
||||
Editor.Changes |= Editor.Update(identifier, entry);
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
354
Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs
Normal file
354
Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Text;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.UI.Classes;
|
||||
using Notification = OtterGui.Classes.Notification;
|
||||
|
||||
namespace Penumbra.UI.AdvancedWindow;
|
||||
|
||||
public partial class ModEditWindow
|
||||
{
|
||||
private readonly FileEditor<PbdTab> _pbdTab;
|
||||
private readonly PbdData _pbdData = new();
|
||||
|
||||
private bool DrawDeformerPanel(PbdTab tab, bool disabled)
|
||||
{
|
||||
_pbdData.Update(tab.File);
|
||||
DrawGenderRaceSelector(tab);
|
||||
ImGui.SameLine();
|
||||
DrawBoneSelector();
|
||||
ImGui.SameLine();
|
||||
return DrawBoneData(tab, disabled);
|
||||
}
|
||||
|
||||
private void DrawGenderRaceSelector(PbdTab tab)
|
||||
{
|
||||
using var group = ImUtf8.Group();
|
||||
var width = ImUtf8.CalcTextSize("Hellsguard - Female (Child)____0000"u8).X + 2 * ImGui.GetStyle().WindowPadding.X;
|
||||
using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0)
|
||||
.Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero))
|
||||
{
|
||||
ImGui.SetNextItemWidth(width);
|
||||
ImUtf8.InputText("##grFilter"u8, ref _pbdData.RaceCodeFilter, "Filter..."u8);
|
||||
}
|
||||
|
||||
using var child = ImUtf8.Child("GenderRace"u8,
|
||||
new Vector2(width, ImGui.GetContentRegionMax().Y - ImGui.GetFrameHeight() - ImGui.GetStyle().WindowPadding.Y), true);
|
||||
if (!child)
|
||||
return;
|
||||
|
||||
var metaColor = ColorId.ItemId.Value();
|
||||
foreach (var (deformer, index) in tab.File.Deformers.WithIndex())
|
||||
{
|
||||
var name = deformer.GenderRace.ToName();
|
||||
var raceCode = deformer.GenderRace.ToRaceCode();
|
||||
// No clipping necessary since this are not that many objects anyway.
|
||||
if (!name.Contains(_pbdData.RaceCodeFilter) && !raceCode.Contains(_pbdData.RaceCodeFilter))
|
||||
continue;
|
||||
|
||||
using var id = ImUtf8.PushId(index);
|
||||
using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), deformer.RacialDeformer.IsEmpty);
|
||||
if (ImUtf8.Selectable(name, deformer.GenderRace == _pbdData.SelectedRaceCode))
|
||||
{
|
||||
_pbdData.SelectedRaceCode = deformer.GenderRace;
|
||||
_pbdData.SelectedDeformer = deformer.RacialDeformer;
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
color.Push(ImGuiCol.Text, metaColor);
|
||||
ImUtf8.TextRightAligned(raceCode);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawBoneSelector()
|
||||
{
|
||||
using var group = ImUtf8.Group();
|
||||
var width = 200 * ImUtf8.GlobalScale;
|
||||
using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0)
|
||||
.Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero))
|
||||
{
|
||||
ImGui.SetNextItemWidth(width);
|
||||
ImUtf8.InputText("##boneFilter"u8, ref _pbdData.BoneFilter, "Filter..."u8);
|
||||
}
|
||||
|
||||
using var child = ImUtf8.Child("Bone"u8,
|
||||
new Vector2(width, ImGui.GetContentRegionMax().Y - ImGui.GetFrameHeight() - ImGui.GetStyle().WindowPadding.Y), true);
|
||||
if (!child)
|
||||
return;
|
||||
|
||||
if (_pbdData.SelectedDeformer == null)
|
||||
return;
|
||||
|
||||
if (_pbdData.SelectedDeformer.IsEmpty)
|
||||
{
|
||||
ImUtf8.Text("<Empty>"u8);
|
||||
}
|
||||
else
|
||||
{
|
||||
var height = ImGui.GetTextLineHeightWithSpacing();
|
||||
var skips = ImGuiClip.GetNecessarySkips(height);
|
||||
var remainder = ImGuiClip.FilteredClippedDraw(_pbdData.SelectedDeformer.DeformMatrices.Keys, skips,
|
||||
b => b.Contains(_pbdData.BoneFilter), bone
|
||||
=>
|
||||
{
|
||||
if (ImUtf8.Selectable(bone, bone == _pbdData.SelectedBone))
|
||||
_pbdData.SelectedBone = bone;
|
||||
});
|
||||
ImGuiClip.DrawEndDummy(remainder, height);
|
||||
}
|
||||
}
|
||||
|
||||
private bool DrawBoneData(PbdTab tab, bool disabled)
|
||||
{
|
||||
using var child = ImUtf8.Child("Data"u8,
|
||||
ImGui.GetContentRegionAvail() with { Y = ImGui.GetContentRegionMax().Y - ImGui.GetStyle().WindowPadding.Y }, true);
|
||||
if (!child)
|
||||
return false;
|
||||
|
||||
if (_pbdData.SelectedBone == null)
|
||||
return false;
|
||||
|
||||
if (!_pbdData.SelectedDeformer!.DeformMatrices.TryGetValue(_pbdData.SelectedBone, out var matrix))
|
||||
return false;
|
||||
|
||||
var width = UiBuilder.MonoFont.GetCharAdvance('0') * 12 + ImGui.GetStyle().FramePadding.X * 2;
|
||||
var dummyHeight = ImGui.GetTextLineHeight() / 2;
|
||||
var ret = DrawAddNewBone(tab, disabled, matrix, width);
|
||||
|
||||
ImUtf8.Dummy(0, dummyHeight);
|
||||
ImGui.Separator();
|
||||
ImUtf8.Dummy(0, dummyHeight);
|
||||
ret |= DrawDeformerMatrix(disabled, matrix, width);
|
||||
ImUtf8.Dummy(0, dummyHeight);
|
||||
ret |= DrawCopyPasteButtons(disabled, matrix, width);
|
||||
|
||||
|
||||
ImUtf8.Dummy(0, dummyHeight);
|
||||
ImGui.Separator();
|
||||
ImUtf8.Dummy(0, dummyHeight);
|
||||
ret |= DrawDecomposedData(disabled, matrix, width);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private bool DrawAddNewBone(PbdTab tab, bool disabled, in TransformMatrix matrix, float width)
|
||||
{
|
||||
var ret = false;
|
||||
ImUtf8.TextFrameAligned("Copy the values of the bone "u8);
|
||||
ImGui.SameLine(0, 0);
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, ColorId.NewMod.Value()))
|
||||
{
|
||||
ImUtf8.TextFrameAligned(_pbdData.SelectedBone);
|
||||
}
|
||||
|
||||
ImGui.SameLine(0, 0);
|
||||
ImUtf8.TextFrameAligned(" to a new bone of name"u8);
|
||||
|
||||
var fullWidth = width * 4 + ImGui.GetStyle().ItemSpacing.X * 3;
|
||||
ImGui.SetNextItemWidth(fullWidth);
|
||||
ImUtf8.InputText("##newBone"u8, ref _pbdData.NewBoneName, "New Bone Name..."u8);
|
||||
ImUtf8.TextFrameAligned("for all races that have a corresponding bone."u8);
|
||||
ImGui.SameLine(0, fullWidth - width - ImGui.GetItemRectSize().X);
|
||||
if (ImUtf8.ButtonEx("Apply"u8, ""u8, new Vector2(width, 0),
|
||||
disabled || _pbdData.NewBoneName.Length == 0 || _pbdData.SelectedBone == null))
|
||||
{
|
||||
foreach (var deformer in tab.File.Deformers)
|
||||
{
|
||||
if (!deformer.RacialDeformer.DeformMatrices.TryGetValue(_pbdData.SelectedBone!, out var existingMatrix))
|
||||
continue;
|
||||
|
||||
if (!deformer.RacialDeformer.DeformMatrices.TryAdd(_pbdData.NewBoneName, existingMatrix)
|
||||
&& deformer.RacialDeformer.DeformMatrices.TryGetValue(_pbdData.NewBoneName, out var newBoneMatrix)
|
||||
&& !newBoneMatrix.Equals(existingMatrix))
|
||||
Penumbra.Messager.AddMessage(new Notification(
|
||||
$"Could not add deformer matrix to {deformer.GenderRace.ToName()}, Bone {_pbdData.NewBoneName} because it already has a deformer that differs from the intended one.",
|
||||
NotificationType.Warning));
|
||||
else
|
||||
ret = true;
|
||||
}
|
||||
|
||||
_pbdData.NewBoneName = string.Empty;
|
||||
}
|
||||
|
||||
if (ImUtf8.ButtonEx("Copy Values to Single New Bone Entry"u8, ""u8, new Vector2(fullWidth, 0),
|
||||
disabled || _pbdData.NewBoneName.Length == 0 || _pbdData.SelectedDeformer!.DeformMatrices.ContainsKey(_pbdData.NewBoneName)))
|
||||
{
|
||||
_pbdData.SelectedDeformer!.DeformMatrices[_pbdData.NewBoneName] = matrix;
|
||||
ret = true;
|
||||
_pbdData.NewBoneName = string.Empty;
|
||||
}
|
||||
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private bool DrawDeformerMatrix(bool disabled, in TransformMatrix matrix, float width)
|
||||
{
|
||||
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
|
||||
using var _ = ImRaii.Disabled(disabled);
|
||||
var ret = false;
|
||||
for (var i = 0; i < 3; ++i)
|
||||
{
|
||||
for (var j = 0; j < 4; ++j)
|
||||
{
|
||||
using var id = ImUtf8.PushId(i * 4 + j);
|
||||
ImGui.SetNextItemWidth(width);
|
||||
var tmp = matrix[i, j];
|
||||
if (ImUtf8.InputScalar(""u8, ref tmp, "% 12.8f"u8))
|
||||
{
|
||||
ret = true;
|
||||
_pbdData.SelectedDeformer!.DeformMatrices[_pbdData.SelectedBone!] = matrix.ChangeValue(i, j, tmp);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
}
|
||||
|
||||
ImGui.NewLine();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private bool DrawCopyPasteButtons(bool disabled, in TransformMatrix matrix, float width)
|
||||
{
|
||||
var size = new Vector2(width, 0);
|
||||
if (ImUtf8.Button("Copy Values"u8, size))
|
||||
_pbdData.CopiedMatrix = matrix;
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
var ret = false;
|
||||
if (ImUtf8.ButtonEx("Paste Values"u8, ""u8, size, disabled || !_pbdData.CopiedMatrix.HasValue))
|
||||
{
|
||||
_pbdData.SelectedDeformer!.DeformMatrices[_pbdData.SelectedBone!] = _pbdData.CopiedMatrix!.Value;
|
||||
ret = true;
|
||||
}
|
||||
|
||||
var modifier = _config.DeleteModModifier.IsActive();
|
||||
ImGui.SameLine();
|
||||
if (modifier)
|
||||
{
|
||||
if (ImUtf8.ButtonEx("Delete"u8, "Delete this bone entry."u8, size, disabled))
|
||||
{
|
||||
ret |= _pbdData.SelectedDeformer!.DeformMatrices.Remove(_pbdData.SelectedBone!);
|
||||
_pbdData.SelectedBone = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ImUtf8.ButtonEx("Delete"u8, $"Delete this bone entry. Hold {_config.DeleteModModifier} to delete.", size, true);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private bool DrawDecomposedData(bool disabled, in TransformMatrix matrix, float width)
|
||||
{
|
||||
var ret = false;
|
||||
|
||||
|
||||
if (!matrix.TryDecompose(out var scale, out var rotation, out var translation))
|
||||
return false;
|
||||
|
||||
using (ImUtf8.Group())
|
||||
{
|
||||
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
|
||||
using var _ = ImRaii.Disabled(disabled);
|
||||
|
||||
ImGui.SetNextItemWidth(width);
|
||||
ret |= ImUtf8.InputScalar("##ScaleX"u8, ref scale.X, "% 12.8f"u8);
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.SetNextItemWidth(width);
|
||||
ret |= ImUtf8.InputScalar("##ScaleY"u8, ref scale.Y, "% 12.8f"u8);
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.SetNextItemWidth(width);
|
||||
ret |= ImUtf8.InputScalar("##ScaleZ"u8, ref scale.Z, "% 12.8f"u8);
|
||||
|
||||
|
||||
ImGui.SetNextItemWidth(width);
|
||||
ret |= ImUtf8.InputScalar("##TranslationX"u8, ref translation.X, "% 12.8f"u8);
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.SetNextItemWidth(width);
|
||||
ret |= ImUtf8.InputScalar("##TranslationY"u8, ref translation.Y, "% 12.8f"u8);
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.SetNextItemWidth(width);
|
||||
ret |= ImUtf8.InputScalar("##TranslationZ"u8, ref translation.Z, "% 12.8f"u8);
|
||||
|
||||
|
||||
ImGui.SetNextItemWidth(width);
|
||||
ret |= ImUtf8.InputScalar("##RotationR"u8, ref rotation.W, "% 12.8f"u8);
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.SetNextItemWidth(width);
|
||||
ret |= ImUtf8.InputScalar("##RotationI"u8, ref rotation.X, "% 12.8f"u8);
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.SetNextItemWidth(width);
|
||||
ret |= ImUtf8.InputScalar("##RotationJ"u8, ref rotation.Y, "% 12.8f"u8);
|
||||
ImGui.SameLine();
|
||||
ImGui.SetNextItemWidth(width);
|
||||
ret |= ImUtf8.InputScalar("##RotationK"u8, ref rotation.Z, "% 12.8f"u8);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
using (ImUtf8.Group())
|
||||
{
|
||||
ImUtf8.TextFrameAligned("Scale"u8);
|
||||
ImUtf8.TextFrameAligned("Translation"u8);
|
||||
ImUtf8.TextFrameAligned("Rotation (Quaternion, rijk)"u8);
|
||||
}
|
||||
|
||||
if (ret)
|
||||
_pbdData.SelectedDeformer!.DeformMatrices[_pbdData.SelectedBone!] = TransformMatrix.Compose(scale, rotation, translation);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public class PbdTab(byte[] data, string filePath) : IWritable
|
||||
{
|
||||
public readonly string FilePath = filePath;
|
||||
|
||||
public readonly PbdFile File = new(data);
|
||||
|
||||
public bool Valid
|
||||
=> File.Valid;
|
||||
|
||||
public byte[] Write()
|
||||
=> File.Write();
|
||||
}
|
||||
|
||||
private class PbdData
|
||||
{
|
||||
public GenderRace SelectedRaceCode = GenderRace.Unknown;
|
||||
public RacialDeformer? SelectedDeformer;
|
||||
public string? SelectedBone;
|
||||
public string NewBoneName = string.Empty;
|
||||
public string BoneFilter = string.Empty;
|
||||
public string RaceCodeFilter = string.Empty;
|
||||
|
||||
public TransformMatrix? CopiedMatrix;
|
||||
|
||||
public void Update(PbdFile file)
|
||||
{
|
||||
if (SelectedRaceCode is GenderRace.Unknown)
|
||||
{
|
||||
SelectedDeformer = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedDeformer = file.Deformers.FirstOrDefault(p => p.GenderRace == SelectedRaceCode).RacialDeformer;
|
||||
if (SelectedDeformer is null)
|
||||
SelectedRaceCode = GenderRace.Unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -97,9 +97,7 @@ public partial class ModEditWindow
|
|||
|
||||
private void DrawImportExport(MdlTab tab, bool disabled)
|
||||
{
|
||||
// TODO: Enable when functional.
|
||||
using var dawntrailDisabled = ImRaii.Disabled();
|
||||
if (!ImGui.CollapsingHeader("Import / Export (currently disabled due to Dawntrail format changes)") || true)
|
||||
if (!ImGui.CollapsingHeader("Import / Export"))
|
||||
return;
|
||||
|
||||
var childSize = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -186,6 +236,8 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
|
|||
_itemSwapTab.DrawContent();
|
||||
}
|
||||
|
||||
_pbdTab.Draw();
|
||||
|
||||
DrawMissingFilesTab();
|
||||
DrawMaterialReassignmentTab();
|
||||
}
|
||||
|
|
@ -407,14 +459,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 +484,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 +639,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 +656,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,
|
||||
|
|
@ -616,12 +667,18 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
|
|||
() => PopulateIsOnPlayer(_editor.Files.Shpk, ResourceType.Shpk), DrawShaderPackagePanel,
|
||||
() => Mod?.ModPath.FullName ?? string.Empty,
|
||||
(bytes, path, _) => new ShpkTab(_fileDialog, bytes, path));
|
||||
_pbdTab = new FileEditor<PbdTab>(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Deformers", ".pbd",
|
||||
() => _editor.Files.Pbd, DrawDeformerPanel,
|
||||
() => Mod?.ModPath.FullName ?? string.Empty,
|
||||
(bytes, path, _) => new PbdTab(bytes, path));
|
||||
_center = new CombinedTexture(_left, _right);
|
||||
_textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor, () => GetPlayerResourcesOfType(ResourceType.Tex));
|
||||
_resourceTreeFactory = resourceTreeFactory;
|
||||
_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()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using ImGuiNET;
|
|||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Text;
|
||||
using Penumbra.Mods.Editor;
|
||||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.Mods.SubMods;
|
||||
|
|
@ -45,9 +46,30 @@ public class ModMergeTab(ModMerger modMerger) : IUiService
|
|||
|
||||
private void DrawMergeInto(float size)
|
||||
{
|
||||
using var bigGroup = ImRaii.Group();
|
||||
using var bigGroup = ImRaii.Group();
|
||||
var minComboSize = 300 * ImGuiHelpers.GlobalScale;
|
||||
var textSize = ImUtf8.CalcTextSize($"Merge {modMerger.MergeFromMod!.Name} into ").X;
|
||||
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted($"Merge {modMerger.MergeFromMod!.Name} into ");
|
||||
|
||||
using (ImRaii.Group())
|
||||
{
|
||||
ImUtf8.Text("Merge "u8);
|
||||
ImGui.SameLine(0, 0);
|
||||
if (size - textSize < minComboSize)
|
||||
{
|
||||
ImUtf8.Text("selected mod"u8, ColorId.FolderLine.Value());
|
||||
ImUtf8.HoverTooltip(modMerger.MergeFromMod!.Name.Text);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImUtf8.Text(modMerger.MergeFromMod!.Name.Text, ColorId.FolderLine.Value());
|
||||
}
|
||||
|
||||
ImGui.SameLine(0, 0);
|
||||
ImUtf8.Text(" into"u8);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
DrawCombo(size - ImGui.GetItemRectSize().X - ImGui.GetStyle().ItemSpacing.X);
|
||||
|
||||
|
|
|
|||
|
|
@ -190,52 +190,6 @@ public class ResourceTreeViewer
|
|||
var frameHeight = ImGui.GetFrameHeight();
|
||||
var cellHeight = _actionCapacity > 0 ? frameHeight : 0.0f;
|
||||
|
||||
bool MatchesFilter(ResourceNode node, ChangedItemIconFlag filterIcon)
|
||||
{
|
||||
if (!_typeFilter.HasFlag(filterIcon))
|
||||
return false;
|
||||
|
||||
if (_nodeFilter.Length == 0)
|
||||
return true;
|
||||
|
||||
return node.Name != null && node.Name.Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase)
|
||||
|| node.FullPath.FullName.Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase)
|
||||
|| node.FullPath.InternalName.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase)
|
||||
|| Array.Exists(node.PossibleGamePaths, path => path.Path.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
NodeVisibility CalculateNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemIconFlag parentFilterIcon)
|
||||
{
|
||||
if (node.Internal && !debugMode)
|
||||
return NodeVisibility.Hidden;
|
||||
|
||||
var filterIcon = node.IconFlag != 0 ? node.IconFlag : parentFilterIcon;
|
||||
if (MatchesFilter(node, filterIcon))
|
||||
return NodeVisibility.Visible;
|
||||
|
||||
foreach (var child in node.Children)
|
||||
{
|
||||
if (GetNodeVisibility(unchecked(nodePathHash * 31 + child.ResourceHandle), child, filterIcon) != NodeVisibility.Hidden)
|
||||
return NodeVisibility.DescendentsOnly;
|
||||
}
|
||||
|
||||
return NodeVisibility.Hidden;
|
||||
}
|
||||
|
||||
NodeVisibility GetNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemIconFlag parentFilterIcon)
|
||||
{
|
||||
if (!_filterCache.TryGetValue(nodePathHash, out var visibility))
|
||||
{
|
||||
visibility = CalculateNodeVisibility(nodePathHash, node, parentFilterIcon);
|
||||
_filterCache.Add(nodePathHash, visibility);
|
||||
}
|
||||
|
||||
return visibility;
|
||||
}
|
||||
|
||||
string GetAdditionalDataSuffix(CiByteString data)
|
||||
=> !debugMode || data.IsEmpty ? string.Empty : $"\n\nAdditional Data: {data}";
|
||||
|
||||
foreach (var (resourceNode, index) in resourceNodes.WithIndex())
|
||||
{
|
||||
var nodePathHash = unchecked(pathHash + resourceNode.ResourceHandle);
|
||||
|
|
@ -346,6 +300,54 @@ public class ResourceTreeViewer
|
|||
if (unfolded)
|
||||
DrawNodes(resourceNode.Children, level + 1, unchecked(nodePathHash * 31), filterIcon);
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
string GetAdditionalDataSuffix(CiByteString data)
|
||||
=> !debugMode || data.IsEmpty ? string.Empty : $"\n\nAdditional Data: {data}";
|
||||
|
||||
NodeVisibility GetNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemIconFlag parentFilterIcon)
|
||||
{
|
||||
if (!_filterCache.TryGetValue(nodePathHash, out var visibility))
|
||||
{
|
||||
visibility = CalculateNodeVisibility(nodePathHash, node, parentFilterIcon);
|
||||
_filterCache.Add(nodePathHash, visibility);
|
||||
}
|
||||
|
||||
return visibility;
|
||||
}
|
||||
|
||||
NodeVisibility CalculateNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemIconFlag parentFilterIcon)
|
||||
{
|
||||
if (node.Internal && !debugMode)
|
||||
return NodeVisibility.Hidden;
|
||||
|
||||
var filterIcon = node.IconFlag != 0 ? node.IconFlag : parentFilterIcon;
|
||||
if (MatchesFilter(node, filterIcon))
|
||||
return NodeVisibility.Visible;
|
||||
|
||||
foreach (var child in node.Children)
|
||||
{
|
||||
if (GetNodeVisibility(unchecked(nodePathHash * 31 + child.ResourceHandle), child, filterIcon) != NodeVisibility.Hidden)
|
||||
return NodeVisibility.DescendentsOnly;
|
||||
}
|
||||
|
||||
return NodeVisibility.Hidden;
|
||||
}
|
||||
|
||||
bool MatchesFilter(ResourceNode node, ChangedItemIconFlag filterIcon)
|
||||
{
|
||||
if (!_typeFilter.HasFlag(filterIcon))
|
||||
return false;
|
||||
|
||||
if (_nodeFilter.Length == 0)
|
||||
return true;
|
||||
|
||||
return node.Name != null && node.Name.Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase)
|
||||
|| node.FullPath.FullName.Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase)
|
||||
|| node.FullPath.InternalName.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase)
|
||||
|| Array.Exists(node.PossibleGamePaths, path => path.Path.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
[Flags]
|
||||
|
|
|
|||
|
|
@ -53,10 +53,42 @@ public class PenumbraChangelog : IUiService
|
|||
Add1_1_0_0(Changelog);
|
||||
Add1_1_1_0(Changelog);
|
||||
Add1_2_1_0(Changelog);
|
||||
Add1_3_0_0(Changelog);
|
||||
}
|
||||
|
||||
#region Changelogs
|
||||
|
||||
private static void Add1_3_0_0(Changelog log)
|
||||
=> log.NextVersion("Version 1.3.0.0")
|
||||
|
||||
.RegisterHighlight("The textures tab in the advanced editing window can now import and export .tga files.")
|
||||
.RegisterEntry("BC4 and BC6 textures can now also be imported.", 1)
|
||||
.RegisterHighlight("Added item swapping from and to the Glasses slot.")
|
||||
.RegisterEntry("Reworked quite a bit of things around face wear / bonus items. Please let me know if anything broke.", 1)
|
||||
.RegisterEntry("The import date of a mod is now shown in the Edit Mod tab, and can be reset via button.")
|
||||
.RegisterEntry("A button to open the file containing local mod data for a mod was also added.", 1)
|
||||
.RegisterHighlight("IMC groups can now be configured to only apply the attribute flags for their entry, and take the other values from the default value.")
|
||||
.RegisterEntry("This allows keeping the material index of every IMC entry of a group, while setting the attributes.", 1)
|
||||
.RegisterHighlight("Model Import/Export was fixed and re-enabled (thanks ackwell and ramen).")
|
||||
.RegisterHighlight("Added a hack to allow bonus items (face wear, glasses) to have VFX.")
|
||||
.RegisterEntry("Also fixed the hack that allowed accessories to have VFX not working anymore.", 1)
|
||||
.RegisterHighlight("Added rudimentary options to edit PBD files in the advanced editing window.")
|
||||
.RegisterEntry("Preparing the advanced editing window for a mod now does not freeze the game until it is ready.")
|
||||
.RegisterEntry("Meta Manipulations in the advanced editing window are now ordered and do not eat into performance as much when drawn.")
|
||||
.RegisterEntry("Added a button to the advanced editing window to remove all default-valued meta manipulations from a mod")
|
||||
.RegisterEntry("Default-valued manipulations will now also be removed on import from archives and .pmps, not just .ttmps, if not configured otherwise.", 1)
|
||||
.RegisterEntry("Checkbox-based mod filters are now tri-state checkboxes instead of two disjoint checkboxes.")
|
||||
.RegisterEntry("Paths from the resource logger can now be copied.")
|
||||
.RegisterEntry("Silenced some redundant error logs when updating mods via Heliosphere.")
|
||||
.RegisterEntry("Added 'Page' to imported mod data for TexTools interop. The value is not used in Penumbra, just persisted.")
|
||||
.RegisterEntry("Updated all external dependencies.")
|
||||
.RegisterEntry("Fixed issue with Demihuman IMC entries.")
|
||||
.RegisterEntry("Fixed some off-by-one errors on the mod import window.")
|
||||
.RegisterEntry("Fixed a race-condition concerning the first-time creation of mod-meta files.")
|
||||
.RegisterEntry("Fixed an issue with long mod titles in the merge mods tab.")
|
||||
.RegisterEntry("A bunch of other miscellaneous fixes.");
|
||||
|
||||
|
||||
private static void Add1_2_1_0(Changelog log)
|
||||
=> log.NextVersion("Version 1.2.1.0")
|
||||
.RegisterHighlight("Penumbra is now released for Dawntrail!")
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -24,13 +24,11 @@ public class AddGroupDrawer : IUiService
|
|||
private bool _imcFileExists;
|
||||
private bool _entryExists;
|
||||
private bool _entryInvalid;
|
||||
private readonly ImcChecker _imcChecker;
|
||||
private readonly ModManager _modManager;
|
||||
|
||||
public AddGroupDrawer(ModManager modManager, ImcChecker imcChecker)
|
||||
public AddGroupDrawer(ModManager modManager)
|
||||
{
|
||||
_modManager = modManager;
|
||||
_imcChecker = imcChecker;
|
||||
UpdateEntry();
|
||||
}
|
||||
|
||||
|
|
@ -142,7 +140,7 @@ public class AddGroupDrawer : IUiService
|
|||
|
||||
private void UpdateEntry()
|
||||
{
|
||||
(_defaultEntry, _imcFileExists, _entryExists) = _imcChecker.GetDefaultEntry(_imcIdentifier, false);
|
||||
(_defaultEntry, _imcFileExists, _entryExists) = ImcChecker.GetDefaultEntry(_imcIdentifier, false);
|
||||
_entryInvalid = !_imcIdentifier.Validate() || _defaultEntry.MaterialId == 0 || !_entryExists;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ using OtterGui;
|
|||
using OtterGui.Raii;
|
||||
using OtterGui.Text;
|
||||
using OtterGui.Text.Widget;
|
||||
using OtterGui.Widgets;
|
||||
using OtterGuiInternal.Utility;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Meta;
|
||||
using Penumbra.Mods.Groups;
|
||||
using Penumbra.Mods.Manager.OptionEditor;
|
||||
using Penumbra.Mods.SubMods;
|
||||
|
|
@ -19,18 +19,25 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr
|
|||
public void Draw()
|
||||
{
|
||||
var identifier = group.Identifier;
|
||||
var defaultEntry = editor.ImcChecker.GetDefaultEntry(identifier, true).Entry;
|
||||
var defaultEntry = ImcChecker.GetDefaultEntry(identifier, true).Entry;
|
||||
var entry = group.DefaultEntry;
|
||||
var changes = false;
|
||||
|
||||
var width = editor.AvailableWidth.X - ImUtf8.ItemInnerSpacing.X - ImUtf8.CalcTextSize("All Variants"u8).X;
|
||||
var width = editor.AvailableWidth.X - 3 * ImUtf8.ItemInnerSpacing.X - ImUtf8.ItemSpacing.X - ImUtf8.CalcTextSize("All Variants"u8).X - ImUtf8.CalcTextSize("Only Attributes"u8).X - 2 * ImUtf8.FrameHeight;
|
||||
ImUtf8.TextFramed(identifier.ToString(), 0, new Vector2(width, 0), borderColor: ImGui.GetColorU32(ImGuiCol.Border));
|
||||
|
||||
ImUtf8.SameLineInner();
|
||||
var allVariants = group.AllVariants;
|
||||
if (ImUtf8.Checkbox("All Variants"u8, ref allVariants))
|
||||
editor.ModManager.OptionEditor.ImcEditor.ChangeAllVariants(group, allVariants);
|
||||
ImUtf8.HoverTooltip("Make this group overwrite all corresponding variants for this identifier, not just the one specified."u8);
|
||||
|
||||
ImGui.SameLine();
|
||||
var onlyAttributes = group.OnlyAttributes;
|
||||
if (ImUtf8.Checkbox("Only Attributes"u8, ref onlyAttributes))
|
||||
editor.ModManager.OptionEditor.ImcEditor.ChangeOnlyAttributes(group, onlyAttributes);
|
||||
ImUtf8.HoverTooltip("Only overwrite the attribute flags and take all the other values from the game's default entry instead of the one configured here.\n\nMainly useful if used with All Variants to keep the material IDs for each variant."u8);
|
||||
|
||||
using (ImUtf8.Group())
|
||||
{
|
||||
ImUtf8.TextFrameAligned("Material ID"u8);
|
||||
|
|
|
|||
|
|
@ -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,65 +441,33 @@ 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)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ModDataChangeType.Name:
|
||||
case ModDataChangeType.Author:
|
||||
case ModDataChangeType.ModTags:
|
||||
case ModDataChangeType.LocalTags:
|
||||
case ModDataChangeType.Favorite:
|
||||
SetFilterDirty();
|
||||
break;
|
||||
}
|
||||
const ModDataChangeType relevantFlags =
|
||||
ModDataChangeType.Name
|
||||
| ModDataChangeType.Author
|
||||
| ModDataChangeType.ModTags
|
||||
| ModDataChangeType.LocalTags
|
||||
| ModDataChangeType.Favorite
|
||||
| ModDataChangeType.ImportDate;
|
||||
if ((type & relevantFlags) != 0)
|
||||
SetFilterDirty();
|
||||
}
|
||||
|
||||
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 +490,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 +530,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 +560,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 +568,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 +704,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 +712,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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSy
|
|||
=> "Conflicts"u8;
|
||||
|
||||
public bool IsVisible
|
||||
=> collectionManager.Active.Current.Conflicts(selector.Selected!).Count > 0;
|
||||
=> collectionManager.Active.Current.Conflicts(selector.Selected!).Any(c => !GetPriority(c).IsHidden);
|
||||
|
||||
private readonly ConditionalWeakTable<IMod, object> _expandedMods = [];
|
||||
|
||||
|
|
@ -58,7 +58,8 @@ public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSy
|
|||
|
||||
// Can not be null because otherwise the tab bar is never drawn.
|
||||
var mod = selector.Selected!;
|
||||
foreach (var (conflict, index) in collectionManager.Active.Current.Conflicts(mod).OrderByDescending(GetPriority)
|
||||
foreach (var (conflict, index) in collectionManager.Active.Current.Conflicts(mod).Where(c => !c.Mod2.Priority.IsHidden)
|
||||
.OrderByDescending(GetPriority)
|
||||
.ThenBy(c => c.Mod2.Name.Lower).WithIndex())
|
||||
{
|
||||
using var id = ImRaii.PushId(index);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using OtterGui.Raii;
|
|||
using OtterGui.Widgets;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Text;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Mods.Editor;
|
||||
using Penumbra.Mods.Manager;
|
||||
|
|
@ -50,6 +51,8 @@ public class ModPanelEditTab(
|
|||
EditButtons();
|
||||
EditRegularMeta();
|
||||
UiHelpers.DefaultLineSpace();
|
||||
EditLocalData();
|
||||
UiHelpers.DefaultLineSpace();
|
||||
|
||||
if (Input.Text("Mod Path", Input.Path, Input.None, _leaf.FullName(), out var newPath, 256, UiHelpers.InputTextWidth.X))
|
||||
try
|
||||
|
|
@ -182,6 +185,40 @@ public class ModPanelEditTab(
|
|||
DrawOpenDefaultMod();
|
||||
}
|
||||
|
||||
private void EditLocalData()
|
||||
{
|
||||
DrawImportDate();
|
||||
DrawOpenLocalData();
|
||||
}
|
||||
|
||||
private void DrawImportDate()
|
||||
{
|
||||
ImUtf8.TextFramed($"{DateTimeOffset.FromUnixTimeMilliseconds(_mod.ImportDate).ToLocalTime():yyyy/MM/dd HH:mm}",
|
||||
ImGui.GetColorU32(ImGuiCol.FrameBg, 0.5f), new Vector2(UiHelpers.InputTextMinusButton3, 0));
|
||||
ImGui.SameLine(0, 3 * ImUtf8.GlobalScale);
|
||||
|
||||
var canRefresh = config.DeleteModModifier.IsActive();
|
||||
var tt = canRefresh
|
||||
? "Reset the import date to the current date and time."
|
||||
: $"Reset the import date to the current date and time.\nHold {config.DeleteModModifier} while clicking to refresh.";
|
||||
|
||||
if (ImUtf8.IconButton(FontAwesomeIcon.Sync, tt, disabled: !canRefresh))
|
||||
modManager.DataEditor.ResetModImportDate(_mod);
|
||||
ImUtf8.SameLineInner();
|
||||
ImUtf8.Text("Import Date"u8);
|
||||
}
|
||||
|
||||
private void DrawOpenLocalData()
|
||||
{
|
||||
var file = filenames.LocalDataFile(_mod);
|
||||
var fileExists = File.Exists(file);
|
||||
var tt = fileExists
|
||||
? "Open the local mod data file in the text editor of your choice."u8
|
||||
: "The local mod data file does not exist."u8;
|
||||
if (ImUtf8.ButtonEx("Open Local Data"u8, tt, UiHelpers.InputTextWidth, !fileExists))
|
||||
Process.Start(new ProcessStartInfo(file) { UseShellExecute = true });
|
||||
}
|
||||
|
||||
private void DrawOpenDefaultMod()
|
||||
{
|
||||
var file = filenames.OptionGroupFile(_mod, -1, false);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@ using ImGuiNET;
|
|||
using OtterGui.Raii;
|
||||
using OtterGui;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Text;
|
||||
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 +17,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 +41,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 +52,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 +67,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 +77,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 +92,19 @@ 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 (new ModPriority(priority).IsHidden)
|
||||
ImUtf8.HoverTooltip($"This priority is special-cased to hide this mod in conflict tabs ({ModPriority.HiddenMin}, {ModPriority.HiddenMax}).");
|
||||
|
||||
|
||||
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 +121,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.");
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using OtterGui;
|
|||
using OtterGui.Classes;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Table;
|
||||
using OtterGui.Text;
|
||||
using Penumbra.Enums;
|
||||
using Penumbra.Interop.Structs;
|
||||
using Penumbra.String;
|
||||
|
|
@ -52,36 +53,41 @@ internal sealed class ResourceWatcherTable : Table<Record>
|
|||
|
||||
private static unsafe void DrawByteString(CiByteString path, float length)
|
||||
{
|
||||
Vector2 vec;
|
||||
ImGuiNative.igCalcTextSize(&vec, path.Path, path.Path + path.Length, 0, 0);
|
||||
if (vec.X <= length)
|
||||
if (path.IsEmpty)
|
||||
return;
|
||||
|
||||
var size = ImUtf8.CalcTextSize(path.Span);
|
||||
var clicked = false;
|
||||
if (size.X <= length)
|
||||
{
|
||||
ImGuiNative.igTextUnformatted(path.Path, path.Path + path.Length);
|
||||
clicked = ImUtf8.Selectable(path.Span);
|
||||
}
|
||||
else
|
||||
{
|
||||
var fileName = path.LastIndexOf((byte)'/');
|
||||
CiByteString shortPath;
|
||||
if (fileName != -1)
|
||||
var fileName = path.LastIndexOf((byte)'/');
|
||||
using (ImRaii.Group())
|
||||
{
|
||||
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(2 * UiHelpers.Scale));
|
||||
using var font = ImRaii.PushFont(UiBuilder.IconFont);
|
||||
ImGui.TextUnformatted(FontAwesomeIcon.EllipsisH.ToIconString());
|
||||
ImGui.SameLine();
|
||||
shortPath = path.Substring(fileName, path.Length - fileName);
|
||||
}
|
||||
else
|
||||
{
|
||||
shortPath = path;
|
||||
CiByteString shortPath;
|
||||
if (fileName != -1)
|
||||
{
|
||||
using var font = ImRaii.PushFont(UiBuilder.IconFont);
|
||||
clicked = ImUtf8.Selectable(FontAwesomeIcon.EllipsisH.ToIconString());
|
||||
ImUtf8.SameLineInner();
|
||||
shortPath = path.Substring(fileName, path.Length - fileName);
|
||||
}
|
||||
else
|
||||
{
|
||||
shortPath = path;
|
||||
}
|
||||
|
||||
clicked |= ImUtf8.Selectable(shortPath.Span, false, ImGuiSelectableFlags.AllowItemOverlap);
|
||||
}
|
||||
|
||||
ImGuiNative.igTextUnformatted(shortPath.Path, shortPath.Path + shortPath.Length);
|
||||
if (ImGui.IsItemClicked())
|
||||
ImGuiNative.igSetClipboardText(path.Path);
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGuiNative.igSetTooltip(path.Path);
|
||||
ImUtf8.HoverTooltip(path.Span);
|
||||
}
|
||||
|
||||
if (clicked)
|
||||
ImUtf8.SetClipboardText(path.Span);
|
||||
}
|
||||
|
||||
private sealed class RecordTypeColumn : ColumnFlags<RecordType, Record>
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -820,13 +820,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)
|
||||
{
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue