mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-15 05:04:15 +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
|
- name: Archive
|
||||||
run: Compress-Archive -Path Penumbra/bin/Release/* -DestinationPath Penumbra.zip
|
run: Compress-Archive -Path Penumbra/bin/Release/* -DestinationPath Penumbra.zip
|
||||||
- name: Upload a Build Artifact
|
- name: Upload a Build Artifact
|
||||||
uses: actions/upload-artifact@v2.2.1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
./Penumbra/bin/Release/*
|
./Penumbra/bin/Release/*
|
||||||
|
|
|
||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -37,7 +37,7 @@ jobs:
|
||||||
- name: Archive
|
- name: Archive
|
||||||
run: Compress-Archive -Path Penumbra/bin/Release/* -DestinationPath Penumbra.zip
|
run: Compress-Archive -Path Penumbra/bin/Release/* -DestinationPath Penumbra.zip
|
||||||
- name: Upload a Build Artifact
|
- name: Upload a Build Artifact
|
||||||
uses: actions/upload-artifact@v2.2.1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
./Penumbra/bin/Release/*
|
./Penumbra/bin/Release/*
|
||||||
|
|
|
||||||
2
.github/workflows/test_release.yml
vendored
2
.github/workflows/test_release.yml
vendored
|
|
@ -37,7 +37,7 @@ jobs:
|
||||||
- name: Archive
|
- name: Archive
|
||||||
run: Compress-Archive -Path Penumbra/bin/Debug/* -DestinationPath Penumbra.zip
|
run: Compress-Archive -Path Penumbra/bin/Debug/* -DestinationPath Penumbra.zip
|
||||||
- name: Upload a Build Artifact
|
- name: Upload a Build Artifact
|
||||||
uses: actions/upload-artifact@v2.2.1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
./Penumbra/bin/Debug/*
|
./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 switch
|
||||||
{
|
{
|
||||||
TextureType.Png => textureManager.SavePng(inputFile, outputFile),
|
TextureType.Png => textureManager.SavePng(inputFile, outputFile),
|
||||||
|
TextureType.Targa => textureManager.SaveTga(inputFile, outputFile),
|
||||||
TextureType.AsIsTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, true, 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.AsIsDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, false, inputFile, outputFile),
|
||||||
TextureType.RgbaTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, true, 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 switch
|
||||||
{
|
{
|
||||||
TextureType.Png => textureManager.SavePng(new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
|
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.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.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),
|
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 Newtonsoft.Json.Linq;
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.Collections;
|
using Penumbra.Collections;
|
||||||
|
using Penumbra.Collections.Cache;
|
||||||
|
using Penumbra.GameData.Files.Utility;
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.GameData.Structs;
|
||||||
using Penumbra.Interop.PathResolving;
|
using Penumbra.Interop.PathResolving;
|
||||||
using Penumbra.Meta.Manipulations;
|
using Penumbra.Meta.Manipulations;
|
||||||
|
|
||||||
namespace Penumbra.Api.Api;
|
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()
|
public string GetPlayerMetaManipulations()
|
||||||
{
|
{
|
||||||
|
|
@ -24,7 +29,32 @@ public class MetaApi(CollectionResolver collectionResolver, ApiHelpers helpers)
|
||||||
return CompressMetaManipulations(collection);
|
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)
|
internal static string CompressMetaManipulations(ModCollection collection)
|
||||||
|
=> CompressMetaManipulationsV0(collection);
|
||||||
|
|
||||||
|
private static string CompressMetaManipulationsV0(ModCollection collection)
|
||||||
{
|
{
|
||||||
var array = new JArray();
|
var array = new JArray();
|
||||||
if (collection.MetaCache is { } cache)
|
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)));
|
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)))
|
!= Path.TrimEndingDirectorySeparator(Path.GetFullPath(dir.Parent.FullName)))
|
||||||
return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
|
return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
|
||||||
|
|
||||||
_modManager.AddMod(dir);
|
_modManager.AddMod(dir, true);
|
||||||
if (_config.MigrateImportedModelsToV6)
|
if (_config.MigrateImportedModelsToV6)
|
||||||
{
|
{
|
||||||
_migrationManager.MigrateMdlDirectory(dir.FullName, false);
|
_migrationManager.MigrateMdlDirectory(dir.FullName, false);
|
||||||
|
|
@ -91,7 +91,7 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
|
||||||
|
|
||||||
if (_config.UseFileSystemCompression)
|
if (_config.UseFileSystemCompression)
|
||||||
new FileCompactor(Penumbra.Log).StartMassCompact(dir.EnumerateFiles("*.*", SearchOption.AllDirectories),
|
new FileCompactor(Penumbra.Log).StartMassCompact(dir.EnumerateFiles("*.*", SearchOption.AllDirectories),
|
||||||
CompressionAlgorithm.Xpress8K);
|
CompressionAlgorithm.Xpress8K, false);
|
||||||
|
|
||||||
return ApiHelpers.Return(PenumbraApiEc.Success, args);
|
return ApiHelpers.Return(PenumbraApiEc.Success, args);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
using OtterGui;
|
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
using Penumbra.Collections.Manager;
|
using Penumbra.Collections.Manager;
|
||||||
using Penumbra.GameData.Actors;
|
using Penumbra.GameData.Actors;
|
||||||
using Penumbra.GameData.Interop;
|
using Penumbra.GameData.Interop;
|
||||||
using Penumbra.Meta.Manipulations;
|
|
||||||
using Penumbra.Mods.Settings;
|
using Penumbra.Mods.Settings;
|
||||||
using Penumbra.String.Classes;
|
using Penumbra.String.Classes;
|
||||||
|
|
||||||
|
|
@ -62,7 +60,7 @@ public class TemporaryApi(
|
||||||
if (!ConvertPaths(paths, out var p))
|
if (!ConvertPaths(paths, out var p))
|
||||||
return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args);
|
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);
|
return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args);
|
||||||
|
|
||||||
var ret = tempMods.Register(tag, null, p, m, new ModPriority(priority)) switch
|
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))
|
if (!ConvertPaths(paths, out var p))
|
||||||
return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args);
|
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);
|
return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args);
|
||||||
|
|
||||||
var ret = tempMods.Register(tag, collection, p, m, new ModPriority(priority)) switch
|
var ret = tempMods.Register(tag, collection, p, m, new ModPriority(priority)) switch
|
||||||
|
|
@ -153,24 +151,4 @@ public class TemporaryApi(
|
||||||
|
|
||||||
return true;
|
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.Enums;
|
||||||
using Penumbra.Api.IpcSubscribers;
|
using Penumbra.Api.IpcSubscribers;
|
||||||
using Penumbra.Collections.Manager;
|
using Penumbra.Collections.Manager;
|
||||||
using Penumbra.Meta.Manipulations;
|
|
||||||
using Penumbra.Mods;
|
using Penumbra.Mods;
|
||||||
using Penumbra.Mods.Manager;
|
using Penumbra.Mods.Manager;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
|
|
@ -28,6 +27,8 @@ public class TemporaryIpcTester(
|
||||||
{
|
{
|
||||||
public Guid LastCreatedCollectionId = Guid.Empty;
|
public Guid LastCreatedCollectionId = Guid.Empty;
|
||||||
|
|
||||||
|
private readonly bool _debug = Assembly.GetAssembly(typeof(TemporaryIpcTester))?.GetName().Version?.Major >= 9;
|
||||||
|
|
||||||
private Guid? _tempGuid;
|
private Guid? _tempGuid;
|
||||||
private string _tempCollectionName = string.Empty;
|
private string _tempCollectionName = string.Empty;
|
||||||
private string _tempCollectionGuidName = string.Empty;
|
private string _tempCollectionGuidName = string.Empty;
|
||||||
|
|
@ -48,9 +49,9 @@ public class TemporaryIpcTester(
|
||||||
ImGui.InputTextWithHint("##tempCollection", "Collection Name...", ref _tempCollectionName, 128);
|
ImGui.InputTextWithHint("##tempCollection", "Collection Name...", ref _tempCollectionName, 128);
|
||||||
ImGuiUtil.GuidInput("##guid", "Collection GUID...", string.Empty, ref _tempGuid, ref _tempCollectionGuidName);
|
ImGuiUtil.GuidInput("##guid", "Collection GUID...", string.Empty, ref _tempGuid, ref _tempCollectionGuidName);
|
||||||
ImGui.InputInt("##tempActorIndex", ref _tempActorIndex, 0, 0);
|
ImGui.InputInt("##tempActorIndex", ref _tempActorIndex, 0, 0);
|
||||||
ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32);
|
ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32);
|
||||||
ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256);
|
ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256);
|
||||||
ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256);
|
ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256);
|
||||||
ImUtf8.InputText("##tempManip"u8, ref _tempManipulation, "Manipulation Base64 String..."u8);
|
ImUtf8.InputText("##tempManip"u8, ref _tempManipulation, "Manipulation Base64 String..."u8);
|
||||||
ImGui.Checkbox("Force Character Collection Overwrite", ref _forceOverwrite);
|
ImGui.Checkbox("Force Character Collection Overwrite", ref _forceOverwrite);
|
||||||
|
|
||||||
|
|
@ -102,7 +103,7 @@ public class TemporaryIpcTester(
|
||||||
!collections.Storage.ByName(_tempModName, out var copyCollection))
|
!collections.Storage.ByName(_tempModName, out var copyCollection))
|
||||||
&& copyCollection is { HasCache: true })
|
&& 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);
|
var manips = MetaApi.CompressMetaManipulations(copyCollection);
|
||||||
_lastTempError = new AddTemporaryMod(pi).Invoke(_tempModName, guid, files, manips, 999);
|
_lastTempError = new AddTemporaryMod(pi).Invoke(_tempModName, guid, files, manips, 999);
|
||||||
}
|
}
|
||||||
|
|
@ -124,11 +125,11 @@ public class TemporaryIpcTester(
|
||||||
|
|
||||||
public void DrawCollections()
|
public void DrawCollections()
|
||||||
{
|
{
|
||||||
using var collTree = ImRaii.TreeNode("Temporary Collections##TempCollections");
|
using var collTree = ImUtf8.TreeNode("Temporary Collections##TempCollections"u8);
|
||||||
if (!collTree)
|
if (!collTree)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
using var table = ImRaii.Table("##collTree", 6, ImGuiTableFlags.SizingFixedFit);
|
using var table = ImUtf8.Table("##collTree"u8, 6, ImGuiTableFlags.SizingFixedFit);
|
||||||
if (!table)
|
if (!table)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
@ -139,7 +140,7 @@ public class TemporaryIpcTester(
|
||||||
var character = tempCollections.Collections.Where(p => p.Collection == collection).Select(p => p.DisplayName)
|
var character = tempCollections.Collections.Where(p => p.Collection == collection).Select(p => p.DisplayName)
|
||||||
.FirstOrDefault()
|
.FirstOrDefault()
|
||||||
?? "Unknown";
|
?? "Unknown";
|
||||||
if (ImGui.Button("Save##Collection"))
|
if (_debug && ImUtf8.Button("Save##Collection"u8))
|
||||||
TemporaryMod.SaveTempCollection(config, saveService, modManager, collection, character);
|
TemporaryMod.SaveTempCollection(config, saveService, modManager, collection, character);
|
||||||
|
|
||||||
using (ImRaii.PushFont(UiBuilder.MonoFont))
|
using (ImRaii.PushFont(UiBuilder.MonoFont))
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using OtterGui.Classes;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.GameData.Structs;
|
||||||
using Penumbra.Meta.Manipulations;
|
using Penumbra.Meta.Manipulations;
|
||||||
|
|
@ -5,7 +6,7 @@ using Penumbra.Mods.Editor;
|
||||||
|
|
||||||
namespace Penumbra.Collections.Cache;
|
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> _doNotHideEarrings = [];
|
||||||
private readonly HashSet<PrimaryId> _doNotHideNecklace = [];
|
private readonly HashSet<PrimaryId> _doNotHideNecklace = [];
|
||||||
|
|
@ -39,7 +40,7 @@ public class GlobalEqpCache : Dictionary<GlobalEqpManipulation, IMod>, IService
|
||||||
original |= EqpEntry.HeadShowHrothgarHat;
|
original |= EqpEntry.HeadShowHrothgarHat;
|
||||||
|
|
||||||
if (_doNotHideEarrings.Contains(armor[5].Set))
|
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))
|
if (_doNotHideNecklace.Contains(armor[6].Set))
|
||||||
original |= EqpEntry.BodyShowNecklace | EqpEntry.HeadShowNecklace;
|
original |= EqpEntry.BodyShowNecklace | EqpEntry.HeadShowNecklace;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ using Penumbra.GameData.Structs;
|
||||||
using Penumbra.Meta;
|
using Penumbra.Meta;
|
||||||
using Penumbra.Meta.Manipulations;
|
using Penumbra.Meta.Manipulations;
|
||||||
using Penumbra.Mods.Editor;
|
using Penumbra.Mods.Editor;
|
||||||
using Penumbra.String.Classes;
|
|
||||||
|
|
||||||
namespace Penumbra.Collections.Cache;
|
namespace Penumbra.Collections.Cache;
|
||||||
|
|
||||||
|
|
@ -16,6 +15,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
||||||
public readonly RspCache Rsp = new(manager, collection);
|
public readonly RspCache Rsp = new(manager, collection);
|
||||||
public readonly ImcCache Imc = new(manager, collection);
|
public readonly ImcCache Imc = new(manager, collection);
|
||||||
public readonly GlobalEqpCache GlobalEqp = new();
|
public readonly GlobalEqpCache GlobalEqp = new();
|
||||||
|
public bool IsDisposed { get; private set; }
|
||||||
|
|
||||||
public int Count
|
public int Count
|
||||||
=> Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + GlobalEqp.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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
if (IsDisposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
IsDisposed = true;
|
||||||
Eqp.Dispose();
|
Eqp.Dispose();
|
||||||
Eqdp.Dispose();
|
Eqdp.Dispose();
|
||||||
Est.Dispose();
|
Est.Dispose();
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using OtterGui.Classes;
|
||||||
using Penumbra.Meta;
|
using Penumbra.Meta;
|
||||||
using Penumbra.Meta.Manipulations;
|
using Penumbra.Meta.Manipulations;
|
||||||
using Penumbra.Mods.Editor;
|
using Penumbra.Mods.Editor;
|
||||||
|
|
@ -5,27 +6,19 @@ using Penumbra.Mods.Editor;
|
||||||
namespace Penumbra.Collections.Cache;
|
namespace Penumbra.Collections.Cache;
|
||||||
|
|
||||||
public abstract class MetaCacheBase<TIdentifier, TEntry>(MetaFileManager manager, ModCollection collection)
|
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 TIdentifier : unmanaged, IMetaIdentifier
|
||||||
where TEntry : unmanaged
|
where TEntry : unmanaged
|
||||||
{
|
{
|
||||||
protected readonly MetaFileManager Manager = manager;
|
protected readonly MetaFileManager Manager = manager;
|
||||||
protected readonly ModCollection Collection = collection;
|
protected readonly ModCollection Collection = collection;
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Dispose(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ApplyMod(IMod source, TIdentifier identifier, TEntry entry)
|
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);
|
ApplyModInternal(identifier, entry);
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -33,17 +26,14 @@ public abstract class MetaCacheBase<TIdentifier, TEntry>(MetaFileManager manager
|
||||||
|
|
||||||
public bool RevertMod(TIdentifier identifier, [NotNullWhen(true)] out IMod? mod)
|
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 = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
mod = pair.Source;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod = pair.Source;
|
||||||
|
|
||||||
RevertModInternal(identifier);
|
RevertModInternal(identifier);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -54,7 +44,4 @@ public abstract class MetaCacheBase<TIdentifier, TEntry>(MetaFileManager manager
|
||||||
|
|
||||||
protected virtual void RevertModInternal(TIdentifier identifier)
|
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"/>
|
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnCollectionChange"/>
|
||||||
ModFileSystemSelector = 0,
|
ModFileSystemSelector = 0,
|
||||||
|
|
||||||
|
/// <seealso cref="Mods.ModSelection.OnCollectionChange"/>
|
||||||
|
ModSelection = 10,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,5 +23,8 @@ public sealed class CollectionInheritanceChanged()
|
||||||
|
|
||||||
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnInheritanceChange"/>
|
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnInheritanceChange"/>
|
||||||
ModFileSystemSelector = 0,
|
ModFileSystemSelector = 0,
|
||||||
|
|
||||||
|
/// <seealso cref="Mods.ModSelection.OnInheritanceChange"/>
|
||||||
|
ModSelection = 10,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using OtterGui.Classes;
|
using OtterGui.Classes;
|
||||||
using Penumbra.Api;
|
|
||||||
using Penumbra.Api.Api;
|
using Penumbra.Api.Api;
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
using Penumbra.Collections;
|
using Penumbra.Collections;
|
||||||
|
|
@ -35,5 +34,8 @@ public sealed class ModSettingChanged()
|
||||||
|
|
||||||
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnSettingChange"/>
|
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnSettingChange"/>
|
||||||
ModFileSystemSelector = 0,
|
ModFileSystemSelector = 0,
|
||||||
|
|
||||||
|
/// <seealso cref="Mods.ModSelection.OnSettingChange"/>
|
||||||
|
ModSelection = 10,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,12 +36,14 @@ public class MaterialExporter
|
||||||
return material.Mtrl.ShaderPackage.Name switch
|
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.
|
// 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),
|
"character.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.MASK, 0.5f),
|
||||||
"characterglass.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.BLEND),
|
"characterlegacy.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.MASK, 0.5f),
|
||||||
"hair.shpk" => BuildHair(material, name),
|
"characterglass.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.BLEND),
|
||||||
"iris.shpk" => BuildIris(material, name),
|
"charactertattoo.shpk" => BuildCharacterTattoo(material, name),
|
||||||
"skin.shpk" => BuildSkin(material, name),
|
"hair.shpk" => BuildHair(material, name),
|
||||||
_ => BuildFallback(material, name, notifier),
|
"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)
|
private static MaterialBuilder BuildCharacter(Material material, string name)
|
||||||
{
|
{
|
||||||
// Build the textures from the color table.
|
// 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);
|
// Merge in opacity from the normal.
|
||||||
ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds, in operation);
|
var baseColor = indexOperation.BaseColor;
|
||||||
|
MultiplyOperation.Execute(baseColor, normalOperation.BaseColorOpacity);
|
||||||
|
|
||||||
// Check if full textures are provided, and merge in if available.
|
// Check if a full diffuse is provided, and merge in if available.
|
||||||
var baseColor = operation.BaseColor;
|
|
||||||
if (material.Textures.TryGetValue(TextureUsage.SamplerDiffuse, out var diffuse))
|
if (material.Textures.TryGetValue(TextureUsage.SamplerDiffuse, out var diffuse))
|
||||||
{
|
{
|
||||||
MultiplyOperation.Execute(diffuse, operation.BaseColor);
|
MultiplyOperation.Execute(diffuse, indexOperation.BaseColor);
|
||||||
baseColor = diffuse;
|
baseColor = diffuse;
|
||||||
}
|
}
|
||||||
|
|
||||||
Image specular = operation.Specular;
|
var specular = indexOperation.Specular;
|
||||||
if (material.Textures.TryGetValue(TextureUsage.SamplerSpecular, out var specularTexture))
|
if (material.Textures.TryGetValue(TextureUsage.SamplerSpecular, out var specularTexture))
|
||||||
{
|
{
|
||||||
MultiplyOperation.Execute(specularTexture, operation.Specular);
|
MultiplyOperation.Execute(specularTexture, indexOperation.Specular);
|
||||||
specular = specularTexture;
|
specular = specularTexture;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pull further information from the mask.
|
// Pull further information from the mask.
|
||||||
if (material.Textures.TryGetValue(TextureUsage.SamplerMask, out var maskTexture))
|
if (material.Textures.TryGetValue(TextureUsage.SamplerMask, out var maskTexture))
|
||||||
{
|
{
|
||||||
// Extract the red channel for "ambient occlusion".
|
var maskOperation = new ProcessCharacterMaskOperation(maskTexture);
|
||||||
maskTexture.Mutate(context => context.Resize(baseColor.Width, baseColor.Height));
|
ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, maskTexture.Bounds, in maskOperation);
|
||||||
maskTexture.ProcessPixelRows(baseColor, (maskAccessor, baseColorAccessor) =>
|
|
||||||
{
|
|
||||||
for (var y = 0; y < maskAccessor.Height; y++)
|
|
||||||
{
|
|
||||||
var maskSpan = maskAccessor.GetRowSpan(y);
|
|
||||||
var baseColorSpan = baseColorAccessor.GetRowSpan(y);
|
|
||||||
|
|
||||||
for (var x = 0; x < maskSpan.Length; x++)
|
// TODO: consider using the occusion gltf material property.
|
||||||
baseColorSpan[x].FromVector4(baseColorSpan[x].ToVector4() * new Vector4(maskSpan[x].R / 255f));
|
MultiplyOperation.Execute(baseColor, maskOperation.Occlusion);
|
||||||
}
|
|
||||||
});
|
// Similar to base color's alpha, this is a pretty wasteful operation for a single channel.
|
||||||
// TODO: handle other textures stored in the mask?
|
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.
|
// 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");
|
var specularImage = BuildImage(specular, name, "specular");
|
||||||
|
|
||||||
return BuildSharedBase(material, name)
|
return BuildSharedBase(material, name)
|
||||||
.WithBaseColor(BuildImage(baseColor, name, "basecolor"))
|
.WithBaseColor(BuildImage(baseColor, name, "basecolor"))
|
||||||
.WithNormal(BuildImage(operation.Normal, name, "normal"))
|
.WithNormal(BuildImage(normalOperation.Normal, name, "normal"))
|
||||||
.WithEmissive(BuildImage(operation.Emissive, name, "emissive"), Vector3.One, 1)
|
.WithEmissive(BuildImage(indexOperation.Emissive, name, "emissive"), Vector3.One, 1)
|
||||||
.WithSpecularFactor(specularImage, 1)
|
.WithSpecularFactor(specularImage, 1)
|
||||||
.WithSpecularColor(specularImage);
|
.WithSpecularColor(specularImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: It feels a little silly to request the entire normal here when extracting the normal only needs some of the components.
|
private readonly struct ProcessCharacterIndexOperation(Image<Rgba32> index, ColorTable table) : IRowOperation
|
||||||
// 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
|
|
||||||
{
|
{
|
||||||
public Image<Rgba32> Normal { get; } = normal.Clone();
|
public Image<Rgba32> BaseColor { get; } = new(index.Width, index.Height);
|
||||||
public Image<Rgba32> BaseColor { get; } = new(normal.Width, normal.Height);
|
public Image<Rgba32> Specular { get; } = new(index.Width, index.Height);
|
||||||
public Image<Rgba32> Specular { get; } = new(normal.Width, normal.Height);
|
public Image<Rgb24> Emissive { get; } = new(index.Width, index.Height);
|
||||||
public Image<Rgb24> Emissive { get; } = new(normal.Width, normal.Height);
|
|
||||||
|
|
||||||
private Buffer2D<Rgba32> NormalBuffer
|
private Buffer2D<Rgba32> IndexBuffer
|
||||||
=> Normal.Frames.RootFrame.PixelBuffer;
|
=> index.Frames.RootFrame.PixelBuffer;
|
||||||
|
|
||||||
private Buffer2D<Rgba32> BaseColorBuffer
|
private Buffer2D<Rgba32> BaseColorBuffer
|
||||||
=> BaseColor.Frames.RootFrame.PixelBuffer;
|
=> BaseColor.Frames.RootFrame.PixelBuffer;
|
||||||
|
|
@ -125,66 +122,96 @@ public class MaterialExporter
|
||||||
|
|
||||||
public void Invoke(int y)
|
public void Invoke(int y)
|
||||||
{
|
{
|
||||||
var normalSpan = NormalBuffer.DangerousGetRowSpan(y);
|
var indexSpan = IndexBuffer.DangerousGetRowSpan(y);
|
||||||
var baseColorSpan = BaseColorBuffer.DangerousGetRowSpan(y);
|
var baseColorSpan = BaseColorBuffer.DangerousGetRowSpan(y);
|
||||||
var specularSpan = SpecularBuffer.DangerousGetRowSpan(y);
|
var specularSpan = SpecularBuffer.DangerousGetRowSpan(y);
|
||||||
var emissiveSpan = EmissiveBuffer.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++)
|
for (var x = 0; x < normalSpan.Length; x++)
|
||||||
{
|
{
|
||||||
ref var normalPixel = ref normalSpan[x];
|
ref var normalPixel = ref normalSpan[x];
|
||||||
|
|
||||||
// Table row data (.a)
|
baseColorOpacitySpan[x].FromVector4(Vector4.One);
|
||||||
var tableRow = GetTableRowIndices(normalPixel.A / 255f);
|
baseColorOpacitySpan[x].A = normalPixel.B;
|
||||||
var prevRow = table[tableRow.Previous];
|
|
||||||
var nextRow = table[tableRow.Next];
|
|
||||||
|
|
||||||
// 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.B = byte.MaxValue;
|
||||||
normalPixel.A = 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.
|
public Image<Rgba32> Occlusion { get; } = new(mask.Width, mask.Height);
|
||||||
var smoothed = MathF.Floor(input * 7.5f % 1.0f * 2)
|
public Image<Rgba32> SpecularFactor { get; } = new(mask.Width, mask.Height);
|
||||||
* (-input * 15 + MathF.Floor(input * 15 + 0.5f))
|
|
||||||
+ input * 15;
|
|
||||||
|
|
||||||
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,
|
var maskSpan = MaskBuffer.DangerousGetRowSpan(y);
|
||||||
Previous = (int)MathF.Floor(smoothed),
|
var occlusionSpan = OcclusionBuffer.DangerousGetRowSpan(y);
|
||||||
Next = (int)MathF.Ceiling(smoothed),
|
var specularFactorSpan = SpecularFactorBuffer.DangerousGetRowSpan(y);
|
||||||
Weight = smoothed % 1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private ref struct TableRow
|
for (var x = 0; x < maskSpan.Length; x++)
|
||||||
{
|
{
|
||||||
public int Stepped;
|
ref var maskPixel = ref maskSpan[x];
|
||||||
public int Previous;
|
|
||||||
public int Next;
|
occlusionSpan[x].FromL8(new L8(maskPixel.B));
|
||||||
public float Weight;
|
|
||||||
|
specularFactorSpan[x].FromVector4(Vector4.One);
|
||||||
|
specularFactorSpan[x].A = maskPixel.R;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly struct MultiplyOperation
|
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.
|
// 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 DefaultHairColor = new Vector4(130, 64, 13, 255) / new Vector4(255);
|
||||||
private static readonly Vector4 DefaultHighlightColor = new Vector4(77, 126, 240, 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++)
|
for (var x = 0; x < normalSpan.Length; x++)
|
||||||
{
|
{
|
||||||
var color = Vector4.Lerp(DefaultHairColor, DefaultHighlightColor, maskSpan[x].A / 255f);
|
var color = Vector4.Lerp(DefaultHairColor, DefaultHighlightColor, normalSpan[x].B / 255f);
|
||||||
baseColorSpan[x].FromVector4(color * new Vector4(maskSpan[x].R / 255f));
|
baseColorSpan[x].FromVector4(color * new Vector4(maskSpan[x].A / 255f));
|
||||||
baseColorSpan[x].A = normalSpan[x].A;
|
baseColorSpan[x].A = normalSpan[x].A;
|
||||||
|
|
||||||
|
normalSpan[x].B = byte.MaxValue;
|
||||||
normalSpan[x].A = 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.
|
// 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)
|
private static MaterialBuilder BuildIris(Material material, string name)
|
||||||
{
|
{
|
||||||
var normal = material.Textures[TextureUsage.SamplerNormal];
|
var normal = material.Textures[TextureUsage.SamplerNormal];
|
||||||
var mask = material.Textures[TextureUsage.SamplerMask];
|
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);
|
baseColor.ProcessPixelRows(mask, (baseColorAccessor, maskAccessor) =>
|
||||||
normal.ProcessPixelRows(mask, baseColor, (normalAccessor, maskAccessor, baseColorAccessor) =>
|
|
||||||
{
|
{
|
||||||
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 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));
|
var eyeColor = Vector4.Lerp(Vector4.One, DefaultEyeColor, maskSpan[x].B / 255f);
|
||||||
baseColorSpan[x].A = normalSpan[x].A;
|
baseColorSpan[x].FromVector4(baseColorSpan[x].ToVector4() * eyeColor);
|
||||||
|
|
||||||
normalSpan[x].A = byte.MaxValue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -314,21 +370,7 @@ public class MaterialExporter
|
||||||
var diffuse = material.Textures[TextureUsage.SamplerDiffuse];
|
var diffuse = material.Textures[TextureUsage.SamplerDiffuse];
|
||||||
var normal = material.Textures[TextureUsage.SamplerNormal];
|
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.
|
// The normal also stores the skin color influence (.b) and wetness mask (.a) - remove.
|
||||||
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.
|
|
||||||
normal.ProcessPixelRows(normalAccessor =>
|
normal.ProcessPixelRows(normalAccessor =>
|
||||||
{
|
{
|
||||||
for (var y = 0; y < normalAccessor.Height; y++)
|
for (var y = 0; y < normalAccessor.Height; y++)
|
||||||
|
|
@ -336,7 +378,10 @@ public class MaterialExporter
|
||||||
var normalSpan = normalAccessor.GetRowSpan(y);
|
var normalSpan = normalAccessor.GetRowSpan(y);
|
||||||
|
|
||||||
for (var x = 0; x < normalSpan.Length; x++)
|
for (var x = 0; x < normalSpan.Length; x++)
|
||||||
|
{
|
||||||
normalSpan[x].B = byte.MaxValue;
|
normalSpan[x].B = byte.MaxValue;
|
||||||
|
normalSpan[x].A = byte.MaxValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
using Lumina.Extensions;
|
using Lumina.Extensions;
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
using Penumbra.GameData.Files;
|
using Penumbra.GameData.Files;
|
||||||
|
|
@ -23,11 +25,11 @@ public class MeshExporter
|
||||||
? scene.AddSkinnedMesh(data.Mesh, Matrix4x4.Identity, [.. skeleton.Value.Joints])
|
? scene.AddSkinnedMesh(data.Mesh, Matrix4x4.Identity, [.. skeleton.Value.Joints])
|
||||||
: scene.AddRigidMesh(data.Mesh, Matrix4x4.Identity);
|
: 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)
|
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`
|
// Named morph targets aren't part of the specification, however `MESH.extras.targetNames`
|
||||||
// is a commonly-accepted means of providing the data.
|
// is a commonly-accepted means of providing the data.
|
||||||
meshBuilder.Extras = JsonContent.CreateFrom(new Dictionary<string, object>()
|
meshBuilder.Extras = new JsonObject { ["targetNames"] = JsonSerializer.SerializeToNode(shapeNames) };
|
||||||
{
|
|
||||||
{ "targetNames", shapeNames },
|
|
||||||
});
|
|
||||||
|
|
||||||
string[] attributes = [];
|
string[] attributes = [];
|
||||||
var maxAttribute = 31 - BitOperations.LeadingZeroCount(attributeMask);
|
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.Single3 => new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()),
|
||||||
MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), 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.UByte4 => reader.ReadBytes(4),
|
||||||
MdlFile.VertexType.NByte4 => new Vector4(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),
|
||||||
reader.ReadByte() / 255f),
|
|
||||||
MdlFile.VertexType.Half2 => new Vector2((float)reader.ReadHalf(), (float)reader.ReadHalf()),
|
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(),
|
MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (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}"),
|
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)
|
private static Type GetSkinningType(IReadOnlyDictionary<MdlFile.VertexUsage, MdlFile.VertexType> usages)
|
||||||
{
|
{
|
||||||
if (usages.ContainsKey(MdlFile.VertexUsage.BlendWeights) && usages.ContainsKey(MdlFile.VertexUsage.BlendIndices))
|
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);
|
return typeof(VertexEmpty);
|
||||||
}
|
}
|
||||||
|
|
@ -456,15 +462,17 @@ public class MeshExporter
|
||||||
if (_skinningType == typeof(VertexEmpty))
|
if (_skinningType == typeof(VertexEmpty))
|
||||||
return new VertexEmpty();
|
return new VertexEmpty();
|
||||||
|
|
||||||
if (_skinningType == typeof(VertexJoints4))
|
if (_skinningType == typeof(VertexJoints4) || _skinningType == typeof(VertexJoints8))
|
||||||
{
|
{
|
||||||
if (_boneIndexMap == null)
|
if (_boneIndexMap == null)
|
||||||
throw _notifier.Exception("Tried to build skinned vertex but no bone mappings are available.");
|
throw _notifier.Exception("Tried to build skinned vertex but no bone mappings are available.");
|
||||||
|
|
||||||
var indices = ToByteArray(attributes[MdlFile.VertexUsage.BlendIndices]);
|
var indiciesData = attributes[MdlFile.VertexUsage.BlendIndices];
|
||||||
var weights = ToVector4(attributes[MdlFile.VertexUsage.BlendWeights]);
|
var weightsData = attributes[MdlFile.VertexUsage.BlendWeights];
|
||||||
|
var indices = ToByteArray(indiciesData);
|
||||||
var bindings = Enumerable.Range(0, 4)
|
var weights = ToFloatArray(weightsData);
|
||||||
|
|
||||||
|
var bindings = Enumerable.Range(0, indices.Length)
|
||||||
.Select(bindingIndex =>
|
.Select(bindingIndex =>
|
||||||
{
|
{
|
||||||
// NOTE: I've not seen any files that throw this error that aren't completely broken.
|
// 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]);
|
return (jointIndex, weights[bindingIndex]);
|
||||||
})
|
})
|
||||||
.ToArray();
|
.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}");
|
throw _notifier.Exception($"Unknown skinning type {_skinningType}");
|
||||||
|
|
@ -518,4 +532,13 @@ public class MeshExporter
|
||||||
byte[] value => value,
|
byte[] value => value,
|
||||||
_ => throw new ArgumentOutOfRangeException($"Invalid byte[] input {data}"),
|
_ => 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.Geometry.VertexTypes;
|
||||||
|
using SharpGLTF.Memory;
|
||||||
using SharpGLTF.Schema2;
|
using SharpGLTF.Schema2;
|
||||||
|
|
||||||
namespace Penumbra.Import.Models.Export;
|
namespace Penumbra.Import.Models.Export;
|
||||||
|
|
@ -11,35 +13,40 @@ and there's reason to overhaul the export pipeline.
|
||||||
|
|
||||||
public struct VertexColorFfxiv : IVertexCustom
|
public struct VertexColorFfxiv : IVertexCustom
|
||||||
{
|
{
|
||||||
// NOTE: We only realistically require UNSIGNED_BYTE for this, however Blender 3.6 errors on that (fixed in 4.0).
|
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
|
||||||
[VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, true)]
|
{
|
||||||
|
// 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 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"];
|
private static readonly string[] CustomNames = ["_FFXIV_COLOR"];
|
||||||
public IEnumerable<string> CustomAttributes => CustomNames;
|
|
||||||
|
public IEnumerable<string> CustomAttributes
|
||||||
|
=> CustomNames;
|
||||||
|
|
||||||
public VertexColorFfxiv(Vector4 ffxivColor)
|
public VertexColorFfxiv(Vector4 ffxivColor)
|
||||||
{
|
=> FfxivColor = ffxivColor;
|
||||||
FfxivColor = ffxivColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add(in VertexMaterialDelta delta)
|
public void Add(in VertexMaterialDelta delta)
|
||||||
{
|
{ }
|
||||||
}
|
|
||||||
|
|
||||||
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
|
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)
|
public Vector2 GetTexCoord(int index)
|
||||||
=> throw new ArgumentOutOfRangeException(nameof(index));
|
=> throw new ArgumentOutOfRangeException(nameof(index));
|
||||||
|
|
||||||
public void SetTexCoord(int setIndex, Vector2 coord)
|
public void SetTexCoord(int setIndex, Vector2 coord)
|
||||||
{
|
{ }
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryGetCustomAttribute(string attributeName, out object? value)
|
public bool TryGetCustomAttribute(string attributeName, out object? value)
|
||||||
{
|
{
|
||||||
|
|
@ -65,12 +72,17 @@ public struct VertexColorFfxiv : IVertexCustom
|
||||||
=> throw new ArgumentOutOfRangeException(nameof(index));
|
=> throw new ArgumentOutOfRangeException(nameof(index));
|
||||||
|
|
||||||
public void SetColor(int setIndex, Vector4 color)
|
public void SetColor(int setIndex, Vector4 color)
|
||||||
{
|
{ }
|
||||||
}
|
|
||||||
|
|
||||||
public void Validate()
|
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))
|
if (components.Any(component => component < 0 || component > 1))
|
||||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor));
|
throw new ArgumentOutOfRangeException(nameof(FfxivColor));
|
||||||
}
|
}
|
||||||
|
|
@ -78,22 +90,32 @@ public struct VertexColorFfxiv : IVertexCustom
|
||||||
|
|
||||||
public struct VertexTexture1ColorFfxiv : 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;
|
public Vector2 TexCoord0;
|
||||||
|
|
||||||
[VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, true)]
|
|
||||||
public Vector4 FfxivColor;
|
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"];
|
private static readonly string[] CustomNames = ["_FFXIV_COLOR"];
|
||||||
public IEnumerable<string> CustomAttributes => CustomNames;
|
|
||||||
|
public IEnumerable<string> CustomAttributes
|
||||||
|
=> CustomNames;
|
||||||
|
|
||||||
public VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor)
|
public VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor)
|
||||||
{
|
{
|
||||||
TexCoord0 = texCoord0;
|
TexCoord0 = texCoord0;
|
||||||
FfxivColor = ffxivColor;
|
FfxivColor = ffxivColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,9 +125,7 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom
|
||||||
}
|
}
|
||||||
|
|
||||||
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
|
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
|
||||||
{
|
=> new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), Vector2.Zero);
|
||||||
return new VertexMaterialDelta(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), Vector2.Zero);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vector2 GetTexCoord(int index)
|
public Vector2 GetTexCoord(int index)
|
||||||
=> index switch
|
=> index switch
|
||||||
|
|
@ -116,8 +136,10 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom
|
||||||
|
|
||||||
public void SetTexCoord(int setIndex, Vector2 coord)
|
public void SetTexCoord(int setIndex, Vector2 coord)
|
||||||
{
|
{
|
||||||
if (setIndex == 0) TexCoord0 = coord;
|
if (setIndex == 0)
|
||||||
if (setIndex >= 1) throw new ArgumentOutOfRangeException(nameof(setIndex));
|
TexCoord0 = coord;
|
||||||
|
if (setIndex >= 1)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(setIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryGetCustomAttribute(string attributeName, out object? value)
|
public bool TryGetCustomAttribute(string attributeName, out object? value)
|
||||||
|
|
@ -144,12 +166,17 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom
|
||||||
=> throw new ArgumentOutOfRangeException(nameof(index));
|
=> throw new ArgumentOutOfRangeException(nameof(index));
|
||||||
|
|
||||||
public void SetColor(int setIndex, Vector4 color)
|
public void SetColor(int setIndex, Vector4 color)
|
||||||
{
|
{ }
|
||||||
}
|
|
||||||
|
|
||||||
public void Validate()
|
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))
|
if (components.Any(component => component < 0 || component > 1))
|
||||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor));
|
throw new ArgumentOutOfRangeException(nameof(FfxivColor));
|
||||||
}
|
}
|
||||||
|
|
@ -157,26 +184,35 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom
|
||||||
|
|
||||||
public struct VertexTexture2ColorFfxiv : 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;
|
public Vector2 TexCoord0;
|
||||||
|
|
||||||
[VertexAttribute("TEXCOORD_1")]
|
|
||||||
public Vector2 TexCoord1;
|
public Vector2 TexCoord1;
|
||||||
|
|
||||||
[VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, true)]
|
|
||||||
public Vector4 FfxivColor;
|
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"];
|
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)
|
public VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor)
|
||||||
{
|
{
|
||||||
TexCoord0 = texCoord0;
|
TexCoord0 = texCoord0;
|
||||||
TexCoord1 = texCoord1;
|
TexCoord1 = texCoord1;
|
||||||
FfxivColor = ffxivColor;
|
FfxivColor = ffxivColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -187,9 +223,7 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom
|
||||||
}
|
}
|
||||||
|
|
||||||
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
|
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
|
||||||
{
|
=> new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1));
|
||||||
return new VertexMaterialDelta(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vector2 GetTexCoord(int index)
|
public Vector2 GetTexCoord(int index)
|
||||||
=> index switch
|
=> index switch
|
||||||
|
|
@ -201,9 +235,12 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom
|
||||||
|
|
||||||
public void SetTexCoord(int setIndex, Vector2 coord)
|
public void SetTexCoord(int setIndex, Vector2 coord)
|
||||||
{
|
{
|
||||||
if (setIndex == 0) TexCoord0 = coord;
|
if (setIndex == 0)
|
||||||
if (setIndex == 1) TexCoord1 = coord;
|
TexCoord0 = coord;
|
||||||
if (setIndex >= 2) throw new ArgumentOutOfRangeException(nameof(setIndex));
|
if (setIndex == 1)
|
||||||
|
TexCoord1 = coord;
|
||||||
|
if (setIndex >= 2)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(setIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryGetCustomAttribute(string attributeName, out object? value)
|
public bool TryGetCustomAttribute(string attributeName, out object? value)
|
||||||
|
|
@ -230,12 +267,17 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom
|
||||||
=> throw new ArgumentOutOfRangeException(nameof(index));
|
=> throw new ArgumentOutOfRangeException(nameof(index));
|
||||||
|
|
||||||
public void SetColor(int setIndex, Vector4 color)
|
public void SetColor(int setIndex, Vector4 color)
|
||||||
{
|
{ }
|
||||||
}
|
|
||||||
|
|
||||||
public void Validate()
|
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))
|
if (components.Any(component => component < 0 || component > 1))
|
||||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor));
|
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())
|
foreach (var (primitive, primitiveIndex) in node.Mesh.Primitives.WithIndex())
|
||||||
{
|
{
|
||||||
// Per glTF specification, an asset with a skin MUST contain skinning attributes on its meshes.
|
// Per glTF specification, an asset with a skin MUST contain skinning attributes on its meshes.
|
||||||
var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0")?.AsVector4Array();
|
var joints0Accessor = primitive.GetVertexAccessor("JOINTS_0")?.AsVector4Array();
|
||||||
var weightsAccessor = primitive.GetVertexAccessor("WEIGHTS_0")?.AsVector4Array();
|
var weights0Accessor = primitive.GetVertexAccessor("WEIGHTS_0")?.AsVector4Array();
|
||||||
|
var joints1Accessor = primitive.GetVertexAccessor("JOINTS_1")?.AsVector4Array();
|
||||||
if (jointsAccessor == null || weightsAccessor == null)
|
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.");
|
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.
|
// 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 joints0 = joints0Accessor[i];
|
||||||
var weights = weightsAccessor[i];
|
var weights0 = weights0Accessor[i];
|
||||||
|
var joints1 = joints1Accessor?[i];
|
||||||
|
var weights1 = weights1Accessor?[i];
|
||||||
for (var index = 0; index < 4; index++)
|
for (var index = 0; index < 4; index++)
|
||||||
{
|
{
|
||||||
// If a joint has absolutely no weight, we omit the bone entirely.
|
// If a joint has absolutely no weight, we omit the bone entirely.
|
||||||
if (weights[index] == 0)
|
if (weights0[index] != 0)
|
||||||
continue;
|
{
|
||||||
|
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
|
try
|
||||||
{
|
{
|
||||||
_morphNames = node.Mesh.Extras.GetNode("targetNames").Deserialize<List<string>>();
|
_morphNames = node.Mesh.Extras["targetNames"].Deserialize<List<string>>();
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ public class VertexAttribute
|
||||||
MdlFile.VertexType.NByte4 => 4,
|
MdlFile.VertexType.NByte4 => 4,
|
||||||
MdlFile.VertexType.Half2 => 4,
|
MdlFile.VertexType.Half2 => 4,
|
||||||
MdlFile.VertexType.Half4 => 8,
|
MdlFile.VertexType.Half4 => 8,
|
||||||
|
MdlFile.VertexType.UShort4 => 8,
|
||||||
|
|
||||||
_ => throw new Exception($"Unhandled vertex type {(MdlFile.VertexType)Element.Type}"),
|
_ => 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)
|
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;
|
return null;
|
||||||
|
|
||||||
if (!accessors.ContainsKey("JOINTS_0"))
|
if (!accessors.ContainsKey("JOINTS_0"))
|
||||||
throw notifier.Exception("Mesh contained WEIGHTS_0 attribute but no corresponding JOINTS_0 attribute.");
|
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()
|
var element = new MdlStructs.VertexElement()
|
||||||
{
|
{
|
||||||
Stream = 0,
|
Stream = 0,
|
||||||
Type = (byte)MdlFile.VertexType.NByte4,
|
Type = (byte)MdlFile.VertexType.UShort4,
|
||||||
Usage = (byte)MdlFile.VertexUsage.BlendWeights,
|
Usage = (byte)MdlFile.VertexUsage.BlendWeights,
|
||||||
};
|
};
|
||||||
|
|
||||||
var values = accessor.AsVector4Array();
|
var weights0 = weights0Accessor.AsVector4Array();
|
||||||
|
var weights1 = weights1Accessor?.AsVector4Array();
|
||||||
|
|
||||||
return new VertexAttribute(
|
return new VertexAttribute(
|
||||||
element,
|
element,
|
||||||
index => {
|
index => BuildBlendWeights(weights0[index], weights1?[index] ?? Vector4.Zero)
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
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;
|
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.");
|
throw notifier.Exception("Mesh contained JOINTS_0 attribute but no corresponding WEIGHTS_0 attribute.");
|
||||||
|
|
||||||
if (boneMap == null)
|
if (boneMap == null)
|
||||||
throw notifier.Exception("Mesh contained JOINTS_0 attribute but no bone mapping was created.");
|
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,
|
Stream = 0,
|
||||||
Type = (byte)MdlFile.VertexType.UByte4,
|
Type = (byte)MdlFile.VertexType.UShort4,
|
||||||
Usage = (byte)MdlFile.VertexUsage.BlendIndices,
|
Usage = (byte)MdlFile.VertexUsage.BlendIndices,
|
||||||
};
|
};
|
||||||
|
|
||||||
var joints = jointsAccessor.AsVector4Array();
|
var joints1 = joints1Accessor?.AsVector4Array();
|
||||||
var weights = weightsAccessor.AsVector4Array();
|
var weights1 = weights1Accessor?.AsVector4Array();
|
||||||
|
|
||||||
return new VertexAttribute(
|
return new VertexAttribute(
|
||||||
element,
|
element,
|
||||||
index =>
|
index =>
|
||||||
{
|
{
|
||||||
var gltfIndices = joints[index];
|
var gltfIndices0 = joints0[index];
|
||||||
var gltfWeights = weights[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(
|
var byteValues = BuildUshort4(v0, v1);
|
||||||
gltfWeights.X == 0 ? 0 : boneMap[(ushort)gltfIndices.X],
|
|
||||||
gltfWeights.Y == 0 ? 0 : boneMap[(ushort)gltfIndices.Y],
|
return byteValues.Select(x => (byte)x).ToArray();
|
||||||
gltfWeights.Z == 0 ? 0 : boneMap[(ushort)gltfIndices.Z],
|
|
||||||
gltfWeights.W == 0 ? 0 : boneMap[(ushort)gltfIndices.W]
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -232,7 +266,7 @@ public class VertexAttribute
|
||||||
var value = values[vertexIndex];
|
var value = values[vertexIndex];
|
||||||
|
|
||||||
var delta = morphValues[morphIndex]?[vertexIndex];
|
var delta = morphValues[morphIndex]?[vertexIndex];
|
||||||
if (delta != null)
|
if (delta != null)
|
||||||
value += delta.Value;
|
value += delta.Value;
|
||||||
|
|
||||||
return BuildSingle3(value);
|
return BuildSingle3(value);
|
||||||
|
|
@ -489,4 +523,11 @@ public class VertexAttribute
|
||||||
(byte)Math.Round(input.Z * 255f),
|
(byte)Math.Round(input.Z * 255f),
|
||||||
(byte)Math.Round(input.W * 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
|
// 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)
|
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)
|
private static string GetStringFromZipEntry(ZipArchiveEntry entry, Encoding encoding)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ public partial class TexToolsImporter
|
||||||
if (name.Length == 0)
|
if (name.Length == 0)
|
||||||
throw new Exception("Invalid mod archive: mod meta has no name.");
|
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.Seek(0, SeekOrigin.Begin);
|
||||||
s.WriteTo(f);
|
s.WriteTo(f);
|
||||||
}
|
}
|
||||||
|
|
@ -155,13 +155,9 @@ public partial class TexToolsImporter
|
||||||
|
|
||||||
ret = directory;
|
ret = directory;
|
||||||
// Check that all other files are also contained in the top-level directory.
|
// Check that all other files are also contained in the top-level directory.
|
||||||
if (ret.IndexOfAny(new[]
|
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 '\\')))
|
||||||
'\\',
|
|
||||||
})
|
|
||||||
>= 0
|
|
||||||
|| !archive.Entries.All(e => e.Key.StartsWith(ret) && (e.Key.Length == ret.Length || e.Key[ret.Length] is '/' or '\\')))
|
|
||||||
throw new Exception(
|
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.");
|
"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();
|
||||||
ImGui.NewLine();
|
ImGui.NewLine();
|
||||||
percentage = _currentNumOptions == 0 ? 1f : _currentOptionIdx / (float)_currentNumOptions;
|
if (_currentOptionIdx >= _currentNumOptions)
|
||||||
ImGui.ProgressBar(percentage, size, $"Option {_currentOptionIdx + 1} / {_currentNumOptions}");
|
ImGui.ProgressBar(1f, size, $"Extracted {_currentNumOptions} Options");
|
||||||
|
else
|
||||||
|
ImGui.ProgressBar(_currentOptionIdx / (float)_currentNumOptions, size,
|
||||||
|
$"Extracting Option {_currentOptionIdx + 1} / {_currentNumOptions}...");
|
||||||
|
|
||||||
ImGui.NewLine();
|
ImGui.NewLine();
|
||||||
if (State != ImporterState.DeduplicatingFiles)
|
if (State != ImporterState.DeduplicatingFiles)
|
||||||
ImGui.TextUnformatted(
|
ImGui.TextUnformatted(
|
||||||
$"Extracting option {(_currentGroupName.Length == 0 ? string.Empty : $"{_currentGroupName} - ")}{_currentOptionName}...");
|
$"Extracting Option {(_currentGroupName.Length == 0 ? string.Empty : $"{_currentGroupName} - ")}{_currentOptionName}...");
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.NewLine();
|
ImGui.NewLine();
|
||||||
ImGui.NewLine();
|
ImGui.NewLine();
|
||||||
percentage = _currentNumFiles == 0 ? 1f : _currentFileIdx / (float)_currentNumFiles;
|
if (_currentFileIdx >= _currentNumFiles)
|
||||||
ImGui.ProgressBar(percentage, size, $"File {_currentFileIdx + 1} / {_currentNumFiles}");
|
ImGui.ProgressBar(1f, size, $"Extracted {_currentNumFiles} Files");
|
||||||
|
else
|
||||||
|
ImGui.ProgressBar(_currentFileIdx / (float)_currentNumFiles, size, $"Extracting File {_currentFileIdx + 1} / {_currentNumFiles}...");
|
||||||
|
|
||||||
ImGui.NewLine();
|
ImGui.NewLine();
|
||||||
if (State != ImporterState.DeduplicatingFiles)
|
if (State != ImporterState.DeduplicatingFiles)
|
||||||
ImGui.TextUnformatted($"Extracting file {_currentFileName}...");
|
ImGui.TextUnformatted($"Extracting File {_currentFileName}...");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,7 @@ public partial class TexToolsImporter
|
||||||
_currentGroupName = string.Empty;
|
_currentGroupName = string.Empty;
|
||||||
_currentOptionName = "Default";
|
_currentOptionName = "Default";
|
||||||
ExtractSimpleModList(_currentModDirectory, modList.SimpleModsList);
|
ExtractSimpleModList(_currentModDirectory, modList.SimpleModsList);
|
||||||
|
++_currentOptionIdx;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterate through all pages
|
// Iterate through all pages
|
||||||
|
|
@ -208,6 +209,7 @@ public partial class TexToolsImporter
|
||||||
options.Insert(idx, MultiSubMod.WithoutGroup(option.Name, option.Description, ModPriority.Default));
|
options.Insert(idx, MultiSubMod.WithoutGroup(option.Name, option.Description, ModPriority.Default));
|
||||||
if (option.IsChecked)
|
if (option.IsChecked)
|
||||||
defaultSettings = Setting.Single(idx);
|
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);
|
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)
|
private void SaveAs(TextureManager textures, string path, TextureSaveType type, bool mipMaps, bool writeTex)
|
||||||
{
|
{
|
||||||
if (!IsLoaded || _current == null)
|
if (!IsLoaded || _current == null)
|
||||||
|
|
@ -72,6 +80,7 @@ public partial class CombinedTexture : IDisposable
|
||||||
".tex" => TextureType.Tex,
|
".tex" => TextureType.Tex,
|
||||||
".dds" => TextureType.Dds,
|
".dds" => TextureType.Dds,
|
||||||
".png" => TextureType.Png,
|
".png" => TextureType.Png,
|
||||||
|
".tga" => TextureType.Targa,
|
||||||
_ => TextureType.Unknown,
|
_ => TextureType.Unknown,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -85,6 +94,9 @@ public partial class CombinedTexture : IDisposable
|
||||||
break;
|
break;
|
||||||
case TextureType.Png:
|
case TextureType.Png:
|
||||||
SaveAsPng(textures, path);
|
SaveAsPng(textures, path);
|
||||||
|
break;
|
||||||
|
case TextureType.Targa:
|
||||||
|
SaveAsTarga(textures, path);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new ArgumentException(
|
throw new ArgumentException(
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,9 @@ public static class TexFileParser
|
||||||
DXGIFormat.BC1UNorm => TexFile.TextureFormat.BC1,
|
DXGIFormat.BC1UNorm => TexFile.TextureFormat.BC1,
|
||||||
DXGIFormat.BC2UNorm => TexFile.TextureFormat.BC2,
|
DXGIFormat.BC2UNorm => TexFile.TextureFormat.BC2,
|
||||||
DXGIFormat.BC3UNorm => TexFile.TextureFormat.BC3,
|
DXGIFormat.BC3UNorm => TexFile.TextureFormat.BC3,
|
||||||
|
DXGIFormat.BC4UNorm => (TexFile.TextureFormat)0x6120, // TODO: upstream to Lumina
|
||||||
DXGIFormat.BC5UNorm => TexFile.TextureFormat.BC5,
|
DXGIFormat.BC5UNorm => TexFile.TextureFormat.BC5,
|
||||||
|
DXGIFormat.BC6HSF16 => (TexFile.TextureFormat)0x6330, // TODO: upstream to Lumina
|
||||||
DXGIFormat.BC7UNorm => TexFile.TextureFormat.BC7,
|
DXGIFormat.BC7UNorm => TexFile.TextureFormat.BC7,
|
||||||
DXGIFormat.R16G16B16A16Typeless => TexFile.TextureFormat.D16,
|
DXGIFormat.R16G16B16A16Typeless => TexFile.TextureFormat.D16,
|
||||||
DXGIFormat.R24G8Typeless => TexFile.TextureFormat.D24S8,
|
DXGIFormat.R24G8Typeless => TexFile.TextureFormat.D24S8,
|
||||||
|
|
@ -202,7 +204,9 @@ public static class TexFileParser
|
||||||
TexFile.TextureFormat.BC1 => DXGIFormat.BC1UNorm,
|
TexFile.TextureFormat.BC1 => DXGIFormat.BC1UNorm,
|
||||||
TexFile.TextureFormat.BC2 => DXGIFormat.BC2UNorm,
|
TexFile.TextureFormat.BC2 => DXGIFormat.BC2UNorm,
|
||||||
TexFile.TextureFormat.BC3 => DXGIFormat.BC3UNorm,
|
TexFile.TextureFormat.BC3 => DXGIFormat.BC3UNorm,
|
||||||
|
(TexFile.TextureFormat)0x6120 => DXGIFormat.BC4UNorm, // TODO: upstream to Lumina
|
||||||
TexFile.TextureFormat.BC5 => DXGIFormat.BC5UNorm,
|
TexFile.TextureFormat.BC5 => DXGIFormat.BC5UNorm,
|
||||||
|
(TexFile.TextureFormat)0x6330 => DXGIFormat.BC6HSF16, // TODO: upstream to Lumina
|
||||||
TexFile.TextureFormat.BC7 => DXGIFormat.BC7UNorm,
|
TexFile.TextureFormat.BC7 => DXGIFormat.BC7UNorm,
|
||||||
TexFile.TextureFormat.D16 => DXGIFormat.R16G16B16A16Typeless,
|
TexFile.TextureFormat.D16 => DXGIFormat.R16G16B16A16Typeless,
|
||||||
TexFile.TextureFormat.D24S8 => DXGIFormat.R24G8Typeless,
|
TexFile.TextureFormat.D24S8 => DXGIFormat.R24G8Typeless,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,21 @@ public enum TextureType
|
||||||
Tex,
|
Tex,
|
||||||
Png,
|
Png,
|
||||||
Bitmap,
|
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
|
public sealed class Texture : IDisposable
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ public static class TextureDrawer
|
||||||
current.Load(textures, paths[0]);
|
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();
|
ImGui.SameLine();
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ using OtterGui.Tasks;
|
||||||
using OtterTex;
|
using OtterTex;
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
using SixLabors.ImageSharp.Formats.Png;
|
using SixLabors.ImageSharp.Formats.Png;
|
||||||
|
using SixLabors.ImageSharp.Formats.Tga;
|
||||||
using SixLabors.ImageSharp.PixelFormats;
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
using Image = SixLabors.ImageSharp.Image;
|
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)
|
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)
|
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)
|
public Task SaveAs(CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, string input, string output)
|
||||||
=> Enqueue(new SaveAsAction(this, type, mipMaps, asTex, input, 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;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class SavePngAction : IAction
|
private class SaveImageSharpAction : IAction
|
||||||
{
|
{
|
||||||
private readonly TextureManager _textures;
|
private readonly TextureManager _textures;
|
||||||
private readonly string _outputPath;
|
private readonly string _outputPath;
|
||||||
private readonly ImageInputData _input;
|
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;
|
_textures = textures;
|
||||||
_input = new ImageInputData(input);
|
_input = new ImageInputData(input);
|
||||||
_outputPath = output;
|
_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;
|
_textures = textures;
|
||||||
_input = new ImageInputData(image, rgba, width, height);
|
_input = new ImageInputData(image, rgba, width, height);
|
||||||
_outputPath = path;
|
_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)
|
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);
|
var (image, rgba, width, height) = _input.GetData(_textures);
|
||||||
cancel.ThrowIfCancellationRequested();
|
cancel.ThrowIfCancellationRequested();
|
||||||
Image<Rgba32>? png = null;
|
Image<Rgba32>? data = null;
|
||||||
if (image.Type is TextureType.Unknown)
|
if (image.Type is TextureType.Unknown)
|
||||||
{
|
{
|
||||||
if (rgba != null && width > 0 && height > 0)
|
if (rgba != null && width > 0 && height > 0)
|
||||||
png = ConvertToPng(rgba, width, height).AsPng!;
|
data = ConvertToPng(rgba, width, height).AsPng!;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
png = ConvertToPng(image, cancel, rgba).AsPng!;
|
data = ConvertToPng(image, cancel, rgba).AsPng!;
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel.ThrowIfCancellationRequested();
|
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()
|
public override string ToString()
|
||||||
|
|
@ -111,7 +140,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
|
||||||
|
|
||||||
public bool Equals(IAction? other)
|
public bool Equals(IAction? other)
|
||||||
{
|
{
|
||||||
if (other is not SavePngAction rhs)
|
if (other is not SaveImageSharpAction rhs)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return string.Equals(_outputPath, rhs._outputPath, StringComparison.OrdinalIgnoreCase) && _input.Equals(rhs._input);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var imageTypeBehaviour = image.Type.ReduceToBehaviour();
|
||||||
var dds = _type switch
|
var dds = _type switch
|
||||||
{
|
{
|
||||||
CombinedTexture.TextureSaveType.AsIs when image.Type is TextureType.Png => ConvertToRgbaDds(image, _mipMaps, cancel, rgba,
|
CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Png => ConvertToRgbaDds(image, _mipMaps, cancel,
|
||||||
width, height),
|
rgba, width, height),
|
||||||
CombinedTexture.TextureSaveType.AsIs when image.Type is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps),
|
CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps),
|
||||||
CombinedTexture.TextureSaveType.Bitmap => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, width, height),
|
CombinedTexture.TextureSaveType.Bitmap => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, width, height),
|
||||||
CombinedTexture.TextureSaveType.BC3 => ConvertToCompressedDds(image, _mipMaps, false, 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),
|
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
|
=> Path.GetExtension(path).ToLowerInvariant() switch
|
||||||
{
|
{
|
||||||
".dds" => (LoadDds(path), TextureType.Dds),
|
".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),
|
".tex" => (LoadTex(path), TextureType.Tex),
|
||||||
_ => throw new Exception($"Extension {Path.GetExtension(path)} unknown."),
|
_ => 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)
|
public BaseImage LoadDds(string path)
|
||||||
=> ScratchImage.LoadDDS(path);
|
=> ScratchImage.LoadDDS(path);
|
||||||
|
|
||||||
/// <summary> Load a .png file from drive using ImageSharp. </summary>
|
/// <summary> Load a supported file type from drive using ImageSharp. </summary>
|
||||||
public BaseImage LoadPng(string path)
|
public BaseImage LoadImageSharp(string path)
|
||||||
{
|
{
|
||||||
using var stream = File.OpenRead(path);
|
using var stream = File.OpenRead(path);
|
||||||
return Image.Load<Rgba32>(stream);
|
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)
|
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.Png: return input;
|
||||||
case TextureType.Dds:
|
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,
|
public static BaseImage ConvertToRgbaDds(BaseImage input, bool mipMaps, CancellationToken cancel, byte[]? rgba = null, int width = 0,
|
||||||
int height = 0)
|
int height = 0)
|
||||||
{
|
{
|
||||||
switch (input.Type)
|
switch (input.Type.ReduceToBehaviour())
|
||||||
{
|
{
|
||||||
case TextureType.Png:
|
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,
|
public static BaseImage ConvertToCompressedDds(BaseImage input, bool mipMaps, bool bc7, CancellationToken cancel, byte[]? rgba = null,
|
||||||
int width = 0, int height = 0)
|
int width = 0, int height = 0)
|
||||||
{
|
{
|
||||||
switch (input.Type)
|
switch (input.Type.ReduceToBehaviour())
|
||||||
{
|
{
|
||||||
case TextureType.Png:
|
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.Dds => $"Custom {_width} x {_height} {_image.Format} Image",
|
||||||
TextureType.Tex => $"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.Png => $"Custom {_width} x {_height} .png Image",
|
||||||
|
TextureType.Targa => $"Custom {_width} x {_height} .tga Image",
|
||||||
TextureType.Bitmap => $"Custom {_width} x {_height} RGBA Image",
|
TextureType.Bitmap => $"Custom {_width} x {_height} RGBA Image",
|
||||||
_ => "Unknown Image",
|
_ => "Unknown Image",
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,10 @@ using Dalamud.Hooking;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.Collections;
|
using Penumbra.Collections;
|
||||||
|
using Penumbra.GameData.Enums;
|
||||||
|
using Penumbra.GameData.Structs;
|
||||||
using Penumbra.Interop.PathResolving;
|
using Penumbra.Interop.PathResolving;
|
||||||
|
using static FFXIVClientStructs.FFXIV.Client.Game.Character.ActionEffectHandler;
|
||||||
|
|
||||||
namespace Penumbra.Interop.Hooks.Resources;
|
namespace Penumbra.Interop.Hooks.Resources;
|
||||||
|
|
||||||
|
|
@ -212,30 +215,74 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
|
||||||
return ret;
|
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)
|
private nint ResolveVfxHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint unkOutParam)
|
||||||
{
|
{
|
||||||
if (slotIndex is <= 4 or >= 10)
|
switch (slotIndex)
|
||||||
return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam);
|
{
|
||||||
|
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;
|
ref var slot = ref changedEquipData[slotIndex];
|
||||||
// Enable vfxs for accessories
|
|
||||||
if (changedEquipData == null)
|
|
||||||
return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam);
|
|
||||||
|
|
||||||
var slot = (ushort*)(changedEquipData + 12 * (nint)slotIndex);
|
if (slot.Model == 0 || slot.Variant == 0 || slot.VfxId == 0)
|
||||||
var model = slot[0];
|
return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam);
|
||||||
var variant = slot[1];
|
|
||||||
var vfxId = slot[4];
|
|
||||||
|
|
||||||
if (model == 0 || variant == 0 || vfxId == 0)
|
if (!Utf8.TryWrite(new Span<byte>((void*)pathBuffer, (int)pathBufferSize),
|
||||||
return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam);
|
$"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",
|
*(ulong*)unkOutParam = 4;
|
||||||
out _))
|
return ResolvePath(drawObject, pathBuffer);
|
||||||
return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam);
|
}
|
||||||
|
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;
|
ref var slot = ref changedEquipData[slotIndex - 6];
|
||||||
return ResolvePath(drawObject, pathBuffer);
|
|
||||||
|
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)
|
private nint VFunc81(nint drawObject, int estType, nint unk)
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,8 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
|
||||||
if (_originalColorTableTexture.Texture == null)
|
if (_originalColorTableTexture.Texture == null)
|
||||||
throw new InvalidOperationException("Material doesn't have a color table");
|
throw new InvalidOperationException("Material doesn't have a color table");
|
||||||
|
|
||||||
Width = (int)_originalColorTableTexture.Texture->Width;
|
Width = (int)_originalColorTableTexture.Texture->ActualWidth;
|
||||||
Height = (int)_originalColorTableTexture.Texture->Height;
|
Height = (int)_originalColorTableTexture.Texture->ActualHeight;
|
||||||
ColorTable = new Half[Width * Height * 4];
|
ColorTable = new Half[Width * Height * 4];
|
||||||
_updatePending = true;
|
_updatePending = true;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,13 +36,13 @@ internal partial record ResolveContext
|
||||||
private Utf8GamePath ResolveEquipmentModelPath()
|
private Utf8GamePath ResolveEquipmentModelPath()
|
||||||
{
|
{
|
||||||
var path = IsEquipmentSlot(SlotIndex)
|
var path = IsEquipmentSlot(SlotIndex)
|
||||||
? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot)
|
? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot.ToSlot())
|
||||||
: GamePaths.Accessory.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot);
|
: GamePaths.Accessory.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), SlotIndex.ToEquipSlot());
|
||||||
return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
|
return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
private GenderRace ResolveModelRaceCode()
|
private GenderRace ResolveModelRaceCode()
|
||||||
=> ResolveEqdpRaceCode(Slot, Equipment.Set);
|
=> ResolveEqdpRaceCode(Slot.ToSlot(), Equipment.Set);
|
||||||
|
|
||||||
private unsafe GenderRace ResolveEqdpRaceCode(EquipSlot slot, PrimaryId primaryId)
|
private unsafe GenderRace ResolveEqdpRaceCode(EquipSlot slot, PrimaryId primaryId)
|
||||||
{
|
{
|
||||||
|
|
@ -161,7 +161,7 @@ internal partial record ResolveContext
|
||||||
return variant.Id;
|
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)
|
if (!exists)
|
||||||
return variant.Id;
|
return variant.Id;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ internal record GlobalResolveContext(
|
||||||
public readonly Dictionary<(Utf8GamePath, nint), ResourceNode> Nodes = new(128);
|
public readonly Dictionary<(Utf8GamePath, nint), ResourceNode> Nodes = new(128);
|
||||||
|
|
||||||
public unsafe ResolveContext CreateContext(CharaBase* characterBase, uint slotIndex = 0xFFFFFFFFu,
|
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);
|
=> new(this, characterBase, slotIndex, slot, equipment, secondaryId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,7 +38,7 @@ internal unsafe partial record ResolveContext(
|
||||||
GlobalResolveContext Global,
|
GlobalResolveContext Global,
|
||||||
Pointer<CharaBase> CharacterBasePointer,
|
Pointer<CharaBase> CharacterBasePointer,
|
||||||
uint SlotIndex,
|
uint SlotIndex,
|
||||||
EquipSlot Slot,
|
FullEquipType Slot,
|
||||||
CharacterArmor Equipment,
|
CharacterArmor Equipment,
|
||||||
SecondaryId SecondaryId)
|
SecondaryId SecondaryId)
|
||||||
{
|
{
|
||||||
|
|
@ -340,19 +340,20 @@ internal unsafe partial record ResolveContext(
|
||||||
|
|
||||||
internal ResourceNode.UiData GuessModelUiData(Utf8GamePath gamePath)
|
internal ResourceNode.UiData GuessModelUiData(Utf8GamePath gamePath)
|
||||||
{
|
{
|
||||||
var path = gamePath.ToString().Split('/', StringSplitOptions.RemoveEmptyEntries);
|
var path = gamePath.Path.Split((byte)'/');
|
||||||
// Weapons intentionally left out.
|
// 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)
|
if (isEquipment)
|
||||||
foreach (var item in Global.Identifier.Identify(Equipment.Set, 0, Equipment.Variant, Slot.ToSlot()))
|
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: ",
|
8 => "R: " + name,
|
||||||
EquipSlot.LFinger => "L: ",
|
9 => "L: " + name,
|
||||||
_ => string.Empty,
|
_ => name,
|
||||||
}
|
};
|
||||||
+ item.Name;
|
|
||||||
return new ResourceNode.UiData(name, item.Type.GetCategoryIcon().ToFlag());
|
return new ResourceNode.UiData(name, item.Type.GetCategoryIcon().ToFlag());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -361,7 +362,7 @@ internal unsafe partial record ResolveContext(
|
||||||
return dataFromPath;
|
return dataFromPath;
|
||||||
|
|
||||||
return isEquipment
|
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);
|
: new ResourceNode.UiData(null, ChangedItemIconFlag.Unknown);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,20 +7,20 @@ namespace Penumbra.Interop.ResourceTree;
|
||||||
|
|
||||||
public class ResourceNode : ICloneable
|
public class ResourceNode : ICloneable
|
||||||
{
|
{
|
||||||
public string? Name;
|
public string? Name;
|
||||||
public string? FallbackName;
|
public string? FallbackName;
|
||||||
public ChangedItemIconFlag IconFlag;
|
public ChangedItemIconFlag IconFlag;
|
||||||
public readonly ResourceType Type;
|
public readonly ResourceType Type;
|
||||||
public readonly nint ObjectAddress;
|
public readonly nint ObjectAddress;
|
||||||
public readonly nint ResourceHandle;
|
public readonly nint ResourceHandle;
|
||||||
public Utf8GamePath[] PossibleGamePaths;
|
public Utf8GamePath[] PossibleGamePaths;
|
||||||
public FullPath FullPath;
|
public FullPath FullPath;
|
||||||
public string? ModName;
|
public string? ModName;
|
||||||
public string? ModRelativePath;
|
public string? ModRelativePath;
|
||||||
public CiByteString AdditionalData;
|
public CiByteString AdditionalData;
|
||||||
public readonly ulong Length;
|
public readonly ulong Length;
|
||||||
public readonly List<ResourceNode> Children;
|
public readonly List<ResourceNode> Children;
|
||||||
internal ResolveContext? ResolveContext;
|
internal ResolveContext? ResolveContext;
|
||||||
|
|
||||||
public Utf8GamePath GamePath
|
public Utf8GamePath GamePath
|
||||||
{
|
{
|
||||||
|
|
@ -53,7 +53,7 @@ public class ResourceNode : ICloneable
|
||||||
{
|
{
|
||||||
Name = other.Name;
|
Name = other.Name;
|
||||||
FallbackName = other.FallbackName;
|
FallbackName = other.FallbackName;
|
||||||
IconFlag = other.IconFlag;
|
IconFlag = other.IconFlag;
|
||||||
Type = other.Type;
|
Type = other.Type;
|
||||||
ObjectAddress = other.ObjectAddress;
|
ObjectAddress = other.ObjectAddress;
|
||||||
ResourceHandle = other.ResourceHandle;
|
ResourceHandle = other.ResourceHandle;
|
||||||
|
|
@ -82,7 +82,7 @@ public class ResourceNode : ICloneable
|
||||||
|
|
||||||
public void SetUiData(UiData uiData)
|
public void SetUiData(UiData uiData)
|
||||||
{
|
{
|
||||||
Name = uiData.Name;
|
Name = uiData.Name;
|
||||||
IconFlag = uiData.IconFlag;
|
IconFlag = uiData.IconFlag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,12 +80,13 @@ public class ResourceTree
|
||||||
{
|
{
|
||||||
ModelType.Human => i switch
|
ModelType.Human => i switch
|
||||||
{
|
{
|
||||||
< 10 => globalContext.CreateContext(model, i, i.ToEquipSlot(), equipment[(int)i]),
|
< 10 => globalContext.CreateContext(model, i, i.ToEquipSlot().ToEquipType(), equipment[(int)i]),
|
||||||
16 or 17 => globalContext.CreateContext(model, i, EquipSlot.Head, equipment[(int)(i - 6)]),
|
16 => globalContext.CreateContext(model, i, FullEquipType.Glasses, equipment[10]),
|
||||||
_ => globalContext.CreateContext(model, i),
|
17 => globalContext.CreateContext(model, i, FullEquipType.Unknown, equipment[11]),
|
||||||
|
_ => globalContext.CreateContext(model, i),
|
||||||
},
|
},
|
||||||
_ => i < equipment.Length
|
_ => 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),
|
: globalContext.CreateContext(model, i),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -133,7 +134,7 @@ public class ResourceTree
|
||||||
var weapon = (Weapon*)subObject;
|
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.
|
// 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 equipment = new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, new StainIds(weapon->Stain0, weapon->Stain1));
|
||||||
var weaponType = weapon->SecondaryId;
|
var weaponType = weapon->SecondaryId;
|
||||||
|
|
||||||
|
|
@ -184,7 +185,7 @@ public class ResourceTree
|
||||||
{
|
{
|
||||||
pbdNode = pbdNode.Clone();
|
pbdNode = pbdNode.Clone();
|
||||||
pbdNode.FallbackName = "Racial Deformer";
|
pbdNode.FallbackName = "Racial Deformer";
|
||||||
pbdNode.IconFlag = ChangedItemIconFlag.Customization;
|
pbdNode.IconFlag = ChangedItemIconFlag.Customization;
|
||||||
}
|
}
|
||||||
|
|
||||||
Nodes.Add(pbdNode);
|
Nodes.Add(pbdNode);
|
||||||
|
|
@ -202,7 +203,7 @@ public class ResourceTree
|
||||||
{
|
{
|
||||||
decalNode = decalNode.Clone();
|
decalNode = decalNode.Clone();
|
||||||
decalNode.FallbackName = "Face Decal";
|
decalNode.FallbackName = "Face Decal";
|
||||||
decalNode.IconFlag = ChangedItemIconFlag.Customization;
|
decalNode.IconFlag = ChangedItemIconFlag.Customization;
|
||||||
}
|
}
|
||||||
|
|
||||||
Nodes.Add(decalNode);
|
Nodes.Add(decalNode);
|
||||||
|
|
@ -219,7 +220,7 @@ public class ResourceTree
|
||||||
{
|
{
|
||||||
legacyDecalNode = legacyDecalNode.Clone();
|
legacyDecalNode = legacyDecalNode.Clone();
|
||||||
legacyDecalNode.FallbackName = "Legacy Body Decal";
|
legacyDecalNode.FallbackName = "Legacy Body Decal";
|
||||||
legacyDecalNode.IconFlag = ChangedItemIconFlag.Customization;
|
legacyDecalNode.IconFlag = ChangedItemIconFlag.Customization;
|
||||||
}
|
}
|
||||||
|
|
||||||
Nodes.Add(legacyDecalNode);
|
Nodes.Add(legacyDecalNode);
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@ namespace Penumbra.Meta;
|
||||||
|
|
||||||
public class ImcChecker
|
public class ImcChecker
|
||||||
{
|
{
|
||||||
private static readonly Dictionary<ImcIdentifier, int> VariantCounts = [];
|
private static readonly Dictionary<ImcIdentifier, int> VariantCounts = [];
|
||||||
private static MetaFileManager? _dataManager;
|
private static MetaFileManager? _dataManager;
|
||||||
|
private static readonly ConcurrentDictionary<ImcIdentifier, CachedEntry> GlobalCachedDefaultEntries = [];
|
||||||
|
|
||||||
public static int GetVariantCount(ImcIdentifier identifier)
|
public static int GetVariantCount(ImcIdentifier identifier)
|
||||||
{
|
{
|
||||||
|
|
@ -26,23 +26,20 @@ public class ImcChecker
|
||||||
|
|
||||||
public readonly record struct CachedEntry(ImcEntry Entry, bool FileExists, bool VariantExists);
|
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)
|
public ImcChecker(MetaFileManager metaFileManager)
|
||||||
{
|
=> _dataManager = metaFileManager;
|
||||||
_metaFileManager = 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;
|
return entry;
|
||||||
|
|
||||||
|
if (_dataManager == null)
|
||||||
|
return new CachedEntry(default, false, false);
|
||||||
|
|
||||||
try
|
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);
|
entry = new CachedEntry(e, true, entryExists);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
|
|
@ -51,7 +48,7 @@ public class ImcChecker
|
||||||
}
|
}
|
||||||
|
|
||||||
if (storeCache)
|
if (storeCache)
|
||||||
_cachedDefaultEntries.Add(identifier, entry);
|
GlobalCachedDefaultEntries.TryAdd(identifier, entry);
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,9 +68,13 @@ public readonly record struct ImcIdentifier(
|
||||||
=> (MetaIndex)(-1);
|
=> (MetaIndex)(-1);
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
=> ObjectType is ObjectType.Equipment or ObjectType.Accessory
|
=> ObjectType switch
|
||||||
? $"Imc - {PrimaryId} - {EquipSlot.ToName()} - {Variant}"
|
{
|
||||||
: $"Imc - {PrimaryId} - {ObjectType.ToName()} - {SecondaryId} - {BodySlot} - {Variant}";
|
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()
|
public bool Validate()
|
||||||
{
|
{
|
||||||
|
|
@ -102,6 +106,7 @@ public readonly record struct ImcIdentifier(
|
||||||
return false;
|
return false;
|
||||||
if (ItemData.AdaptOffhandImc(PrimaryId, out _))
|
if (ItemData.AdaptOffhandImc(PrimaryId, out _))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,7 +168,7 @@ public readonly record struct ImcIdentifier(
|
||||||
case ObjectType.DemiHuman:
|
case ObjectType.DemiHuman:
|
||||||
{
|
{
|
||||||
var secondaryId = new SecondaryId(jObj["SecondaryId"]?.ToObject<ushort>() ?? 0);
|
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);
|
ret = new ImcIdentifier(primaryId, (Variant)variant, objectType, secondaryId, slot, BodySlot.Unknown);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ public class MetaDictionary
|
||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
|
Count = 0;
|
||||||
_imc.Clear();
|
_imc.Clear();
|
||||||
_eqp.Clear();
|
_eqp.Clear();
|
||||||
_eqdp.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))
|
if (!useModManager || !modManager.TryGetMod(modDirectory.Name, string.Empty, out var mod))
|
||||||
{
|
{
|
||||||
mod = new Mod(modDirectory);
|
mod = new Mod(modDirectory);
|
||||||
modManager.Creator.ReloadMod(mod, true, out _);
|
modManager.Creator.ReloadMod(mod, true, true, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
Clear();
|
Clear();
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,21 @@ public class ModEditor(
|
||||||
public readonly MdlMaterialEditor MdlMaterialEditor = mdlMaterialEditor;
|
public readonly MdlMaterialEditor MdlMaterialEditor = mdlMaterialEditor;
|
||||||
public readonly FileCompactor Compactor = compactor;
|
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 Mod? Mod { get; private set; }
|
||||||
public int GroupIdx { get; private set; }
|
public int GroupIdx { get; private set; }
|
||||||
public int DataIdx { get; private set; }
|
public int DataIdx { get; private set; }
|
||||||
|
|
@ -32,28 +47,42 @@ public class ModEditor(
|
||||||
public IModGroup? Group { get; private set; }
|
public IModGroup? Group { get; private set; }
|
||||||
public IModDataContainer? Option { get; private set; }
|
public IModDataContainer? Option { get; private set; }
|
||||||
|
|
||||||
public void LoadMod(Mod mod)
|
public async Task LoadMod(Mod mod, int groupIdx, int dataIdx)
|
||||||
=> LoadMod(mod, -1, 0);
|
|
||||||
|
|
||||||
public void LoadMod(Mod mod, int groupIdx, int dataIdx)
|
|
||||||
{
|
{
|
||||||
Mod = mod;
|
await AppendTask(() =>
|
||||||
LoadOption(groupIdx, dataIdx, true);
|
{
|
||||||
Files.UpdateAll(mod, Option!);
|
Mod = mod;
|
||||||
SwapEditor.Revert(Option!);
|
LoadOption(groupIdx, dataIdx, true);
|
||||||
MetaEditor.Load(Mod!, Option!);
|
Files.UpdateAll(mod, Option!);
|
||||||
Duplicates.Clear();
|
SwapEditor.Revert(Option!);
|
||||||
MdlMaterialEditor.ScanModels(Mod!);
|
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);
|
lock (_lock)
|
||||||
SwapEditor.Revert(Option!);
|
{
|
||||||
Files.UpdatePaths(Mod!, Option!);
|
if (_loadingMod == null || _loadingMod.IsCompleted)
|
||||||
MetaEditor.Load(Mod!, Option!);
|
return _loadingMod = Task.Run(run);
|
||||||
FileEditor.Clear();
|
|
||||||
Duplicates.Clear();
|
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>
|
/// <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> _mdl = [];
|
||||||
private readonly List<FileRegistry> _tex = [];
|
private readonly List<FileRegistry> _tex = [];
|
||||||
private readonly List<FileRegistry> _shpk = [];
|
private readonly List<FileRegistry> _shpk = [];
|
||||||
|
private readonly List<FileRegistry> _pbd = [];
|
||||||
|
|
||||||
private readonly SortedSet<FullPath> _missing = [];
|
private readonly SortedSet<FullPath> _missing = [];
|
||||||
private readonly HashSet<Utf8GamePath> _usedPaths = [];
|
private readonly HashSet<Utf8GamePath> _usedPaths = [];
|
||||||
|
|
@ -23,19 +24,22 @@ public class ModFileCollection : IDisposable, IService
|
||||||
=> Ready ? _usedPaths : [];
|
=> Ready ? _usedPaths : [];
|
||||||
|
|
||||||
public IReadOnlyList<FileRegistry> Available
|
public IReadOnlyList<FileRegistry> Available
|
||||||
=> Ready ? _available : Array.Empty<FileRegistry>();
|
=> Ready ? _available : [];
|
||||||
|
|
||||||
public IReadOnlyList<FileRegistry> Mtrl
|
public IReadOnlyList<FileRegistry> Mtrl
|
||||||
=> Ready ? _mtrl : Array.Empty<FileRegistry>();
|
=> Ready ? _mtrl : [];
|
||||||
|
|
||||||
public IReadOnlyList<FileRegistry> Mdl
|
public IReadOnlyList<FileRegistry> Mdl
|
||||||
=> Ready ? _mdl : Array.Empty<FileRegistry>();
|
=> Ready ? _mdl : [];
|
||||||
|
|
||||||
public IReadOnlyList<FileRegistry> Tex
|
public IReadOnlyList<FileRegistry> Tex
|
||||||
=> Ready ? _tex : Array.Empty<FileRegistry>();
|
=> Ready ? _tex : [];
|
||||||
|
|
||||||
public IReadOnlyList<FileRegistry> Shpk
|
public IReadOnlyList<FileRegistry> Shpk
|
||||||
=> Ready ? _shpk : Array.Empty<FileRegistry>();
|
=> Ready ? _shpk : [];
|
||||||
|
|
||||||
|
public IReadOnlyList<FileRegistry> Pbd
|
||||||
|
=> Ready ? _pbd : [];
|
||||||
|
|
||||||
public bool Ready { get; private set; } = true;
|
public bool Ready { get; private set; } = true;
|
||||||
|
|
||||||
|
|
@ -128,6 +132,9 @@ public class ModFileCollection : IDisposable, IService
|
||||||
case ".shpk":
|
case ".shpk":
|
||||||
_shpk.Add(registry);
|
_shpk.Add(registry);
|
||||||
break;
|
break;
|
||||||
|
case ".pbd":
|
||||||
|
_pbd.Add(registry);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -139,6 +146,7 @@ public class ModFileCollection : IDisposable, IService
|
||||||
_mdl.Clear();
|
_mdl.Clear();
|
||||||
_tex.Clear();
|
_tex.Clear();
|
||||||
_shpk.Clear();
|
_shpk.Clear();
|
||||||
|
_pbd.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ClearPaths(bool clearRegistries, CancellationToken tok)
|
private void ClearPaths(bool clearRegistries, CancellationToken tok)
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu
|
||||||
if (deletions <= 0)
|
if (deletions <= 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
modManager.Creator.ReloadMod(mod, false, out _);
|
modManager.Creator.ReloadMod(mod, false, false, out _);
|
||||||
files.UpdateAll(mod, option);
|
files.UpdateAll(mod, option);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,22 +10,21 @@ using Penumbra.Mods.Manager.OptionEditor;
|
||||||
using Penumbra.Mods.SubMods;
|
using Penumbra.Mods.SubMods;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
using Penumbra.String.Classes;
|
using Penumbra.String.Classes;
|
||||||
using Penumbra.UI.ModsTab;
|
|
||||||
|
|
||||||
namespace Penumbra.Mods.Editor;
|
namespace Penumbra.Mods.Editor;
|
||||||
|
|
||||||
public class ModMerger : IDisposable, IService
|
public class ModMerger : IDisposable, IService
|
||||||
{
|
{
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
private readonly CommunicatorService _communicator;
|
private readonly CommunicatorService _communicator;
|
||||||
private readonly ModGroupEditor _editor;
|
private readonly ModGroupEditor _editor;
|
||||||
private readonly ModFileSystemSelector _selector;
|
private readonly ModSelection _selection;
|
||||||
private readonly DuplicateManager _duplicates;
|
private readonly DuplicateManager _duplicates;
|
||||||
private readonly ModManager _mods;
|
private readonly ModManager _mods;
|
||||||
private readonly ModCreator _creator;
|
private readonly ModCreator _creator;
|
||||||
|
|
||||||
public Mod? MergeFromMod
|
public Mod? MergeFromMod
|
||||||
=> _selector.Selected;
|
=> _selection.Mod;
|
||||||
|
|
||||||
public Mod? MergeToMod;
|
public Mod? MergeToMod;
|
||||||
public string OptionGroupName = "Merges";
|
public string OptionGroupName = "Merges";
|
||||||
|
|
@ -41,23 +40,23 @@ public class ModMerger : IDisposable, IService
|
||||||
public readonly IReadOnlyList<string> Warnings = new List<string>();
|
public readonly IReadOnlyList<string> Warnings = new List<string>();
|
||||||
public Exception? Error { get; private set; }
|
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)
|
CommunicatorService communicator, ModCreator creator, Configuration config)
|
||||||
{
|
{
|
||||||
_editor = editor;
|
_editor = editor;
|
||||||
_selector = selector;
|
_selection = selection;
|
||||||
_duplicates = duplicates;
|
_duplicates = duplicates;
|
||||||
_communicator = communicator;
|
_communicator = communicator;
|
||||||
_creator = creator;
|
_creator = creator;
|
||||||
_config = config;
|
_config = config;
|
||||||
_mods = mods;
|
_mods = mods;
|
||||||
_selector.SelectionChanged += OnSelectionChange;
|
_selection.Subscribe(OnSelectionChange, ModSelection.Priority.ModMerger);
|
||||||
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModMerger);
|
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModMerger);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_selector.SelectionChanged -= OnSelectionChange;
|
_selection.Unsubscribe(OnSelectionChange);
|
||||||
_communicator.ModPathChanged.Unsubscribe(OnModPathChange);
|
_communicator.ModPathChanged.Unsubscribe(OnModPathChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -257,7 +256,7 @@ public class ModMerger : IDisposable, IService
|
||||||
if (dir == null)
|
if (dir == null)
|
||||||
throw new Exception($"Could not split off mods, unable to create new mod with name {modName}.");
|
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];
|
result = _mods[^1];
|
||||||
if (mods.Count == 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)
|
if (OptionGroupName == "Merges" && OptionName.Length == 0 || OptionName == oldSelection?.Name.Text)
|
||||||
OptionName = newSelection?.Name.Text ?? string.Empty;
|
OptionName = newSelection?.Name.Text ?? string.Empty;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
using System.Collections.Frozen;
|
using System.Collections.Frozen;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
|
using Penumbra.Meta;
|
||||||
|
using Penumbra.Meta.Files;
|
||||||
using Penumbra.Meta.Manipulations;
|
using Penumbra.Meta.Manipulations;
|
||||||
using Penumbra.Mods.Manager;
|
using Penumbra.Mods.Manager.OptionEditor;
|
||||||
using Penumbra.Mods.SubMods;
|
using Penumbra.Mods.SubMods;
|
||||||
|
|
||||||
namespace Penumbra.Mods.Editor;
|
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>
|
public sealed class OtherOptionData : HashSet<string>
|
||||||
{
|
{
|
||||||
|
|
@ -62,12 +66,111 @@ public class ModMetaEditor(ModManager modManager) : MetaDictionary, IService
|
||||||
Changes = false;
|
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)
|
public void Apply(IModDataContainer container)
|
||||||
{
|
{
|
||||||
if (!Changes)
|
if (!Changes)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
modManager.OptionEditor.SetManipulations(container, this);
|
groupEditor.SetManipulations(container, this);
|
||||||
Changes = false;
|
Changes = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ public class ModNormalizer(ModManager modManager, Configuration config, SaveServ
|
||||||
if (!config.AutoReduplicateUiOnImport)
|
if (!config.AutoReduplicateUiOnImport)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (modManager.Creator.LoadMod(modDirectory, false) is not { } mod)
|
if (modManager.Creator.LoadMod(modDirectory, false, false) is not { } mod)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Dictionary<FullPath, List<(IModDataContainer, Utf8GamePath)>> paths = [];
|
Dictionary<FullPath, List<(IModDataContainer, Utf8GamePath)>> paths = [];
|
||||||
|
|
|
||||||
|
|
@ -24,14 +24,21 @@ public interface IModGroup
|
||||||
{
|
{
|
||||||
public const int MaxMultiOptions = 32;
|
public const int MaxMultiOptions = 32;
|
||||||
|
|
||||||
public Mod Mod { get; }
|
public Mod Mod { get; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string Description { get; set; }
|
public string Description { get; set; }
|
||||||
public string Image { get; set; }
|
|
||||||
public GroupType Type { get; }
|
/// <summary> Unused in Penumbra but for better TexTools interop. </summary>
|
||||||
public GroupDrawBehaviour Behaviour { get; }
|
public string Image { get; set; }
|
||||||
public ModPriority Priority { get; set; }
|
|
||||||
public Setting DefaultSettings { 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 FullPath? FindBestMatch(Utf8GamePath gamePath);
|
||||||
public IModOption? AddOption(string name, string description = "");
|
public IModOption? AddOption(string name, string description = "");
|
||||||
|
|
|
||||||
|
|
@ -29,11 +29,13 @@ public class ImcModGroup(Mod mod) : IModGroup
|
||||||
=> GroupDrawBehaviour.MultiSelection;
|
=> GroupDrawBehaviour.MultiSelection;
|
||||||
|
|
||||||
public ModPriority Priority { get; set; } = ModPriority.Default;
|
public ModPriority Priority { get; set; } = ModPriority.Default;
|
||||||
|
public int Page { get; set; }
|
||||||
public Setting DefaultSettings { get; set; } = Setting.Zero;
|
public Setting DefaultSettings { get; set; } = Setting.Zero;
|
||||||
|
|
||||||
public ImcIdentifier Identifier;
|
public ImcIdentifier Identifier;
|
||||||
public ImcEntry DefaultEntry;
|
public ImcEntry DefaultEntry;
|
||||||
public bool AllVariants;
|
public bool AllVariants;
|
||||||
|
public bool OnlyAttributes;
|
||||||
|
|
||||||
|
|
||||||
public FullPath? FindBestMatch(Utf8GamePath gamePath)
|
public FullPath? FindBestMatch(Utf8GamePath gamePath)
|
||||||
|
|
@ -96,28 +98,36 @@ public class ImcModGroup(Mod mod) : IModGroup
|
||||||
public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer)
|
public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer)
|
||||||
=> new ImcModGroupEditDrawer(editDrawer, this);
|
=> new ImcModGroupEditDrawer(editDrawer, this);
|
||||||
|
|
||||||
public ImcEntry GetEntry(ushort mask)
|
private ImcEntry GetEntry(Variant variant, ushort mask)
|
||||||
=> DefaultEntry with { AttributeMask = 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)
|
public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, MetaDictionary manipulations)
|
||||||
{
|
{
|
||||||
if (IsDisabled(setting))
|
if (IsDisabled(setting))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var mask = GetCurrentMask(setting);
|
var mask = GetCurrentMask(setting);
|
||||||
var entry = GetEntry(mask);
|
|
||||||
if (AllVariants)
|
if (AllVariants)
|
||||||
{
|
{
|
||||||
var count = ImcChecker.GetVariantCount(Identifier);
|
var count = ImcChecker.GetVariantCount(Identifier);
|
||||||
if (count == 0)
|
if (count == 0)
|
||||||
manipulations.TryAdd(Identifier, entry);
|
manipulations.TryAdd(Identifier, GetEntry(Identifier.Variant, mask));
|
||||||
else
|
else
|
||||||
for (var i = 0; i <= count; ++i)
|
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
|
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);
|
serializer.Serialize(jWriter, DefaultEntry);
|
||||||
jWriter.WritePropertyName(nameof(AllVariants));
|
jWriter.WritePropertyName(nameof(AllVariants));
|
||||||
jWriter.WriteValue(AllVariants);
|
jWriter.WriteValue(AllVariants);
|
||||||
|
jWriter.WritePropertyName(nameof(OnlyAttributes));
|
||||||
|
jWriter.WriteValue(OnlyAttributes);
|
||||||
jWriter.WritePropertyName("Options");
|
jWriter.WritePropertyName("Options");
|
||||||
jWriter.WriteStartArray();
|
jWriter.WriteStartArray();
|
||||||
foreach (var option in OptionData)
|
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 identifier = ImcIdentifier.FromJson(json[nameof(Identifier)] as JObject);
|
||||||
var ret = new ImcModGroup(mod)
|
var ret = new ImcModGroup(mod)
|
||||||
{
|
{
|
||||||
Name = json[nameof(Name)]?.ToObject<string>() ?? string.Empty,
|
DefaultEntry = json[nameof(DefaultEntry)]?.ToObject<ImcEntry>() ?? new ImcEntry(),
|
||||||
Description = json[nameof(Description)]?.ToObject<string>() ?? string.Empty,
|
AllVariants = json[nameof(AllVariants)]?.ToObject<bool>() ?? false,
|
||||||
Image = json[nameof(Image)]?.ToObject<string>() ?? string.Empty,
|
OnlyAttributes = json[nameof(OnlyAttributes)]?.ToObject<bool>() ?? false,
|
||||||
Priority = json[nameof(Priority)]?.ToObject<ModPriority>() ?? ModPriority.Default,
|
|
||||||
DefaultEntry = json[nameof(DefaultEntry)]?.ToObject<ImcEntry>() ?? new ImcEntry(),
|
|
||||||
AllVariants = json[nameof(AllVariants)]?.ToObject<bool>() ?? false,
|
|
||||||
};
|
};
|
||||||
if (ret.Name.Length == 0)
|
if (!ModSaveGroup.ReadJsonBase(json, ret))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (!identifier.HasValue || ret.DefaultEntry.MaterialId == 0)
|
if (!identifier.HasValue || ret.DefaultEntry.MaterialId == 0)
|
||||||
|
|
@ -215,7 +224,6 @@ public class ImcModGroup(Mod mod) : IModGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.Identifier = identifier.Value;
|
ret.Identifier = identifier.Value;
|
||||||
ret.DefaultSettings = json[nameof(DefaultSettings)]?.ToObject<Setting>() ?? Setting.Zero;
|
|
||||||
ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings);
|
ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Penumbra.GameData.Files.ShaderStructs;
|
||||||
|
using Penumbra.Mods.Settings;
|
||||||
using Penumbra.Mods.SubMods;
|
using Penumbra.Mods.SubMods;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
|
|
||||||
|
|
@ -90,6 +93,8 @@ public readonly struct ModSaveGroup : ISavable
|
||||||
jWriter.WriteValue(group.Description);
|
jWriter.WriteValue(group.Description);
|
||||||
jWriter.WritePropertyName(nameof(group.Image));
|
jWriter.WritePropertyName(nameof(group.Image));
|
||||||
jWriter.WriteValue(group.Image);
|
jWriter.WriteValue(group.Image);
|
||||||
|
jWriter.WritePropertyName(nameof(group.Page));
|
||||||
|
jWriter.WriteValue(group.Page);
|
||||||
jWriter.WritePropertyName(nameof(group.Priority));
|
jWriter.WritePropertyName(nameof(group.Priority));
|
||||||
jWriter.WriteValue(group.Priority.Value);
|
jWriter.WriteValue(group.Priority.Value);
|
||||||
jWriter.WritePropertyName(nameof(group.Type));
|
jWriter.WritePropertyName(nameof(group.Type));
|
||||||
|
|
@ -97,4 +102,16 @@ public readonly struct ModSaveGroup : ISavable
|
||||||
jWriter.WritePropertyName(nameof(group.DefaultSettings));
|
jWriter.WritePropertyName(nameof(group.DefaultSettings));
|
||||||
jWriter.WriteValue(group.DefaultSettings.Value);
|
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 Description { get; set; } = string.Empty;
|
||||||
public string Image { get; set; } = string.Empty;
|
public string Image { get; set; } = string.Empty;
|
||||||
public ModPriority Priority { get; set; }
|
public ModPriority Priority { get; set; }
|
||||||
|
public int Page { get; set; }
|
||||||
public Setting DefaultSettings { get; set; }
|
public Setting DefaultSettings { get; set; }
|
||||||
public readonly List<MultiSubMod> OptionData = [];
|
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)
|
public static MultiModGroup? Load(Mod mod, JObject json)
|
||||||
{
|
{
|
||||||
var ret = new MultiModGroup(mod)
|
var ret = new MultiModGroup(mod);
|
||||||
{
|
if (!ModSaveGroup.ReadJsonBase(json, ret))
|
||||||
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)
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var options = json["Options"];
|
var options = json["Options"];
|
||||||
|
|
@ -105,6 +99,8 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup
|
||||||
Name = Name,
|
Name = Name,
|
||||||
Description = Description,
|
Description = Description,
|
||||||
Priority = Priority,
|
Priority = Priority,
|
||||||
|
Image = Image,
|
||||||
|
Page = Page,
|
||||||
DefaultSettings = DefaultSettings.TurnMulti(OptionData.Count),
|
DefaultSettings = DefaultSettings.TurnMulti(OptionData.Count),
|
||||||
};
|
};
|
||||||
single.OptionData.AddRange(OptionData.Select(o => o.ConvertToSingle(single)));
|
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 Description { get; set; } = string.Empty;
|
||||||
public string Image { get; set; } = string.Empty;
|
public string Image { get; set; } = string.Empty;
|
||||||
public ModPriority Priority { get; set; }
|
public ModPriority Priority { get; set; }
|
||||||
|
public int Page { get; set; }
|
||||||
public Setting DefaultSettings { get; set; }
|
public Setting DefaultSettings { get; set; }
|
||||||
|
|
||||||
public readonly List<SingleSubMod> OptionData = [];
|
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)
|
public static SingleModGroup? Load(Mod mod, JObject json)
|
||||||
{
|
{
|
||||||
var options = json["Options"];
|
var options = json["Options"];
|
||||||
var ret = new SingleModGroup(mod)
|
var ret = new SingleModGroup(mod);
|
||||||
{
|
if (!ModSaveGroup.ReadJsonBase(json, ret))
|
||||||
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)
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (options != null)
|
if (options != null)
|
||||||
|
|
@ -91,6 +85,8 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup
|
||||||
Name = Name,
|
Name = Name,
|
||||||
Description = Description,
|
Description = Description,
|
||||||
Priority = Priority,
|
Priority = Priority,
|
||||||
|
Image = Image,
|
||||||
|
Page = Page,
|
||||||
DefaultSettings = Setting.Multi((int)DefaultSettings.Value),
|
DefaultSettings = Setting.Multi((int)DefaultSettings.Value),
|
||||||
};
|
};
|
||||||
multi.OptionData.AddRange(OptionData.Select((o, i) => o.ConvertToMulti(multi, new ModPriority(i))));
|
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)
|
EquipSlot slotFrom, EquipItem itemFrom, EquipSlot slotTo, EquipItem itemTo)
|
||||||
{
|
{
|
||||||
LookupItem(itemFrom, out var actualSlotFrom, out var idFrom, out var variantFrom);
|
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())
|
if (actualSlotFrom != slotFrom.ToSlot() || actualSlotTo != slotTo.ToSlot())
|
||||||
throw new ItemSwap.InvalidItemTypeException();
|
throw new ItemSwap.InvalidItemTypeException();
|
||||||
|
|
||||||
var (imcFileFrom, variants, affectedItems) = GetVariants(manager, identifier, slotFrom, idFrom, idTo, variantFrom);
|
var (imcFileFrom, variants, affectedItems) = GetVariants(manager, identifier, slotFrom, idFrom, idTo, variantFrom);
|
||||||
var imcIdentifierTo = new ImcIdentifier(slotTo, idTo, variantTo);
|
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)
|
var imcEntry = manips.TryGetValue(imcIdentifierTo, out var entry)
|
||||||
? entry
|
? entry
|
||||||
: imcFileTo.GetEntry(imcIdentifierTo.EquipSlot, imcIdentifierTo.Variant);
|
: imcFileTo.GetEntry(imcIdentifierTo.EquipSlot, imcIdentifierTo.Variant);
|
||||||
var mtrlVariantTo = imcEntry.MaterialId;
|
var mtrlVariantTo = imcEntry.MaterialId;
|
||||||
var skipFemale = false;
|
var skipFemale = false;
|
||||||
var skipMale = false;
|
var skipMale = false;
|
||||||
foreach (var gr in Enum.GetValues<GenderRace>())
|
foreach (var gr in Enum.GetValues<GenderRace>())
|
||||||
{
|
{
|
||||||
switch (gr.Split().Item1)
|
switch (gr.Split().Item1)
|
||||||
{
|
{
|
||||||
case Gender.Male when skipMale: continue;
|
case Gender.Male when skipMale: continue;
|
||||||
case Gender.Female when skipFemale: continue;
|
case Gender.Female when skipFemale: continue;
|
||||||
case Gender.MaleNpc when skipMale: continue;
|
case Gender.MaleNpc when skipMale: continue;
|
||||||
case Gender.FemaleNpc when skipFemale: 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.
|
// 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(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)
|
if (slotFrom != slotTo)
|
||||||
throw new ItemSwap.InvalidItemTypeException();
|
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 imcFileFrom, var variants, affectedItems) = GetVariants(manager, identifier, slot, idFrom, idTo, variantFrom);
|
||||||
var imcIdentifierTo = new ImcIdentifier(slotTo, idTo, variantTo);
|
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)
|
var imcEntry = manips.TryGetValue(imcIdentifierTo, out var entry)
|
||||||
? entry
|
? entry
|
||||||
: imcFileTo.GetEntry(imcIdentifierTo.EquipSlot, imcIdentifierTo.Variant);
|
: imcFileTo.GetEntry(imcIdentifierTo.EquipSlot, imcIdentifierTo.Variant);
|
||||||
|
|
@ -122,18 +122,18 @@ public static class EquipmentSwap
|
||||||
{
|
{
|
||||||
EquipSlot.Head => EstType.Head,
|
EquipSlot.Head => EstType.Head,
|
||||||
EquipSlot.Body => EstType.Body,
|
EquipSlot.Body => EstType.Body,
|
||||||
_ => (EstType)0,
|
_ => (EstType)0,
|
||||||
};
|
};
|
||||||
|
|
||||||
var skipFemale = false;
|
var skipFemale = false;
|
||||||
var skipMale = false;
|
var skipMale = false;
|
||||||
foreach (var gr in Enum.GetValues<GenderRace>())
|
foreach (var gr in Enum.GetValues<GenderRace>())
|
||||||
{
|
{
|
||||||
switch (gr.Split().Item1)
|
switch (gr.Split().Item1)
|
||||||
{
|
{
|
||||||
case Gender.Male when skipMale: continue;
|
case Gender.Male when skipMale: continue;
|
||||||
case Gender.Female when skipFemale: continue;
|
case Gender.Female when skipFemale: continue;
|
||||||
case Gender.MaleNpc when skipMale: continue;
|
case Gender.MaleNpc when skipMale: continue;
|
||||||
case Gender.FemaleNpc when skipFemale: continue;
|
case Gender.FemaleNpc when skipFemale: continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -148,7 +148,7 @@ public static class EquipmentSwap
|
||||||
swaps.Add(eqdp);
|
swaps.Add(eqdp);
|
||||||
|
|
||||||
var ownMdl = eqdp?.SwapToModdedEntry.Model ?? false;
|
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)
|
if (est != null)
|
||||||
swaps.Add(est);
|
swaps.Add(est);
|
||||||
}
|
}
|
||||||
|
|
@ -176,7 +176,6 @@ public static class EquipmentSwap
|
||||||
|
|
||||||
return affectedItems;
|
return affectedItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MetaSwap<EqdpIdentifier, EqdpEntryInternal>? CreateEqdp(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections,
|
public static MetaSwap<EqdpIdentifier, EqdpEntryInternal>? CreateEqdp(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections,
|
||||||
MetaDictionary manips, EquipSlot slot, GenderRace gr, PrimaryId idFrom, PrimaryId idTo, byte mtrlTo)
|
MetaDictionary manips, EquipSlot slot, GenderRace gr, PrimaryId idFrom, PrimaryId idTo, byte mtrlTo)
|
||||||
=> CreateEqdp(manager, redirections, manips, slot, slot, gr, idFrom, idTo, mtrlTo);
|
=> CreateEqdp(manager, redirections, manips, slot, slot, gr, idFrom, idTo, mtrlTo);
|
||||||
|
|
@ -186,9 +185,9 @@ public static class EquipmentSwap
|
||||||
PrimaryId idTo, byte mtrlTo)
|
PrimaryId idTo, byte mtrlTo)
|
||||||
{
|
{
|
||||||
var eqdpFromIdentifier = new EqdpIdentifier(idFrom, slotFrom, gr);
|
var eqdpFromIdentifier = new EqdpIdentifier(idFrom, slotFrom, gr);
|
||||||
var eqdpToIdentifier = new EqdpIdentifier(idTo, slotTo, gr);
|
var eqdpToIdentifier = new EqdpIdentifier(idTo, slotTo, gr);
|
||||||
var eqdpFromDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpFromIdentifier), slotFrom);
|
var eqdpFromDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpFromIdentifier), slotFrom);
|
||||||
var eqdpToDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpToIdentifier), slotTo);
|
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,
|
var meta = new MetaSwap<EqdpIdentifier, EqdpEntryInternal>(i => manips.TryGetValue(i, out var e) ? e : null, eqdpFromIdentifier,
|
||||||
eqdpFromDefault, eqdpToIdentifier,
|
eqdpFromDefault, eqdpToIdentifier,
|
||||||
eqdpToDefault);
|
eqdpToDefault);
|
||||||
|
|
@ -217,7 +216,7 @@ public static class EquipmentSwap
|
||||||
? GamePaths.Accessory.Mdl.Path(idFrom, gr, slotFrom)
|
? GamePaths.Accessory.Mdl.Path(idFrom, gr, slotFrom)
|
||||||
: GamePaths.Equipment.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 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())
|
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,
|
private static (ImcFile, Variant[], EquipItem[]) GetVariants(MetaFileManager manager, ObjectIdentification identifier, EquipSlot slotFrom,
|
||||||
PrimaryId idFrom, PrimaryId idTo, Variant variantFrom)
|
PrimaryId idFrom, PrimaryId idTo, Variant variantFrom)
|
||||||
{
|
{
|
||||||
var ident = new ImcIdentifier(slotFrom, idFrom, variantFrom);
|
var ident = new ImcIdentifier(slotFrom, idFrom, variantFrom);
|
||||||
var imc = new ImcFile(manager, ident);
|
var imc = new ImcFile(manager, ident);
|
||||||
EquipItem[] items;
|
EquipItem[] items;
|
||||||
Variant[] variants;
|
Variant[] variants;
|
||||||
if (idFrom == idTo)
|
if (idFrom == idTo)
|
||||||
{
|
{
|
||||||
items = identifier.Identify(idFrom, 0, variantFrom, slotFrom).ToArray();
|
items = identifier.Identify(idFrom, 0, variantFrom, slotFrom).ToArray();
|
||||||
variants = [variantFrom];
|
variants = [variantFrom];
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -271,9 +270,9 @@ public static class EquipmentSwap
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var manipFromIdentifier = new GmpIdentifier(idFrom);
|
var manipFromIdentifier = new GmpIdentifier(idFrom);
|
||||||
var manipToIdentifier = new GmpIdentifier(idTo);
|
var manipToIdentifier = new GmpIdentifier(idTo);
|
||||||
var manipFromDefault = ExpandedGmpFile.GetDefault(manager, manipFromIdentifier);
|
var manipFromDefault = ExpandedGmpFile.GetDefault(manager, manipFromIdentifier);
|
||||||
var manipToDefault = ExpandedGmpFile.GetDefault(manager, manipToIdentifier);
|
var manipToDefault = ExpandedGmpFile.GetDefault(manager, manipToIdentifier);
|
||||||
return new MetaSwap<GmpIdentifier, GmpEntry>(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault,
|
return new MetaSwap<GmpIdentifier, GmpEntry>(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault,
|
||||||
manipToIdentifier, manipToDefault);
|
manipToIdentifier, manipToDefault);
|
||||||
}
|
}
|
||||||
|
|
@ -288,9 +287,9 @@ public static class EquipmentSwap
|
||||||
Variant variantFrom, Variant variantTo, ImcFile imcFileFrom, ImcFile imcFileTo)
|
Variant variantFrom, Variant variantTo, ImcFile imcFileFrom, ImcFile imcFileTo)
|
||||||
{
|
{
|
||||||
var manipFromIdentifier = new ImcIdentifier(slotFrom, idFrom, variantFrom);
|
var manipFromIdentifier = new ImcIdentifier(slotFrom, idFrom, variantFrom);
|
||||||
var manipToIdentifier = new ImcIdentifier(slotTo, idTo, variantTo);
|
var manipToIdentifier = new ImcIdentifier(slotTo, idTo, variantTo);
|
||||||
var manipFromDefault = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom);
|
var manipFromDefault = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom);
|
||||||
var manipToDefault = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo);
|
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,
|
var imc = new MetaSwap<ImcIdentifier, ImcEntry>(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault,
|
||||||
manipToIdentifier, manipToDefault);
|
manipToIdentifier, manipToDefault);
|
||||||
|
|
||||||
|
|
@ -329,7 +328,7 @@ public static class EquipmentSwap
|
||||||
var vfxPathFrom = GamePaths.Equipment.Avfx.Path(idFrom, vfxId);
|
var vfxPathFrom = GamePaths.Equipment.Avfx.Path(idFrom, vfxId);
|
||||||
vfxPathFrom = ItemSwap.ReplaceType(vfxPathFrom, slotFrom, slotTo, idFrom);
|
vfxPathFrom = ItemSwap.ReplaceType(vfxPathFrom, slotFrom, slotTo, idFrom);
|
||||||
var vfxPathTo = GamePaths.Equipment.Avfx.Path(idTo, vfxId);
|
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())
|
foreach (ref var filePath in avfx.AsAvfx()!.Textures.AsSpan())
|
||||||
{
|
{
|
||||||
|
|
@ -347,9 +346,9 @@ public static class EquipmentSwap
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var manipFromIdentifier = new EqpIdentifier(idFrom, slot);
|
var manipFromIdentifier = new EqpIdentifier(idFrom, slot);
|
||||||
var manipToIdentifier = new EqpIdentifier(idTo, slot);
|
var manipToIdentifier = new EqpIdentifier(idTo, slot);
|
||||||
var manipFromDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idFrom), slot);
|
var manipFromDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idFrom), slot);
|
||||||
var manipToDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idTo), 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,
|
return new MetaSwap<EqpIdentifier, EqpEntryInternal>(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier,
|
||||||
manipFromDefault, manipToIdentifier, manipToDefault);
|
manipFromDefault, manipToIdentifier, manipToDefault);
|
||||||
}
|
}
|
||||||
|
|
@ -381,7 +380,7 @@ public static class EquipmentSwap
|
||||||
|
|
||||||
if (newFileName != fileName)
|
if (newFileName != fileName)
|
||||||
{
|
{
|
||||||
fileName = newFileName;
|
fileName = newFileName;
|
||||||
dataWasChanged = true;
|
dataWasChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -406,13 +405,13 @@ public static class EquipmentSwap
|
||||||
EquipSlot slotTo, PrimaryId idFrom, PrimaryId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged)
|
EquipSlot slotTo, PrimaryId idFrom, PrimaryId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged)
|
||||||
{
|
{
|
||||||
var addedDashes = GamePaths.Tex.HandleDx11Path(texture, out var path);
|
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.ReplaceSlot(newPath, slotTo, slotFrom, slotTo != slotFrom);
|
||||||
newPath = ItemSwap.ReplaceType(newPath, slotFrom, slotTo, idFrom);
|
newPath = ItemSwap.ReplaceType(newPath, slotFrom, slotTo, idFrom);
|
||||||
newPath = ItemSwap.AddSuffix(newPath, ".tex", $"_{Path.GetFileName(texture.Path).GetStableHashCode():x8}");
|
newPath = ItemSwap.AddSuffix(newPath, ".tex", $"_{Path.GetFileName(texture.Path).GetStableHashCode():x8}");
|
||||||
if (newPath != path)
|
if (newPath != path)
|
||||||
{
|
{
|
||||||
texture.Path = addedDashes ? newPath.Replace("--", string.Empty) : newPath;
|
texture.Path = addedDashes ? newPath.Replace("--", string.Empty) : newPath;
|
||||||
dataWasChanged = true;
|
dataWasChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -430,8 +429,8 @@ public static class EquipmentSwap
|
||||||
PrimaryId idFrom, ref string filePath, ref bool dataWasChanged)
|
PrimaryId idFrom, ref string filePath, ref bool dataWasChanged)
|
||||||
{
|
{
|
||||||
var oldPath = filePath;
|
var oldPath = filePath;
|
||||||
filePath = ItemSwap.AddSuffix(filePath, ".atex", $"_{Path.GetFileName(filePath).GetStableHashCode():x8}");
|
filePath = ItemSwap.AddSuffix(filePath, ".atex", $"_{Path.GetFileName(filePath).GetStableHashCode():x8}");
|
||||||
filePath = ItemSwap.ReplaceType(filePath, slotFrom, slotTo, idFrom);
|
filePath = ItemSwap.ReplaceType(filePath, slotFrom, slotTo, idFrom);
|
||||||
dataWasChanged = true;
|
dataWasChanged = true;
|
||||||
|
|
||||||
return FileSwap.CreateSwap(manager, ResourceType.Atex, redirections, filePath, oldPath, oldPath);
|
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.Description = description ?? mod.Description;
|
||||||
mod.Version = version ?? mod.Version;
|
mod.Version = version ?? mod.Version;
|
||||||
mod.Website = website ?? mod.Website;
|
mod.Website = website ?? mod.Website;
|
||||||
saveService.ImmediateSave(new ModMeta(mod));
|
saveService.ImmediateSaveSync(new ModMeta(mod));
|
||||||
}
|
}
|
||||||
|
|
||||||
public ModDataChangeType LoadLocalData(Mod mod)
|
public ModDataChangeType LoadLocalData(Mod mod)
|
||||||
|
|
@ -249,6 +249,17 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic
|
||||||
communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null);
|
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)
|
public void ChangeModNote(Mod mod, string newNote)
|
||||||
{
|
{
|
||||||
if (mod.Note == newNote)
|
if (mod.Note == newNote)
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ public class ModImportManager(ModManager modManager, Configuration config, ModEd
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
modManager.AddMod(directory);
|
modManager.AddMod(directory, true);
|
||||||
mod = modManager.LastOrDefault();
|
mod = modManager.LastOrDefault();
|
||||||
return mod != null && mod.ModPath == directory;
|
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>
|
/// <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))
|
if (this.Any(m => m.ModPath.Name == modFolder.Name))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Creator.SplitMultiGroups(modFolder);
|
Creator.SplitMultiGroups(modFolder);
|
||||||
var mod = Creator.LoadMod(modFolder, true);
|
var mod = Creator.LoadMod(modFolder, true, deleteDefaultMeta);
|
||||||
if (mod == null)
|
if (mod == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
@ -141,7 +141,7 @@ public sealed class ModManager : ModStorage, IDisposable, IService
|
||||||
var oldName = mod.Name;
|
var oldName = mod.Name;
|
||||||
|
|
||||||
_communicator.ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath);
|
_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
|
Penumbra.Log.Warning(mod.Name.Length == 0
|
||||||
? $"Reloading mod {oldName} has failed, new name is empty. Removing from loaded mods instead."
|
? $"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();
|
dir.Refresh();
|
||||||
mod.ModPath = dir;
|
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}.");
|
Penumbra.Log.Error($"Error reloading moved mod {mod.Name}.");
|
||||||
return;
|
return;
|
||||||
|
|
@ -332,7 +332,7 @@ public sealed class ModManager : ModStorage, IDisposable, IService
|
||||||
var queue = new ConcurrentQueue<Mod>();
|
var queue = new ConcurrentQueue<Mod>();
|
||||||
Parallel.ForEach(BasePath.EnumerateDirectories(), options, dir =>
|
Parallel.ForEach(BasePath.EnumerateDirectories(), options, dir =>
|
||||||
{
|
{
|
||||||
var mod = Creator.LoadMod(dir, false);
|
var mod = Creator.LoadMod(dir, false, false);
|
||||||
if (mod != null)
|
if (mod != null)
|
||||||
queue.Enqueue(mod);
|
queue.Enqueue(mod);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ public static partial class ModMigration
|
||||||
foreach (var (gamePath, swapPath) in swaps)
|
foreach (var (gamePath, swapPath) in swaps)
|
||||||
mod.Default.FileSwaps.Add(gamePath, swapPath);
|
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)
|
foreach (var group in mod.Groups)
|
||||||
saveService.ImmediateSave(new ModSaveGroup(group, creator.Config.ReplaceNonAsciiOnImport));
|
saveService.ImmediateSave(new ModSaveGroup(group, creator.Config.ReplaceNonAsciiOnImport));
|
||||||
|
|
||||||
|
|
@ -182,7 +182,7 @@ public static partial class ModMigration
|
||||||
Description = option.OptionDesc,
|
Description = option.OptionDesc,
|
||||||
};
|
};
|
||||||
AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles);
|
AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles);
|
||||||
creator.IncorporateMetaChanges(subMod, mod.ModPath, false);
|
creator.IncorporateMetaChanges(subMod, mod.ModPath, false, true);
|
||||||
return subMod;
|
return subMod;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,7 +196,7 @@ public static partial class ModMigration
|
||||||
Priority = priority,
|
Priority = priority,
|
||||||
};
|
};
|
||||||
AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles);
|
AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles);
|
||||||
creator.IncorporateMetaChanges(subMod, mod.ModPath, false);
|
creator.IncorporateMetaChanges(subMod, mod.ModPath, false, true);
|
||||||
return subMod;
|
return subMod;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ using OtterGui.Classes;
|
||||||
using OtterGui.Filesystem;
|
using OtterGui.Filesystem;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.GameData.Structs;
|
||||||
using Penumbra.Meta;
|
|
||||||
using Penumbra.Meta.Manipulations;
|
using Penumbra.Meta.Manipulations;
|
||||||
using Penumbra.Mods.Groups;
|
using Penumbra.Mods.Groups;
|
||||||
using Penumbra.Mods.Settings;
|
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);
|
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)
|
public void ChangeCanBeDisabled(ImcModGroup group, bool canBeDisabled, SaveType saveType = SaveType.Queue)
|
||||||
{
|
{
|
||||||
if (group.CanBeDisabled == canBeDisabled)
|
if (group.CanBeDisabled == canBeDisabled)
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ public class ModGroupEditor(
|
||||||
ImcModGroupEditor imcEditor,
|
ImcModGroupEditor imcEditor,
|
||||||
CommunicatorService communicator,
|
CommunicatorService communicator,
|
||||||
SaveService saveService,
|
SaveService saveService,
|
||||||
Configuration Config) : IService
|
Configuration config) : IService
|
||||||
{
|
{
|
||||||
public SingleModGroupEditor SingleEditor
|
public SingleModGroupEditor SingleEditor
|
||||||
=> singleEditor;
|
=> singleEditor;
|
||||||
|
|
@ -57,7 +57,7 @@ public class ModGroupEditor(
|
||||||
return;
|
return;
|
||||||
|
|
||||||
group.DefaultSettings = defaultOption;
|
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);
|
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))
|
if (oldName == newName || !VerifyFileName(group.Mod, group, newName, true))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
saveService.ImmediateDelete(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
|
saveService.ImmediateDelete(new ModSaveGroup(group, config.ReplaceNonAsciiOnImport));
|
||||||
group.Name = newName;
|
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);
|
communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, group.Mod, group, null, null, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,7 +81,7 @@ public class ModGroupEditor(
|
||||||
var idx = group.GetIndex();
|
var idx = group.GetIndex();
|
||||||
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, group, null, null, -1);
|
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, group, null, null, -1);
|
||||||
mod.Groups.RemoveAt(idx);
|
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);
|
communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, null, null, null, idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,7 +93,7 @@ public class ModGroupEditor(
|
||||||
if (!mod.Groups.Move(idxFrom, groupIdxTo))
|
if (!mod.Groups.Move(idxFrom, groupIdxTo))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
saveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport);
|
saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport);
|
||||||
communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, group, null, null, idxFrom);
|
communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, group, null, null, idxFrom);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,7 +104,7 @@ public class ModGroupEditor(
|
||||||
return;
|
return;
|
||||||
|
|
||||||
group.Priority = newPriority;
|
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);
|
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, group.Mod, group, null, null, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,7 +115,7 @@ public class ModGroupEditor(
|
||||||
return;
|
return;
|
||||||
|
|
||||||
group.Description = newDescription;
|
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);
|
communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, group.Mod, group, null, null, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,7 +126,7 @@ public class ModGroupEditor(
|
||||||
return;
|
return;
|
||||||
|
|
||||||
option.Name = newName;
|
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);
|
communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, option.Mod, option.Group, option, null, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,7 +137,7 @@ public class ModGroupEditor(
|
||||||
return;
|
return;
|
||||||
|
|
||||||
option.Description = newDescription;
|
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);
|
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);
|
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1);
|
||||||
subMod.Manipulations.SetTo(manipulations);
|
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);
|
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);
|
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1);
|
||||||
subMod.Files.SetTo(replacements);
|
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);
|
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>
|
/// <summary> Forces a file save of the given container's group. </summary>
|
||||||
public void ForceSave(IModDataContainer subMod, SaveType saveType = SaveType.Queue)
|
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>
|
/// <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)
|
public void AddFiles(IModDataContainer subMod, IReadOnlyDictionary<Utf8GamePath, FullPath> additions)
|
||||||
|
|
@ -176,7 +176,7 @@ public class ModGroupEditor(
|
||||||
subMod.Files.AddFrom(additions);
|
subMod.Files.AddFrom(additions);
|
||||||
if (oldCount != subMod.Files.Count)
|
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);
|
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);
|
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1);
|
||||||
subMod.FileSwaps.SetTo(swaps);
|
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);
|
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;
|
||||||
using Penumbra.Import.Structs;
|
using Penumbra.Import.Structs;
|
||||||
using Penumbra.Meta;
|
using Penumbra.Meta;
|
||||||
|
using Penumbra.Mods.Editor;
|
||||||
using Penumbra.Mods.Groups;
|
using Penumbra.Mods.Groups;
|
||||||
using Penumbra.Mods.Manager;
|
using Penumbra.Mods.Manager;
|
||||||
using Penumbra.Mods.Settings;
|
using Penumbra.Mods.Settings;
|
||||||
|
|
@ -20,11 +21,11 @@ using Penumbra.String.Classes;
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods;
|
||||||
|
|
||||||
public partial class ModCreator(
|
public partial class ModCreator(
|
||||||
SaveService _saveService,
|
SaveService saveService,
|
||||||
Configuration config,
|
Configuration config,
|
||||||
ModDataEditor _dataEditor,
|
ModDataEditor dataEditor,
|
||||||
MetaFileManager _metaFileManager,
|
MetaFileManager metaFileManager,
|
||||||
GamePathParser _gamePathParser) : IService
|
GamePathParser gamePathParser) : IService
|
||||||
{
|
{
|
||||||
public readonly Configuration Config = config;
|
public readonly Configuration Config = config;
|
||||||
|
|
||||||
|
|
@ -34,7 +35,7 @@ public partial class ModCreator(
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var newDir = CreateModFolder(basePath, newName, Config.ReplaceNonAsciiOnImport, true);
|
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);
|
CreateDefaultFiles(newDir);
|
||||||
return newDir;
|
return newDir;
|
||||||
}
|
}
|
||||||
|
|
@ -46,7 +47,7 @@ public partial class ModCreator(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Load a mod by its directory. </summary>
|
/// <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();
|
modPath.Refresh();
|
||||||
if (!modPath.Exists)
|
if (!modPath.Exists)
|
||||||
|
|
@ -56,7 +57,7 @@ public partial class ModCreator(
|
||||||
}
|
}
|
||||||
|
|
||||||
var mod = new Mod(modPath);
|
var mod = new Mod(modPath);
|
||||||
if (ReloadMod(mod, incorporateMetaChanges, out _))
|
if (ReloadMod(mod, incorporateMetaChanges, deleteDefaultMetaChanges, out _))
|
||||||
return mod;
|
return mod;
|
||||||
|
|
||||||
// Can not be base path not existing because that is checked before.
|
// 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>
|
/// <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;
|
modDataChange = ModDataChangeType.Deletion;
|
||||||
if (!Directory.Exists(mod.ModPath.FullName))
|
if (!Directory.Exists(mod.ModPath.FullName))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
modDataChange = _dataEditor.LoadMeta(this, mod);
|
modDataChange = dataEditor.LoadMeta(this, mod);
|
||||||
if (modDataChange.HasFlag(ModDataChangeType.Deletion) || mod.Name.Length == 0)
|
if (modDataChange.HasFlag(ModDataChangeType.Deletion) || mod.Name.Length == 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
_dataEditor.LoadLocalData(mod);
|
modDataChange |= dataEditor.LoadLocalData(mod);
|
||||||
LoadDefaultOption(mod);
|
LoadDefaultOption(mod);
|
||||||
LoadAllGroups(mod);
|
LoadAllGroups(mod);
|
||||||
if (incorporateMetaChanges)
|
if (incorporateMetaChanges)
|
||||||
IncorporateAllMetaChanges(mod, true);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -89,13 +98,13 @@ public partial class ModCreator(
|
||||||
{
|
{
|
||||||
mod.Groups.Clear();
|
mod.Groups.Clear();
|
||||||
var changes = false;
|
var changes = false;
|
||||||
foreach (var file in _saveService.FileNames.GetOptionGroupFiles(mod))
|
foreach (var file in saveService.FileNames.GetOptionGroupFiles(mod))
|
||||||
{
|
{
|
||||||
var group = LoadModGroup(mod, file);
|
var group = LoadModGroup(mod, file);
|
||||||
if (group != null && mod.Groups.All(g => g.Name != group.Name))
|
if (group != null && mod.Groups.All(g => g.Name != group.Name))
|
||||||
{
|
{
|
||||||
changes = changes
|
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));
|
!= Path.Combine(file.DirectoryName!, ReplaceBadXivSymbols(file.Name, true));
|
||||||
mod.Groups.Add(group);
|
mod.Groups.Add(group);
|
||||||
}
|
}
|
||||||
|
|
@ -106,13 +115,13 @@ public partial class ModCreator(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changes)
|
if (changes)
|
||||||
_saveService.SaveAllOptionGroups(mod, true, Config.ReplaceNonAsciiOnImport);
|
saveService.SaveAllOptionGroups(mod, true, Config.ReplaceNonAsciiOnImport);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Load the default option for a given mod.</summary>
|
/// <summary> Load the default option for a given mod.</summary>
|
||||||
public void LoadDefaultOption(Mod mod)
|
public void LoadDefaultOption(Mod mod)
|
||||||
{
|
{
|
||||||
var defaultFile = _saveService.FileNames.OptionGroupFile(mod, -1, Config.ReplaceNonAsciiOnImport);
|
var defaultFile = saveService.FileNames.OptionGroupFile(mod, -1, Config.ReplaceNonAsciiOnImport);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var jObject = File.Exists(defaultFile) ? JObject.Parse(File.ReadAllText(defaultFile)) : new JObject();
|
var jObject = File.Exists(defaultFile) ? JObject.Parse(File.ReadAllText(defaultFile)) : new JObject();
|
||||||
|
|
@ -157,7 +166,7 @@ public partial class ModCreator(
|
||||||
List<string> deleteList = new();
|
List<string> deleteList = new();
|
||||||
foreach (var subMod in mod.AllDataContainers)
|
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;
|
changes |= localChanges;
|
||||||
if (delete)
|
if (delete)
|
||||||
deleteList.AddRange(localDeleteList);
|
deleteList.AddRange(localDeleteList);
|
||||||
|
|
@ -168,8 +177,8 @@ public partial class ModCreator(
|
||||||
if (!changes)
|
if (!changes)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_saveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport);
|
saveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport);
|
||||||
_saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, mod.Default, 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 .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.
|
/// If delete is true, the files are deleted afterwards.
|
||||||
/// </summary>
|
/// </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 deleteList = new List<string>();
|
||||||
var oldSize = option.Manipulations.Count;
|
var oldSize = option.Manipulations.Count;
|
||||||
|
|
@ -194,7 +203,7 @@ public partial class ModCreator(
|
||||||
if (!file.Exists)
|
if (!file.Exists)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var meta = new TexToolsMeta(_metaFileManager, _gamePathParser, File.ReadAllBytes(file.FullName),
|
var meta = new TexToolsMeta(metaFileManager, gamePathParser, File.ReadAllBytes(file.FullName),
|
||||||
Config.KeepDefaultMetaChanges);
|
Config.KeepDefaultMetaChanges);
|
||||||
Penumbra.Log.Verbose(
|
Penumbra.Log.Verbose(
|
||||||
$"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}");
|
$"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}");
|
||||||
|
|
@ -207,7 +216,7 @@ public partial class ModCreator(
|
||||||
if (!file.Exists)
|
if (!file.Exists)
|
||||||
continue;
|
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);
|
Config.KeepDefaultMetaChanges);
|
||||||
Penumbra.Log.Verbose(
|
Penumbra.Log.Verbose(
|
||||||
$"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}");
|
$"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}");
|
||||||
|
|
@ -223,7 +232,11 @@ public partial class ModCreator(
|
||||||
}
|
}
|
||||||
|
|
||||||
DeleteDeleteList(deleteList, delete);
|
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>
|
/// <summary>
|
||||||
|
|
@ -250,7 +263,7 @@ public partial class ModCreator(
|
||||||
group.Priority = priority;
|
group.Priority = priority;
|
||||||
group.DefaultSettings = defaultSettings;
|
group.DefaultSettings = defaultSettings;
|
||||||
group.OptionData.AddRange(subMods.Select(s => s.Clone(group)));
|
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;
|
break;
|
||||||
}
|
}
|
||||||
case GroupType.Single:
|
case GroupType.Single:
|
||||||
|
|
@ -260,7 +273,7 @@ public partial class ModCreator(
|
||||||
group.Priority = priority;
|
group.Priority = priority;
|
||||||
group.DefaultSettings = defaultSettings;
|
group.DefaultSettings = defaultSettings;
|
||||||
group.OptionData.AddRange(subMods.Select(s => s.ConvertToSingle(group)));
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -277,7 +290,8 @@ public partial class ModCreator(
|
||||||
foreach (var (_, gamePath, file) in list)
|
foreach (var (_, gamePath, file) in list)
|
||||||
mod.Files.TryAdd(gamePath, file);
|
mod.Files.TryAdd(gamePath, file);
|
||||||
|
|
||||||
IncorporateMetaChanges(mod, baseFolder, true);
|
IncorporateMetaChanges(mod, baseFolder, true, true);
|
||||||
|
|
||||||
return mod;
|
return mod;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -288,15 +302,15 @@ public partial class ModCreator(
|
||||||
internal void CreateDefaultFiles(DirectoryInfo directory)
|
internal void CreateDefaultFiles(DirectoryInfo directory)
|
||||||
{
|
{
|
||||||
var mod = new Mod(directory);
|
var mod = new Mod(directory);
|
||||||
ReloadMod(mod, false, out _);
|
ReloadMod(mod, false, false, out _);
|
||||||
foreach (var file in mod.FindUnusedFiles())
|
foreach (var file in mod.FindUnusedFiles())
|
||||||
{
|
{
|
||||||
if (Utf8GamePath.FromFile(new FileInfo(file.FullName), directory, out var gamePath))
|
if (Utf8GamePath.FromFile(new FileInfo(file.FullName), directory, out var gamePath))
|
||||||
mod.Default.Files.TryAdd(gamePath, file);
|
mod.Default.Files.TryAdd(gamePath, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
IncorporateMetaChanges(mod.Default, directory, true);
|
IncorporateMetaChanges(mod.Default, directory, true, true);
|
||||||
_saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, mod.Default, Config.ReplaceNonAsciiOnImport));
|
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>
|
/// <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 mod = new Mod(baseDir);
|
||||||
|
|
||||||
var files = _saveService.FileNames.GetOptionGroupFiles(mod).ToList();
|
var files = saveService.FileNames.GetOptionGroupFiles(mod).ToList();
|
||||||
var idx = 0;
|
var idx = 0;
|
||||||
var reorder = false;
|
var reorder = false;
|
||||||
foreach (var groupFile in files)
|
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)
|
public int CompareTo(ModPriority other)
|
||||||
=> Value.CompareTo(other.Value);
|
=> 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);
|
defaultMod.Manipulations.UnionWith(manips);
|
||||||
|
|
||||||
saveService.ImmediateSaveSync(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport));
|
saveService.ImmediateSaveSync(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport));
|
||||||
modManager.AddMod(dir);
|
modManager.AddMod(dir, false);
|
||||||
Penumbra.Log.Information(
|
Penumbra.Log.Information(
|
||||||
$"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Identifier}.");
|
$"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>()!;
|
var itemSheet = _services.GetService<IDataManager>().GetExcelSheet<Item>()!;
|
||||||
_communicatorService.ChangedItemHover.Subscribe(it =>
|
_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.");
|
ImGui.TextUnformatted("Left Click to create an item link in chat.");
|
||||||
}, ChangedItemHover.Priority.Link);
|
}, ChangedItemHover.Priority.Link);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -86,11 +86,13 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<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="SixLabors.ImageSharp" Version="3.1.5" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.33.0" />
|
<PackageReference Include="SharpCompress" Version="0.37.2" />
|
||||||
<PackageReference Include="SharpGLTF.Core" Version="1.0.0-alpha0030" />
|
<PackageReference Include="SharpGLTF.Core" Version="1.0.1" />
|
||||||
<PackageReference Include="SharpGLTF.Toolkit" Version="1.0.0-alpha0030" />
|
<PackageReference Include="SharpGLTF.Toolkit" Version="1.0.1" />
|
||||||
<PackageReference Include="PeNet" Version="4.0.5" />
|
<PackageReference Include="PeNet" Version="4.0.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -242,7 +242,7 @@ public class MigrationManager(Configuration config) : IService
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var path = Path.Combine(directory, reader.Entry.Key);
|
var path = Path.Combine(directory, reader.Entry.Key!);
|
||||||
using var s = new MemoryStream();
|
using var s = new MemoryStream();
|
||||||
using var e = reader.OpenEntryStream();
|
using var e = reader.OpenEntryStream();
|
||||||
e.CopyTo(s);
|
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)>
|
_selectors = new Dictionary<SwapType, (ItemSelector Source, ItemSelector Target, string TextFrom, string TextTo)>
|
||||||
{
|
{
|
||||||
// @formatter:off
|
// @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.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.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.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.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.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.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.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.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.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
|
// @formatter:on
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -129,6 +130,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService
|
||||||
Ears,
|
Ears,
|
||||||
Tail,
|
Tail,
|
||||||
Weapon,
|
Weapon,
|
||||||
|
Glasses,
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ItemSelector(ItemData data, ModFileSystemSelector? selector, FullEquipType type)
|
private class ItemSelector(ItemData data, ModFileSystemSelector? selector, FullEquipType type)
|
||||||
|
|
@ -158,14 +160,14 @@ public class ItemSwapTab : IDisposable, ITab, IUiService
|
||||||
private ModSettings? _modSettings;
|
private ModSettings? _modSettings;
|
||||||
private bool _dirty;
|
private bool _dirty;
|
||||||
|
|
||||||
private SwapType _lastTab = SwapType.Hair;
|
private SwapType _lastTab = SwapType.Hair;
|
||||||
private Gender _currentGender = Gender.Male;
|
private Gender _currentGender = Gender.Male;
|
||||||
private ModelRace _currentRace = ModelRace.Midlander;
|
private ModelRace _currentRace = ModelRace.Midlander;
|
||||||
private int _targetId;
|
private int _targetId;
|
||||||
private int _sourceId;
|
private int _sourceId;
|
||||||
private Exception? _loadException;
|
private Exception? _loadException;
|
||||||
private EquipSlot _slotFrom = EquipSlot.Head;
|
private BetweenSlotTypes _slotFrom = BetweenSlotTypes.Hat;
|
||||||
private EquipSlot _slotTo = EquipSlot.Ears;
|
private BetweenSlotTypes _slotTo = BetweenSlotTypes.Earrings;
|
||||||
|
|
||||||
private string _newModName = string.Empty;
|
private string _newModName = string.Empty;
|
||||||
private string _newGroupName = "Swaps";
|
private string _newGroupName = "Swaps";
|
||||||
|
|
@ -200,18 +202,19 @@ public class ItemSwapTab : IDisposable, ITab, IUiService
|
||||||
case SwapType.Necklace:
|
case SwapType.Necklace:
|
||||||
case SwapType.Bracelet:
|
case SwapType.Bracelet:
|
||||||
case SwapType.Ring:
|
case SwapType.Ring:
|
||||||
|
case SwapType.Glasses:
|
||||||
var values = _selectors[_lastTab];
|
var values = _selectors[_lastTab];
|
||||||
if (values.Source.CurrentSelection.Item.Type != FullEquipType.Unknown
|
if (values.Source.CurrentSelection.Item.Type != FullEquipType.Unknown
|
||||||
&& values.Target.CurrentSelection.Item.Type != FullEquipType.Unknown)
|
&& values.Target.CurrentSelection.Item.Type != FullEquipType.Unknown)
|
||||||
_affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection.Item, values.Source.CurrentSelection.Item,
|
_affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection.Item, values.Source.CurrentSelection.Item,
|
||||||
_useCurrentCollection ? _collectionManager.Active.Current : null, _useRightRing, _useLeftRing);
|
_useCurrentCollection ? _collectionManager.Active.Current : null, _useRightRing, _useLeftRing);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case SwapType.BetweenSlots:
|
case SwapType.BetweenSlots:
|
||||||
var (_, _, selectorFrom) = GetAccessorySelector(_slotFrom, true);
|
var (_, _, selectorFrom) = GetAccessorySelector(_slotFrom, true);
|
||||||
var (_, _, selectorTo) = GetAccessorySelector(_slotTo, false);
|
var (_, _, selectorTo) = GetAccessorySelector(_slotTo, false);
|
||||||
if (selectorFrom.CurrentSelection.Item.Valid && selectorTo.CurrentSelection.Item.Valid)
|
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);
|
_useCurrentCollection ? _collectionManager.Active.Current : null);
|
||||||
break;
|
break;
|
||||||
case SwapType.Hair when _targetId > 0 && _sourceId > 0:
|
case SwapType.Hair when _targetId > 0 && _sourceId > 0:
|
||||||
|
|
@ -264,7 +267,23 @@ public class ItemSwapTab : IDisposable, ITab, IUiService
|
||||||
}
|
}
|
||||||
|
|
||||||
private string CreateDescription()
|
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()
|
private void UpdateOption()
|
||||||
{
|
{
|
||||||
|
|
@ -281,7 +300,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService
|
||||||
if (newDir == null)
|
if (newDir == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_modManager.AddMod(newDir);
|
_modManager.AddMod(newDir, false);
|
||||||
var mod = _modManager[^1];
|
var mod = _modManager[^1];
|
||||||
if (!_swapData.WriteMod(_modManager, mod, mod.Default,
|
if (!_swapData.WriteMod(_modManager, mod, mod.Default,
|
||||||
_useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps))
|
_useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps))
|
||||||
|
|
@ -416,6 +435,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService
|
||||||
DrawEquipmentSwap(SwapType.Necklace);
|
DrawEquipmentSwap(SwapType.Necklace);
|
||||||
DrawEquipmentSwap(SwapType.Bracelet);
|
DrawEquipmentSwap(SwapType.Bracelet);
|
||||||
DrawEquipmentSwap(SwapType.Ring);
|
DrawEquipmentSwap(SwapType.Ring);
|
||||||
|
DrawEquipmentSwap(SwapType.Glasses);
|
||||||
DrawAccessorySwap();
|
DrawAccessorySwap();
|
||||||
DrawHairSwap();
|
DrawHairSwap();
|
||||||
//DrawFaceSwap();
|
//DrawFaceSwap();
|
||||||
|
|
@ -454,23 +474,24 @@ public class ItemSwapTab : IDisposable, ITab, IUiService
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
ImGui.SetNextItemWidth(100 * UiHelpers.Scale);
|
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)
|
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;
|
continue;
|
||||||
|
|
||||||
_dirty = true;
|
_dirty = true;
|
||||||
_slotFrom = slot;
|
_slotFrom = slot;
|
||||||
if (slot == _slotTo)
|
if (slot == _slotTo)
|
||||||
_slotTo = EquipSlotExtensions.AccessorySlots.First(s => slot != s);
|
_slotTo = AvailableToTypes.First(s => slot != s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
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());
|
ImGui.GetTextLineHeightWithSpacing());
|
||||||
|
|
||||||
(article1, _, selector) = GetAccessorySelector(_slotTo, false);
|
(article1, _, selector) = GetAccessorySelector(_slotTo, false);
|
||||||
|
|
@ -480,12 +501,12 @@ public class ItemSwapTab : IDisposable, ITab, IUiService
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
ImGui.SetNextItemWidth(100 * UiHelpers.Scale);
|
ImGui.SetNextItemWidth(100 * UiHelpers.Scale);
|
||||||
using (var combo = ImRaii.Combo("##toType", _slotTo.ToName()))
|
using (var combo = ImRaii.Combo("##toType", ToName(_slotTo)))
|
||||||
{
|
{
|
||||||
if (combo)
|
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;
|
continue;
|
||||||
|
|
||||||
_dirty = true;
|
_dirty = true;
|
||||||
|
|
@ -508,17 +529,18 @@ public class ItemSwapTab : IDisposable, ITab, IUiService
|
||||||
.Select(i => i.Name)));
|
.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
|
var (type, article1, article2) = slot switch
|
||||||
{
|
{
|
||||||
EquipSlot.Head => (SwapType.Hat, "this", "it"),
|
BetweenSlotTypes.Hat => (SwapType.Hat, "this", "it"),
|
||||||
EquipSlot.Ears => (SwapType.Earrings, "these", "them"),
|
BetweenSlotTypes.Earrings => (SwapType.Earrings, "these", "them"),
|
||||||
EquipSlot.Neck => (SwapType.Necklace, "this", "it"),
|
BetweenSlotTypes.Necklace => (SwapType.Necklace, "this", "it"),
|
||||||
EquipSlot.Wrists => (SwapType.Bracelet, "these", "them"),
|
BetweenSlotTypes.Bracelets => (SwapType.Bracelet, "these", "them"),
|
||||||
EquipSlot.RFinger => (SwapType.Ring, "this", "it"),
|
BetweenSlotTypes.RightRing => (SwapType.Ring, "this", "it"),
|
||||||
EquipSlot.LFinger => (SwapType.Ring, "this", "it"),
|
BetweenSlotTypes.LeftRing => (SwapType.Ring, "this", "it"),
|
||||||
_ => (SwapType.Ring, "this", "it"),
|
BetweenSlotTypes.Glasses => (SwapType.Glasses, "these", "them"),
|
||||||
|
_ => (SwapType.Ring, "this", "it"),
|
||||||
};
|
};
|
||||||
var (itemSelector, target, _, _) = _selectors[type];
|
var (itemSelector, target, _, _) = _selectors[type];
|
||||||
return (article1, article2, source ? itemSelector : target);
|
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.Necklace => "One of the selected necklaces does not seem to exist.",
|
||||||
SwapType.Bracelet => "One of the selected bracelets 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.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.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.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.",
|
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();
|
UpdateOption();
|
||||||
_dirty = true;
|
_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
|
_textureSize = _firstNonNullTexture switch
|
||||||
{
|
{
|
||||||
null => Vector2.Zero,
|
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))
|
if (float.IsNaN(itemHeight))
|
||||||
|
|
@ -192,10 +192,10 @@ public sealed unsafe class MaterialTemplatePickers : IUiService
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var position = regionStart with { X = regionStart.X + (itemSize.X + itemSpacing) * j };
|
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;
|
position += (itemSize - size) * 0.5f;
|
||||||
ImGui.GetWindowDrawList().AddImage(handle, position, position + size, Vector2.Zero,
|
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()
|
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)
|
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()
|
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)
|
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()
|
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)
|
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()
|
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)
|
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()
|
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)
|
private static bool DrawIdentifierInput(ref GmpIdentifier identifier)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateEntry()
|
private void UpdateEntry()
|
||||||
=> (Entry, _fileExists, _) = MetaFiles.ImcChecker.GetDefaultEntry(Identifier, true);
|
=> (Entry, _fileExists, _) = ImcChecker.GetDefaultEntry(Identifier, true);
|
||||||
|
|
||||||
protected override void DrawNew()
|
protected override void DrawNew()
|
||||||
{
|
{
|
||||||
|
|
@ -54,7 +54,7 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
|
||||||
DrawMetaButtons(identifier, entry);
|
DrawMetaButtons(identifier, entry);
|
||||||
DrawIdentifier(identifier);
|
DrawIdentifier(identifier);
|
||||||
|
|
||||||
var defaultEntry = MetaFiles.ImcChecker.GetDefaultEntry(identifier, true).Entry;
|
var defaultEntry = ImcChecker.GetDefaultEntry(identifier, true).Entry;
|
||||||
if (DrawEntry(defaultEntry, ref entry, true))
|
if (DrawEntry(defaultEntry, ref entry, true))
|
||||||
Editor.Changes |= Editor.Update(identifier, entry);
|
Editor.Changes |= Editor.Update(identifier, entry);
|
||||||
}
|
}
|
||||||
|
|
@ -140,7 +140,17 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
|
||||||
|
|
||||||
|
|
||||||
protected override IEnumerable<(ImcIdentifier, ImcEntry)> Enumerate()
|
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)
|
public static bool DrawObjectType(ref ImcIdentifier identifier, float width = 110)
|
||||||
{
|
{
|
||||||
|
|
@ -149,18 +159,18 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
|
||||||
|
|
||||||
if (ret)
|
if (ret)
|
||||||
{
|
{
|
||||||
var equipSlot = type switch
|
var (equipSlot, secondaryId) = type switch
|
||||||
{
|
{
|
||||||
ObjectType.Equipment => identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head,
|
ObjectType.Equipment => (identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, (SecondaryId) 0),
|
||||||
ObjectType.DemiHuman => identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head,
|
ObjectType.DemiHuman => (identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId),
|
||||||
ObjectType.Accessory => identifier.EquipSlot.IsAccessory() ? identifier.EquipSlot : EquipSlot.Ears,
|
ObjectType.Accessory => (identifier.EquipSlot.IsAccessory() ? identifier.EquipSlot : EquipSlot.Ears, (SecondaryId)0),
|
||||||
_ => EquipSlot.Unknown,
|
_ => (EquipSlot.Unknown, identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId),
|
||||||
};
|
};
|
||||||
identifier = identifier with
|
identifier = identifier with
|
||||||
{
|
{
|
||||||
ObjectType = type,
|
ObjectType = type,
|
||||||
EquipSlot = equipSlot,
|
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);
|
using var id = ImUtf8.PushId((int)Identifier.Type);
|
||||||
DrawNew();
|
DrawNew();
|
||||||
foreach (var ((identifier, entry), idx) in Enumerate().WithIndex())
|
|
||||||
{
|
var height = ImUtf8.FrameHeightSpacing;
|
||||||
id.Push(idx);
|
var skips = ImGuiClip.GetNecessarySkipsAtPos(height, ImGui.GetCursorPosY());
|
||||||
DrawEntry(identifier, entry);
|
var remainder = ImGuiClip.ClippedTableDraw(Enumerate(), skips, DrawLine, Count);
|
||||||
id.Pop();
|
ImGuiClip.DrawEndDummy(remainder, height);
|
||||||
}
|
|
||||||
|
void DrawLine((TIdentifier Identifier, TEntry Value) pair)
|
||||||
|
=> DrawEntry(pair.Identifier, pair.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract ReadOnlySpan<byte> Label { get; }
|
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 void DrawEntry(TIdentifier identifier, TEntry entry);
|
||||||
|
|
||||||
protected abstract IEnumerable<(TIdentifier, TEntry)> Enumerate();
|
protected abstract IEnumerable<(TIdentifier, TEntry)> Enumerate();
|
||||||
|
protected abstract int Count { get; }
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,13 @@ public sealed class RspMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IEnumerable<(RspIdentifier, RspEntry)> Enumerate()
|
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)
|
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 Dalamud.Interface;
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
using OtterGui.Classes;
|
using OtterGui.Classes;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
|
using OtterGui.Text;
|
||||||
using Penumbra.Mods.Editor;
|
using Penumbra.Mods.Editor;
|
||||||
using Penumbra.Mods.SubMods;
|
using Penumbra.Mods.SubMods;
|
||||||
using Penumbra.String.Classes;
|
using Penumbra.String.Classes;
|
||||||
|
|
@ -144,22 +146,20 @@ public partial class ModEditWindow
|
||||||
|
|
||||||
private static string DrawFileTooltip(FileRegistry registry, ColorId color)
|
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
|
var (text, groupCount) = color switch
|
||||||
{
|
{
|
||||||
ColorId.ConflictingMod => (string.Empty, 0),
|
ColorId.ConflictingMod => (null, 0),
|
||||||
ColorId.NewMod => (registry.SubModUsage[0].Item1.GetName(), 1),
|
ColorId.NewMod => ([registry.SubModUsage[0].Item1.GetName()], 1),
|
||||||
ColorId.InheritedMod => GetMulti(),
|
ColorId.InheritedMod => GetMulti(),
|
||||||
_ => (string.Empty, 0),
|
_ => (null, 0),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (text.Length > 0 && ImGui.IsItemHovered())
|
if (text != null && ImGui.IsItemHovered())
|
||||||
ImGui.SetTooltip(text);
|
{
|
||||||
|
using var tt = ImUtf8.Tooltip();
|
||||||
|
using var c = ImRaii.DefaultColors();
|
||||||
|
ImUtf8.Text(string.Join('\n', text));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return (groupCount, registry.SubModUsage.Count) switch
|
return (groupCount, registry.SubModUsage.Count) switch
|
||||||
|
|
@ -169,6 +169,12 @@ public partial class ModEditWindow
|
||||||
(1, > 1) => $"(used {registry.SubModUsage.Count} times in 1 group)",
|
(1, > 1) => $"(used {registry.SubModUsage.Count} times in 1 group)",
|
||||||
_ => $"(used {registry.SubModUsage.Count} times over {groupCount} groups)",
|
_ => $"(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)
|
private void DrawSelectable(FileRegistry registry)
|
||||||
|
|
|
||||||
|
|
@ -16,21 +16,21 @@ public partial class ModEditWindow
|
||||||
|
|
||||||
private void DrawMetaTab()
|
private void DrawMetaTab()
|
||||||
{
|
{
|
||||||
using var tab = ImRaii.TabItem("Meta Manipulations");
|
using var tab = ImUtf8.TabItem("Meta Manipulations"u8);
|
||||||
if (!tab)
|
if (!tab)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
DrawOptionSelectHeader();
|
DrawOptionSelectHeader();
|
||||||
|
|
||||||
var setsEqual = !_editor.MetaEditor.Changes;
|
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();
|
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!);
|
_editor.MetaEditor.Apply(_editor.Option!);
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
tt = setsEqual ? "No changes staged." : "Revert all currently staged changes.";
|
tt = setsEqual ? "No changes staged."u8 : "Revert all currently staged changes."u8;
|
||||||
if (ImGuiUtil.DrawDisabledButton("Revert Changes", Vector2.Zero, tt, setsEqual))
|
if (ImUtf8.ButtonEx("Revert Changes"u8, tt, Vector2.Zero, setsEqual))
|
||||||
_editor.MetaEditor.Load(_editor.Mod!, _editor.Option!);
|
_editor.MetaEditor.Load(_editor.Mod!, _editor.Option!);
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
|
|
@ -40,8 +40,11 @@ public partial class ModEditWindow
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
CopyToClipboardButton("Copy all current manipulations to clipboard.", _iconSize, _editor.MetaEditor);
|
CopyToClipboardButton("Copy all current manipulations to clipboard.", _iconSize, _editor.MetaEditor);
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if (ImGui.Button("Write as TexTools Files"))
|
if (ImUtf8.Button("Write as TexTools Files"u8))
|
||||||
_metaFileManager.WriteAllTexToolsMeta(Mod!);
|
_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);
|
using var child = ImRaii.Child("##meta", -Vector2.One, true);
|
||||||
if (!child)
|
if (!child)
|
||||||
|
|
|
||||||
|
|
@ -97,9 +97,7 @@ public partial class ModEditWindow
|
||||||
|
|
||||||
private void DrawImportExport(MdlTab tab, bool disabled)
|
private void DrawImportExport(MdlTab tab, bool disabled)
|
||||||
{
|
{
|
||||||
// TODO: Enable when functional.
|
if (!ImGui.CollapsingHeader("Import / Export"))
|
||||||
using var dawntrailDisabled = ImRaii.Disabled();
|
|
||||||
if (!ImGui.CollapsingHeader("Import / Export (currently disabled due to Dawntrail format changes)") || true)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var childSize = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0);
|
var childSize = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0);
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ public partial class ModEditWindow
|
||||||
|
|
||||||
ImGuiUtil.SelectableHelpMarker(newDesc);
|
ImGuiUtil.SelectableHelpMarker(newDesc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RedrawOnSaveBox()
|
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."
|
||||||
: $"This saves the texture in place. This is not revertible. Hold {_config.DeleteModModifier} to save.";
|
: $"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,
|
if (ImGuiUtil.DrawDisabledButton("Save in place", buttonSize2,
|
||||||
tt, !isActive || !canSaveInPlace || _center.IsLeftCopy && _currentSaveAs == (int)CombinedTexture.TextureSaveType.AsIs))
|
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))
|
if (ImGui.Button("Save as TEX", buttonSize2))
|
||||||
OpenSaveAsDialog(".tex");
|
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");
|
OpenSaveAsDialog(".png");
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if (ImGui.Button("Export as DDS", buttonSize2))
|
if (ImGui.Button("Export as DDS", buttonSize3))
|
||||||
OpenSaveAsDialog(".dds");
|
OpenSaveAsDialog(".dds");
|
||||||
|
|
||||||
ImGui.NewLine();
|
ImGui.NewLine();
|
||||||
|
|
||||||
var canConvertInPlace = canSaveInPlace && _left.Type is TextureType.Tex && _center.IsLeftCopy;
|
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,
|
if (ImGuiUtil.DrawDisabledButton("Convert to BC7", buttonSize3,
|
||||||
"This converts the texture to BC7 format in place. This is not revertible.",
|
"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))
|
!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)
|
private void OpenSaveAsDialog(string defaultExtension)
|
||||||
{
|
{
|
||||||
var fileName = Path.GetFileNameWithoutExtension(_left.Path.Length > 0 ? _left.Path : _right.Path);
|
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) =>
|
(a, b) =>
|
||||||
{
|
{
|
||||||
if (a)
|
if (a)
|
||||||
|
|
@ -329,5 +332,6 @@ public partial class ModEditWindow
|
||||||
".png",
|
".png",
|
||||||
".dds",
|
".dds",
|
||||||
".tex",
|
".tex",
|
||||||
|
".tga",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ using ImGuiNET;
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
|
using OtterGui.Text;
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
using Penumbra.Collections.Manager;
|
using Penumbra.Collections.Manager;
|
||||||
using Penumbra.Communication;
|
using Penumbra.Communication;
|
||||||
|
|
@ -36,8 +37,6 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
|
||||||
{
|
{
|
||||||
private const string WindowBaseLabel = "###SubModEdit";
|
private const string WindowBaseLabel = "###SubModEdit";
|
||||||
|
|
||||||
public readonly MigrationManager MigrationManager;
|
|
||||||
|
|
||||||
private readonly PerformanceTracker _performance;
|
private readonly PerformanceTracker _performance;
|
||||||
private readonly ModEditor _editor;
|
private readonly ModEditor _editor;
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
|
|
@ -53,34 +52,68 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
|
||||||
private Vector2 _iconSize = Vector2.Zero;
|
private Vector2 _iconSize = Vector2.Zero;
|
||||||
private bool _allowReduplicate;
|
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)
|
public void ChangeMod(Mod mod)
|
||||||
{
|
{
|
||||||
if (mod == Mod)
|
if (mod == Mod)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_editor.LoadMod(mod, -1, 0);
|
WindowName = $"{mod.Name} (LOADING){WindowBaseLabel}";
|
||||||
Mod = mod;
|
AppendTask(() =>
|
||||||
|
|
||||||
SizeConstraints = new WindowSizeConstraints
|
|
||||||
{
|
{
|
||||||
MinimumSize = new Vector2(1240, 600),
|
_editor.LoadMod(mod, -1, 0).Wait();
|
||||||
MaximumSize = 4000 * Vector2.One,
|
Mod = mod;
|
||||||
};
|
|
||||||
_selectedFiles.Clear();
|
SizeConstraints = new WindowSizeConstraints
|
||||||
_modelTab.Reset();
|
{
|
||||||
_materialTab.Reset();
|
MinimumSize = new Vector2(1240, 600),
|
||||||
_shaderPackageTab.Reset();
|
MaximumSize = 4000 * Vector2.One,
|
||||||
_itemSwapTab.UpdateMod(mod, _activeCollections.Current[mod.Index].Settings);
|
};
|
||||||
UpdateModels();
|
_selectedFiles.Clear();
|
||||||
_forceTextureStartPath = true;
|
_modelTab.Reset();
|
||||||
|
_materialTab.Reset();
|
||||||
|
_shaderPackageTab.Reset();
|
||||||
|
_itemSwapTab.UpdateMod(mod, _activeCollections.Current[mod.Index].Settings);
|
||||||
|
UpdateModels();
|
||||||
|
_forceTextureStartPath = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ChangeOption(IModDataContainer? subMod)
|
public void ChangeOption(IModDataContainer? subMod)
|
||||||
{
|
{
|
||||||
var (groupIdx, dataIdx) = subMod?.GetDataIndices() ?? (-1, 0);
|
AppendTask(() =>
|
||||||
_editor.LoadOption(groupIdx, dataIdx);
|
{
|
||||||
|
var (groupIdx, dataIdx) = subMod?.GetDataIndices() ?? (-1, 0);
|
||||||
|
_editor.LoadOption(groupIdx, dataIdx).Wait();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateModels()
|
public void UpdateModels()
|
||||||
|
|
@ -94,6 +127,9 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
|
||||||
|
|
||||||
public override void PreDraw()
|
public override void PreDraw()
|
||||||
{
|
{
|
||||||
|
if (IsLoading)
|
||||||
|
return;
|
||||||
|
|
||||||
using var performance = _performance.Measure(PerformanceType.UiAdvancedWindow);
|
using var performance = _performance.Measure(PerformanceType.UiAdvancedWindow);
|
||||||
|
|
||||||
var sb = new StringBuilder(256);
|
var sb = new StringBuilder(256);
|
||||||
|
|
@ -146,13 +182,16 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
|
||||||
|
|
||||||
public override void OnClose()
|
public override void OnClose()
|
||||||
{
|
{
|
||||||
_left.Dispose();
|
|
||||||
_right.Dispose();
|
|
||||||
_materialTab.Reset();
|
|
||||||
_modelTab.Reset();
|
|
||||||
_shaderPackageTab.Reset();
|
|
||||||
_config.Ephemeral.AdvancedEditingOpen = false;
|
_config.Ephemeral.AdvancedEditingOpen = false;
|
||||||
_config.Ephemeral.Save();
|
_config.Ephemeral.Save();
|
||||||
|
AppendTask(() =>
|
||||||
|
{
|
||||||
|
_left.Dispose();
|
||||||
|
_right.Dispose();
|
||||||
|
_materialTab.Reset();
|
||||||
|
_modelTab.Reset();
|
||||||
|
_shaderPackageTab.Reset();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Draw()
|
public override void Draw()
|
||||||
|
|
@ -165,6 +204,17 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
|
||||||
_config.Ephemeral.Save();
|
_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");
|
using var tabBar = ImRaii.TabBar("##tabs");
|
||||||
if (!tabBar)
|
if (!tabBar)
|
||||||
return;
|
return;
|
||||||
|
|
@ -186,6 +236,8 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
|
||||||
_itemSwapTab.DrawContent();
|
_itemSwapTab.DrawContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_pbdTab.Draw();
|
||||||
|
|
||||||
DrawMissingFilesTab();
|
DrawMissingFilesTab();
|
||||||
DrawMaterialReassignmentTab();
|
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.",
|
if (ImGuiUtil.DrawDisabledButton(defaultOption, width, "Switch to the default option for the mod.\nThis resets unsaved changes.",
|
||||||
_editor.Option is DefaultSubMod))
|
_editor.Option is DefaultSubMod))
|
||||||
{
|
{
|
||||||
_editor.LoadOption(-1, 0);
|
_editor.LoadOption(-1, 0).Wait();
|
||||||
ret = true;
|
ret = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if (ImGuiUtil.DrawDisabledButton("Refresh Data", width, "Refresh data for the current option.\nThis resets unsaved changes.", false))
|
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;
|
ret = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -432,7 +484,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
|
||||||
if (ImGui.Selectable(option.GetFullName(), option == _editor.Option))
|
if (ImGui.Selectable(option.GetFullName(), option == _editor.Option))
|
||||||
{
|
{
|
||||||
var (groupIdx, dataIdx) = option.GetDataIndices();
|
var (groupIdx, dataIdx) = option.GetDataIndices();
|
||||||
_editor.LoadOption(groupIdx, dataIdx);
|
_editor.LoadOption(groupIdx, dataIdx).Wait();
|
||||||
ret = true;
|
ret = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -587,7 +639,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
|
||||||
CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager,
|
CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager,
|
||||||
ResourceTreeViewerFactory resourceTreeViewerFactory, IFramework framework,
|
ResourceTreeViewerFactory resourceTreeViewerFactory, IFramework framework,
|
||||||
MetaDrawers metaDrawers, MigrationManager migrationManager,
|
MetaDrawers metaDrawers, MigrationManager migrationManager,
|
||||||
MtrlTabFactory mtrlTabFactory)
|
MtrlTabFactory mtrlTabFactory, ModSelection selection)
|
||||||
: base(WindowBaseLabel)
|
: base(WindowBaseLabel)
|
||||||
{
|
{
|
||||||
_performance = performance;
|
_performance = performance;
|
||||||
|
|
@ -604,7 +656,6 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
|
||||||
_models = models;
|
_models = models;
|
||||||
_fileDialog = fileDialog;
|
_fileDialog = fileDialog;
|
||||||
_framework = framework;
|
_framework = framework;
|
||||||
MigrationManager = migrationManager;
|
|
||||||
_metaDrawers = metaDrawers;
|
_metaDrawers = metaDrawers;
|
||||||
_materialTab = new FileEditor<MtrlTab>(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl",
|
_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,
|
() => 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,
|
() => PopulateIsOnPlayer(_editor.Files.Shpk, ResourceType.Shpk), DrawShaderPackagePanel,
|
||||||
() => Mod?.ModPath.FullName ?? string.Empty,
|
() => Mod?.ModPath.FullName ?? string.Empty,
|
||||||
(bytes, path, _) => new ShpkTab(_fileDialog, bytes, path));
|
(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);
|
_center = new CombinedTexture(_left, _right);
|
||||||
_textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor, () => GetPlayerResourcesOfType(ResourceType.Tex));
|
_textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor, () => GetPlayerResourcesOfType(ResourceType.Tex));
|
||||||
_resourceTreeFactory = resourceTreeFactory;
|
_resourceTreeFactory = resourceTreeFactory;
|
||||||
_quickImportViewer = resourceTreeViewerFactory.Create(2, OnQuickImportRefresh, DrawQuickImportActions);
|
_quickImportViewer = resourceTreeViewerFactory.Create(2, OnQuickImportRefresh, DrawQuickImportActions);
|
||||||
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModEditWindow);
|
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModEditWindow);
|
||||||
IsOpen = _config is { OpenWindowAtStart: true, Ephemeral.AdvancedEditingOpen: true };
|
IsOpen = _config is { OpenWindowAtStart: true, Ephemeral.AdvancedEditingOpen: true };
|
||||||
|
if (IsOpen && selection.Mod != null)
|
||||||
|
ChangeMod(selection.Mod);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using ImGuiNET;
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
|
using OtterGui.Text;
|
||||||
using Penumbra.Mods.Editor;
|
using Penumbra.Mods.Editor;
|
||||||
using Penumbra.Mods.Manager;
|
using Penumbra.Mods.Manager;
|
||||||
using Penumbra.Mods.SubMods;
|
using Penumbra.Mods.SubMods;
|
||||||
|
|
@ -45,9 +46,30 @@ public class ModMergeTab(ModMerger modMerger) : IUiService
|
||||||
|
|
||||||
private void DrawMergeInto(float size)
|
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.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();
|
ImGui.SameLine();
|
||||||
DrawCombo(size - ImGui.GetItemRectSize().X - ImGui.GetStyle().ItemSpacing.X);
|
DrawCombo(size - ImGui.GetItemRectSize().X - ImGui.GetStyle().ItemSpacing.X);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -190,52 +190,6 @@ public class ResourceTreeViewer
|
||||||
var frameHeight = ImGui.GetFrameHeight();
|
var frameHeight = ImGui.GetFrameHeight();
|
||||||
var cellHeight = _actionCapacity > 0 ? frameHeight : 0.0f;
|
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())
|
foreach (var (resourceNode, index) in resourceNodes.WithIndex())
|
||||||
{
|
{
|
||||||
var nodePathHash = unchecked(pathHash + resourceNode.ResourceHandle);
|
var nodePathHash = unchecked(pathHash + resourceNode.ResourceHandle);
|
||||||
|
|
@ -346,6 +300,54 @@ public class ResourceTreeViewer
|
||||||
if (unfolded)
|
if (unfolded)
|
||||||
DrawNodes(resourceNode.Children, level + 1, unchecked(nodePathHash * 31), filterIcon);
|
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]
|
[Flags]
|
||||||
|
|
|
||||||
|
|
@ -53,10 +53,42 @@ public class PenumbraChangelog : IUiService
|
||||||
Add1_1_0_0(Changelog);
|
Add1_1_0_0(Changelog);
|
||||||
Add1_1_1_0(Changelog);
|
Add1_1_1_0(Changelog);
|
||||||
Add1_2_1_0(Changelog);
|
Add1_2_1_0(Changelog);
|
||||||
|
Add1_3_0_0(Changelog);
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Changelogs
|
#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)
|
private static void Add1_2_1_0(Changelog log)
|
||||||
=> log.NextVersion("Version 1.2.1.0")
|
=> log.NextVersion("Version 1.2.1.0")
|
||||||
.RegisterHighlight("Penumbra is now released for Dawntrail!")
|
.RegisterHighlight("Penumbra is now released for Dawntrail!")
|
||||||
|
|
|
||||||
|
|
@ -5,24 +5,24 @@ using OtterGui.Services;
|
||||||
using Penumbra.Collections;
|
using Penumbra.Collections;
|
||||||
using Penumbra.Collections.Manager;
|
using Penumbra.Collections.Manager;
|
||||||
using Penumbra.Interop.PathResolving;
|
using Penumbra.Interop.PathResolving;
|
||||||
|
using Penumbra.Mods;
|
||||||
using Penumbra.UI.CollectionTab;
|
using Penumbra.UI.CollectionTab;
|
||||||
using Penumbra.UI.ModsTab;
|
|
||||||
|
|
||||||
namespace Penumbra.UI.Classes;
|
namespace Penumbra.UI.Classes;
|
||||||
|
|
||||||
public class CollectionSelectHeader : IUiService
|
public class CollectionSelectHeader : IUiService
|
||||||
{
|
{
|
||||||
private readonly CollectionCombo _collectionCombo;
|
private readonly CollectionCombo _collectionCombo;
|
||||||
private readonly ActiveCollections _activeCollections;
|
private readonly ActiveCollections _activeCollections;
|
||||||
private readonly TutorialService _tutorial;
|
private readonly TutorialService _tutorial;
|
||||||
private readonly ModFileSystemSelector _selector;
|
private readonly ModSelection _selection;
|
||||||
private readonly CollectionResolver _resolver;
|
private readonly CollectionResolver _resolver;
|
||||||
|
|
||||||
public CollectionSelectHeader(CollectionManager collectionManager, TutorialService tutorial, ModFileSystemSelector selector,
|
public CollectionSelectHeader(CollectionManager collectionManager, TutorialService tutorial, ModSelection selection,
|
||||||
CollectionResolver resolver)
|
CollectionResolver resolver)
|
||||||
{
|
{
|
||||||
_tutorial = tutorial;
|
_tutorial = tutorial;
|
||||||
_selector = selector;
|
_selection = selection;
|
||||||
_resolver = resolver;
|
_resolver = resolver;
|
||||||
_activeCollections = collectionManager.Active;
|
_activeCollections = collectionManager.Active;
|
||||||
_collectionCombo = new CollectionCombo(collectionManager, () => collectionManager.Storage.OrderBy(c => c.Name).ToList());
|
_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()
|
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
|
return CheckCollection(collection, true) switch
|
||||||
{
|
{
|
||||||
CollectionState.Unavailable => (null, "Not Inherited",
|
CollectionState.Unavailable => (null, "Not Inherited",
|
||||||
|
|
|
||||||
|
|
@ -24,13 +24,11 @@ public class AddGroupDrawer : IUiService
|
||||||
private bool _imcFileExists;
|
private bool _imcFileExists;
|
||||||
private bool _entryExists;
|
private bool _entryExists;
|
||||||
private bool _entryInvalid;
|
private bool _entryInvalid;
|
||||||
private readonly ImcChecker _imcChecker;
|
|
||||||
private readonly ModManager _modManager;
|
private readonly ModManager _modManager;
|
||||||
|
|
||||||
public AddGroupDrawer(ModManager modManager, ImcChecker imcChecker)
|
public AddGroupDrawer(ModManager modManager)
|
||||||
{
|
{
|
||||||
_modManager = modManager;
|
_modManager = modManager;
|
||||||
_imcChecker = imcChecker;
|
|
||||||
UpdateEntry();
|
UpdateEntry();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,7 +140,7 @@ public class AddGroupDrawer : IUiService
|
||||||
|
|
||||||
private void UpdateEntry()
|
private void UpdateEntry()
|
||||||
{
|
{
|
||||||
(_defaultEntry, _imcFileExists, _entryExists) = _imcChecker.GetDefaultEntry(_imcIdentifier, false);
|
(_defaultEntry, _imcFileExists, _entryExists) = ImcChecker.GetDefaultEntry(_imcIdentifier, false);
|
||||||
_entryInvalid = !_imcIdentifier.Validate() || _defaultEntry.MaterialId == 0 || !_entryExists;
|
_entryInvalid = !_imcIdentifier.Validate() || _defaultEntry.MaterialId == 0 || !_entryExists;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@ using OtterGui;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
using OtterGui.Text;
|
using OtterGui.Text;
|
||||||
using OtterGui.Text.Widget;
|
using OtterGui.Text.Widget;
|
||||||
using OtterGui.Widgets;
|
|
||||||
using OtterGuiInternal.Utility;
|
using OtterGuiInternal.Utility;
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.GameData.Structs;
|
||||||
|
using Penumbra.Meta;
|
||||||
using Penumbra.Mods.Groups;
|
using Penumbra.Mods.Groups;
|
||||||
using Penumbra.Mods.Manager.OptionEditor;
|
using Penumbra.Mods.Manager.OptionEditor;
|
||||||
using Penumbra.Mods.SubMods;
|
using Penumbra.Mods.SubMods;
|
||||||
|
|
@ -19,18 +19,25 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr
|
||||||
public void Draw()
|
public void Draw()
|
||||||
{
|
{
|
||||||
var identifier = group.Identifier;
|
var identifier = group.Identifier;
|
||||||
var defaultEntry = editor.ImcChecker.GetDefaultEntry(identifier, true).Entry;
|
var defaultEntry = ImcChecker.GetDefaultEntry(identifier, true).Entry;
|
||||||
var entry = group.DefaultEntry;
|
var entry = group.DefaultEntry;
|
||||||
var changes = false;
|
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.TextFramed(identifier.ToString(), 0, new Vector2(width, 0), borderColor: ImGui.GetColorU32(ImGuiCol.Border));
|
||||||
|
|
||||||
ImUtf8.SameLineInner();
|
ImUtf8.SameLineInner();
|
||||||
var allVariants = group.AllVariants;
|
var allVariants = group.AllVariants;
|
||||||
if (ImUtf8.Checkbox("All Variants"u8, ref allVariants))
|
if (ImUtf8.Checkbox("All Variants"u8, ref allVariants))
|
||||||
editor.ModManager.OptionEditor.ImcEditor.ChangeAllVariants(group, 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);
|
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())
|
using (ImUtf8.Group())
|
||||||
{
|
{
|
||||||
ImUtf8.TextFrameAligned("Material ID"u8);
|
ImUtf8.TextFrameAligned("Material ID"u8);
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ using OtterGui.Filesystem;
|
||||||
using OtterGui.FileSystem.Selector;
|
using OtterGui.FileSystem.Selector;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
|
using OtterGui.Text;
|
||||||
|
using OtterGui.Text.Widget;
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
using Penumbra.Collections;
|
using Penumbra.Collections;
|
||||||
using Penumbra.Collections.Manager;
|
using Penumbra.Collections.Manager;
|
||||||
|
|
@ -25,7 +27,6 @@ namespace Penumbra.UI.ModsTab;
|
||||||
public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSystemSelector.ModState>, IUiService
|
public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSystemSelector.ModState>, IUiService
|
||||||
{
|
{
|
||||||
private readonly CommunicatorService _communicator;
|
private readonly CommunicatorService _communicator;
|
||||||
private readonly MessageService _messager;
|
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
private readonly FileDialogService _fileDialog;
|
private readonly FileDialogService _fileDialog;
|
||||||
private readonly ModManager _modManager;
|
private readonly ModManager _modManager;
|
||||||
|
|
@ -33,15 +34,12 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
||||||
private readonly TutorialService _tutorial;
|
private readonly TutorialService _tutorial;
|
||||||
private readonly ModImportManager _modImportManager;
|
private readonly ModImportManager _modImportManager;
|
||||||
private readonly IDragDropManager _dragDrop;
|
private readonly IDragDropManager _dragDrop;
|
||||||
private readonly ModSearchStringSplitter Filter = new();
|
private readonly ModSearchStringSplitter _filter = new();
|
||||||
|
private readonly ModSelection _selection;
|
||||||
public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty;
|
|
||||||
public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty;
|
|
||||||
|
|
||||||
|
|
||||||
public ModFileSystemSelector(IKeyState keyState, CommunicatorService communicator, ModFileSystem fileSystem, ModManager modManager,
|
public ModFileSystemSelector(IKeyState keyState, CommunicatorService communicator, ModFileSystem fileSystem, ModManager modManager,
|
||||||
CollectionManager collectionManager, Configuration config, TutorialService tutorial, FileDialogService fileDialog,
|
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)
|
: base(fileSystem, keyState, Penumbra.Log, HandleException, allowMultipleSelection: true)
|
||||||
{
|
{
|
||||||
_communicator = communicator;
|
_communicator = communicator;
|
||||||
|
|
@ -50,9 +48,9 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
||||||
_config = config;
|
_config = config;
|
||||||
_tutorial = tutorial;
|
_tutorial = tutorial;
|
||||||
_fileDialog = fileDialog;
|
_fileDialog = fileDialog;
|
||||||
_messager = messager;
|
|
||||||
_modImportManager = modImportManager;
|
_modImportManager = modImportManager;
|
||||||
_dragDrop = dragDrop;
|
_dragDrop = dragDrop;
|
||||||
|
_selection = selection;
|
||||||
|
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
SubscribeRightClickFolder(EnableDescendants, 10);
|
SubscribeRightClickFolder(EnableDescendants, 10);
|
||||||
|
|
@ -78,22 +76,16 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
SetFilterTooltip();
|
SetFilterTooltip();
|
||||||
|
|
||||||
SelectionChanged += OnSelectionChange;
|
if (_selection.Mod != null)
|
||||||
if (_config.Ephemeral.LastModPath.Length > 0)
|
SelectByValue(_selection.Mod);
|
||||||
{
|
|
||||||
var mod = _modManager.FirstOrDefault(m
|
|
||||||
=> string.Equals(m.Identifier, _config.Ephemeral.LastModPath, StringComparison.OrdinalIgnoreCase));
|
|
||||||
if (mod != null)
|
|
||||||
SelectByValue(mod);
|
|
||||||
}
|
|
||||||
|
|
||||||
_communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.ModFileSystemSelector);
|
_communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.ModFileSystemSelector);
|
||||||
_communicator.ModSettingChanged.Subscribe(OnSettingChange, ModSettingChanged.Priority.ModFileSystemSelector);
|
_communicator.ModSettingChanged.Subscribe(OnSettingChange, ModSettingChanged.Priority.ModFileSystemSelector);
|
||||||
_communicator.CollectionInheritanceChanged.Subscribe(OnInheritanceChange, CollectionInheritanceChanged.Priority.ModFileSystemSelector);
|
_communicator.CollectionInheritanceChanged.Subscribe(OnInheritanceChange, CollectionInheritanceChanged.Priority.ModFileSystemSelector);
|
||||||
_communicator.ModDataChanged.Subscribe(OnModDataChange, ModDataChanged.Priority.ModFileSystemSelector);
|
_communicator.ModDataChanged.Subscribe(OnModDataChange, ModDataChanged.Priority.ModFileSystemSelector);
|
||||||
_communicator.ModDiscoveryStarted.Subscribe(StoreCurrentSelection, ModDiscoveryStarted.Priority.ModFileSystemSelector);
|
_communicator.ModDiscoveryStarted.Subscribe(StoreCurrentSelection, ModDiscoveryStarted.Priority.ModFileSystemSelector);
|
||||||
_communicator.ModDiscoveryFinished.Subscribe(RestoreLastSelection, ModDiscoveryFinished.Priority.ModFileSystemSelector);
|
_communicator.ModDiscoveryFinished.Subscribe(RestoreLastSelection, ModDiscoveryFinished.Priority.ModFileSystemSelector);
|
||||||
OnCollectionChange(CollectionType.Current, null, _collectionManager.Active.Current, "");
|
SetFilterDirty();
|
||||||
|
SelectionChanged += OnSelectionChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetRenameSearchPath(RenameField value)
|
public void SetRenameSearchPath(RenameField value)
|
||||||
|
|
@ -190,7 +182,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
||||||
var newDir = _modManager.Creator.CreateEmptyMod(_modManager.BasePath, _newModName);
|
var newDir = _modManager.Creator.CreateEmptyMod(_modManager.BasePath, _newModName);
|
||||||
if (newDir != null)
|
if (newDir != null)
|
||||||
{
|
{
|
||||||
_modManager.AddMod(newDir);
|
_modManager.AddMod(newDir, false);
|
||||||
_newModName = string.Empty;
|
_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)
|
private void OnSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx, bool inherited)
|
||||||
{
|
{
|
||||||
if (collection != _collectionManager.Active.Current)
|
if (collection == _collectionManager.Active.Current)
|
||||||
return;
|
SetFilterDirty();
|
||||||
|
|
||||||
SetFilterDirty();
|
|
||||||
if (mod == Selected)
|
|
||||||
OnSelectionChange(Selected, Selected, default);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnModDataChange(ModDataChangeType type, Mod mod, string? oldName)
|
private void OnModDataChange(ModDataChangeType type, Mod mod, string? oldName)
|
||||||
{
|
{
|
||||||
switch (type)
|
const ModDataChangeType relevantFlags =
|
||||||
{
|
ModDataChangeType.Name
|
||||||
case ModDataChangeType.Name:
|
| ModDataChangeType.Author
|
||||||
case ModDataChangeType.Author:
|
| ModDataChangeType.ModTags
|
||||||
case ModDataChangeType.ModTags:
|
| ModDataChangeType.LocalTags
|
||||||
case ModDataChangeType.LocalTags:
|
| ModDataChangeType.Favorite
|
||||||
case ModDataChangeType.Favorite:
|
| ModDataChangeType.ImportDate;
|
||||||
SetFilterDirty();
|
if ((type & relevantFlags) != 0)
|
||||||
break;
|
SetFilterDirty();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnInheritanceChange(ModCollection collection, bool _)
|
private void OnInheritanceChange(ModCollection collection, bool _)
|
||||||
{
|
{
|
||||||
if (collection != _collectionManager.Active.Current)
|
if (collection == _collectionManager.Active.Current)
|
||||||
return;
|
SetFilterDirty();
|
||||||
|
|
||||||
SetFilterDirty();
|
|
||||||
OnSelectionChange(Selected, Selected, default);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string _)
|
private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string _)
|
||||||
{
|
{
|
||||||
if (collectionType is not CollectionType.Current || oldCollection == newCollection)
|
if (collectionType is CollectionType.Current && oldCollection != newCollection)
|
||||||
return;
|
SetFilterDirty();
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep selections across rediscoveries if possible.
|
// Keep selections across rediscoveries if possible.
|
||||||
|
|
@ -530,6 +490,9 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
||||||
_lastSelectedDirectory = string.Empty;
|
_lastSelectedDirectory = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnSelectionChanged(Mod? oldSelection, Mod? newSelection, in ModState state)
|
||||||
|
=> _selection.SelectMod(newSelection);
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Filters
|
#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>
|
/// <summary> Appropriately identify and set the string filter and its type. </summary>
|
||||||
protected override bool ChangeFilter(string filterValue)
|
protected override bool ChangeFilter(string filterValue)
|
||||||
{
|
{
|
||||||
Filter.Parse(filterValue);
|
_filter.Parse(filterValue);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -597,7 +560,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
||||||
{
|
{
|
||||||
state = default;
|
state = default;
|
||||||
return ModFilterExtensions.UnfilteredStateMods != _stateFilter
|
return ModFilterExtensions.UnfilteredStateMods != _stateFilter
|
||||||
|| !Filter.IsVisible(f);
|
|| !_filter.IsVisible(f);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ApplyFiltersAndState((ModFileSystem.Leaf)path, out state);
|
return ApplyFiltersAndState((ModFileSystem.Leaf)path, out state);
|
||||||
|
|
@ -605,7 +568,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
||||||
|
|
||||||
/// <summary> Apply the string filters. </summary>
|
/// <summary> Apply the string filters. </summary>
|
||||||
private bool ApplyStringFilters(ModFileSystem.Leaf leaf, Mod mod)
|
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>
|
/// <summary> Only get the text color for a mod if no filters are set. </summary>
|
||||||
private ColorId GetTextColor(Mod mod, ModSettings? settings, ModCollection collection)
|
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,
|
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing,
|
||||||
ImGui.GetStyle().ItemSpacing with { Y = 3 * UiHelpers.Scale });
|
ImGui.GetStyle().ItemSpacing with { Y = 3 * UiHelpers.Scale });
|
||||||
var flags = (int)_stateFilter;
|
|
||||||
|
|
||||||
|
|
||||||
if (ImGui.Checkbox("Everything", ref everything))
|
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));
|
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))
|
if (TriStateCheckbox.Instance.Draw(name, ref _stateFilter, onFlag, offFlag))
|
||||||
{
|
|
||||||
_stateFilter = (ModFilter)flags;
|
|
||||||
SetFilterDirty();
|
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 const ModFilter UnfilteredStateMods = (ModFilter)((1 << 20) - 1);
|
||||||
|
|
||||||
public static string ToName(this ModFilter filter)
|
public static IReadOnlyList<(ModFilter On, ModFilter Off, string Name)> TriStatePairs =
|
||||||
=> filter switch
|
[
|
||||||
{
|
(ModFilter.Enabled, ModFilter.Disabled, "Enabled"),
|
||||||
ModFilter.Enabled => "Enabled",
|
(ModFilter.IsNew, ModFilter.NotNew, "Newly Imported"),
|
||||||
ModFilter.Disabled => "Disabled",
|
(ModFilter.Favorite, ModFilter.NotFavorite, "Favorite"),
|
||||||
ModFilter.Favorite => "Favorite",
|
(ModFilter.HasConfig, ModFilter.HasNoConfig, "Has Options"),
|
||||||
ModFilter.NotFavorite => "No Favorite",
|
(ModFilter.HasFiles, ModFilter.HasNoFiles, "Has Redirections"),
|
||||||
ModFilter.NoConflict => "No Conflicts",
|
(ModFilter.HasMetaManipulations, ModFilter.HasNoMetaManipulations, "Has Meta Manipulations"),
|
||||||
ModFilter.SolvedConflict => "Solved Conflicts",
|
(ModFilter.HasFileSwaps, ModFilter.HasNoFileSwaps, "Has File Swaps"),
|
||||||
ModFilter.UnsolvedConflict => "Unsolved Conflicts",
|
];
|
||||||
ModFilter.HasNoMetaManipulations => "No Meta Manipulations",
|
|
||||||
ModFilter.HasMetaManipulations => "Meta Manipulations",
|
public static IReadOnlyList<IReadOnlyList<(ModFilter Filter, string Name)>> Groups =
|
||||||
ModFilter.HasNoFileSwaps => "No File Swaps",
|
[
|
||||||
ModFilter.HasFileSwaps => "File Swaps",
|
[
|
||||||
ModFilter.HasNoConfig => "No Configuration",
|
(ModFilter.NoConflict, "Has No Conflicts"),
|
||||||
ModFilter.HasConfig => "Configuration",
|
(ModFilter.SolvedConflict, "Has Solved Conflicts"),
|
||||||
ModFilter.HasNoFiles => "No Files",
|
(ModFilter.UnsolvedConflict, "Has Unsolved Conflicts"),
|
||||||
ModFilter.HasFiles => "Files",
|
],
|
||||||
ModFilter.IsNew => "Newly Imported",
|
[
|
||||||
ModFilter.NotNew => "Not Newly Imported",
|
(ModFilter.Undefined, "Not Configured"),
|
||||||
ModFilter.Inherited => "Inherited Configuration",
|
(ModFilter.Inherited, "Inherited Configuration"),
|
||||||
ModFilter.Uninherited => "Own Configuration",
|
(ModFilter.Uninherited, "Own Configuration"),
|
||||||
ModFilter.Undefined => "Not Configured",
|
],
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(filter), filter, null),
|
];
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,22 +10,23 @@ namespace Penumbra.UI.ModsTab;
|
||||||
|
|
||||||
public class ModPanel : IDisposable, IUiService
|
public class ModPanel : IDisposable, IUiService
|
||||||
{
|
{
|
||||||
private readonly MultiModPanel _multiModPanel;
|
private readonly MultiModPanel _multiModPanel;
|
||||||
private readonly ModFileSystemSelector _selector;
|
private readonly ModSelection _selection;
|
||||||
private readonly ModEditWindow _editWindow;
|
private readonly ModEditWindow _editWindow;
|
||||||
private readonly ModPanelHeader _header;
|
private readonly ModPanelHeader _header;
|
||||||
private readonly ModPanelTabBar _tabs;
|
private readonly ModPanelTabBar _tabs;
|
||||||
private bool _resetCursor;
|
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)
|
MultiModPanel multiModPanel, CommunicatorService communicator)
|
||||||
{
|
{
|
||||||
_selector = selector;
|
_selection = selection;
|
||||||
_editWindow = editWindow;
|
_editWindow = editWindow;
|
||||||
_tabs = tabs;
|
_tabs = tabs;
|
||||||
_multiModPanel = multiModPanel;
|
_multiModPanel = multiModPanel;
|
||||||
_header = new ModPanelHeader(pi, communicator);
|
_header = new ModPanelHeader(pi, communicator);
|
||||||
_selector.SelectionChanged += OnSelectionChange;
|
_selection.Subscribe(OnSelectionChange, ModSelection.Priority.ModPanel);
|
||||||
|
OnSelectionChange(null, _selection.Mod);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Draw()
|
public void Draw()
|
||||||
|
|
@ -52,17 +53,17 @@ public class ModPanel : IDisposable, IUiService
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_selector.SelectionChanged -= OnSelectionChange;
|
_selection.Unsubscribe(OnSelectionChange);
|
||||||
_header.Dispose();
|
_header.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _valid;
|
private bool _valid;
|
||||||
private Mod _mod = null!;
|
private Mod _mod = null!;
|
||||||
|
|
||||||
private void OnSelectionChange(Mod? old, Mod? mod, in ModFileSystemSelector.ModState _)
|
private void OnSelectionChange(Mod? old, Mod? mod)
|
||||||
{
|
{
|
||||||
_resetCursor = true;
|
_resetCursor = true;
|
||||||
if (mod == null || _selector.Selected == null)
|
if (mod == null || _selection.Mod == null)
|
||||||
{
|
{
|
||||||
_editWindow.IsOpen = false;
|
_editWindow.IsOpen = false;
|
||||||
_valid = false;
|
_valid = false;
|
||||||
|
|
@ -73,7 +74,7 @@ public class ModPanel : IDisposable, IUiService
|
||||||
_editWindow.ChangeMod(mod);
|
_editWindow.ChangeMod(mod);
|
||||||
_valid = true;
|
_valid = true;
|
||||||
_mod = mod;
|
_mod = mod;
|
||||||
_header.UpdateModData(_mod);
|
_header.ChangeMod(_mod);
|
||||||
_tabs.Settings.Reset();
|
_tabs.Settings.Reset();
|
||||||
_tabs.Edit.Reset();
|
_tabs.Edit.Reset();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSy
|
||||||
=> "Conflicts"u8;
|
=> "Conflicts"u8;
|
||||||
|
|
||||||
public bool IsVisible
|
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 = [];
|
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.
|
// Can not be null because otherwise the tab bar is never drawn.
|
||||||
var mod = selector.Selected!;
|
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())
|
.ThenBy(c => c.Mod2.Name.Lower).WithIndex())
|
||||||
{
|
{
|
||||||
using var id = ImRaii.PushId(index);
|
using var id = ImRaii.PushId(index);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ using OtterGui.Raii;
|
||||||
using OtterGui.Widgets;
|
using OtterGui.Widgets;
|
||||||
using OtterGui.Classes;
|
using OtterGui.Classes;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
|
using OtterGui.Text;
|
||||||
using Penumbra.Mods;
|
using Penumbra.Mods;
|
||||||
using Penumbra.Mods.Editor;
|
using Penumbra.Mods.Editor;
|
||||||
using Penumbra.Mods.Manager;
|
using Penumbra.Mods.Manager;
|
||||||
|
|
@ -50,6 +51,8 @@ public class ModPanelEditTab(
|
||||||
EditButtons();
|
EditButtons();
|
||||||
EditRegularMeta();
|
EditRegularMeta();
|
||||||
UiHelpers.DefaultLineSpace();
|
UiHelpers.DefaultLineSpace();
|
||||||
|
EditLocalData();
|
||||||
|
UiHelpers.DefaultLineSpace();
|
||||||
|
|
||||||
if (Input.Text("Mod Path", Input.Path, Input.None, _leaf.FullName(), out var newPath, 256, UiHelpers.InputTextWidth.X))
|
if (Input.Text("Mod Path", Input.Path, Input.None, _leaf.FullName(), out var newPath, 256, UiHelpers.InputTextWidth.X))
|
||||||
try
|
try
|
||||||
|
|
@ -182,6 +185,40 @@ public class ModPanelEditTab(
|
||||||
DrawOpenDefaultMod();
|
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()
|
private void DrawOpenDefaultMod()
|
||||||
{
|
{
|
||||||
var file = filenames.OptionGroupFile(_mod, -1, false);
|
var file = filenames.OptionGroupFile(_mod, -1, false);
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@ public class ModPanelHeader : IDisposable
|
||||||
private readonly IFontHandle _nameFont;
|
private readonly IFontHandle _nameFont;
|
||||||
|
|
||||||
private readonly CommunicatorService _communicator;
|
private readonly CommunicatorService _communicator;
|
||||||
private float _lastPreSettingsHeight = 0;
|
private float _lastPreSettingsHeight;
|
||||||
|
private bool _dirty = true;
|
||||||
|
|
||||||
public ModPanelHeader(IDalamudPluginInterface pi, CommunicatorService communicator)
|
public ModPanelHeader(IDalamudPluginInterface pi, CommunicatorService communicator)
|
||||||
{
|
{
|
||||||
|
|
@ -33,6 +34,7 @@ public class ModPanelHeader : IDisposable
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Draw()
|
public void Draw()
|
||||||
{
|
{
|
||||||
|
UpdateModData();
|
||||||
var height = ImGui.GetContentRegionAvail().Y;
|
var height = ImGui.GetContentRegionAvail().Y;
|
||||||
var maxHeight = 3 * height / 4;
|
var maxHeight = 3 * height / 4;
|
||||||
using var child = _lastPreSettingsHeight > maxHeight && _communicator.PreSettingsTabBarDraw.HasSubscribers
|
using var child = _lastPreSettingsHeight > maxHeight && _communicator.PreSettingsTabBarDraw.HasSubscribers
|
||||||
|
|
@ -49,16 +51,25 @@ public class ModPanelHeader : IDisposable
|
||||||
_lastPreSettingsHeight = ImGui.GetCursorPosY();
|
_lastPreSettingsHeight = ImGui.GetCursorPosY();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ChangeMod(Mod mod)
|
||||||
|
{
|
||||||
|
_mod = mod;
|
||||||
|
_dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update all mod header data. Should someone change frame padding or item spacing,
|
/// 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.
|
/// or his default font, this will break, but he will just have to select a different mod to restore.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void UpdateModData(Mod mod)
|
private void UpdateModData()
|
||||||
{
|
{
|
||||||
|
if (!_dirty)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_dirty = false;
|
||||||
_lastPreSettingsHeight = 0;
|
_lastPreSettingsHeight = 0;
|
||||||
_mod = mod;
|
|
||||||
// Name
|
// Name
|
||||||
var name = $" {mod.Name} ";
|
var name = $" {_mod.Name} ";
|
||||||
if (name != _modName)
|
if (name != _modName)
|
||||||
{
|
{
|
||||||
using var f = _nameFont.Push();
|
using var f = _nameFont.Push();
|
||||||
|
|
@ -67,16 +78,16 @@ public class ModPanelHeader : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
// Author
|
// Author
|
||||||
if (mod.Author != _modAuthor)
|
if (_mod.Author != _modAuthor)
|
||||||
{
|
{
|
||||||
var author = mod.Author.IsEmpty ? string.Empty : $"by {mod.Author}";
|
var author = _mod.Author.IsEmpty ? string.Empty : $"by {_mod.Author}";
|
||||||
_modAuthor = mod.Author.Text;
|
_modAuthor = _mod.Author.Text;
|
||||||
_modAuthorWidth = ImGui.CalcTextSize(author).X;
|
_modAuthorWidth = ImGui.CalcTextSize(author).X;
|
||||||
_secondRowWidth = _modAuthorWidth + _modWebsiteButtonWidth + ImGui.GetStyle().ItemSpacing.X;
|
_secondRowWidth = _modAuthorWidth + _modWebsiteButtonWidth + ImGui.GetStyle().ItemSpacing.X;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Version
|
// Version
|
||||||
var version = mod.Version.Length > 0 ? $"({mod.Version})" : string.Empty;
|
var version = _mod.Version.Length > 0 ? $"({_mod.Version})" : string.Empty;
|
||||||
if (version != _modVersion)
|
if (version != _modVersion)
|
||||||
{
|
{
|
||||||
_modVersion = version;
|
_modVersion = version;
|
||||||
|
|
@ -84,9 +95,9 @@ public class ModPanelHeader : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
// Website
|
// Website
|
||||||
if (_modWebsite != mod.Website)
|
if (_modWebsite != _mod.Website)
|
||||||
{
|
{
|
||||||
_modWebsite = mod.Website;
|
_modWebsite = _mod.Website;
|
||||||
_websiteValid = Uri.TryCreate(_modWebsite, UriKind.Absolute, out var uriResult)
|
_websiteValid = Uri.TryCreate(_modWebsite, UriKind.Absolute, out var uriResult)
|
||||||
&& (uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp);
|
&& (uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp);
|
||||||
_modWebsiteButton = _websiteValid ? "Open Website" : _modWebsite.Length == 0 ? string.Empty : $"from {_modWebsite}";
|
_modWebsiteButton = _websiteValid ? "Open Website" : _modWebsite.Length == 0 ? string.Empty : $"from {_modWebsite}";
|
||||||
|
|
@ -253,7 +264,6 @@ public class ModPanelHeader : IDisposable
|
||||||
{
|
{
|
||||||
const ModDataChangeType relevantChanges =
|
const ModDataChangeType relevantChanges =
|
||||||
ModDataChangeType.Author | ModDataChangeType.Name | ModDataChangeType.Website | ModDataChangeType.Version;
|
ModDataChangeType.Author | ModDataChangeType.Name | ModDataChangeType.Website | ModDataChangeType.Version;
|
||||||
if ((changeType & relevantChanges) != 0)
|
_dirty = (changeType & relevantChanges) != 0;
|
||||||
UpdateModData(mod);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@ using ImGuiNET;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
|
using OtterGui.Text;
|
||||||
using OtterGui.Widgets;
|
using OtterGui.Widgets;
|
||||||
using Penumbra.Collections;
|
|
||||||
using Penumbra.UI.Classes;
|
using Penumbra.UI.Classes;
|
||||||
using Penumbra.Collections.Manager;
|
using Penumbra.Collections.Manager;
|
||||||
|
using Penumbra.Mods;
|
||||||
using Penumbra.Mods.Manager;
|
using Penumbra.Mods.Manager;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
using Penumbra.Mods.Settings;
|
using Penumbra.Mods.Settings;
|
||||||
|
|
@ -16,16 +17,14 @@ namespace Penumbra.UI.ModsTab;
|
||||||
public class ModPanelSettingsTab(
|
public class ModPanelSettingsTab(
|
||||||
CollectionManager collectionManager,
|
CollectionManager collectionManager,
|
||||||
ModManager modManager,
|
ModManager modManager,
|
||||||
ModFileSystemSelector selector,
|
ModSelection selection,
|
||||||
TutorialService tutorial,
|
TutorialService tutorial,
|
||||||
CommunicatorService communicator,
|
CommunicatorService communicator,
|
||||||
ModGroupDrawer modGroupDrawer)
|
ModGroupDrawer modGroupDrawer)
|
||||||
: ITab, IUiService
|
: ITab, IUiService
|
||||||
{
|
{
|
||||||
private bool _inherited;
|
private bool _inherited;
|
||||||
private ModSettings _settings = null!;
|
private int? _currentPriority;
|
||||||
private ModCollection _collection = null!;
|
|
||||||
private int? _currentPriority;
|
|
||||||
|
|
||||||
public ReadOnlySpan<byte> Label
|
public ReadOnlySpan<byte> Label
|
||||||
=> "Settings"u8;
|
=> "Settings"u8;
|
||||||
|
|
@ -42,12 +41,10 @@ public class ModPanelSettingsTab(
|
||||||
if (!child)
|
if (!child)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_settings = selector.SelectedSettings;
|
_inherited = selection.Collection != collectionManager.Active.Current;
|
||||||
_collection = selector.SelectedSettingCollection;
|
|
||||||
_inherited = _collection != collectionManager.Active.Current;
|
|
||||||
DrawInheritedWarning();
|
DrawInheritedWarning();
|
||||||
UiHelpers.DefaultLineSpace();
|
UiHelpers.DefaultLineSpace();
|
||||||
communicator.PreSettingsPanelDraw.Invoke(selector.Selected!.Identifier);
|
communicator.PreSettingsPanelDraw.Invoke(selection.Mod!.Identifier);
|
||||||
DrawEnabledInput();
|
DrawEnabledInput();
|
||||||
tutorial.OpenTutorial(BasicTutorialSteps.EnablingMods);
|
tutorial.OpenTutorial(BasicTutorialSteps.EnablingMods);
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
|
|
@ -55,11 +52,11 @@ public class ModPanelSettingsTab(
|
||||||
tutorial.OpenTutorial(BasicTutorialSteps.Priority);
|
tutorial.OpenTutorial(BasicTutorialSteps.Priority);
|
||||||
DrawRemoveSettings();
|
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();
|
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>
|
/// <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);
|
using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.PressEnterWarningBg);
|
||||||
var width = new Vector2(ImGui.GetContentRegionAvail().X, 0);
|
var width = new Vector2(ImGui.GetContentRegionAvail().X, 0);
|
||||||
if (ImGui.Button($"These settings are inherited from {_collection.Name}.", width))
|
if (ImGui.Button($"These settings are inherited from {selection.Collection.Name}.", width))
|
||||||
collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selector.Selected!, false);
|
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"
|
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.");
|
+ "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>
|
/// <summary> Draw a checkbox for the enabled status of the mod. </summary>
|
||||||
private void DrawEnabledInput()
|
private void DrawEnabledInput()
|
||||||
{
|
{
|
||||||
var enabled = _settings.Enabled;
|
var enabled = selection.Settings.Enabled;
|
||||||
if (!ImGui.Checkbox("Enabled", ref enabled))
|
if (!ImGui.Checkbox("Enabled", ref enabled))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
modManager.SetKnown(selector.Selected!);
|
modManager.SetKnown(selection.Mod!);
|
||||||
collectionManager.Editor.SetModState(collectionManager.Active.Current, selector.Selected!, enabled);
|
collectionManager.Editor.SetModState(collectionManager.Active.Current, selection.Mod!, enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -95,15 +92,19 @@ public class ModPanelSettingsTab(
|
||||||
private void DrawPriorityInput()
|
private void DrawPriorityInput()
|
||||||
{
|
{
|
||||||
using var group = ImRaii.Group();
|
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);
|
ImGui.SetNextItemWidth(50 * UiHelpers.Scale);
|
||||||
if (ImGui.InputInt("##Priority", ref priority, 0, 0))
|
if (ImGui.InputInt("##Priority", ref priority, 0, 0))
|
||||||
_currentPriority = priority;
|
_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 (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue)
|
||||||
{
|
{
|
||||||
if (_currentPriority != _settings.Priority.Value)
|
if (_currentPriority != settings.Priority.Value)
|
||||||
collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selector.Selected!,
|
collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selection.Mod!,
|
||||||
new ModPriority(_currentPriority.Value));
|
new ModPriority(_currentPriority.Value));
|
||||||
|
|
||||||
_currentPriority = null;
|
_currentPriority = null;
|
||||||
|
|
@ -120,13 +121,13 @@ public class ModPanelSettingsTab(
|
||||||
private void DrawRemoveSettings()
|
private void DrawRemoveSettings()
|
||||||
{
|
{
|
||||||
const string text = "Inherit Settings";
|
const string text = "Inherit Settings";
|
||||||
if (_inherited || _settings == ModSettings.Empty)
|
if (_inherited || selection.Settings == ModSettings.Empty)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0;
|
var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0;
|
||||||
ImGui.SameLine(ImGui.GetWindowWidth() - ImGui.CalcTextSize(text).X - ImGui.GetStyle().FramePadding.X * 2 - scroll);
|
ImGui.SameLine(ImGui.GetWindowWidth() - ImGui.CalcTextSize(text).X - ImGui.GetStyle().FramePadding.X * 2 - scroll);
|
||||||
if (ImGui.Button(text))
|
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"
|
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.");
|
+ "If no inherited collection has settings for this mod, it will be disabled.");
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using OtterGui;
|
||||||
using OtterGui.Classes;
|
using OtterGui.Classes;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
using OtterGui.Table;
|
using OtterGui.Table;
|
||||||
|
using OtterGui.Text;
|
||||||
using Penumbra.Enums;
|
using Penumbra.Enums;
|
||||||
using Penumbra.Interop.Structs;
|
using Penumbra.Interop.Structs;
|
||||||
using Penumbra.String;
|
using Penumbra.String;
|
||||||
|
|
@ -52,36 +53,41 @@ internal sealed class ResourceWatcherTable : Table<Record>
|
||||||
|
|
||||||
private static unsafe void DrawByteString(CiByteString path, float length)
|
private static unsafe void DrawByteString(CiByteString path, float length)
|
||||||
{
|
{
|
||||||
Vector2 vec;
|
if (path.IsEmpty)
|
||||||
ImGuiNative.igCalcTextSize(&vec, path.Path, path.Path + path.Length, 0, 0);
|
return;
|
||||||
if (vec.X <= length)
|
|
||||||
|
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
|
else
|
||||||
{
|
{
|
||||||
var fileName = path.LastIndexOf((byte)'/');
|
var fileName = path.LastIndexOf((byte)'/');
|
||||||
CiByteString shortPath;
|
using (ImRaii.Group())
|
||||||
if (fileName != -1)
|
|
||||||
{
|
{
|
||||||
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(2 * UiHelpers.Scale));
|
CiByteString shortPath;
|
||||||
using var font = ImRaii.PushFont(UiBuilder.IconFont);
|
if (fileName != -1)
|
||||||
ImGui.TextUnformatted(FontAwesomeIcon.EllipsisH.ToIconString());
|
{
|
||||||
ImGui.SameLine();
|
using var font = ImRaii.PushFont(UiBuilder.IconFont);
|
||||||
shortPath = path.Substring(fileName, path.Length - fileName);
|
clicked = ImUtf8.Selectable(FontAwesomeIcon.EllipsisH.ToIconString());
|
||||||
}
|
ImUtf8.SameLineInner();
|
||||||
else
|
shortPath = path.Substring(fileName, path.Length - fileName);
|
||||||
{
|
}
|
||||||
shortPath = path;
|
else
|
||||||
|
{
|
||||||
|
shortPath = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
clicked |= ImUtf8.Selectable(shortPath.Span, false, ImGuiSelectableFlags.AllowItemOverlap);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGuiNative.igTextUnformatted(shortPath.Path, shortPath.Path + shortPath.Length);
|
ImUtf8.HoverTooltip(path.Span);
|
||||||
if (ImGui.IsItemClicked())
|
|
||||||
ImGuiNative.igSetClipboardText(path.Path);
|
|
||||||
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
ImGuiNative.igSetTooltip(path.Path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (clicked)
|
||||||
|
ImUtf8.SetClipboardText(path.Span);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class RecordTypeColumn : ColumnFlags<RecordType, Record>
|
private sealed class RecordTypeColumn : ColumnFlags<RecordType, Record>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Utility;
|
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using Dalamud.Interface.Windowing;
|
using Dalamud.Interface.Windowing;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
|
|
@ -43,7 +42,6 @@ using Penumbra.Api.IpcTester;
|
||||||
using Penumbra.Interop.Hooks.PostProcessing;
|
using Penumbra.Interop.Hooks.PostProcessing;
|
||||||
using Penumbra.Interop.Hooks.ResourceLoading;
|
using Penumbra.Interop.Hooks.ResourceLoading;
|
||||||
using Penumbra.GameData.Files.StainMapStructs;
|
using Penumbra.GameData.Files.StainMapStructs;
|
||||||
using Penumbra.UI.AdvancedWindow;
|
|
||||||
using Penumbra.UI.AdvancedWindow.Materials;
|
using Penumbra.UI.AdvancedWindow.Materials;
|
||||||
|
|
||||||
namespace Penumbra.UI.Tabs.Debug;
|
namespace Penumbra.UI.Tabs.Debug;
|
||||||
|
|
@ -721,7 +719,8 @@ public class DebugTab : Window, ITab, IUiService
|
||||||
if (!tree)
|
if (!tree)
|
||||||
continue;
|
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)
|
if (!table)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,8 +82,7 @@ public class ModsTab(
|
||||||
+ $"{selector.SortMode.Name} Sort Mode\n"
|
+ $"{selector.SortMode.Name} Sort Mode\n"
|
||||||
+ $"{selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n"
|
+ $"{selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n"
|
||||||
+ $"{selector.Selected?.Name ?? "NULL"} Selected Mod\n"
|
+ $"{selector.Selected?.Name ?? "NULL"} Selected Mod\n"
|
||||||
+ $"{string.Join(", ", _activeCollections.Current.DirectlyInheritsFrom.Select(c => c.AnonymizedName))} Inheritances\n"
|
+ $"{string.Join(", ", _activeCollections.Current.DirectlyInheritsFrom.Select(c => c.AnonymizedName))} Inheritances\n");
|
||||||
+ $"{selector.SelectedSettingCollection.AnonymizedName} Collection\n");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -820,13 +820,13 @@ public class SettingsTab : ITab, IUiService
|
||||||
if (ImGuiUtil.DrawDisabledButton("Compress Existing Files", Vector2.Zero,
|
if (ImGuiUtil.DrawDisabledButton("Compress Existing Files", Vector2.Zero,
|
||||||
"Try to compress all files in your root directory. This will take a while.",
|
"Try to compress all files in your root directory. This will take a while.",
|
||||||
_compactor.MassCompactRunning || !_modManager.Valid))
|
_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();
|
ImGui.SameLine();
|
||||||
if (ImGuiUtil.DrawDisabledButton("Decompress Existing Files", Vector2.Zero,
|
if (ImGuiUtil.DrawDisabledButton("Decompress Existing Files", Vector2.Zero,
|
||||||
"Try to decompress all files in your root directory. This will take a while.",
|
"Try to decompress all files in your root directory. This will take a while.",
|
||||||
_compactor.MassCompactRunning || !_modManager.Valid))
|
_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)
|
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