Merge branch 'xivdev:master' into main

This commit is contained in:
Noah Bazer 2024-08-18 10:59:52 -04:00 committed by GitHub
commit 271367eaf2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
431 changed files with 26539 additions and 15420 deletions

View file

@ -16,7 +16,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: '7.x.x'
dotnet-version: '8.x.x'
- name: Restore dependencies
run: dotnet restore
- name: Download Dalamud

View file

@ -15,7 +15,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: '7.x.x'
dotnet-version: '8.x.x'
- name: Restore dependencies
run: dotnet restore
- name: Download Dalamud

View file

@ -15,7 +15,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: '7.x.x'
dotnet-version: '8.x.x'
- name: Restore dependencies
run: dotnet restore
- name: Download Dalamud

@ -1 +1 @@
Subproject commit c6f101bbef976b74eb651523445563dd81fafbaf
Subproject commit 07a009134bf5eb7da9a54ba40e82c88fc613544a

@ -1 +1 @@
Subproject commit 31bf4ad9b82fc980d6bda049da595368ad754931
Subproject commit 552246e595ffab2aaba2c75f578d564f8938fc9a

View file

@ -0,0 +1,121 @@
using System.Text.Json.Nodes;
namespace Penumbra.CrashHandler.Buffers;
/// <summary> The types of currently hooked and relevant animation loading functions. </summary>
public enum AnimationInvocationType : int
{
PapLoad,
ActionLoad,
ScheduleClipUpdate,
LoadTimelineResources,
LoadCharacterVfx,
LoadCharacterSound,
ApricotSoundPlay,
LoadAreaVfx,
CharacterBaseLoadAnimation,
}
/// <summary> The full crash entry for an invoked vfx function. </summary>
public record struct VfxFuncInvokedEntry(
double Age,
DateTimeOffset Timestamp,
int ThreadId,
string InvocationType,
string CharacterName,
string CharacterAddress,
Guid CollectionId) : ICrashDataEntry;
/// <summary> Only expose the write interface for the buffer. </summary>
public interface IAnimationInvocationBufferWriter
{
/// <summary> Write a line into the buffer with the given data. </summary>
/// <param name="characterAddress"> The address of the related character, if known. </param>
/// <param name="characterName"> The name of the related character, anonymized or relying on index if unavailable, if known. </param>
/// <param name="collectionId"> The GUID of the associated collection. </param>
/// <param name="type"> The type of VFX func called. </param>
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, Guid collectionId, AnimationInvocationType type);
}
internal sealed class AnimationInvocationBuffer : MemoryMappedBuffer, IAnimationInvocationBufferWriter, IBufferReader
{
private const int _version = 1;
private const int _lineCount = 64;
private const int _lineCapacity = 128;
private const string _name = "Penumbra.AnimationInvocation";
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, Guid collectionId, AnimationInvocationType type)
{
var accessor = GetCurrentLineLocking();
lock (accessor)
{
accessor.Write(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
accessor.Write(8, Environment.CurrentManagedThreadId);
accessor.Write(12, (int)type);
accessor.Write(16, characterAddress);
var span = GetSpan(accessor, 24, 16);
collectionId.TryWriteBytes(span);
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
span = GetSpan(accessor, 40);
WriteSpan(characterName, span);
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
}
}
public uint TotalCount
=> TotalWrittenLines;
public IEnumerable<JsonObject> GetLines(DateTimeOffset crashTime)
{
var lineCount = (int)CurrentLineCount;
for (var i = lineCount - 1; i >= 0; --i)
{
var line = GetLine(i);
var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line));
var thread = BitConverter.ToInt32(line[8..]);
var type = (AnimationInvocationType)BitConverter.ToInt32(line[12..]);
var address = BitConverter.ToUInt64(line[16..]);
var collectionId = new Guid(line[24..40]);
var characterName = ReadString(line[40..]);
yield return new JsonObject()
{
[nameof(VfxFuncInvokedEntry.Age)] = (crashTime - timestamp).TotalSeconds,
[nameof(VfxFuncInvokedEntry.Timestamp)] = timestamp,
[nameof(VfxFuncInvokedEntry.ThreadId)] = thread,
[nameof(VfxFuncInvokedEntry.InvocationType)] = ToName(type),
[nameof(VfxFuncInvokedEntry.CharacterName)] = characterName,
[nameof(VfxFuncInvokedEntry.CharacterAddress)] = address.ToString("X"),
[nameof(VfxFuncInvokedEntry.CollectionId)] = collectionId,
};
}
}
public static IBufferReader CreateReader(int pid)
=> new AnimationInvocationBuffer(false, pid);
public static IAnimationInvocationBufferWriter CreateWriter(int pid)
=> new AnimationInvocationBuffer(pid);
private AnimationInvocationBuffer(bool writer, int pid)
: base($"{_name}_{pid}_{_version}", _version)
{ }
private AnimationInvocationBuffer(int pid)
: base($"{_name}_{pid}_{_version}", _version, _lineCount, _lineCapacity)
{ }
private static string ToName(AnimationInvocationType type)
=> type switch
{
AnimationInvocationType.PapLoad => "PAP Load",
AnimationInvocationType.ActionLoad => "Action Load",
AnimationInvocationType.ScheduleClipUpdate => "Schedule Clip Update",
AnimationInvocationType.LoadTimelineResources => "Load Timeline Resources",
AnimationInvocationType.LoadCharacterVfx => "Load Character VFX",
AnimationInvocationType.LoadCharacterSound => "Load Character Sound",
AnimationInvocationType.ApricotSoundPlay => "Apricot Sound Play",
AnimationInvocationType.LoadAreaVfx => "Load Area VFX",
AnimationInvocationType.CharacterBaseLoadAnimation => "Load Animation (CharacterBase)",
_ => $"Unknown ({(int)type})",
};
}

View file

@ -0,0 +1,87 @@
using System.Text.Json.Nodes;
namespace Penumbra.CrashHandler.Buffers;
/// <summary> Only expose the write interface for the buffer. </summary>
public interface ICharacterBaseBufferWriter
{
/// <summary> Write a line into the buffer with the given data. </summary>
/// <param name="characterAddress"> The address of the related character, if known. </param>
/// <param name="characterName"> The name of the related character, anonymized or relying on index if unavailable, if known. </param>
/// <param name="collectionId"> The GUID of the associated collection. </param>
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, Guid collectionId);
}
/// <summary> The full crash entry for a loaded character base. </summary>
public record struct CharacterLoadedEntry(
double Age,
DateTimeOffset Timestamp,
int ThreadId,
string CharacterName,
string CharacterAddress,
Guid CollectionId) : ICrashDataEntry;
internal sealed class CharacterBaseBuffer : MemoryMappedBuffer, ICharacterBaseBufferWriter, IBufferReader
{
private const int _version = 1;
private const int _lineCount = 10;
private const int _lineCapacity = 128;
private const string _name = "Penumbra.CharacterBase";
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, Guid collectionId)
{
var accessor = GetCurrentLineLocking();
lock (accessor)
{
accessor.Write(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
accessor.Write(8, Environment.CurrentManagedThreadId);
accessor.Write(12, characterAddress);
var span = GetSpan(accessor, 20, 16);
collectionId.TryWriteBytes(span);
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
span = GetSpan(accessor, 36);
WriteSpan(characterName, span);
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
}
}
public IEnumerable<JsonObject> GetLines(DateTimeOffset crashTime)
{
var lineCount = (int)CurrentLineCount;
for (var i = lineCount - 1; i >= 0; --i)
{
var line = GetLine(i);
var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line));
var thread = BitConverter.ToInt32(line[8..]);
var address = BitConverter.ToUInt64(line[12..]);
var collectionId = new Guid(line[20..36]);
var characterName = ReadString(line[36..]);
yield return new JsonObject
{
[nameof(CharacterLoadedEntry.Age)] = (crashTime - timestamp).TotalSeconds,
[nameof(CharacterLoadedEntry.Timestamp)] = timestamp,
[nameof(CharacterLoadedEntry.ThreadId)] = thread,
[nameof(CharacterLoadedEntry.CharacterName)] = characterName,
[nameof(CharacterLoadedEntry.CharacterAddress)] = address.ToString("X"),
[nameof(CharacterLoadedEntry.CollectionId)] = collectionId,
};
}
}
public uint TotalCount
=> TotalWrittenLines;
public static IBufferReader CreateReader(int pid)
=> new CharacterBaseBuffer(false, pid);
public static ICharacterBaseBufferWriter CreateWriter(int pid)
=> new CharacterBaseBuffer(pid);
private CharacterBaseBuffer(bool writer, int pid)
: base($"{_name}_{pid}_{_version}", _version)
{ }
private CharacterBaseBuffer(int pid)
: base($"{_name}_{pid}_{_version}", _version, _lineCount, _lineCapacity)
{ }
}

View file

@ -0,0 +1,217 @@
using System.Diagnostics.CodeAnalysis;
using System.IO.MemoryMappedFiles;
using System.Numerics;
using System.Text;
namespace Penumbra.CrashHandler.Buffers;
public class MemoryMappedBuffer : IDisposable
{
private const int MinHeaderLength = 4 + 4 + 4 + 4 + 4 + 4 + 4 + 4;
private readonly MemoryMappedFile _file;
private readonly MemoryMappedViewAccessor _header;
private readonly MemoryMappedViewAccessor[] _lines = [];
public readonly int Version;
public readonly uint LineCount;
public readonly uint LineCapacity;
private readonly uint _lineMask;
private bool _disposed;
protected uint CurrentLineCount
{
get => _header.ReadUInt32(16);
set => _header.Write(16, value);
}
protected uint CurrentLinePosition
{
get => _header.ReadUInt32(20);
set => _header.Write(20, value);
}
public uint TotalWrittenLines
{
get => _header.ReadUInt32(24);
protected set => _header.Write(24, value);
}
public MemoryMappedBuffer(string mapName, int version, uint lineCount, uint lineCapacity)
{
Version = version;
LineCount = BitOperations.RoundUpToPowerOf2(Math.Clamp(lineCount, 2, int.MaxValue >> 3));
LineCapacity = BitOperations.RoundUpToPowerOf2(Math.Clamp(lineCapacity, 2, int.MaxValue >> 3));
_lineMask = LineCount - 1;
var fileName = Encoding.UTF8.GetBytes(mapName);
var headerLength = (uint)(4 + 4 + 4 + 4 + 4 + 4 + 4 + fileName.Length + 1);
headerLength = (headerLength & 0b111) > 0 ? (headerLength & ~0b111u) + 0b1000 : headerLength;
var capacity = LineCount * LineCapacity + headerLength;
_file = MemoryMappedFile.CreateNew(mapName, capacity, MemoryMappedFileAccess.ReadWrite, MemoryMappedFileOptions.None,
HandleInheritability.Inheritable);
_header = _file.CreateViewAccessor(0, headerLength);
_header.Write(0, headerLength);
_header.Write(4, Version);
_header.Write(8, LineCount);
_header.Write(12, LineCapacity);
_header.WriteArray(28, fileName, 0, fileName.Length);
_header.Write(fileName.Length + 28, (byte)0);
_lines = Enumerable.Range(0, (int)LineCount).Select(i
=> _file.CreateViewAccessor(headerLength + i * LineCapacity, LineCapacity, MemoryMappedFileAccess.ReadWrite))
.ToArray();
}
public MemoryMappedBuffer(string mapName, int? expectedVersion = null, uint? expectedMinLineCount = null,
uint? expectedMinLineCapacity = null)
{
_lines = [];
_file = MemoryMappedFile.OpenExisting(mapName, MemoryMappedFileRights.ReadWrite, HandleInheritability.Inheritable);
using var headerLine = _file.CreateViewAccessor(0, 4, MemoryMappedFileAccess.Read);
var headerLength = headerLine.ReadUInt32(0);
if (headerLength < MinHeaderLength)
Throw($"Map {mapName} did not contain a valid header.");
_header = _file.CreateViewAccessor(0, headerLength, MemoryMappedFileAccess.ReadWrite);
Version = _header.ReadInt32(4);
LineCount = _header.ReadUInt32(8);
LineCapacity = _header.ReadUInt32(12);
_lineMask = LineCount - 1;
if (expectedVersion.HasValue && expectedVersion.Value != Version)
Throw($"Map {mapName} has version {Version} instead of {expectedVersion.Value}.");
if (LineCount < expectedMinLineCount)
Throw($"Map {mapName} has line count {LineCount} but line count >= {expectedMinLineCount.Value} is required.");
if (LineCapacity < expectedMinLineCapacity)
Throw($"Map {mapName} has line capacity {LineCapacity} but line capacity >= {expectedMinLineCapacity.Value} is required.");
var name = ReadString(GetSpan(_header, 28));
if (name != mapName)
Throw($"Map {mapName} does not contain its map name at the expected location.");
_lines = Enumerable.Range(0, (int)LineCount).Select(i
=> _file.CreateViewAccessor(headerLength + i * LineCapacity, LineCapacity, MemoryMappedFileAccess.ReadWrite))
.ToArray();
[DoesNotReturn]
void Throw(string text)
{
_file.Dispose();
_header?.Dispose();
_disposed = true;
throw new Exception(text);
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
_disposed = true;
}
protected static string ReadString(Span<byte> span)
{
if (span.IsEmpty)
throw new Exception("String from empty span requested.");
var termination = span.IndexOf((byte)0);
if (termination < 0)
throw new Exception("String in span is not terminated.");
return Encoding.UTF8.GetString(span[..termination]);
}
protected static int WriteString(string text, Span<byte> span)
{
var bytes = Encoding.UTF8.GetBytes(text);
var source = (Span<byte>)bytes;
var length = source.Length + 1;
if (length > span.Length)
source = source[..(span.Length - 1)];
source.CopyTo(span);
span[bytes.Length] = 0;
return source.Length + 1;
}
protected static int WriteSpan(ReadOnlySpan<byte> input, Span<byte> span)
{
var length = input.Length + 1;
if (length > span.Length)
input = input[..(span.Length - 1)];
input.CopyTo(span);
span[input.Length] = 0;
return input.Length + 1;
}
protected Span<byte> GetLine(int i)
{
if (i < 0 || i > LineCount)
return null;
lock (_header)
{
var lineIdx = (CurrentLinePosition + i) & _lineMask;
if (lineIdx > CurrentLineCount)
return null;
return GetSpan(_lines[lineIdx]);
}
}
protected MemoryMappedViewAccessor GetCurrentLineLocking()
{
MemoryMappedViewAccessor view;
lock (_header)
{
var currentLineCount = CurrentLineCount;
if (currentLineCount == LineCount)
{
var currentLinePos = CurrentLinePosition;
view = _lines[currentLinePos]!;
CurrentLinePosition = (currentLinePos + 1) & _lineMask;
}
else
{
view = _lines[currentLineCount];
++CurrentLineCount;
}
++TotalWrittenLines;
_header.Flush();
}
return view;
}
protected static Span<byte> GetSpan(MemoryMappedViewAccessor accessor, int offset = 0)
=> GetSpan(accessor, offset, (int)accessor.Capacity - offset);
protected static unsafe Span<byte> GetSpan(MemoryMappedViewAccessor accessor, int offset, int size)
{
byte* ptr = null;
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr);
size = Math.Min(size, (int)accessor.Capacity - offset);
if (size < 0)
return [];
var span = new Span<byte>(ptr + offset + accessor.PointerOffset, size);
return span;
}
protected void Dispose(bool disposing)
{
if (_disposed)
return;
_header?.Dispose();
foreach (var line in _lines)
line?.Dispose();
_file?.Dispose();
}
~MemoryMappedBuffer()
=> Dispose(false);
}

View file

@ -0,0 +1,103 @@
using System.Text.Json.Nodes;
namespace Penumbra.CrashHandler.Buffers;
/// <summary> Only expose the write interface for the buffer. </summary>
public interface IModdedFileBufferWriter
{
/// <summary> Write a line into the buffer with the given data. </summary>
/// <param name="characterAddress"> The address of the related character, if known. </param>
/// <param name="characterName"> The name of the related character, anonymized or relying on index if unavailable, if known. </param>
/// <param name="collectionId"> The GUID of the associated collection. </param>
/// <param name="requestedFileName"> The file name as requested by the game. </param>
/// <param name="actualFileName"> The actual modded file name loaded. </param>
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, Guid collectionId, ReadOnlySpan<byte> requestedFileName,
ReadOnlySpan<byte> actualFileName);
}
/// <summary> The full crash entry for a loaded modded file. </summary>
public record struct ModdedFileLoadedEntry(
double Age,
DateTimeOffset Timestamp,
int ThreadId,
string CharacterName,
string CharacterAddress,
Guid CollectionId,
string RequestedFileName,
string ActualFileName) : ICrashDataEntry;
internal sealed class ModdedFileBuffer : MemoryMappedBuffer, IModdedFileBufferWriter, IBufferReader
{
private const int _version = 1;
private const int _lineCount = 128;
private const int _lineCapacity = 1024;
private const string _name = "Penumbra.ModdedFile";
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, Guid collectionId, ReadOnlySpan<byte> requestedFileName,
ReadOnlySpan<byte> actualFileName)
{
var accessor = GetCurrentLineLocking();
lock (accessor)
{
accessor.Write(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
accessor.Write(8, Environment.CurrentManagedThreadId);
accessor.Write(12, characterAddress);
var span = GetSpan(accessor, 20, 16);
collectionId.TryWriteBytes(span);
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
span = GetSpan(accessor, 36, 80);
WriteSpan(characterName, span);
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
span = GetSpan(accessor, 116, 260);
WriteSpan(requestedFileName, span);
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
span = GetSpan(accessor, 376);
WriteSpan(actualFileName, span);
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
}
}
public uint TotalCount
=> TotalWrittenLines;
public IEnumerable<JsonObject> GetLines(DateTimeOffset crashTime)
{
var lineCount = (int)CurrentLineCount;
for (var i = lineCount - 1; i >= 0; --i)
{
var line = GetLine(i);
var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line));
var thread = BitConverter.ToInt32(line[8..]);
var address = BitConverter.ToUInt64(line[12..]);
var collectionId = new Guid(line[20..36]);
var characterName = ReadString(line[36..]);
var requestedFileName = ReadString(line[116..]);
var actualFileName = ReadString(line[376..]);
yield return new JsonObject()
{
[nameof(ModdedFileLoadedEntry.Age)] = (crashTime - timestamp).TotalSeconds,
[nameof(ModdedFileLoadedEntry.Timestamp)] = timestamp,
[nameof(ModdedFileLoadedEntry.ThreadId)] = thread,
[nameof(ModdedFileLoadedEntry.CharacterName)] = characterName,
[nameof(ModdedFileLoadedEntry.CharacterAddress)] = address.ToString("X"),
[nameof(ModdedFileLoadedEntry.CollectionId)] = collectionId,
[nameof(ModdedFileLoadedEntry.RequestedFileName)] = requestedFileName,
[nameof(ModdedFileLoadedEntry.ActualFileName)] = actualFileName,
};
}
}
public static IBufferReader CreateReader(int pid)
=> new ModdedFileBuffer(false, pid);
public static IModdedFileBufferWriter CreateWriter(int pid)
=> new ModdedFileBuffer(pid);
private ModdedFileBuffer(bool writer, int pid)
: base($"{_name}_{pid}_{_version}", _version)
{ }
private ModdedFileBuffer(int pid)
: base($"{_name}_{pid}_{_version}", _version, _lineCount, _lineCapacity)
{ }
}

View file

@ -0,0 +1,68 @@
using Penumbra.CrashHandler.Buffers;
namespace Penumbra.CrashHandler;
/// <summary> A base entry for crash data. </summary>
public interface ICrashDataEntry
{
/// <summary> The timestamp of the event. </summary>
DateTimeOffset Timestamp { get; }
/// <summary> The thread invoking the event. </summary>
int ThreadId { get; }
/// <summary> The age of the event compared to the crash. (Redundantly with the timestamp) </summary>
double Age { get; }
}
/// <summary> A full set of crash data. </summary>
public class CrashData
{
/// <summary> The mode this data was obtained - manually or from a crash. </summary>
public string Mode { get; set; } = "Unknown";
/// <summary> The time this crash data was generated. </summary>
public DateTimeOffset CrashTime { get; set; } = DateTimeOffset.UnixEpoch;
/// <summary> Penumbra's Version when this crash data was created. </summary>
public string Version { get; set; } = string.Empty;
/// <summary> The Game's Version when this crash data was created. </summary>
public string GameVersion { get; set; } = string.Empty;
/// <summary> The FFXIV process ID when this data was generated. </summary>
public int ProcessId { get; set; } = 0;
/// <summary> The FFXIV Exit Code (if any) when this data was generated. </summary>
public int ExitCode { get; set; } = 0;
/// <summary> The total amount of characters loaded during this session. </summary>
public int TotalCharactersLoaded { get; set; } = 0;
/// <summary> The total amount of modded files loaded during this session. </summary>
public int TotalModdedFilesLoaded { get; set; } = 0;
/// <summary> The total amount of vfx functions invoked during this session. </summary>
public int TotalVFXFuncsInvoked { get; set; } = 0;
/// <summary> The last character loaded before this crash data was generated. </summary>
public CharacterLoadedEntry? LastCharacterLoaded
=> LastCharactersLoaded.Count == 0 ? default : LastCharactersLoaded[0];
/// <summary> The last modded file loaded before this crash data was generated. </summary>
public ModdedFileLoadedEntry? LastModdedFileLoaded
=> LastModdedFilesLoaded.Count == 0 ? default : LastModdedFilesLoaded[0];
/// <summary> The last vfx function invoked before this crash data was generated. </summary>
public VfxFuncInvokedEntry? LastVfxFuncInvoked
=> LastVFXFuncsInvoked.Count == 0 ? default : LastVFXFuncsInvoked[0];
/// <summary> A collection of the last few characters loaded before this crash data was generated. </summary>
public List<CharacterLoadedEntry> LastCharactersLoaded { get; set; } = [];
/// <summary> A collection of the last few modded files loaded before this crash data was generated. </summary>
public List<ModdedFileLoadedEntry> LastModdedFilesLoaded { get; set; } = [];
/// <summary> A collection of the last few vfx functions invoked before this crash data was generated. </summary>
public List<VfxFuncInvokedEntry> LastVFXFuncsInvoked { get; set; } = [];
}

View file

@ -0,0 +1,55 @@
using System.Text.Json.Nodes;
using Penumbra.CrashHandler.Buffers;
namespace Penumbra.CrashHandler;
public interface IBufferReader
{
public uint TotalCount { get; }
public IEnumerable<JsonObject> GetLines(DateTimeOffset crashTime);
}
public sealed class GameEventLogReader(int pid) : IDisposable
{
public readonly (IBufferReader Reader, string TypeSingular, string TypePlural)[] Readers =
[
(CharacterBaseBuffer.CreateReader(pid), "CharacterLoaded", "CharactersLoaded"),
(ModdedFileBuffer.CreateReader(pid), "ModdedFileLoaded", "ModdedFilesLoaded"),
(AnimationInvocationBuffer.CreateReader(pid), "VFXFuncInvoked", "VFXFuncsInvoked"),
];
public void Dispose()
{
foreach (var (reader, _, _) in Readers)
(reader as IDisposable)?.Dispose();
}
public JsonObject Dump(string mode, int processId, int exitCode, string version, string gameVersion)
{
var crashTime = DateTimeOffset.UtcNow;
var obj = new JsonObject
{
[nameof(CrashData.Mode)] = mode,
[nameof(CrashData.CrashTime)] = DateTimeOffset.UtcNow,
[nameof(CrashData.ProcessId)] = processId,
[nameof(CrashData.ExitCode)] = exitCode,
[nameof(CrashData.Version)] = version,
[nameof(CrashData.GameVersion)] = gameVersion,
};
foreach (var (reader, singular, _) in Readers)
obj["Last" + singular] = reader.GetLines(crashTime).FirstOrDefault();
foreach (var (reader, _, plural) in Readers)
{
obj["Total" + plural] = reader.TotalCount;
var array = new JsonArray();
foreach (var file in reader.GetLines(crashTime))
array.Add(file);
obj["Last" + plural] = array;
}
return obj;
}
}

View file

@ -0,0 +1,17 @@
using Penumbra.CrashHandler.Buffers;
namespace Penumbra.CrashHandler;
public sealed class GameEventLogWriter(int pid) : IDisposable
{
public readonly ICharacterBaseBufferWriter CharacterBase = CharacterBaseBuffer.CreateWriter(pid);
public readonly IModdedFileBufferWriter FileLoaded = ModdedFileBuffer.CreateWriter(pid);
public readonly IAnimationInvocationBufferWriter AnimationFuncInvoked = AnimationInvocationBuffer.CreateWriter(pid);
public void Dispose()
{
(CharacterBase as IDisposable)?.Dispose();
(FileLoaded as IDisposable)?.Dispose();
(AnimationFuncInvoked as IDisposable)?.Dispose();
}
}

View file

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<PlatformTarget>x64</PlatformTarget>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
<PropertyGroup>
<DalamudLibPath Condition="$([MSBuild]::IsOSPlatform('Windows'))">$(appdata)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath>
<DalamudLibPath Condition="$([MSBuild]::IsOSPlatform('Linux'))">$(HOME)/.xlcore/dalamud/Hooks/dev/</DalamudLibPath>
<DalamudLibPath Condition="$(DALAMUD_HOME) != ''">$(DALAMUD_HOME)/</DalamudLibPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,39 @@
using System.Diagnostics;
using System.Text.Json;
namespace Penumbra.CrashHandler;
public class CrashHandler
{
public static void Main(string[] args)
{
if (args.Length < 4 || !int.TryParse(args[1], out var pid))
return;
try
{
using var reader = new GameEventLogReader(pid);
var parent = Process.GetProcessById(pid);
using var handle = parent.SafeHandle;
parent.WaitForExit();
int exitCode;
try
{
exitCode = parent.ExitCode;
}
catch
{
exitCode = -1;
}
var obj = reader.Dump("Crash", pid, exitCode, args[2], args[3]);
using var fs = File.Open(args[0], FileMode.Create);
using var w = new Utf8JsonWriter(fs, new JsonWriterOptions { Indented = true });
obj.WriteTo(w, new JsonSerializerOptions() { WriteIndented = true });
}
catch (Exception ex)
{
File.WriteAllText(args[0], $"{DateTime.UtcNow} {pid} {ex}");
}
}
}

@ -1 +1 @@
Subproject commit 63f4de7305616b6cb8921513e5d83baa8913353f
Subproject commit b7fdfe9d19f7e3229834480db446478b0bf6acee

@ -1 +1 @@
Subproject commit 620a7edf009b92288257ce7d64fffb8fba44d8b5
Subproject commit bd52d080b72d67263dc47068e461f17c93bdc779

View file

@ -8,6 +8,7 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F89C9EAE-25C8-43BE-8108-5921E5A93502}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
repo.json = repo.json
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.GameData", "Penumbra.GameData\Penumbra.GameData.csproj", "{EE551E87-FDB3-4612-B500-DC870C07C605}"
@ -18,6 +19,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.Api", "Penumbra.Ap
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.String", "Penumbra.String\Penumbra.String.csproj", "{5549BAFD-6357-4B1A-800C-75AC36E5B76D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.CrashHandler", "Penumbra.CrashHandler\Penumbra.CrashHandler.csproj", "{EE834491-A98F-4395-BE0D-6861AE5AD953}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -44,6 +47,10 @@ Global
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|Any CPU.Build.0 = Release|Any CPU
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View file

@ -0,0 +1,78 @@
using OtterGui.Log;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Interop;
using Penumbra.Interop.PathResolving;
namespace Penumbra.Api.Api;
public class ApiHelpers(
CollectionManager collectionManager,
ObjectManager objects,
CollectionResolver collectionResolver,
ActorManager actors) : IApiService
{
/// <summary> Return the associated identifier for an object given by its index. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
internal ActorIdentifier AssociatedIdentifier(int gameObjectIdx)
{
if (gameObjectIdx < 0 || gameObjectIdx >= objects.TotalCount)
return ActorIdentifier.Invalid;
var ptr = objects[gameObjectIdx];
return actors.FromObject(ptr, out _, false, true, true);
}
/// <summary>
/// Return the collection associated to a current game object. If it does not exist, return the default collection.
/// If the index is invalid, returns false and the default collection.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
internal unsafe bool AssociatedCollection(int gameObjectIdx, out ModCollection collection)
{
collection = collectionManager.Active.Default;
if (gameObjectIdx < 0 || gameObjectIdx >= objects.TotalCount)
return false;
var ptr = objects[gameObjectIdx];
var data = collectionResolver.IdentifyCollection(ptr.AsObject, false);
if (data.Valid)
collection = data.ModCollection;
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
internal static PenumbraApiEc Return(PenumbraApiEc ec, LazyString args, [CallerMemberName] string name = "Unknown")
{
if (ec is PenumbraApiEc.Success or PenumbraApiEc.NothingChanged)
Penumbra.Log.Verbose($"[{name}] Called with {args}, returned {ec}.");
else
Penumbra.Log.Debug($"[{name}] Called with {args}, returned {ec}.");
return ec;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
internal static LazyString Args(params object[] arguments)
{
if (arguments.Length == 0)
return new LazyString(() => "no arguments");
return new LazyString(() =>
{
var sb = new StringBuilder();
for (var i = 0; i < arguments.Length / 2; ++i)
{
sb.Append(arguments[2 * i]);
sb.Append(" = ");
sb.Append(arguments[2 * i + 1]);
sb.Append(", ");
}
return sb.ToString(0, sb.Length - 2);
});
}
}

View file

@ -0,0 +1,159 @@
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
namespace Penumbra.Api.Api;
public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : IPenumbraApiCollection, IApiService
{
public Dictionary<Guid, string> GetCollections()
=> collections.Storage.ToDictionary(c => c.Id, c => c.Name);
public List<(Guid Id, string Name)> GetCollectionsByIdentifier(string identifier)
{
if (identifier.Length == 0)
return [];
var list = new List<(Guid Id, string Name)>(4);
if (Guid.TryParse(identifier, out var guid) && collections.Storage.ById(guid, out var collection) && collection != ModCollection.Empty)
list.Add((collection.Id, collection.Name));
else if (identifier.Length >= 8)
list.AddRange(collections.Storage.Where(c => c.Identifier.StartsWith(identifier, StringComparison.OrdinalIgnoreCase))
.Select(c => (c.Id, c.Name)));
list.AddRange(collections.Storage
.Where(c => string.Equals(c.Name, identifier, StringComparison.OrdinalIgnoreCase) && !list.Contains((c.Id, c.Name)))
.Select(c => (c.Id, c.Name)));
return list;
}
public Dictionary<string, object?> GetChangedItemsForCollection(Guid collectionId)
{
try
{
if (!collections.Storage.ById(collectionId, out var collection))
collection = ModCollection.Empty;
if (collection.HasCache)
return collection.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Item2?.ToInternalObject());
Penumbra.Log.Warning($"Collection {collectionId} does not exist or is not loaded.");
return [];
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not obtain Changed Items for {collectionId}:\n{e}");
throw;
}
}
public (Guid Id, string Name)? GetCollection(ApiCollectionType type)
{
if (!Enum.IsDefined(type))
return null;
var collection = collections.Active.ByType((CollectionType)type);
return collection == null ? null : (collection.Id, collection.Name);
}
internal (Guid Id, string Name)? GetCollection(byte type)
=> GetCollection((ApiCollectionType)type);
public (bool ObjectValid, bool IndividualSet, (Guid Id, string Name) EffectiveCollection) GetCollectionForObject(int gameObjectIdx)
{
var id = helpers.AssociatedIdentifier(gameObjectIdx);
if (!id.IsValid)
return (false, false, (collections.Active.Default.Id, collections.Active.Default.Name));
if (collections.Active.Individuals.TryGetValue(id, out var collection))
return (true, true, (collection.Id, collection.Name));
helpers.AssociatedCollection(gameObjectIdx, out collection);
return (true, false, (collection.Id, collection.Name));
}
public Guid[] GetCollectionByName(string name)
=> collections.Storage.Where(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase)).Select(c => c.Id).ToArray();
public (PenumbraApiEc, (Guid Id, string Name)? OldCollection) SetCollection(ApiCollectionType type, Guid? collectionId,
bool allowCreateNew, bool allowDelete)
{
if (!Enum.IsDefined(type))
return (PenumbraApiEc.InvalidArgument, null);
var oldCollection = collections.Active.ByType((CollectionType)type);
var old = oldCollection != null ? (oldCollection.Id, oldCollection.Name) : new ValueTuple<Guid, string>?();
if (collectionId == null)
{
if (old == null)
return (PenumbraApiEc.NothingChanged, old);
if (!allowDelete || type is ApiCollectionType.Current or ApiCollectionType.Default or ApiCollectionType.Interface)
return (PenumbraApiEc.AssignmentDeletionDisallowed, old);
collections.Active.RemoveSpecialCollection((CollectionType)type);
return (PenumbraApiEc.Success, old);
}
if (!collections.Storage.ById(collectionId.Value, out var collection))
return (PenumbraApiEc.CollectionMissing, old);
if (old == null)
{
if (!allowCreateNew)
return (PenumbraApiEc.AssignmentCreationDisallowed, old);
collections.Active.CreateSpecialCollection((CollectionType)type);
}
else if (old.Value.Item1 == collection.Id)
{
return (PenumbraApiEc.NothingChanged, old);
}
collections.Active.SetCollection(collection, (CollectionType)type);
return (PenumbraApiEc.Success, old);
}
public (PenumbraApiEc, (Guid Id, string Name)? OldCollection) SetCollectionForObject(int gameObjectIdx, Guid? collectionId,
bool allowCreateNew, bool allowDelete)
{
var id = helpers.AssociatedIdentifier(gameObjectIdx);
if (!id.IsValid)
return (PenumbraApiEc.InvalidIdentifier, (collections.Active.Default.Id, collections.Active.Default.Name));
var oldCollection = collections.Active.Individuals.TryGetValue(id, out var c) ? c : null;
var old = oldCollection != null ? (oldCollection.Id, oldCollection.Name) : new ValueTuple<Guid, string>?();
if (collectionId == null)
{
if (old == null)
return (PenumbraApiEc.NothingChanged, old);
if (!allowDelete)
return (PenumbraApiEc.AssignmentDeletionDisallowed, old);
var idx = collections.Active.Individuals.Index(id);
collections.Active.RemoveIndividualCollection(idx);
return (PenumbraApiEc.Success, old);
}
if (!collections.Storage.ById(collectionId.Value, out var collection))
return (PenumbraApiEc.CollectionMissing, old);
if (old == null)
{
if (!allowCreateNew)
return (PenumbraApiEc.AssignmentCreationDisallowed, old);
var ids = collections.Active.Individuals.GetGroup(id);
collections.Active.CreateIndividualCollection(ids);
}
else if (old.Value.Item1 == collection.Id)
{
return (PenumbraApiEc.NothingChanged, old);
}
collections.Active.SetCollection(collection, CollectionType.Individual, collections.Active.Individuals.Index(id));
return (PenumbraApiEc.Success, old);
}
}

View file

@ -0,0 +1,40 @@
using OtterGui.Services;
using Penumbra.Import.Textures;
using TextureType = Penumbra.Api.Enums.TextureType;
namespace Penumbra.Api.Api;
public class EditingApi(TextureManager textureManager) : IPenumbraApiEditing, IApiService
{
public Task ConvertTextureFile(string inputFile, string outputFile, TextureType textureType, bool mipMaps)
=> textureType switch
{
TextureType.Png => textureManager.SavePng(inputFile, outputFile),
TextureType.AsIsTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, true, inputFile, outputFile),
TextureType.AsIsDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, false, inputFile, outputFile),
TextureType.RgbaTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, true, inputFile, outputFile),
TextureType.RgbaDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, false, inputFile, outputFile),
TextureType.Bc3Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, true, inputFile, outputFile),
TextureType.Bc3Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, false, inputFile, outputFile),
TextureType.Bc7Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, true, inputFile, outputFile),
TextureType.Bc7Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, false, inputFile, outputFile),
_ => Task.FromException(new Exception($"Invalid input value {textureType}.")),
};
// @formatter:off
public Task ConvertTextureData(byte[] rgbaData, int width, string outputFile, TextureType textureType, bool mipMaps)
=> textureType switch
{
TextureType.Png => textureManager.SavePng(new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.AsIsTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.AsIsDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.RgbaTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.RgbaDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.Bc3Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.Bc3Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.Bc7Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.Bc7Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
_ => Task.FromException(new Exception($"Invalid input value {textureType}.")),
};
// @formatter:on
}

View file

@ -0,0 +1,97 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Interop.Hooks.ResourceLoading;
using Penumbra.Interop.PathResolving;
using Penumbra.Interop.Structs;
using Penumbra.Services;
using Penumbra.String.Classes;
namespace Penumbra.Api.Api;
public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable
{
private readonly CommunicatorService _communicator;
private readonly CollectionResolver _collectionResolver;
private readonly CutsceneService _cutsceneService;
private readonly ResourceLoader _resourceLoader;
public unsafe GameStateApi(CommunicatorService communicator, CollectionResolver collectionResolver, CutsceneService cutsceneService,
ResourceLoader resourceLoader)
{
_communicator = communicator;
_collectionResolver = collectionResolver;
_cutsceneService = cutsceneService;
_resourceLoader = resourceLoader;
_resourceLoader.ResourceLoaded += OnResourceLoaded;
_resourceLoader.PapRequested += OnPapRequested;
_communicator.CreatedCharacterBase.Subscribe(OnCreatedCharacterBase, Communication.CreatedCharacterBase.Priority.Api);
}
public unsafe void Dispose()
{
_resourceLoader.ResourceLoaded -= OnResourceLoaded;
_resourceLoader.PapRequested -= OnPapRequested;
_communicator.CreatedCharacterBase.Unsubscribe(OnCreatedCharacterBase);
}
public event CreatedCharacterBaseDelegate? CreatedCharacterBase;
public event GameObjectResourceResolvedDelegate? GameObjectResourceResolved;
public event CreatingCharacterBaseDelegate? CreatingCharacterBase
{
add
{
if (value == null)
return;
_communicator.CreatingCharacterBase.Subscribe(new Action<nint, Guid, nint, nint, nint>(value),
Communication.CreatingCharacterBase.Priority.Api);
}
remove
{
if (value == null)
return;
_communicator.CreatingCharacterBase.Unsubscribe(new Action<nint, Guid, nint, nint, nint>(value));
}
}
public unsafe (nint GameObject, (Guid Id, string Name) Collection) GetDrawObjectInfo(nint drawObject)
{
var data = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
return (data.AssociatedGameObject, (data.ModCollection.Id, data.ModCollection.Name));
}
public int GetCutsceneParentIndex(int actorIdx)
=> _cutsceneService.GetParentIndex(actorIdx);
public PenumbraApiEc SetCutsceneParentIndex(int copyIdx, int newParentIdx)
=> _cutsceneService.SetParentIndex(copyIdx, newParentIdx)
? PenumbraApiEc.Success
: PenumbraApiEc.InvalidArgument;
private unsafe void OnResourceLoaded(ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, ResolveData resolveData)
{
if (resolveData.AssociatedGameObject != nint.Zero && GameObjectResourceResolved != null)
{
var original = originalPath.ToString();
GameObjectResourceResolved.Invoke(resolveData.AssociatedGameObject, original,
manipulatedPath?.ToString() ?? original);
}
}
private void OnPapRequested(Utf8GamePath originalPath, FullPath? manipulatedPath, ResolveData resolveData)
{
if (resolveData.AssociatedGameObject != nint.Zero && GameObjectResourceResolved != null)
{
var original = originalPath.ToString();
GameObjectResourceResolved.Invoke(resolveData.AssociatedGameObject, original,
manipulatedPath?.ToString() ?? original);
}
}
private void OnCreatedCharacterBase(nint gameObject, ModCollection collection, nint drawObject)
=> CreatedCharacterBase?.Invoke(gameObject, collection.Id, drawObject);
}

View file

@ -0,0 +1,43 @@
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.GameData.Structs;
using Penumbra.Interop.PathResolving;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Api.Api;
public class MetaApi(CollectionResolver collectionResolver, ApiHelpers helpers) : IPenumbraApiMeta, IApiService
{
public const int CurrentVersion = 0;
public string GetPlayerMetaManipulations()
{
var collection = collectionResolver.PlayerCollection();
return CompressMetaManipulations(collection);
}
public string GetMetaManipulations(int gameObjectIdx)
{
helpers.AssociatedCollection(gameObjectIdx, out var collection);
return CompressMetaManipulations(collection);
}
internal static string CompressMetaManipulations(ModCollection collection)
{
var array = new JArray();
if (collection.MetaCache is { } cache)
{
MetaDictionary.SerializeTo(array, cache.GlobalEqp.Select(kvp => kvp.Key));
MetaDictionary.SerializeTo(array, cache.Imc.Select(kvp => new KeyValuePair<ImcIdentifier, ImcEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Eqp.Select(kvp => new KeyValuePair<EqpIdentifier, EqpEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Eqdp.Select(kvp => new KeyValuePair<EqdpIdentifier, EqdpEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Est.Select(kvp => new KeyValuePair<EstIdentifier, EstEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Rsp.Select(kvp => new KeyValuePair<RspIdentifier, RspEntry>(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);
}
}

View file

@ -0,0 +1,286 @@
using OtterGui;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Communication;
using Penumbra.Interop.PathResolving;
using Penumbra.Mods;
using Penumbra.Mods.Editor;
using Penumbra.Mods.Groups;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Manager.OptionEditor;
using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods;
using Penumbra.Services;
namespace Penumbra.Api.Api;
public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
{
private readonly CollectionResolver _collectionResolver;
private readonly ModManager _modManager;
private readonly CollectionManager _collectionManager;
private readonly CollectionEditor _collectionEditor;
private readonly CommunicatorService _communicator;
public ModSettingsApi(CollectionResolver collectionResolver,
ModManager modManager,
CollectionManager collectionManager,
CollectionEditor collectionEditor,
CommunicatorService communicator)
{
_collectionResolver = collectionResolver;
_modManager = modManager;
_collectionManager = collectionManager;
_collectionEditor = collectionEditor;
_communicator = communicator;
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ApiModSettings);
_communicator.ModSettingChanged.Subscribe(OnModSettingChange, Communication.ModSettingChanged.Priority.Api);
_communicator.ModOptionChanged.Subscribe(OnModOptionEdited, ModOptionChanged.Priority.Api);
_communicator.ModFileChanged.Subscribe(OnModFileChanged, ModFileChanged.Priority.Api);
}
public void Dispose()
{
_communicator.ModPathChanged.Unsubscribe(OnModPathChange);
_communicator.ModSettingChanged.Unsubscribe(OnModSettingChange);
_communicator.ModOptionChanged.Unsubscribe(OnModOptionEdited);
_communicator.ModFileChanged.Unsubscribe(OnModFileChanged);
}
public event ModSettingChangedDelegate? ModSettingChanged;
public AvailableModSettings? GetAvailableModSettings(string modDirectory, string modName)
{
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
return null;
var dict = new Dictionary<string, (string[], int)>(mod.Groups.Count);
foreach (var g in mod.Groups)
dict.Add(g.Name, (g.Options.Select(o => o.Name).ToArray(), (int)g.Type));
return new AvailableModSettings(dict);
}
public Dictionary<string, (string[], int)>? GetAvailableModSettingsBase(string modDirectory, string modName)
=> _modManager.TryGetMod(modDirectory, modName, out var mod)
? mod.Groups.ToDictionary(g => g.Name, g => (g.Options.Select(o => o.Name).ToArray(), (int)g.Type))
: null;
public (PenumbraApiEc, (bool, int, Dictionary<string, List<string>>, bool)?) GetCurrentModSettings(Guid collectionId, string modDirectory,
string modName, bool ignoreInheritance)
{
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
return (PenumbraApiEc.ModMissing, null);
if (!_collectionManager.Storage.ById(collectionId, out var collection))
return (PenumbraApiEc.CollectionMissing, null);
var settings = collection.Id == Guid.Empty
? null
: ignoreInheritance
? collection.Settings[mod.Index]
: collection[mod.Index].Settings;
if (settings == null)
return (PenumbraApiEc.Success, null);
var (enabled, priority, dict) = settings.ConvertToShareable(mod);
return (PenumbraApiEc.Success,
(enabled, priority.Value, dict, collection.Settings[mod.Index] == null));
}
public PenumbraApiEc TryInheritMod(Guid collectionId, string modDirectory, string modName, bool inherit)
{
var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Inherit",
inherit.ToString());
if (collectionId == Guid.Empty)
return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
if (!_collectionManager.Storage.ById(collectionId, out var collection))
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
return ApiHelpers.Return(PenumbraApiEc.ModMissing, args);
var ret = _collectionEditor.SetModInheritance(collection, mod, inherit)
? PenumbraApiEc.Success
: PenumbraApiEc.NothingChanged;
return ApiHelpers.Return(ret, args);
}
public PenumbraApiEc TrySetMod(Guid collectionId, string modDirectory, string modName, bool enabled)
{
var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Enabled", enabled);
if (!_collectionManager.Storage.ById(collectionId, out var collection))
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
return ApiHelpers.Return(PenumbraApiEc.ModMissing, args);
var ret = _collectionEditor.SetModState(collection, mod, enabled)
? PenumbraApiEc.Success
: PenumbraApiEc.NothingChanged;
return ApiHelpers.Return(ret, args);
}
public PenumbraApiEc TrySetModPriority(Guid collectionId, string modDirectory, string modName, int priority)
{
var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Priority", priority);
if (!_collectionManager.Storage.ById(collectionId, out var collection))
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
return ApiHelpers.Return(PenumbraApiEc.ModMissing, args);
var ret = _collectionEditor.SetModPriority(collection, mod, new ModPriority(priority))
? PenumbraApiEc.Success
: PenumbraApiEc.NothingChanged;
return ApiHelpers.Return(ret, args);
}
public PenumbraApiEc TrySetModSetting(Guid collectionId, string modDirectory, string modName, string optionGroupName, string optionName)
{
var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName",
optionGroupName, "OptionName", optionName);
if (!_collectionManager.Storage.ById(collectionId, out var collection))
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
return ApiHelpers.Return(PenumbraApiEc.ModMissing, args);
var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName);
if (groupIdx < 0)
return ApiHelpers.Return(PenumbraApiEc.OptionGroupMissing, args);
var optionIdx = mod.Groups[groupIdx].Options.IndexOf(o => o.Name == optionName);
if (optionIdx < 0)
return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args);
var setting = mod.Groups[groupIdx].Behaviour switch
{
GroupDrawBehaviour.MultiSelection => Setting.Multi(optionIdx),
GroupDrawBehaviour.SingleSelection => Setting.Single(optionIdx),
_ => Setting.Zero,
};
var ret = _collectionEditor.SetModSetting(collection, mod, groupIdx, setting)
? PenumbraApiEc.Success
: PenumbraApiEc.NothingChanged;
return ApiHelpers.Return(ret, args);
}
public PenumbraApiEc TrySetModSettings(Guid collectionId, string modDirectory, string modName, string optionGroupName,
IReadOnlyList<string> optionNames)
{
var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName",
optionGroupName, "#optionNames", optionNames.Count);
if (!_collectionManager.Storage.ById(collectionId, out var collection))
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
return ApiHelpers.Return(PenumbraApiEc.ModMissing, args);
var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName);
if (groupIdx < 0)
return ApiHelpers.Return(PenumbraApiEc.OptionGroupMissing, args);
var setting = Setting.Zero;
switch (mod.Groups[groupIdx])
{
case { Behaviour: GroupDrawBehaviour.SingleSelection } single:
{
var optionIdx = optionNames.Count == 0 ? -1 : single.Options.IndexOf(o => o.Name == optionNames[^1]);
if (optionIdx < 0)
return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args);
setting = Setting.Single(optionIdx);
break;
}
case { Behaviour: GroupDrawBehaviour.MultiSelection } multi:
{
foreach (var name in optionNames)
{
var optionIdx = multi.Options.IndexOf(o => o.Name == name);
if (optionIdx < 0)
return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args);
setting |= Setting.Multi(optionIdx);
}
break;
}
}
var ret = _collectionEditor.SetModSetting(collection, mod, groupIdx, setting)
? PenumbraApiEc.Success
: PenumbraApiEc.NothingChanged;
return ApiHelpers.Return(ret, args);
}
public PenumbraApiEc CopyModSettings(Guid? collectionId, string modDirectoryFrom, string modDirectoryTo)
{
var args = ApiHelpers.Args("CollectionId", collectionId.HasValue ? collectionId.Value.ToString() : "NULL",
"From", modDirectoryFrom, "To", modDirectoryTo);
var sourceMod = _modManager.FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryFrom, StringComparison.OrdinalIgnoreCase));
var targetMod = _modManager.FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryTo, StringComparison.OrdinalIgnoreCase));
if (collectionId == null)
foreach (var collection in _collectionManager.Storage)
_collectionEditor.CopyModSettings(collection, sourceMod, modDirectoryFrom, targetMod, modDirectoryTo);
else if (_collectionManager.Storage.ById(collectionId.Value, out var collection))
_collectionEditor.CopyModSettings(collection, sourceMod, modDirectoryFrom, targetMod, modDirectoryTo);
else
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
return ApiHelpers.Return(PenumbraApiEc.Success, args);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private void TriggerSettingEdited(Mod mod)
{
var collection = _collectionResolver.PlayerCollection();
var (settings, parent) = collection[mod.Index];
if (settings is { Enabled: true })
ModSettingChanged?.Invoke(ModSettingChange.Edited, collection.Id, mod.Identifier, parent != collection);
}
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? _1, DirectoryInfo? _2)
{
if (type == ModPathChangeType.Reloaded)
TriggerSettingEdited(mod);
}
private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting _1, int _2, bool inherited)
=> ModSettingChanged?.Invoke(type, collection.Id, mod?.ModPath.Name ?? string.Empty, inherited);
private void OnModOptionEdited(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container,
int moveIndex)
{
switch (type)
{
case ModOptionChangeType.GroupDeleted:
case ModOptionChangeType.GroupMoved:
case ModOptionChangeType.GroupTypeChanged:
case ModOptionChangeType.PriorityChanged:
case ModOptionChangeType.OptionDeleted:
case ModOptionChangeType.OptionMoved:
case ModOptionChangeType.OptionFilesChanged:
case ModOptionChangeType.OptionFilesAdded:
case ModOptionChangeType.OptionSwapsChanged:
case ModOptionChangeType.OptionMetaChanged:
TriggerSettingEdited(mod);
break;
}
}
private void OnModFileChanged(Mod mod, FileRegistry file)
{
if (file.CurrentUsage == 0)
return;
TriggerSettingEdited(mod);
}
}

148
Penumbra/Api/Api/ModsApi.cs Normal file
View file

@ -0,0 +1,148 @@
using OtterGui.Compression;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Communication;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Services;
namespace Penumbra.Api.Api;
public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
{
private readonly CommunicatorService _communicator;
private readonly ModManager _modManager;
private readonly ModImportManager _modImportManager;
private readonly Configuration _config;
private readonly ModFileSystem _modFileSystem;
private readonly MigrationManager _migrationManager;
public ModsApi(ModManager modManager, ModImportManager modImportManager, Configuration config, ModFileSystem modFileSystem,
CommunicatorService communicator, MigrationManager migrationManager)
{
_modManager = modManager;
_modImportManager = modImportManager;
_config = config;
_modFileSystem = modFileSystem;
_communicator = communicator;
_migrationManager = migrationManager;
_communicator.ModPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.ApiMods);
}
private void OnModPathChanged(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, DirectoryInfo? newDirectory)
{
switch (type)
{
case ModPathChangeType.Deleted when oldDirectory != null:
ModDeleted?.Invoke(oldDirectory.Name);
break;
case ModPathChangeType.Added when newDirectory != null:
ModAdded?.Invoke(newDirectory.Name);
break;
case ModPathChangeType.Moved when newDirectory != null && oldDirectory != null:
ModMoved?.Invoke(oldDirectory.Name, newDirectory.Name);
break;
}
}
public void Dispose()
=> _communicator.ModPathChanged.Unsubscribe(OnModPathChanged);
public Dictionary<string, string> GetModList()
=> _modManager.ToDictionary(m => m.ModPath.Name, m => m.Name.Text);
public PenumbraApiEc InstallMod(string modFilePackagePath)
{
if (!File.Exists(modFilePackagePath))
return ApiHelpers.Return(PenumbraApiEc.FileMissing, ApiHelpers.Args("ModFilePackagePath", modFilePackagePath));
_modImportManager.AddUnpack(modFilePackagePath);
return ApiHelpers.Return(PenumbraApiEc.Success, ApiHelpers.Args("ModFilePackagePath", modFilePackagePath));
}
public PenumbraApiEc ReloadMod(string modDirectory, string modName)
{
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
return ApiHelpers.Return(PenumbraApiEc.ModMissing, ApiHelpers.Args("ModDirectory", modDirectory, "ModName", modName));
_modManager.ReloadMod(mod);
return ApiHelpers.Return(PenumbraApiEc.Success, ApiHelpers.Args("ModDirectory", modDirectory, "ModName", modName));
}
public PenumbraApiEc AddMod(string modDirectory)
{
var args = ApiHelpers.Args("ModDirectory", modDirectory);
var dir = new DirectoryInfo(Path.Join(_modManager.BasePath.FullName, Path.GetFileName(modDirectory)));
if (!dir.Exists)
return ApiHelpers.Return(PenumbraApiEc.FileMissing, args);
if (dir.Parent == null
|| Path.TrimEndingDirectorySeparator(Path.GetFullPath(_modManager.BasePath.FullName))
!= Path.TrimEndingDirectorySeparator(Path.GetFullPath(dir.Parent.FullName)))
return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
_modManager.AddMod(dir);
if (_config.MigrateImportedModelsToV6)
{
_migrationManager.MigrateMdlDirectory(dir.FullName, false);
_migrationManager.Await();
}
if (_config.UseFileSystemCompression)
new FileCompactor(Penumbra.Log).StartMassCompact(dir.EnumerateFiles("*.*", SearchOption.AllDirectories),
CompressionAlgorithm.Xpress8K);
return ApiHelpers.Return(PenumbraApiEc.Success, args);
}
public PenumbraApiEc DeleteMod(string modDirectory, string modName)
{
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
return ApiHelpers.Return(PenumbraApiEc.NothingChanged, ApiHelpers.Args("ModDirectory", modDirectory, "ModName", modName));
_modManager.DeleteMod(mod);
return ApiHelpers.Return(PenumbraApiEc.Success, ApiHelpers.Args("ModDirectory", modDirectory, "ModName", modName));
}
public event Action<string>? ModDeleted;
public event Action<string>? ModAdded;
public event Action<string, string>? ModMoved;
public (PenumbraApiEc, string, bool, bool) GetModPath(string modDirectory, string modName)
{
if (!_modManager.TryGetMod(modDirectory, modName, out var mod)
|| !_modFileSystem.FindLeaf(mod, out var leaf))
return (PenumbraApiEc.ModMissing, string.Empty, false, false);
var fullPath = leaf.FullName();
var isDefault = ModFileSystem.ModHasDefaultPath(mod, fullPath);
var isNameDefault = isDefault || ModFileSystem.ModHasDefaultPath(mod, leaf.Name);
return (PenumbraApiEc.Success, fullPath, !isDefault, !isNameDefault);
}
public PenumbraApiEc SetModPath(string modDirectory, string modName, string newPath)
{
if (newPath.Length == 0)
return PenumbraApiEc.InvalidArgument;
if (!_modManager.TryGetMod(modDirectory, modName, out var mod)
|| !_modFileSystem.FindLeaf(mod, out var leaf))
return PenumbraApiEc.ModMissing;
try
{
_modFileSystem.RenameAndMove(leaf, newPath);
return PenumbraApiEc.Success;
}
catch
{
return PenumbraApiEc.PathRenameFailed;
}
}
public Dictionary<string, object?> GetChangedItems(string modDirectory, string modName)
=> _modManager.TryGetMod(modDirectory, modName, out var mod)
? mod.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToInternalObject())
: [];
}

View file

@ -0,0 +1,40 @@
using OtterGui.Services;
namespace Penumbra.Api.Api;
public class PenumbraApi(
CollectionApi collection,
EditingApi editing,
GameStateApi gameState,
MetaApi meta,
ModsApi mods,
ModSettingsApi modSettings,
PluginStateApi pluginState,
RedrawApi redraw,
ResolveApi resolve,
ResourceTreeApi resourceTree,
TemporaryApi temporary,
UiApi ui) : IDisposable, IApiService, IPenumbraApi
{
public void Dispose()
{
Valid = false;
}
public (int Breaking, int Feature) ApiVersion
=> (5, 3);
public bool Valid { get; private set; } = true;
public IPenumbraApiCollection Collection { get; } = collection;
public IPenumbraApiEditing Editing { get; } = editing;
public IPenumbraApiGameState GameState { get; } = gameState;
public IPenumbraApiMeta Meta { get; } = meta;
public IPenumbraApiMods Mods { get; } = mods;
public IPenumbraApiModSettings ModSettings { get; } = modSettings;
public IPenumbraApiPluginState PluginState { get; } = pluginState;
public IPenumbraApiRedraw Redraw { get; } = redraw;
public IPenumbraApiResolve Resolve { get; } = resolve;
public IPenumbraApiResourceTree ResourceTree { get; } = resourceTree;
public IPenumbraApiTemporary Temporary { get; } = temporary;
public IPenumbraApiUi Ui { get; } = ui;
}

View file

@ -0,0 +1,39 @@
using Newtonsoft.Json;
using OtterGui.Services;
using Penumbra.Communication;
using Penumbra.Services;
namespace Penumbra.Api.Api;
public class PluginStateApi : IPenumbraApiPluginState, IApiService
{
private readonly Configuration _config;
private readonly CommunicatorService _communicator;
public PluginStateApi(Configuration config, CommunicatorService communicator)
{
_config = config;
_communicator = communicator;
}
public string GetModDirectory()
=> _config.ModDirectory;
public string GetConfiguration()
=> JsonConvert.SerializeObject(_config, Formatting.Indented);
public event Action<string, bool>? ModDirectoryChanged
{
add => _communicator.ModDirectoryChanged.Subscribe(value!, Communication.ModDirectoryChanged.Priority.Api);
remove => _communicator.ModDirectoryChanged.Unsubscribe(value!);
}
public bool GetEnabledState()
=> _config.EnableMods;
public event Action<bool>? EnabledChange
{
add => _communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api);
remove => _communicator.EnabledChanged.Unsubscribe(value!);
}
}

View file

@ -0,0 +1,27 @@
using Dalamud.Game.ClientState.Objects.Types;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Interop.Services;
namespace Penumbra.Api.Api;
public class RedrawApi(RedrawService redrawService) : IPenumbraApiRedraw, IApiService
{
public void RedrawObject(int gameObjectIndex, RedrawType setting)
=> redrawService.RedrawObject(gameObjectIndex, setting);
public void RedrawObject(string name, RedrawType setting)
=> redrawService.RedrawObject(name, setting);
public void RedrawObject(IGameObject? gameObject, RedrawType setting)
=> redrawService.RedrawObject(gameObject, setting);
public void RedrawAll(RedrawType setting)
=> redrawService.RedrawAll(setting);
public event GameObjectRedrawnDelegate? GameObjectRedrawn
{
add => redrawService.GameObjectRedrawn += value;
remove => redrawService.GameObjectRedrawn -= value;
}
}

View file

@ -0,0 +1,101 @@
using Dalamud.Plugin.Services;
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Interop.PathResolving;
using Penumbra.Mods.Manager;
using Penumbra.String.Classes;
namespace Penumbra.Api.Api;
public class ResolveApi(
ModManager modManager,
CollectionManager collectionManager,
Configuration config,
CollectionResolver collectionResolver,
ApiHelpers helpers,
IFramework framework) : IPenumbraApiResolve, IApiService
{
public string ResolveDefaultPath(string gamePath)
=> ResolvePath(gamePath, modManager, collectionManager.Active.Default);
public string ResolveInterfacePath(string gamePath)
=> ResolvePath(gamePath, modManager, collectionManager.Active.Interface);
public string ResolveGameObjectPath(string gamePath, int gameObjectIdx)
{
helpers.AssociatedCollection(gameObjectIdx, out var collection);
return ResolvePath(gamePath, modManager, collection);
}
public string ResolvePlayerPath(string gamePath)
=> ResolvePath(gamePath, modManager, collectionResolver.PlayerCollection());
public string[] ReverseResolveGameObjectPath(string moddedPath, int gameObjectIdx)
{
if (!config.EnableMods)
return [moddedPath];
helpers.AssociatedCollection(gameObjectIdx, out var collection);
var ret = collection.ReverseResolvePath(new FullPath(moddedPath));
return ret.Select(r => r.ToString()).ToArray();
}
public string[] ReverseResolvePlayerPath(string moddedPath)
{
if (!config.EnableMods)
return [moddedPath];
var ret = collectionResolver.PlayerCollection().ReverseResolvePath(new FullPath(moddedPath));
return ret.Select(r => r.ToString()).ToArray();
}
public (string[], string[][]) ResolvePlayerPaths(string[] forward, string[] reverse)
{
if (!config.EnableMods)
return (forward, reverse.Select(p => new[]
{
p,
}).ToArray());
var playerCollection = collectionResolver.PlayerCollection();
var resolved = forward.Select(p => ResolvePath(p, modManager, playerCollection)).ToArray();
var reverseResolved = playerCollection.ReverseResolvePaths(reverse);
return (resolved, reverseResolved.Select(a => a.Select(p => p.ToString()).ToArray()).ToArray());
}
public async Task<(string[], string[][])> ResolvePlayerPathsAsync(string[] forward, string[] reverse)
{
if (!config.EnableMods)
return (forward, reverse.Select(p => new[]
{
p,
}).ToArray());
return await Task.Run(async () =>
{
var playerCollection = await framework.RunOnFrameworkThread(collectionResolver.PlayerCollection).ConfigureAwait(false);
var forwardTask = Task.Run(() =>
{
var forwardRet = new string[forward.Length];
Parallel.For(0, forward.Length, idx => forwardRet[idx] = ResolvePath(forward[idx], modManager, playerCollection));
return forwardRet;
}).ConfigureAwait(false);
var reverseTask = Task.Run(() => playerCollection.ReverseResolvePaths(reverse)).ConfigureAwait(false);
var reverseResolved = (await reverseTask).Select(a => a.Select(p => p.ToString()).ToArray()).ToArray();
return (await forwardTask, reverseResolved);
}).ConfigureAwait(false);
}
/// <summary> Resolve a path given by string for a specific collection. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private string ResolvePath(string path, ModManager _, ModCollection collection)
{
if (!config.EnableMods)
return path;
var gamePath = Utf8GamePath.FromString(path, out var p) ? p : Utf8GamePath.Empty;
var ret = collection.ResolvePath(gamePath);
return ret?.ToString() ?? path;
}
}

View file

@ -0,0 +1,63 @@
using Dalamud.Game.ClientState.Objects.Types;
using Newtonsoft.Json.Linq;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.GameData.Interop;
using Penumbra.Interop.ResourceTree;
namespace Penumbra.Api.Api;
public class ResourceTreeApi(ResourceTreeFactory resourceTreeFactory, ObjectManager objects) : IPenumbraApiResourceTree, IApiService
{
public Dictionary<string, HashSet<string>>?[] GetGameObjectResourcePaths(params ushort[] gameObjects)
{
var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType<ICharacter>();
var resourceTrees = resourceTreeFactory.FromCharacters(characters, 0);
var pathDictionaries = ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees);
return Array.ConvertAll(gameObjects, obj => pathDictionaries.GetValueOrDefault(obj));
}
public Dictionary<ushort, Dictionary<string, HashSet<string>>> GetPlayerResourcePaths()
{
var resourceTrees = resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly);
return ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees);
}
public GameResourceDict?[] GetGameObjectResourcesOfType(ResourceType type, bool withUiData,
params ushort[] gameObjects)
{
var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType<ICharacter>();
var resourceTrees = resourceTreeFactory.FromCharacters(characters, withUiData ? ResourceTreeFactory.Flags.WithUiData : 0);
var resDictionaries = ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type);
return Array.ConvertAll(gameObjects, obj => resDictionaries.GetValueOrDefault(obj));
}
public Dictionary<ushort, GameResourceDict> GetPlayerResourcesOfType(ResourceType type,
bool withUiData)
{
var resourceTrees = resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly
| (withUiData ? ResourceTreeFactory.Flags.WithUiData : 0));
return ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type);
}
public JObject?[] GetGameObjectResourceTrees(bool withUiData, params ushort[] gameObjects)
{
var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType<ICharacter>();
var resourceTrees = resourceTreeFactory.FromCharacters(characters, withUiData ? ResourceTreeFactory.Flags.WithUiData : 0);
var resDictionary = ResourceTreeApiHelper.EncapsulateResourceTrees(resourceTrees);
return Array.ConvertAll(gameObjects, obj => resDictionary.GetValueOrDefault(obj));
}
public Dictionary<ushort, JObject> GetPlayerResourceTrees(bool withUiData)
{
var resourceTrees = resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly
| (withUiData ? ResourceTreeFactory.Flags.WithUiData : 0));
var resDictionary = ResourceTreeApiHelper.EncapsulateResourceTrees(resourceTrees);
return resDictionary;
}
}

View file

@ -0,0 +1,176 @@
using OtterGui;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Interop;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Settings;
using Penumbra.String.Classes;
namespace Penumbra.Api.Api;
public class TemporaryApi(
TempCollectionManager tempCollections,
ObjectManager objects,
ActorManager actors,
CollectionManager collectionManager,
TempModManager tempMods) : IPenumbraApiTemporary, IApiService
{
public Guid CreateTemporaryCollection(string name)
=> tempCollections.CreateTemporaryCollection(name);
public PenumbraApiEc DeleteTemporaryCollection(Guid collectionId)
=> tempCollections.RemoveTemporaryCollection(collectionId)
? PenumbraApiEc.Success
: PenumbraApiEc.CollectionMissing;
public PenumbraApiEc AssignTemporaryCollection(Guid collectionId, int actorIndex, bool forceAssignment)
{
var args = ApiHelpers.Args("CollectionId", collectionId, "ActorIndex", actorIndex, "Forced", forceAssignment);
if (actorIndex < 0 || actorIndex >= objects.TotalCount)
return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
var identifier = actors.FromObject(objects[actorIndex], out _, false, false, true);
if (!identifier.IsValid)
return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
if (!tempCollections.CollectionById(collectionId, out var collection))
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
if (forceAssignment)
{
if (tempCollections.Collections.ContainsKey(identifier) && !tempCollections.Collections.Delete(identifier))
return ApiHelpers.Return(PenumbraApiEc.AssignmentDeletionFailed, args);
}
else if (tempCollections.Collections.ContainsKey(identifier)
|| collectionManager.Active.Individuals.ContainsKey(identifier))
{
return ApiHelpers.Return(PenumbraApiEc.CharacterCollectionExists, args);
}
var group = tempCollections.Collections.GetGroup(identifier);
var ret = tempCollections.AddIdentifier(collection, group)
? PenumbraApiEc.Success
: PenumbraApiEc.UnknownError;
return ApiHelpers.Return(ret, args);
}
public PenumbraApiEc AddTemporaryModAll(string tag, Dictionary<string, string> paths, string manipString, int priority)
{
var args = ApiHelpers.Args("Tag", tag, "#Paths", paths.Count, "ManipString", manipString, "Priority", priority);
if (!ConvertPaths(paths, out var p))
return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args);
if (!ConvertManips(manipString, out var m))
return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args);
var ret = tempMods.Register(tag, null, p, m, new ModPriority(priority)) switch
{
RedirectResult.Success => PenumbraApiEc.Success,
_ => PenumbraApiEc.UnknownError,
};
return ApiHelpers.Return(ret, args);
}
public PenumbraApiEc AddTemporaryMod(string tag, Guid collectionId, Dictionary<string, string> paths, string manipString, int priority)
{
var args = ApiHelpers.Args("Tag", tag, "CollectionId", collectionId, "#Paths", paths.Count, "ManipString",
manipString, "Priority", priority);
if (collectionId == Guid.Empty)
return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
if (!tempCollections.CollectionById(collectionId, out var collection)
&& !collectionManager.Storage.ById(collectionId, out collection))
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
if (!ConvertPaths(paths, out var p))
return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args);
if (!ConvertManips(manipString, out var m))
return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args);
var ret = tempMods.Register(tag, collection, p, m, new ModPriority(priority)) switch
{
RedirectResult.Success => PenumbraApiEc.Success,
_ => PenumbraApiEc.UnknownError,
};
return ApiHelpers.Return(ret, args);
}
public PenumbraApiEc RemoveTemporaryModAll(string tag, int priority)
{
var ret = tempMods.Unregister(tag, null, new ModPriority(priority)) switch
{
RedirectResult.Success => PenumbraApiEc.Success,
RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged,
_ => PenumbraApiEc.UnknownError,
};
return ApiHelpers.Return(ret, ApiHelpers.Args("Tag", tag, "Priority", priority));
}
public PenumbraApiEc RemoveTemporaryMod(string tag, Guid collectionId, int priority)
{
var args = ApiHelpers.Args("Tag", tag, "CollectionId", collectionId, "Priority", priority);
if (!tempCollections.CollectionById(collectionId, out var collection)
&& !collectionManager.Storage.ById(collectionId, out collection))
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
var ret = tempMods.Unregister(tag, collection, new ModPriority(priority)) switch
{
RedirectResult.Success => PenumbraApiEc.Success,
RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged,
_ => PenumbraApiEc.UnknownError,
};
return ApiHelpers.Return(ret, args);
}
/// <summary>
/// Convert a dictionary of strings to a dictionary of game paths to full paths.
/// Only returns true if all paths can successfully be converted and added.
/// </summary>
private static bool ConvertPaths(IReadOnlyDictionary<string, string> redirections,
[NotNullWhen(true)] out Dictionary<Utf8GamePath, FullPath>? paths)
{
paths = new Dictionary<Utf8GamePath, FullPath>(redirections.Count);
foreach (var (gString, fString) in redirections)
{
if (!Utf8GamePath.FromString(gString, out var path))
{
paths = null;
return false;
}
var fullPath = new FullPath(fString);
if (!paths.TryAdd(path, fullPath))
{
paths = null;
return false;
}
}
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;
}
}

101
Penumbra/Api/Api/UiApi.cs Normal file
View file

@ -0,0 +1,101 @@
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Communication;
using Penumbra.GameData.Data;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.UI;
namespace Penumbra.Api.Api;
public class UiApi : IPenumbraApiUi, IApiService, IDisposable
{
private readonly CommunicatorService _communicator;
private readonly ConfigWindow _configWindow;
private readonly ModManager _modManager;
public UiApi(CommunicatorService communicator, ConfigWindow configWindow, ModManager modManager)
{
_communicator = communicator;
_configWindow = configWindow;
_modManager = modManager;
_communicator.ChangedItemHover.Subscribe(OnChangedItemHover, ChangedItemHover.Priority.Default);
_communicator.ChangedItemClick.Subscribe(OnChangedItemClick, ChangedItemClick.Priority.Default);
}
public void Dispose()
{
_communicator.ChangedItemHover.Unsubscribe(OnChangedItemHover);
_communicator.ChangedItemClick.Unsubscribe(OnChangedItemClick);
}
public event Action<ChangedItemType, uint>? ChangedItemTooltip;
public event Action<MouseButton, ChangedItemType, uint>? ChangedItemClicked;
public event Action<string, float, float>? PreSettingsTabBarDraw
{
add => _communicator.PreSettingsTabBarDraw.Subscribe(value!, Communication.PreSettingsTabBarDraw.Priority.Default);
remove => _communicator.PreSettingsTabBarDraw.Unsubscribe(value!);
}
public event Action<string>? PreSettingsPanelDraw
{
add => _communicator.PreSettingsPanelDraw.Subscribe(value!, Communication.PreSettingsPanelDraw.Priority.Default);
remove => _communicator.PreSettingsPanelDraw.Unsubscribe(value!);
}
public event Action<string>? PostEnabledDraw
{
add => _communicator.PostEnabledDraw.Subscribe(value!, Communication.PostEnabledDraw.Priority.Default);
remove => _communicator.PostEnabledDraw.Unsubscribe(value!);
}
public event Action<string>? PostSettingsPanelDraw
{
add => _communicator.PostSettingsPanelDraw.Subscribe(value!, Communication.PostSettingsPanelDraw.Priority.Default);
remove => _communicator.PostSettingsPanelDraw.Unsubscribe(value!);
}
public PenumbraApiEc OpenMainWindow(TabType tab, string modDirectory, string modName)
{
_configWindow.IsOpen = true;
if (!Enum.IsDefined(tab))
return PenumbraApiEc.InvalidArgument;
if (tab == TabType.Mods && (modDirectory.Length > 0 || modName.Length > 0))
{
if (_modManager.TryGetMod(modDirectory, modName, out var mod))
_communicator.SelectTab.Invoke(tab, mod);
else
return PenumbraApiEc.ModMissing;
}
else if (tab != TabType.None)
{
_communicator.SelectTab.Invoke(tab, null);
}
return PenumbraApiEc.Success;
}
public void CloseMainWindow()
=> _configWindow.IsOpen = false;
private void OnChangedItemClick(MouseButton button, IIdentifiedObjectData? data)
{
if (ChangedItemClicked == null)
return;
var (type, id) = data?.ToApiObject() ?? (ChangedItemType.None, 0);
ChangedItemClicked.Invoke(button, type, id);
}
private void OnChangedItemHover(IIdentifiedObjectData? data)
{
if (ChangedItemTooltip == null)
return;
var (type, id) = data?.ToApiObject() ?? (ChangedItemType.None, 0);
ChangedItemTooltip.Invoke(type, id);
}
}

View file

@ -1,17 +1,19 @@
using Dalamud.Interface;
using Dalamud.Plugin.Services;
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Communication;
using Penumbra.Mods;
using Penumbra.Mods.Editor;
using Penumbra.Services;
using Penumbra.String.Classes;
namespace Penumbra.Api;
public class DalamudSubstitutionProvider : IDisposable
public class DalamudSubstitutionProvider : IDisposable, IApiService
{
private readonly ITextureSubstitutionProvider _substitution;
private readonly IUiBuilder _uiBuilder;
private readonly ActiveCollectionData _activeCollectionData;
private readonly Configuration _config;
private readonly CommunicatorService _communicator;
@ -20,9 +22,10 @@ public class DalamudSubstitutionProvider : IDisposable
=> _config.UseDalamudUiTextureRedirection;
public DalamudSubstitutionProvider(ITextureSubstitutionProvider substitution, ActiveCollectionData activeCollectionData,
Configuration config, CommunicatorService communicator)
Configuration config, CommunicatorService communicator, IUiBuilder ui)
{
_substitution = substitution;
_uiBuilder = ui;
_activeCollectionData = activeCollectionData;
_config = config;
_communicator = communicator;
@ -40,6 +43,9 @@ public class DalamudSubstitutionProvider : IDisposable
public void ResetSubstitutions(IEnumerable<Utf8GamePath> paths)
{
if (!_uiBuilder.UiPrepared)
return;
var transformed = paths
.Where(p => (p.Path.StartsWith("ui/"u8) || p.Path.StartsWith("common/font/"u8)) && p.Path.EndsWith(".tex"u8))
.Select(p => p.ToString());
@ -90,10 +96,7 @@ public class DalamudSubstitutionProvider : IDisposable
case ResolvedFileChanged.Type.Added:
case ResolvedFileChanged.Type.Removed:
case ResolvedFileChanged.Type.Replaced:
ResetSubstitutions(new[]
{
key,
});
ResetSubstitutions([key]);
break;
case ResolvedFileChanged.Type.FullRecomputeStart:
case ResolvedFileChanged.Type.FullRecomputeFinished:
@ -126,7 +129,7 @@ public class DalamudSubstitutionProvider : IDisposable
try
{
if (!Utf8GamePath.FromString(path, out var utf8Path, true))
if (!Utf8GamePath.FromString(path, out var utf8Path))
return;
var resolved = _activeCollectionData.Interface.ResolvePath(utf8Path);

View file

@ -1,11 +1,13 @@
using EmbedIO;
using EmbedIO.Routing;
using EmbedIO.WebApi;
using OtterGui.Services;
using Penumbra.Api.Api;
using Penumbra.Api.Enums;
namespace Penumbra.Api;
public class HttpApi : IDisposable
public class HttpApi : IDisposable, IApiService
{
private partial class Controller : WebApiController
{
@ -67,7 +69,7 @@ public class HttpApi : IDisposable
public partial object? GetMods()
{
Penumbra.Log.Debug($"[HTTP] {nameof(GetMods)} triggered.");
return _api.GetModList();
return _api.Mods.GetModList();
}
public async partial Task Redraw()
@ -75,17 +77,15 @@ public class HttpApi : IDisposable
var data = await HttpContext.GetRequestDataAsync<RedrawData>();
Penumbra.Log.Debug($"[HTTP] {nameof(Redraw)} triggered with {data}.");
if (data.ObjectTableIndex >= 0)
_api.RedrawObject(data.ObjectTableIndex, data.Type);
else if (data.Name.Length > 0)
_api.RedrawObject(data.Name, data.Type);
_api.Redraw.RedrawObject(data.ObjectTableIndex, data.Type);
else
_api.RedrawAll(data.Type);
_api.Redraw.RedrawAll(data.Type);
}
public partial void RedrawAll()
{
Penumbra.Log.Debug($"[HTTP] {nameof(RedrawAll)} triggered.");
_api.RedrawAll(RedrawType.Redraw);
_api.Redraw.RedrawAll(RedrawType.Redraw);
}
public async partial Task ReloadMod()
@ -95,10 +95,10 @@ public class HttpApi : IDisposable
// Add the mod if it is not already loaded and if the directory name is given.
// AddMod returns Success if the mod is already loaded.
if (data.Path.Length != 0)
_api.AddMod(data.Path);
_api.Mods.AddMod(data.Path);
// Reload the mod by path or name, which will also remove no-longer existing mods.
_api.ReloadMod(data.Path, data.Name);
_api.Mods.ReloadMod(data.Path, data.Name);
}
public async partial Task InstallMod()
@ -106,13 +106,13 @@ public class HttpApi : IDisposable
var data = await HttpContext.GetRequestDataAsync<ModInstallData>();
Penumbra.Log.Debug($"[HTTP] {nameof(InstallMod)} triggered with {data}.");
if (data.Path.Length != 0)
_api.InstallMod(data.Path);
_api.Mods.InstallMod(data.Path);
}
public partial void OpenWindow()
{
Penumbra.Log.Debug($"[HTTP] {nameof(OpenWindow)} triggered.");
_api.OpenMainWindow(TabType.Mods, string.Empty, string.Empty);
_api.Ui.OpenMainWindow(TabType.Mods, string.Empty, string.Empty);
}
private record ModReloadData(string Path, string Name)

View file

@ -0,0 +1,122 @@
using Dalamud.Plugin;
using OtterGui.Services;
using Penumbra.Api.Api;
using Penumbra.Api.Helpers;
namespace Penumbra.Api;
public sealed class IpcProviders : IDisposable, IApiService
{
private readonly List<IDisposable> _providers;
private readonly EventProvider _disposedProvider;
private readonly EventProvider _initializedProvider;
public IpcProviders(IDalamudPluginInterface pi, IPenumbraApi api)
{
_disposedProvider = IpcSubscribers.Disposed.Provider(pi);
_initializedProvider = IpcSubscribers.Initialized.Provider(pi);
_providers =
[
IpcSubscribers.GetCollections.Provider(pi, api.Collection),
IpcSubscribers.GetCollectionsByIdentifier.Provider(pi, api.Collection),
IpcSubscribers.GetChangedItemsForCollection.Provider(pi, api.Collection),
IpcSubscribers.GetCollection.Provider(pi, api.Collection),
IpcSubscribers.GetCollectionForObject.Provider(pi, api.Collection),
IpcSubscribers.SetCollection.Provider(pi, api.Collection),
IpcSubscribers.SetCollectionForObject.Provider(pi, api.Collection),
IpcSubscribers.ConvertTextureFile.Provider(pi, api.Editing),
IpcSubscribers.ConvertTextureData.Provider(pi, api.Editing),
IpcSubscribers.GetDrawObjectInfo.Provider(pi, api.GameState),
IpcSubscribers.GetCutsceneParentIndex.Provider(pi, api.GameState),
IpcSubscribers.SetCutsceneParentIndex.Provider(pi, api.GameState),
IpcSubscribers.CreatingCharacterBase.Provider(pi, api.GameState),
IpcSubscribers.CreatedCharacterBase.Provider(pi, api.GameState),
IpcSubscribers.GameObjectResourcePathResolved.Provider(pi, api.GameState),
IpcSubscribers.GetPlayerMetaManipulations.Provider(pi, api.Meta),
IpcSubscribers.GetMetaManipulations.Provider(pi, api.Meta),
IpcSubscribers.GetModList.Provider(pi, api.Mods),
IpcSubscribers.InstallMod.Provider(pi, api.Mods),
IpcSubscribers.ReloadMod.Provider(pi, api.Mods),
IpcSubscribers.AddMod.Provider(pi, api.Mods),
IpcSubscribers.DeleteMod.Provider(pi, api.Mods),
IpcSubscribers.ModDeleted.Provider(pi, api.Mods),
IpcSubscribers.ModAdded.Provider(pi, api.Mods),
IpcSubscribers.ModMoved.Provider(pi, api.Mods),
IpcSubscribers.GetModPath.Provider(pi, api.Mods),
IpcSubscribers.SetModPath.Provider(pi, api.Mods),
IpcSubscribers.GetChangedItems.Provider(pi, api.Mods),
IpcSubscribers.GetAvailableModSettings.Provider(pi, api.ModSettings),
IpcSubscribers.GetCurrentModSettings.Provider(pi, api.ModSettings),
IpcSubscribers.TryInheritMod.Provider(pi, api.ModSettings),
IpcSubscribers.TrySetMod.Provider(pi, api.ModSettings),
IpcSubscribers.TrySetModPriority.Provider(pi, api.ModSettings),
IpcSubscribers.TrySetModSetting.Provider(pi, api.ModSettings),
IpcSubscribers.TrySetModSettings.Provider(pi, api.ModSettings),
IpcSubscribers.ModSettingChanged.Provider(pi, api.ModSettings),
IpcSubscribers.CopyModSettings.Provider(pi, api.ModSettings),
IpcSubscribers.ApiVersion.Provider(pi, api),
new FuncProvider<(int Major, int Minor)>(pi, "Penumbra.ApiVersions", () => api.ApiVersion), // backward compatibility
new FuncProvider<int>(pi, "Penumbra.ApiVersion", () => api.ApiVersion.Breaking), // backward compatibility
IpcSubscribers.GetModDirectory.Provider(pi, api.PluginState),
IpcSubscribers.GetConfiguration.Provider(pi, api.PluginState),
IpcSubscribers.ModDirectoryChanged.Provider(pi, api.PluginState),
IpcSubscribers.GetEnabledState.Provider(pi, api.PluginState),
IpcSubscribers.EnabledChange.Provider(pi, api.PluginState),
IpcSubscribers.RedrawObject.Provider(pi, api.Redraw),
IpcSubscribers.RedrawAll.Provider(pi, api.Redraw),
IpcSubscribers.GameObjectRedrawn.Provider(pi, api.Redraw),
IpcSubscribers.ResolveDefaultPath.Provider(pi, api.Resolve),
IpcSubscribers.ResolveInterfacePath.Provider(pi, api.Resolve),
IpcSubscribers.ResolveGameObjectPath.Provider(pi, api.Resolve),
IpcSubscribers.ResolvePlayerPath.Provider(pi, api.Resolve),
IpcSubscribers.ReverseResolveGameObjectPath.Provider(pi, api.Resolve),
IpcSubscribers.ReverseResolvePlayerPath.Provider(pi, api.Resolve),
IpcSubscribers.ResolvePlayerPaths.Provider(pi, api.Resolve),
IpcSubscribers.ResolvePlayerPathsAsync.Provider(pi, api.Resolve),
IpcSubscribers.GetGameObjectResourcePaths.Provider(pi, api.ResourceTree),
IpcSubscribers.GetPlayerResourcePaths.Provider(pi, api.ResourceTree),
IpcSubscribers.GetGameObjectResourcesOfType.Provider(pi, api.ResourceTree),
IpcSubscribers.GetPlayerResourcesOfType.Provider(pi, api.ResourceTree),
IpcSubscribers.GetGameObjectResourceTrees.Provider(pi, api.ResourceTree),
IpcSubscribers.GetPlayerResourceTrees.Provider(pi, api.ResourceTree),
IpcSubscribers.CreateTemporaryCollection.Provider(pi, api.Temporary),
IpcSubscribers.DeleteTemporaryCollection.Provider(pi, api.Temporary),
IpcSubscribers.AssignTemporaryCollection.Provider(pi, api.Temporary),
IpcSubscribers.AddTemporaryModAll.Provider(pi, api.Temporary),
IpcSubscribers.AddTemporaryMod.Provider(pi, api.Temporary),
IpcSubscribers.RemoveTemporaryModAll.Provider(pi, api.Temporary),
IpcSubscribers.RemoveTemporaryMod.Provider(pi, api.Temporary),
IpcSubscribers.ChangedItemTooltip.Provider(pi, api.Ui),
IpcSubscribers.ChangedItemClicked.Provider(pi, api.Ui),
IpcSubscribers.PreSettingsTabBarDraw.Provider(pi, api.Ui),
IpcSubscribers.PreSettingsDraw.Provider(pi, api.Ui),
IpcSubscribers.PostEnabledDraw.Provider(pi, api.Ui),
IpcSubscribers.PostSettingsDraw.Provider(pi, api.Ui),
IpcSubscribers.OpenMainWindow.Provider(pi, api.Ui),
IpcSubscribers.CloseMainWindow.Provider(pi, api.Ui),
];
_initializedProvider.Invoke();
}
public void Dispose()
{
foreach (var provider in _providers)
provider.Dispose();
_providers.Clear();
_initializedProvider.Dispose();
_disposedProvider.Invoke();
_disposedProvider.Dispose();
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,185 @@
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Dalamud.Plugin;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Api.IpcSubscribers;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Data;
using ImGuiClip = OtterGui.ImGuiClip;
namespace Penumbra.Api.IpcTester;
public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService
{
private int _objectIdx;
private string _collectionIdString = string.Empty;
private Guid? _collectionId;
private bool _allowCreation = true;
private bool _allowDeletion = true;
private ApiCollectionType _type = ApiCollectionType.Yourself;
private Dictionary<Guid, string> _collections = [];
private (string, ChangedItemType, uint)[] _changedItems = [];
private PenumbraApiEc _returnCode = PenumbraApiEc.Success;
private (Guid Id, string Name)? _oldCollection;
public void Draw()
{
using var _ = ImRaii.TreeNode("Collections");
if (!_)
return;
ImGuiUtil.GenericEnumCombo("Collection Type", 200, _type, out _type, t => ((CollectionType)t).ToName());
ImGui.InputInt("Object Index##Collections", ref _objectIdx, 0, 0);
ImGuiUtil.GuidInput("Collection Id##Collections", "Collection Identifier...", string.Empty, ref _collectionId, ref _collectionIdString);
ImGui.Checkbox("Allow Assignment Creation", ref _allowCreation);
ImGui.SameLine();
ImGui.Checkbox("Allow Assignment Deletion", ref _allowDeletion);
using var table = ImRaii.Table(string.Empty, 4, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
IpcTester.DrawIntro("Last Return Code", _returnCode.ToString());
if (_oldCollection != null)
ImGui.TextUnformatted(!_oldCollection.HasValue ? "Created" : _oldCollection.ToString());
IpcTester.DrawIntro(GetCollectionsByIdentifier.Label, "Collection Identifier");
var collectionList = new GetCollectionsByIdentifier(pi).Invoke(_collectionIdString);
if (collectionList.Count == 0)
{
DrawCollection(null);
}
else
{
DrawCollection(collectionList[0]);
foreach (var pair in collectionList.Skip(1))
{
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TableNextColumn();
ImGui.TableNextColumn();
DrawCollection(pair);
}
}
IpcTester.DrawIntro(GetCollection.Label, "Current Collection");
DrawCollection(new GetCollection(pi).Invoke(ApiCollectionType.Current));
IpcTester.DrawIntro(GetCollection.Label, "Default Collection");
DrawCollection(new GetCollection(pi).Invoke(ApiCollectionType.Default));
IpcTester.DrawIntro(GetCollection.Label, "Interface Collection");
DrawCollection(new GetCollection(pi).Invoke(ApiCollectionType.Interface));
IpcTester.DrawIntro(GetCollection.Label, "Special Collection");
DrawCollection(new GetCollection(pi).Invoke(_type));
IpcTester.DrawIntro(GetCollections.Label, "Collections");
DrawCollectionPopup();
if (ImGui.Button("Get##Collections"))
{
_collections = new GetCollections(pi).Invoke();
ImGui.OpenPopup("Collections");
}
IpcTester.DrawIntro(GetCollectionForObject.Label, "Get Object Collection");
var (valid, individual, effectiveCollection) = new GetCollectionForObject(pi).Invoke(_objectIdx);
DrawCollection(effectiveCollection);
ImGui.SameLine();
ImGui.TextUnformatted($"({(valid ? "Valid" : "Invalid")} Object{(individual ? ", Individual Assignment)" : ")")}");
IpcTester.DrawIntro(SetCollection.Label, "Set Special Collection");
if (ImGui.Button("Set##SpecialCollection"))
(_returnCode, _oldCollection) =
new SetCollection(pi).Invoke(_type, _collectionId.GetValueOrDefault(Guid.Empty), _allowCreation, _allowDeletion);
ImGui.TableNextColumn();
if (ImGui.Button("Remove##SpecialCollection"))
(_returnCode, _oldCollection) = new SetCollection(pi).Invoke(_type, null, _allowCreation, _allowDeletion);
IpcTester.DrawIntro(SetCollectionForObject.Label, "Set Object Collection");
if (ImGui.Button("Set##ObjectCollection"))
(_returnCode, _oldCollection) = new SetCollectionForObject(pi).Invoke(_objectIdx, _collectionId.GetValueOrDefault(Guid.Empty),
_allowCreation, _allowDeletion);
ImGui.TableNextColumn();
if (ImGui.Button("Remove##ObjectCollection"))
(_returnCode, _oldCollection) = new SetCollectionForObject(pi).Invoke(_objectIdx, null, _allowCreation, _allowDeletion);
IpcTester.DrawIntro(GetChangedItemsForCollection.Label, "Changed Item List");
DrawChangedItemPopup();
if (ImGui.Button("Get##ChangedItems"))
{
var items = new GetChangedItemsForCollection(pi).Invoke(_collectionId.GetValueOrDefault(Guid.Empty));
_changedItems = items.Select(kvp =>
{
var (type, id) = kvp.Value.ToApiObject();
return (kvp.Key, type, id);
}).ToArray();
ImGui.OpenPopup("Changed Item List");
}
}
private void DrawChangedItemPopup()
{
ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500));
using var p = ImRaii.Popup("Changed Item List");
if (!p)
return;
using (var table = ImRaii.Table("##ChangedItems", 3, ImGuiTableFlags.SizingFixedFit))
{
if (table)
ImGuiClip.ClippedDraw(_changedItems, t =>
{
ImGuiUtil.DrawTableColumn(t.Item1);
ImGuiUtil.DrawTableColumn(t.Item2.ToString());
ImGuiUtil.DrawTableColumn(t.Item3.ToString());
}, ImGui.GetTextLineHeightWithSpacing());
}
if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
ImGui.CloseCurrentPopup();
}
private void DrawCollectionPopup()
{
ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500));
using var p = ImRaii.Popup("Collections");
if (!p)
return;
using (var t = ImRaii.Table("collections", 2, ImGuiTableFlags.SizingFixedFit))
{
if (t)
foreach (var collection in _collections)
{
ImGui.TableNextColumn();
DrawCollection((collection.Key, collection.Value));
}
}
if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
ImGui.CloseCurrentPopup();
}
private static void DrawCollection((Guid Id, string Name)? collection)
{
if (collection == null)
{
ImGui.TextUnformatted("<Unassigned>");
ImGui.TableNextColumn();
return;
}
ImGui.TextUnformatted(collection.Value.Name);
ImGui.TableNextColumn();
using (ImRaii.PushFont(UiBuilder.MonoFont))
{
ImGuiUtil.CopyOnClickSelectable(collection.Value.Id.ToString());
}
}
}

View file

@ -0,0 +1,70 @@
using Dalamud.Plugin;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Api.IpcSubscribers;
namespace Penumbra.Api.IpcTester;
public class EditingIpcTester(IDalamudPluginInterface pi) : IUiService
{
private string _inputPath = string.Empty;
private string _inputPath2 = string.Empty;
private string _outputPath = string.Empty;
private string _outputPath2 = string.Empty;
private TextureType _typeSelector;
private bool _mipMaps = true;
private Task? _task1;
private Task? _task2;
public void Draw()
{
using var _ = ImRaii.TreeNode("Editing");
if (!_)
return;
ImGui.InputTextWithHint("##inputPath", "Input Texture Path...", ref _inputPath, 256);
ImGui.InputTextWithHint("##outputPath", "Output Texture Path...", ref _outputPath, 256);
ImGui.InputTextWithHint("##inputPath2", "Input Texture Path 2...", ref _inputPath2, 256);
ImGui.InputTextWithHint("##outputPath2", "Output Texture Path 2...", ref _outputPath2, 256);
TypeCombo();
ImGui.Checkbox("Add MipMaps", ref _mipMaps);
using var table = ImRaii.Table("...", 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
IpcTester.DrawIntro(ConvertTextureFile.Label, (string)"Convert Texture 1");
if (ImGuiUtil.DrawDisabledButton("Save 1", Vector2.Zero, string.Empty, _task1 is { IsCompleted: false }))
_task1 = new ConvertTextureFile(pi).Invoke(_inputPath, _outputPath, _typeSelector, _mipMaps);
ImGui.SameLine();
ImGui.TextUnformatted(_task1 == null ? "Not Initiated" : _task1.Status.ToString());
if (ImGui.IsItemHovered() && _task1?.Status == TaskStatus.Faulted)
ImGui.SetTooltip(_task1.Exception?.ToString());
IpcTester.DrawIntro(ConvertTextureFile.Label, (string)"Convert Texture 2");
if (ImGuiUtil.DrawDisabledButton("Save 2", Vector2.Zero, string.Empty, _task2 is { IsCompleted: false }))
_task2 = new ConvertTextureFile(pi).Invoke(_inputPath2, _outputPath2, _typeSelector, _mipMaps);
ImGui.SameLine();
ImGui.TextUnformatted(_task2 == null ? "Not Initiated" : _task2.Status.ToString());
if (ImGui.IsItemHovered() && _task2?.Status == TaskStatus.Faulted)
ImGui.SetTooltip(_task2.Exception?.ToString());
}
private void TypeCombo()
{
using var combo = ImRaii.Combo("Convert To", _typeSelector.ToString());
if (!combo)
return;
foreach (var value in Enum.GetValues<TextureType>())
{
if (ImGui.Selectable(value.ToString(), _typeSelector == value))
_typeSelector = value;
}
}
}

View file

@ -0,0 +1,139 @@
using Dalamud.Interface;
using Dalamud.Plugin;
using ImGuiNET;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.Api.IpcSubscribers;
using Penumbra.String;
namespace Penumbra.Api.IpcTester;
public class GameStateIpcTester : IUiService, IDisposable
{
private readonly IDalamudPluginInterface _pi;
public readonly EventSubscriber<nint, Guid, nint, nint, nint> CharacterBaseCreating;
public readonly EventSubscriber<nint, Guid, nint> CharacterBaseCreated;
public readonly EventSubscriber<nint, string, string> GameObjectResourcePathResolved;
private string _lastCreatedGameObjectName = string.Empty;
private nint _lastCreatedDrawObject = nint.Zero;
private DateTimeOffset _lastCreatedGameObjectTime = DateTimeOffset.MaxValue;
private string _lastResolvedGamePath = string.Empty;
private string _lastResolvedFullPath = string.Empty;
private string _lastResolvedObject = string.Empty;
private DateTimeOffset _lastResolvedGamePathTime = DateTimeOffset.MaxValue;
private string _currentDrawObjectString = string.Empty;
private nint _currentDrawObject = nint.Zero;
private int _currentCutsceneActor;
private int _currentCutsceneParent;
private PenumbraApiEc _cutsceneError = PenumbraApiEc.Success;
public GameStateIpcTester(IDalamudPluginInterface pi)
{
_pi = pi;
CharacterBaseCreating = IpcSubscribers.CreatingCharacterBase.Subscriber(pi, UpdateLastCreated);
CharacterBaseCreated = IpcSubscribers.CreatedCharacterBase.Subscriber(pi, UpdateLastCreated2);
GameObjectResourcePathResolved = IpcSubscribers.GameObjectResourcePathResolved.Subscriber(pi, UpdateGameObjectResourcePath);
CharacterBaseCreating.Disable();
CharacterBaseCreated.Disable();
GameObjectResourcePathResolved.Disable();
}
public void Dispose()
{
CharacterBaseCreating.Dispose();
CharacterBaseCreated.Dispose();
GameObjectResourcePathResolved.Dispose();
}
public void Draw()
{
using var _ = ImRaii.TreeNode("Game State");
if (!_)
return;
if (ImGui.InputTextWithHint("##drawObject", "Draw Object Address..", ref _currentDrawObjectString, 16,
ImGuiInputTextFlags.CharsHexadecimal))
_currentDrawObject = nint.TryParse(_currentDrawObjectString, NumberStyles.HexNumber, CultureInfo.InvariantCulture,
out var tmp)
? tmp
: nint.Zero;
ImGui.InputInt("Cutscene Actor", ref _currentCutsceneActor, 0);
ImGui.InputInt("Cutscene Parent", ref _currentCutsceneParent, 0);
if (_cutsceneError is not PenumbraApiEc.Success)
{
ImGui.SameLine();
ImGui.TextUnformatted("Invalid Argument on last Call");
}
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
IpcTester.DrawIntro(GetDrawObjectInfo.Label, "Draw Object Info");
if (_currentDrawObject == nint.Zero)
{
ImGui.TextUnformatted("Invalid");
}
else
{
var (ptr, (collectionId, collectionName)) = new GetDrawObjectInfo(_pi).Invoke(_currentDrawObject);
ImGui.TextUnformatted(ptr == nint.Zero ? $"No Actor Associated, {collectionName}" : $"{ptr:X}, {collectionName}");
ImGui.SameLine();
using (ImRaii.PushFont(UiBuilder.MonoFont))
{
ImGui.TextUnformatted(collectionId.ToString());
}
}
IpcTester.DrawIntro(GetCutsceneParentIndex.Label, "Cutscene Parent");
ImGui.TextUnformatted(new GetCutsceneParentIndex(_pi).Invoke(_currentCutsceneActor).ToString());
IpcTester.DrawIntro(SetCutsceneParentIndex.Label, "Cutscene Parent");
if (ImGui.Button("Set Parent"))
_cutsceneError = new SetCutsceneParentIndex(_pi)
.Invoke(_currentCutsceneActor, _currentCutsceneParent);
IpcTester.DrawIntro(CreatingCharacterBase.Label, "Last Drawobject created");
if (_lastCreatedGameObjectTime < DateTimeOffset.Now)
ImGui.TextUnformatted(_lastCreatedDrawObject != nint.Zero
? $"0x{_lastCreatedDrawObject:X} for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}"
: $"NULL for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}");
IpcTester.DrawIntro(IpcSubscribers.GameObjectResourcePathResolved.Label, "Last GamePath resolved");
if (_lastResolvedGamePathTime < DateTimeOffset.Now)
ImGui.TextUnformatted(
$"{_lastResolvedGamePath} -> {_lastResolvedFullPath} for <{_lastResolvedObject}> at {_lastResolvedGamePathTime}");
}
private void UpdateLastCreated(nint gameObject, Guid _, nint _2, nint _3, nint _4)
{
_lastCreatedGameObjectName = GetObjectName(gameObject);
_lastCreatedGameObjectTime = DateTimeOffset.Now;
_lastCreatedDrawObject = nint.Zero;
}
private void UpdateLastCreated2(nint gameObject, Guid _, nint drawObject)
{
_lastCreatedGameObjectName = GetObjectName(gameObject);
_lastCreatedGameObjectTime = DateTimeOffset.Now;
_lastCreatedDrawObject = drawObject;
}
private void UpdateGameObjectResourcePath(nint gameObject, string gamePath, string fullPath)
{
_lastResolvedObject = GetObjectName(gameObject);
_lastResolvedGamePath = gamePath;
_lastResolvedFullPath = fullPath;
_lastResolvedGamePathTime = DateTimeOffset.Now;
}
private static unsafe string GetObjectName(nint gameObject)
{
var obj = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject;
return obj != null && obj->Name[0] != 0 ? new ByteString(obj->Name).ToString() : "Unknown";
}
}

View file

@ -0,0 +1,133 @@
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using ImGuiNET;
using OtterGui.Services;
using Penumbra.Api.Api;
namespace Penumbra.Api.IpcTester;
public class IpcTester(
IpcProviders ipcProviders,
IPenumbraApi api,
PluginStateIpcTester pluginStateIpcTester,
UiIpcTester uiIpcTester,
RedrawingIpcTester redrawingIpcTester,
GameStateIpcTester gameStateIpcTester,
ResolveIpcTester resolveIpcTester,
CollectionsIpcTester collectionsIpcTester,
MetaIpcTester metaIpcTester,
ModsIpcTester modsIpcTester,
ModSettingsIpcTester modSettingsIpcTester,
EditingIpcTester editingIpcTester,
TemporaryIpcTester temporaryIpcTester,
ResourceTreeIpcTester resourceTreeIpcTester,
IFramework framework) : IUiService
{
private readonly IpcProviders _ipcProviders = ipcProviders;
private DateTime _lastUpdate;
private bool _subscribed = false;
public void Draw()
{
try
{
_lastUpdate = framework.LastUpdateUTC.AddSeconds(1);
Subscribe();
ImGui.TextUnformatted($"API Version: {api.ApiVersion.Breaking}.{api.ApiVersion.Feature:D4}");
collectionsIpcTester.Draw();
editingIpcTester.Draw();
gameStateIpcTester.Draw();
metaIpcTester.Draw();
modSettingsIpcTester.Draw();
modsIpcTester.Draw();
pluginStateIpcTester.Draw();
redrawingIpcTester.Draw();
resolveIpcTester.Draw();
resourceTreeIpcTester.Draw();
uiIpcTester.Draw();
temporaryIpcTester.Draw();
temporaryIpcTester.DrawCollections();
temporaryIpcTester.DrawMods();
}
catch (Exception e)
{
Penumbra.Log.Error($"Error during IPC Tests:\n{e}");
}
}
internal static void DrawIntro(string label, string info)
{
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted(label);
ImGui.TableNextColumn();
ImGui.TextUnformatted(info);
ImGui.TableNextColumn();
}
private void Subscribe()
{
if (_subscribed)
return;
Penumbra.Log.Debug("[IPCTester] Subscribed to IPC events for IPC tester.");
gameStateIpcTester.GameObjectResourcePathResolved.Enable();
gameStateIpcTester.CharacterBaseCreated.Enable();
gameStateIpcTester.CharacterBaseCreating.Enable();
modSettingsIpcTester.SettingChanged.Enable();
modsIpcTester.DeleteSubscriber.Enable();
modsIpcTester.AddSubscriber.Enable();
modsIpcTester.MoveSubscriber.Enable();
pluginStateIpcTester.ModDirectoryChanged.Enable();
pluginStateIpcTester.Initialized.Enable();
pluginStateIpcTester.Disposed.Enable();
pluginStateIpcTester.EnabledChange.Enable();
redrawingIpcTester.Redrawn.Enable();
uiIpcTester.PreSettingsTabBar.Enable();
uiIpcTester.PreSettingsPanel.Enable();
uiIpcTester.PostEnabled.Enable();
uiIpcTester.PostSettingsPanelDraw.Enable();
uiIpcTester.ChangedItemTooltip.Enable();
uiIpcTester.ChangedItemClicked.Enable();
framework.Update += CheckUnsubscribe;
_subscribed = true;
}
private void CheckUnsubscribe(IFramework framework1)
{
if (_lastUpdate > framework.LastUpdateUTC)
return;
Unsubscribe();
framework.Update -= CheckUnsubscribe;
}
private void Unsubscribe()
{
if (!_subscribed)
return;
Penumbra.Log.Debug("[IPCTester] Unsubscribed from IPC events for IPC tester.");
_subscribed = false;
gameStateIpcTester.GameObjectResourcePathResolved.Disable();
gameStateIpcTester.CharacterBaseCreated.Disable();
gameStateIpcTester.CharacterBaseCreating.Disable();
modSettingsIpcTester.SettingChanged.Disable();
modsIpcTester.DeleteSubscriber.Disable();
modsIpcTester.AddSubscriber.Disable();
modsIpcTester.MoveSubscriber.Disable();
pluginStateIpcTester.ModDirectoryChanged.Disable();
pluginStateIpcTester.Initialized.Disable();
pluginStateIpcTester.Disposed.Disable();
pluginStateIpcTester.EnabledChange.Disable();
redrawingIpcTester.Redrawn.Disable();
uiIpcTester.PreSettingsTabBar.Disable();
uiIpcTester.PreSettingsPanel.Disable();
uiIpcTester.PostEnabled.Disable();
uiIpcTester.PostSettingsPanelDraw.Disable();
uiIpcTester.ChangedItemTooltip.Disable();
uiIpcTester.ChangedItemClicked.Disable();
}
}

View file

@ -0,0 +1,38 @@
using Dalamud.Plugin;
using ImGuiNET;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.IpcSubscribers;
namespace Penumbra.Api.IpcTester;
public class MetaIpcTester(IDalamudPluginInterface pi) : IUiService
{
private int _gameObjectIndex;
public void Draw()
{
using var _ = ImRaii.TreeNode("Meta");
if (!_)
return;
ImGui.InputInt("##metaIdx", ref _gameObjectIndex, 0, 0);
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
IpcTester.DrawIntro(GetPlayerMetaManipulations.Label, "Player Meta Manipulations");
if (ImGui.Button("Copy to Clipboard##Player"))
{
var base64 = new GetPlayerMetaManipulations(pi).Invoke();
ImGui.SetClipboardText(base64);
}
IpcTester.DrawIntro(GetMetaManipulations.Label, "Game Object Manipulations");
if (ImGui.Button("Copy to Clipboard##GameObject"))
{
var base64 = new GetMetaManipulations(pi).Invoke(_gameObjectIndex);
ImGui.SetClipboardText(base64);
}
}
}

View file

@ -0,0 +1,182 @@
using Dalamud.Plugin;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.Api.IpcSubscribers;
using Penumbra.UI;
namespace Penumbra.Api.IpcTester;
public class ModSettingsIpcTester : IUiService, IDisposable
{
private readonly IDalamudPluginInterface _pi;
public readonly EventSubscriber<ModSettingChange, Guid, string, bool> SettingChanged;
private PenumbraApiEc _lastSettingsError = PenumbraApiEc.Success;
private ModSettingChange _lastSettingChangeType;
private Guid _lastSettingChangeCollection = Guid.Empty;
private string _lastSettingChangeMod = string.Empty;
private bool _lastSettingChangeInherited;
private DateTimeOffset _lastSettingChange;
private string _settingsModDirectory = string.Empty;
private string _settingsModName = string.Empty;
private Guid? _settingsCollection;
private string _settingsCollectionName = string.Empty;
private bool _settingsIgnoreInheritance;
private bool _settingsInherit;
private bool _settingsEnabled;
private int _settingsPriority;
private IReadOnlyDictionary<string, (string[], GroupType)>? _availableSettings;
private Dictionary<string, List<string>>? _currentSettings;
public ModSettingsIpcTester(IDalamudPluginInterface pi)
{
_pi = pi;
SettingChanged = ModSettingChanged.Subscriber(pi, UpdateLastModSetting);
SettingChanged.Disable();
}
public void Dispose()
{
SettingChanged.Dispose();
}
public void Draw()
{
using var _ = ImRaii.TreeNode("Mod Settings");
if (!_)
return;
ImGui.InputTextWithHint("##settingsDir", "Mod Directory Name...", ref _settingsModDirectory, 100);
ImGui.InputTextWithHint("##settingsName", "Mod Name...", ref _settingsModName, 100);
ImGuiUtil.GuidInput("##settingsCollection", "Collection...", string.Empty, ref _settingsCollection, ref _settingsCollectionName);
ImGui.Checkbox("Ignore Inheritance", ref _settingsIgnoreInheritance);
var collection = _settingsCollection.GetValueOrDefault(Guid.Empty);
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
IpcTester.DrawIntro("Last Error", _lastSettingsError.ToString());
IpcTester.DrawIntro(ModSettingChanged.Label, "Last Mod Setting Changed");
ImGui.TextUnformatted(_lastSettingChangeMod.Length > 0
? $"{_lastSettingChangeType} of {_lastSettingChangeMod} in {_lastSettingChangeCollection}{(_lastSettingChangeInherited ? " (Inherited)" : string.Empty)} at {_lastSettingChange}"
: "None");
IpcTester.DrawIntro(GetAvailableModSettings.Label, "Get Available Settings");
if (ImGui.Button("Get##Available"))
{
_availableSettings = new GetAvailableModSettings(_pi).Invoke(_settingsModDirectory, _settingsModName);
_lastSettingsError = _availableSettings == null ? PenumbraApiEc.ModMissing : PenumbraApiEc.Success;
}
IpcTester.DrawIntro(GetCurrentModSettings.Label, "Get Current Settings");
if (ImGui.Button("Get##Current"))
{
var ret = new GetCurrentModSettings(_pi)
.Invoke(collection, _settingsModDirectory, _settingsModName, _settingsIgnoreInheritance);
_lastSettingsError = ret.Item1;
if (ret.Item1 == PenumbraApiEc.Success)
{
_settingsEnabled = ret.Item2?.Item1 ?? false;
_settingsInherit = ret.Item2?.Item4 ?? true;
_settingsPriority = ret.Item2?.Item2 ?? 0;
_currentSettings = ret.Item2?.Item3;
}
else
{
_currentSettings = null;
}
}
IpcTester.DrawIntro(TryInheritMod.Label, "Inherit Mod");
ImGui.Checkbox("##inherit", ref _settingsInherit);
ImGui.SameLine();
if (ImGui.Button("Set##Inherit"))
_lastSettingsError = new TryInheritMod(_pi)
.Invoke(collection, _settingsModDirectory, _settingsInherit, _settingsModName);
IpcTester.DrawIntro(TrySetMod.Label, "Set Enabled");
ImGui.Checkbox("##enabled", ref _settingsEnabled);
ImGui.SameLine();
if (ImGui.Button("Set##Enabled"))
_lastSettingsError = new TrySetMod(_pi)
.Invoke(collection, _settingsModDirectory, _settingsEnabled, _settingsModName);
IpcTester.DrawIntro(TrySetModPriority.Label, "Set Priority");
ImGui.SetNextItemWidth(200 * UiHelpers.Scale);
ImGui.DragInt("##Priority", ref _settingsPriority);
ImGui.SameLine();
if (ImGui.Button("Set##Priority"))
_lastSettingsError = new TrySetModPriority(_pi)
.Invoke(collection, _settingsModDirectory, _settingsPriority, _settingsModName);
IpcTester.DrawIntro(CopyModSettings.Label, "Copy Mod Settings");
if (ImGui.Button("Copy Settings"))
_lastSettingsError = new CopyModSettings(_pi)
.Invoke(_settingsCollection, _settingsModDirectory, _settingsModName);
ImGuiUtil.HoverTooltip("Copy settings from Mod Directory Name to Mod Name (as directory) in collection.");
IpcTester.DrawIntro(TrySetModSetting.Label, "Set Setting(s)");
if (_availableSettings == null)
return;
foreach (var (group, (list, type)) in _availableSettings)
{
using var id = ImRaii.PushId(group);
var preview = list.Length > 0 ? list[0] : string.Empty;
if (_currentSettings != null && _currentSettings.TryGetValue(group, out var current) && current.Count > 0)
{
preview = current[0];
}
else
{
current = [];
if (_currentSettings != null)
_currentSettings[group] = current;
}
ImGui.SetNextItemWidth(200 * UiHelpers.Scale);
using (var c = ImRaii.Combo("##group", preview))
{
if (c)
foreach (var s in list)
{
var contained = current.Contains(s);
if (ImGui.Checkbox(s, ref contained))
{
if (contained)
current.Add(s);
else
current.Remove(s);
}
}
}
ImGui.SameLine();
if (ImGui.Button("Set##setting"))
_lastSettingsError = type == GroupType.Single
? new TrySetModSetting(_pi).Invoke(collection, _settingsModDirectory, group, current.Count > 0 ? current[0] : string.Empty,
_settingsModName)
: new TrySetModSettings(_pi).Invoke(collection, _settingsModDirectory, group, current.ToArray(), _settingsModName);
ImGui.SameLine();
ImGui.TextUnformatted(group);
}
}
private void UpdateLastModSetting(ModSettingChange type, Guid collection, string mod, bool inherited)
{
_lastSettingChangeType = type;
_lastSettingChangeCollection = collection;
_lastSettingChangeMod = mod;
_lastSettingChangeInherited = inherited;
_lastSettingChange = DateTimeOffset.Now;
}
}

View file

@ -0,0 +1,184 @@
using Dalamud.Interface.Utility;
using Dalamud.Plugin;
using ImGuiNET;
using OtterGui.Raii;
using OtterGui.Services;
using OtterGui.Text;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.Api.IpcSubscribers;
namespace Penumbra.Api.IpcTester;
public class ModsIpcTester : IUiService, IDisposable
{
private readonly IDalamudPluginInterface _pi;
private string _modDirectory = string.Empty;
private string _modName = string.Empty;
private string _pathInput = string.Empty;
private string _newInstallPath = string.Empty;
private PenumbraApiEc _lastReloadEc;
private PenumbraApiEc _lastAddEc;
private PenumbraApiEc _lastDeleteEc;
private PenumbraApiEc _lastSetPathEc;
private PenumbraApiEc _lastInstallEc;
private Dictionary<string, string> _mods = [];
private Dictionary<string, object?> _changedItems = [];
public readonly EventSubscriber<string> DeleteSubscriber;
public readonly EventSubscriber<string> AddSubscriber;
public readonly EventSubscriber<string, string> MoveSubscriber;
private DateTimeOffset _lastDeletedModTime = DateTimeOffset.UnixEpoch;
private string _lastDeletedMod = string.Empty;
private DateTimeOffset _lastAddedModTime = DateTimeOffset.UnixEpoch;
private string _lastAddedMod = string.Empty;
private DateTimeOffset _lastMovedModTime = DateTimeOffset.UnixEpoch;
private string _lastMovedModFrom = string.Empty;
private string _lastMovedModTo = string.Empty;
public ModsIpcTester(IDalamudPluginInterface pi)
{
_pi = pi;
DeleteSubscriber = ModDeleted.Subscriber(pi, s =>
{
_lastDeletedModTime = DateTimeOffset.UtcNow;
_lastDeletedMod = s;
});
AddSubscriber = ModAdded.Subscriber(pi, s =>
{
_lastAddedModTime = DateTimeOffset.UtcNow;
_lastAddedMod = s;
});
MoveSubscriber = ModMoved.Subscriber(pi, (s1, s2) =>
{
_lastMovedModTime = DateTimeOffset.UtcNow;
_lastMovedModFrom = s1;
_lastMovedModTo = s2;
});
DeleteSubscriber.Disable();
AddSubscriber.Disable();
MoveSubscriber.Disable();
}
public void Dispose()
{
DeleteSubscriber.Dispose();
DeleteSubscriber.Disable();
AddSubscriber.Dispose();
AddSubscriber.Disable();
MoveSubscriber.Dispose();
MoveSubscriber.Disable();
}
public void Draw()
{
using var _ = ImRaii.TreeNode("Mods");
if (!_)
return;
ImGui.InputTextWithHint("##install", "Install File Path...", ref _newInstallPath, 100);
ImGui.InputTextWithHint("##modDir", "Mod Directory Name...", ref _modDirectory, 100);
ImGui.InputTextWithHint("##modName", "Mod Name...", ref _modName, 100);
ImGui.InputTextWithHint("##path", "New Path...", ref _pathInput, 100);
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
IpcTester.DrawIntro(GetModList.Label, "Mods");
DrawModsPopup();
if (ImGui.Button("Get##Mods"))
{
_mods = new GetModList(_pi).Invoke();
ImGui.OpenPopup("Mods");
}
IpcTester.DrawIntro(ReloadMod.Label, "Reload Mod");
if (ImGui.Button("Reload"))
_lastReloadEc = new ReloadMod(_pi).Invoke(_modDirectory, _modName);
ImGui.SameLine();
ImGui.TextUnformatted(_lastReloadEc.ToString());
IpcTester.DrawIntro(InstallMod.Label, "Install Mod");
if (ImGui.Button("Install"))
_lastInstallEc = new InstallMod(_pi).Invoke(_newInstallPath);
ImGui.SameLine();
ImGui.TextUnformatted(_lastInstallEc.ToString());
IpcTester.DrawIntro(AddMod.Label, "Add Mod");
if (ImGui.Button("Add"))
_lastAddEc = new AddMod(_pi).Invoke(_modDirectory);
ImGui.SameLine();
ImGui.TextUnformatted(_lastAddEc.ToString());
IpcTester.DrawIntro(DeleteMod.Label, "Delete Mod");
if (ImGui.Button("Delete"))
_lastDeleteEc = new DeleteMod(_pi).Invoke(_modDirectory, _modName);
ImGui.SameLine();
ImGui.TextUnformatted(_lastDeleteEc.ToString());
IpcTester.DrawIntro(GetChangedItems.Label, "Get Changed Items");
DrawChangedItemsPopup();
if (ImUtf8.Button("Get##ChangedItems"u8))
{
_changedItems = new GetChangedItems(_pi).Invoke(_modDirectory, _modName);
ImUtf8.OpenPopup("ChangedItems"u8);
}
IpcTester.DrawIntro(GetModPath.Label, "Current Path");
var (ec, path, def, nameDef) = new GetModPath(_pi).Invoke(_modDirectory, _modName);
ImGui.TextUnformatted($"{path} ({(def ? "Custom" : "Default")} Path, {(nameDef ? "Custom" : "Default")} Name) [{ec}]");
IpcTester.DrawIntro(SetModPath.Label, "Set Path");
if (ImGui.Button("Set"))
_lastSetPathEc = new SetModPath(_pi).Invoke(_modDirectory, _pathInput, _modName);
ImGui.SameLine();
ImGui.TextUnformatted(_lastSetPathEc.ToString());
IpcTester.DrawIntro(ModDeleted.Label, "Last Mod Deleted");
if (_lastDeletedModTime > DateTimeOffset.UnixEpoch)
ImGui.TextUnformatted($"{_lastDeletedMod} at {_lastDeletedModTime}");
IpcTester.DrawIntro(ModAdded.Label, "Last Mod Added");
if (_lastAddedModTime > DateTimeOffset.UnixEpoch)
ImGui.TextUnformatted($"{_lastAddedMod} at {_lastAddedModTime}");
IpcTester.DrawIntro(ModMoved.Label, "Last Mod Moved");
if (_lastMovedModTime > DateTimeOffset.UnixEpoch)
ImGui.TextUnformatted($"{_lastMovedModFrom} -> {_lastMovedModTo} at {_lastMovedModTime}");
}
private void DrawModsPopup()
{
ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500));
using var p = ImRaii.Popup("Mods");
if (!p)
return;
foreach (var (modDir, modName) in _mods)
ImGui.TextUnformatted($"{modDir}: {modName}");
if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
ImGui.CloseCurrentPopup();
}
private void DrawChangedItemsPopup()
{
ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500));
using var p = ImUtf8.Popup("ChangedItems"u8);
if (!p)
return;
foreach (var (name, data) in _changedItems)
ImUtf8.Text($"{name}: {data}");
if (ImUtf8.Button("Close"u8, -Vector2.UnitX) || !ImGui.IsWindowFocused())
ImGui.CloseCurrentPopup();
}
}

View file

@ -0,0 +1,134 @@
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Dalamud.Plugin;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.Helpers;
using Penumbra.Api.IpcSubscribers;
namespace Penumbra.Api.IpcTester;
public class PluginStateIpcTester : IUiService, IDisposable
{
private readonly IDalamudPluginInterface _pi;
public readonly EventSubscriber<string, bool> ModDirectoryChanged;
public readonly EventSubscriber Initialized;
public readonly EventSubscriber Disposed;
public readonly EventSubscriber<bool> EnabledChange;
private string _currentConfiguration = string.Empty;
private string _lastModDirectory = string.Empty;
private bool _lastModDirectoryValid;
private DateTimeOffset _lastModDirectoryTime = DateTimeOffset.MinValue;
private readonly List<DateTimeOffset> _initializedList = [];
private readonly List<DateTimeOffset> _disposedList = [];
private DateTimeOffset _lastEnabledChange = DateTimeOffset.UnixEpoch;
private bool? _lastEnabledValue;
public PluginStateIpcTester(IDalamudPluginInterface pi)
{
_pi = pi;
ModDirectoryChanged = IpcSubscribers.ModDirectoryChanged.Subscriber(pi, UpdateModDirectoryChanged);
Initialized = IpcSubscribers.Initialized.Subscriber(pi, AddInitialized);
Disposed = IpcSubscribers.Disposed.Subscriber(pi, AddDisposed);
EnabledChange = IpcSubscribers.EnabledChange.Subscriber(pi, SetLastEnabled);
ModDirectoryChanged.Disable();
EnabledChange.Disable();
}
public void Dispose()
{
ModDirectoryChanged.Dispose();
Initialized.Dispose();
Disposed.Dispose();
EnabledChange.Dispose();
}
public void Draw()
{
using var _ = ImRaii.TreeNode("Plugin State");
if (!_)
return;
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
DrawList(IpcSubscribers.Initialized.Label, "Last Initialized", _initializedList);
DrawList(IpcSubscribers.Disposed.Label, "Last Disposed", _disposedList);
IpcTester.DrawIntro(ApiVersion.Label, "Current Version");
var (breaking, features) = new ApiVersion(_pi).Invoke();
ImGui.TextUnformatted($"{breaking}.{features:D4}");
IpcTester.DrawIntro(GetEnabledState.Label, "Current State");
ImGui.TextUnformatted($"{new GetEnabledState(_pi).Invoke()}");
IpcTester.DrawIntro(IpcSubscribers.EnabledChange.Label, "Last Change");
ImGui.TextUnformatted(_lastEnabledValue is { } v ? $"{_lastEnabledChange} (to {v})" : "Never");
DrawConfigPopup();
IpcTester.DrawIntro(GetConfiguration.Label, "Configuration");
if (ImGui.Button("Get"))
{
_currentConfiguration = new GetConfiguration(_pi).Invoke();
ImGui.OpenPopup("Config Popup");
}
IpcTester.DrawIntro(GetModDirectory.Label, "Current Mod Directory");
ImGui.TextUnformatted(new GetModDirectory(_pi).Invoke());
IpcTester.DrawIntro(IpcSubscribers.ModDirectoryChanged.Label, "Last Mod Directory Change");
ImGui.TextUnformatted(_lastModDirectoryTime > DateTimeOffset.MinValue
? $"{_lastModDirectory} ({(_lastModDirectoryValid ? "Valid" : "Invalid")}) at {_lastModDirectoryTime}"
: "None");
void DrawList(string label, string text, List<DateTimeOffset> list)
{
IpcTester.DrawIntro(label, text);
if (list.Count == 0)
{
ImGui.TextUnformatted("Never");
}
else
{
ImGui.TextUnformatted(list[^1].LocalDateTime.ToString(CultureInfo.CurrentCulture));
if (list.Count > 1 && ImGui.IsItemHovered())
ImGui.SetTooltip(string.Join("\n",
list.SkipLast(1).Select(t => t.LocalDateTime.ToString(CultureInfo.CurrentCulture))));
}
}
}
private void DrawConfigPopup()
{
ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500));
using var popup = ImRaii.Popup("Config Popup");
if (!popup)
return;
using (ImRaii.PushFont(UiBuilder.MonoFont))
{
ImGuiUtil.TextWrapped(_currentConfiguration);
}
if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
ImGui.CloseCurrentPopup();
}
private void UpdateModDirectoryChanged(string path, bool valid)
=> (_lastModDirectory, _lastModDirectoryValid, _lastModDirectoryTime) = (path, valid, DateTimeOffset.Now);
private void AddInitialized()
=> _initializedList.Add(DateTimeOffset.UtcNow);
private void AddDisposed()
=> _disposedList.Add(DateTimeOffset.UtcNow);
private void SetLastEnabled(bool val)
=> (_lastEnabledChange, _lastEnabledValue) = (DateTimeOffset.Now, val);
}

View file

@ -0,0 +1,73 @@
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using ImGuiNET;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.Api.IpcSubscribers;
using Penumbra.GameData.Interop;
using Penumbra.UI;
namespace Penumbra.Api.IpcTester;
public class RedrawingIpcTester : IUiService, IDisposable
{
private readonly IDalamudPluginInterface _pi;
private readonly ObjectManager _objects;
public readonly EventSubscriber<nint, int> Redrawn;
private int _redrawIndex;
private string _lastRedrawnString = "None";
public RedrawingIpcTester(IDalamudPluginInterface pi, ObjectManager objects)
{
_pi = pi;
_objects = objects;
Redrawn = GameObjectRedrawn.Subscriber(_pi, SetLastRedrawn);
Redrawn.Disable();
}
public void Dispose()
{
Redrawn.Dispose();
}
public void Draw()
{
using var _ = ImRaii.TreeNode("Redrawing");
if (!_)
return;
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
IpcTester.DrawIntro(RedrawObject.Label, "Redraw by Index");
var tmp = _redrawIndex;
ImGui.SetNextItemWidth(100 * UiHelpers.Scale);
if (ImGui.DragInt("##redrawIndex", ref tmp, 0.1f, 0, _objects.TotalCount))
_redrawIndex = Math.Clamp(tmp, 0, _objects.TotalCount);
ImGui.SameLine();
if (ImGui.Button("Redraw##Index"))
new RedrawObject(_pi).Invoke(_redrawIndex);
IpcTester.DrawIntro(RedrawAll.Label, "Redraw All");
if (ImGui.Button("Redraw##All"))
new RedrawAll(_pi).Invoke();
IpcTester.DrawIntro(GameObjectRedrawn.Label, "Last Redrawn Object:");
ImGui.TextUnformatted(_lastRedrawnString);
}
private void SetLastRedrawn(nint address, int index)
{
if (index < 0
|| index > _objects.TotalCount
|| address == nint.Zero
|| _objects[index].Address != address)
_lastRedrawnString = "Invalid";
_lastRedrawnString = $"{_objects[index].Utf8Name} (0x{address:X}, {index})";
}
}

View file

@ -0,0 +1,114 @@
using Dalamud.Plugin;
using ImGuiNET;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.IpcSubscribers;
using Penumbra.String.Classes;
namespace Penumbra.Api.IpcTester;
public class ResolveIpcTester(IDalamudPluginInterface pi) : IUiService
{
private string _currentResolvePath = string.Empty;
private string _currentReversePath = string.Empty;
private int _currentReverseIdx;
private Task<(string[], string[][])> _task = Task.FromResult<(string[], string[][])>(([], []));
public void Draw()
{
using var tree = ImRaii.TreeNode("Resolving");
if (!tree)
return;
ImGui.InputTextWithHint("##resolvePath", "Resolve this game path...", ref _currentResolvePath, Utf8GamePath.MaxGamePathLength);
ImGui.InputTextWithHint("##resolveInversePath", "Reverse-resolve this path...", ref _currentReversePath,
Utf8GamePath.MaxGamePathLength);
ImGui.InputInt("##resolveIdx", ref _currentReverseIdx, 0, 0);
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
IpcTester.DrawIntro(ResolveDefaultPath.Label, "Default Collection Resolve");
if (_currentResolvePath.Length != 0)
ImGui.TextUnformatted(new ResolveDefaultPath(pi).Invoke(_currentResolvePath));
IpcTester.DrawIntro(ResolveInterfacePath.Label, "Interface Collection Resolve");
if (_currentResolvePath.Length != 0)
ImGui.TextUnformatted(new ResolveInterfacePath(pi).Invoke(_currentResolvePath));
IpcTester.DrawIntro(ResolvePlayerPath.Label, "Player Collection Resolve");
if (_currentResolvePath.Length != 0)
ImGui.TextUnformatted(new ResolvePlayerPath(pi).Invoke(_currentResolvePath));
IpcTester.DrawIntro(ResolveGameObjectPath.Label, "Game Object Collection Resolve");
if (_currentResolvePath.Length != 0)
ImGui.TextUnformatted(new ResolveGameObjectPath(pi).Invoke(_currentResolvePath, _currentReverseIdx));
IpcTester.DrawIntro(ReverseResolvePlayerPath.Label, "Reversed Game Paths (Player)");
if (_currentReversePath.Length > 0)
{
var list = new ReverseResolvePlayerPath(pi).Invoke(_currentReversePath);
if (list.Length > 0)
{
ImGui.TextUnformatted(list[0]);
if (list.Length > 1 && ImGui.IsItemHovered())
ImGui.SetTooltip(string.Join("\n", list.Skip(1)));
}
}
IpcTester.DrawIntro(ReverseResolveGameObjectPath.Label, "Reversed Game Paths (Game Object)");
if (_currentReversePath.Length > 0)
{
var list = new ReverseResolveGameObjectPath(pi).Invoke(_currentReversePath, _currentReverseIdx);
if (list.Length > 0)
{
ImGui.TextUnformatted(list[0]);
if (list.Length > 1 && ImGui.IsItemHovered())
ImGui.SetTooltip(string.Join("\n", list.Skip(1)));
}
}
var forwardArray = _currentResolvePath.Length > 0
? [_currentResolvePath]
: Array.Empty<string>();
var reverseArray = _currentReversePath.Length > 0
? [_currentReversePath]
: Array.Empty<string>();
IpcTester.DrawIntro(ResolvePlayerPaths.Label, "Resolved Paths (Player)");
if (forwardArray.Length > 0 || reverseArray.Length > 0)
{
var ret = new ResolvePlayerPaths(pi).Invoke(forwardArray, reverseArray);
ImGui.TextUnformatted(ConvertText(ret));
}
IpcTester.DrawIntro(ResolvePlayerPathsAsync.Label, "Resolved Paths Async (Player)");
if (ImGui.Button("Start"))
_task = new ResolvePlayerPathsAsync(pi).Invoke(forwardArray, reverseArray);
var hovered = ImGui.IsItemHovered();
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(_task.Status.ToString());
if ((hovered || ImGui.IsItemHovered()) && _task.IsCompletedSuccessfully)
ImGui.SetTooltip(ConvertText(_task.Result));
return;
static string ConvertText((string[], string[][]) data)
{
var text = string.Empty;
if (data.Item1.Length > 0)
{
if (data.Item2.Length > 0)
text = $"Forward: {data.Item1[0]} | Reverse: {string.Join("; ", data.Item2[0])}.";
else
text = $"Forward: {data.Item1[0]}.";
}
else if (data.Item2.Length > 0)
{
text = $"Reverse: {string.Join("; ", data.Item2[0])}.";
}
return text;
}
}
}

View file

@ -0,0 +1,349 @@
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Dalamud.Plugin;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.Api.IpcSubscribers;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs;
namespace Penumbra.Api.IpcTester;
public class ResourceTreeIpcTester(IDalamudPluginInterface pi, ObjectManager objects) : IUiService
{
private readonly Stopwatch _stopwatch = new();
private string _gameObjectIndices = "0";
private ResourceType _type = ResourceType.Mtrl;
private bool _withUiData;
private (string, Dictionary<string, HashSet<string>>?)[]? _lastGameObjectResourcePaths;
private (string, Dictionary<string, HashSet<string>>?)[]? _lastPlayerResourcePaths;
private (string, IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>?)[]? _lastGameObjectResourcesOfType;
private (string, IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>?)[]? _lastPlayerResourcesOfType;
private (string, ResourceTreeDto?)[]? _lastGameObjectResourceTrees;
private (string, ResourceTreeDto)[]? _lastPlayerResourceTrees;
private TimeSpan _lastCallDuration;
public void Draw()
{
using var _ = ImRaii.TreeNode("Resource Tree");
if (!_)
return;
ImGui.InputText("GameObject indices", ref _gameObjectIndices, 511);
ImGuiUtil.GenericEnumCombo("Resource type", ImGui.CalcItemWidth(), _type, out _type, Enum.GetValues<ResourceType>());
ImGui.Checkbox("Also get names and icons", ref _withUiData);
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
IpcTester.DrawIntro(GetGameObjectResourcePaths.Label, "Get GameObject resource paths");
if (ImGui.Button("Get##GameObjectResourcePaths"))
{
var gameObjects = GetSelectedGameObjects();
var subscriber = new GetGameObjectResourcePaths(pi);
_stopwatch.Restart();
var resourcePaths = subscriber.Invoke(gameObjects);
_lastCallDuration = _stopwatch.Elapsed;
_lastGameObjectResourcePaths = gameObjects
.Select(i => GameObjectToString(i))
.Zip(resourcePaths)
.ToArray();
ImGui.OpenPopup(nameof(GetGameObjectResourcePaths));
}
IpcTester.DrawIntro(GetPlayerResourcePaths.Label, "Get local player resource paths");
if (ImGui.Button("Get##PlayerResourcePaths"))
{
var subscriber = new GetPlayerResourcePaths(pi);
_stopwatch.Restart();
var resourcePaths = subscriber.Invoke();
_lastCallDuration = _stopwatch.Elapsed;
_lastPlayerResourcePaths = resourcePaths
.Select(pair => (GameObjectToString(pair.Key), pair.Value))
.ToArray()!;
ImGui.OpenPopup(nameof(GetPlayerResourcePaths));
}
IpcTester.DrawIntro(GetGameObjectResourcesOfType.Label, "Get GameObject resources of type");
if (ImGui.Button("Get##GameObjectResourcesOfType"))
{
var gameObjects = GetSelectedGameObjects();
var subscriber = new GetGameObjectResourcesOfType(pi);
_stopwatch.Restart();
var resourcesOfType = subscriber.Invoke(_type, _withUiData, gameObjects);
_lastCallDuration = _stopwatch.Elapsed;
_lastGameObjectResourcesOfType = gameObjects
.Select(i => GameObjectToString(i))
.Zip(resourcesOfType)
.ToArray();
ImGui.OpenPopup(nameof(GetGameObjectResourcesOfType));
}
IpcTester.DrawIntro(GetPlayerResourcesOfType.Label, "Get local player resources of type");
if (ImGui.Button("Get##PlayerResourcesOfType"))
{
var subscriber = new GetPlayerResourcesOfType(pi);
_stopwatch.Restart();
var resourcesOfType = subscriber.Invoke(_type, _withUiData);
_lastCallDuration = _stopwatch.Elapsed;
_lastPlayerResourcesOfType = resourcesOfType
.Select(pair => (GameObjectToString(pair.Key), (IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>?)pair.Value))
.ToArray();
ImGui.OpenPopup(nameof(GetPlayerResourcesOfType));
}
IpcTester.DrawIntro(GetGameObjectResourceTrees.Label, "Get GameObject resource trees");
if (ImGui.Button("Get##GameObjectResourceTrees"))
{
var gameObjects = GetSelectedGameObjects();
var subscriber = new GetGameObjectResourceTrees(pi);
_stopwatch.Restart();
var trees = subscriber.Invoke(_withUiData, gameObjects);
_lastCallDuration = _stopwatch.Elapsed;
_lastGameObjectResourceTrees = gameObjects
.Select(i => GameObjectToString(i))
.Zip(trees)
.ToArray();
ImGui.OpenPopup(nameof(GetGameObjectResourceTrees));
}
IpcTester.DrawIntro(GetPlayerResourceTrees.Label, "Get local player resource trees");
if (ImGui.Button("Get##PlayerResourceTrees"))
{
var subscriber = new GetPlayerResourceTrees(pi);
_stopwatch.Restart();
var trees = subscriber.Invoke(_withUiData);
_lastCallDuration = _stopwatch.Elapsed;
_lastPlayerResourceTrees = trees
.Select(pair => (GameObjectToString(pair.Key), pair.Value))
.ToArray();
ImGui.OpenPopup(nameof(GetPlayerResourceTrees));
}
DrawPopup(nameof(GetGameObjectResourcePaths), ref _lastGameObjectResourcePaths, DrawResourcePaths,
_lastCallDuration);
DrawPopup(nameof(GetPlayerResourcePaths), ref _lastPlayerResourcePaths!, DrawResourcePaths, _lastCallDuration);
DrawPopup(nameof(GetGameObjectResourcesOfType), ref _lastGameObjectResourcesOfType, DrawResourcesOfType,
_lastCallDuration);
DrawPopup(nameof(GetPlayerResourcesOfType), ref _lastPlayerResourcesOfType, DrawResourcesOfType,
_lastCallDuration);
DrawPopup(nameof(GetGameObjectResourceTrees), ref _lastGameObjectResourceTrees, DrawResourceTrees,
_lastCallDuration);
DrawPopup(nameof(GetPlayerResourceTrees), ref _lastPlayerResourceTrees, DrawResourceTrees!, _lastCallDuration);
}
private static void DrawPopup<T>(string popupId, ref T? result, Action<T> drawResult, TimeSpan duration) where T : class
{
ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(1000, 500));
using var popup = ImRaii.Popup(popupId);
if (!popup)
{
result = null;
return;
}
if (result == null)
{
ImGui.CloseCurrentPopup();
return;
}
drawResult(result);
ImGui.TextUnformatted($"Invoked in {duration.TotalMilliseconds} ms");
if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
{
result = null;
ImGui.CloseCurrentPopup();
}
}
private static void DrawWithHeaders<T>((string, T?)[] result, Action<T> drawItem) where T : class
{
var firstSeen = new Dictionary<T, string>();
foreach (var (label, item) in result)
{
if (item == null)
{
ImRaii.TreeNode($"{label}: null", ImGuiTreeNodeFlags.Leaf).Dispose();
continue;
}
if (firstSeen.TryGetValue(item, out var firstLabel))
{
ImRaii.TreeNode($"{label}: same as {firstLabel}", ImGuiTreeNodeFlags.Leaf).Dispose();
continue;
}
firstSeen.Add(item, label);
using var header = ImRaii.TreeNode(label);
if (!header)
continue;
drawItem(item);
}
}
private static void DrawResourcePaths((string, Dictionary<string, HashSet<string>>?)[] result)
{
DrawWithHeaders(result, paths =>
{
using var table = ImRaii.Table(string.Empty, 2, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.6f);
ImGui.TableSetupColumn("Game Paths", ImGuiTableColumnFlags.WidthStretch, 0.4f);
ImGui.TableHeadersRow();
foreach (var (actualPath, gamePaths) in paths)
{
ImGui.TableNextColumn();
ImGui.TextUnformatted(actualPath);
ImGui.TableNextColumn();
foreach (var gamePath in gamePaths)
ImGui.TextUnformatted(gamePath);
}
});
}
private void DrawResourcesOfType((string, IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>?)[] result)
{
DrawWithHeaders(result, resources =>
{
using var table = ImRaii.Table(string.Empty, _withUiData ? 3 : 2, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
ImGui.TableSetupColumn("Resource Handle", ImGuiTableColumnFlags.WidthStretch, 0.15f);
ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, _withUiData ? 0.55f : 0.85f);
if (_withUiData)
ImGui.TableSetupColumn("Icon & Name", ImGuiTableColumnFlags.WidthStretch, 0.3f);
ImGui.TableHeadersRow();
foreach (var (resourceHandle, (actualPath, name, icon)) in resources)
{
ImGui.TableNextColumn();
TextUnformattedMono($"0x{resourceHandle:X}");
ImGui.TableNextColumn();
ImGui.TextUnformatted(actualPath);
if (_withUiData)
{
ImGui.TableNextColumn();
TextUnformattedMono(icon.ToString());
ImGui.SameLine();
ImGui.TextUnformatted(name);
}
}
});
}
private void DrawResourceTrees((string, ResourceTreeDto?)[] result)
{
DrawWithHeaders(result, tree =>
{
ImGui.TextUnformatted($"Name: {tree.Name}\nRaceCode: {(GenderRace)tree.RaceCode}");
using var table = ImRaii.Table(string.Empty, _withUiData ? 7 : 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.Resizable);
if (!table)
return;
if (_withUiData)
{
ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch, 0.5f);
ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthStretch, 0.1f);
ImGui.TableSetupColumn("Icon", ImGuiTableColumnFlags.WidthStretch, 0.15f);
}
else
{
ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthStretch, 0.5f);
}
ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthStretch, 0.5f);
ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f);
ImGui.TableSetupColumn("Object Address", ImGuiTableColumnFlags.WidthStretch, 0.2f);
ImGui.TableSetupColumn("Resource Handle", ImGuiTableColumnFlags.WidthStretch, 0.2f);
ImGui.TableHeadersRow();
void DrawNode(ResourceNodeDto node)
{
ImGui.TableNextRow();
ImGui.TableNextColumn();
var hasChildren = node.Children.Any();
using var treeNode = ImRaii.TreeNode(
$"{(_withUiData ? node.Name ?? "Unknown" : node.Type)}##{node.ObjectAddress:X8}",
hasChildren
? ImGuiTreeNodeFlags.SpanFullWidth
: ImGuiTreeNodeFlags.SpanFullWidth | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen);
if (_withUiData)
{
ImGui.TableNextColumn();
TextUnformattedMono(node.Type.ToString());
ImGui.TableNextColumn();
TextUnformattedMono(node.Icon.ToString());
}
ImGui.TableNextColumn();
ImGui.TextUnformatted(node.GamePath ?? "Unknown");
ImGui.TableNextColumn();
ImGui.TextUnformatted(node.ActualPath);
ImGui.TableNextColumn();
TextUnformattedMono($"0x{node.ObjectAddress:X8}");
ImGui.TableNextColumn();
TextUnformattedMono($"0x{node.ResourceHandle:X8}");
if (treeNode)
foreach (var child in node.Children)
DrawNode(child);
}
foreach (var node in tree.Nodes)
DrawNode(node);
});
}
private static void TextUnformattedMono(string text)
{
using var _ = ImRaii.PushFont(UiBuilder.MonoFont);
ImGui.TextUnformatted(text);
}
private ushort[] GetSelectedGameObjects()
=> _gameObjectIndices.Split(',')
.SelectWhere(index => (ushort.TryParse(index.Trim(), out var i), i))
.ToArray();
private unsafe string GameObjectToString(ObjectIndex gameObjectIndex)
{
var gameObject = objects[gameObjectIndex];
return gameObject.Valid
? $"[{gameObjectIndex}] {gameObject.Utf8Name} ({(ObjectKind)gameObject.AsObject->ObjectKind})"
: $"[{gameObjectIndex}] null";
}
}

View file

@ -0,0 +1,204 @@
using Dalamud.Interface;
using Dalamud.Plugin;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Services;
using OtterGui.Text;
using Penumbra.Api.Api;
using Penumbra.Api.Enums;
using Penumbra.Api.IpcSubscribers;
using Penumbra.Collections.Manager;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Services;
namespace Penumbra.Api.IpcTester;
public class TemporaryIpcTester(
IDalamudPluginInterface pi,
ModManager modManager,
CollectionManager collections,
TempModManager tempMods,
TempCollectionManager tempCollections,
SaveService saveService,
Configuration config)
: IUiService
{
public Guid LastCreatedCollectionId = Guid.Empty;
private Guid? _tempGuid;
private string _tempCollectionName = string.Empty;
private string _tempCollectionGuidName = string.Empty;
private string _tempModName = string.Empty;
private string _tempGamePath = "test/game/path.mtrl";
private string _tempFilePath = "test/success.mtrl";
private string _tempManipulation = string.Empty;
private PenumbraApiEc _lastTempError;
private int _tempActorIndex;
private bool _forceOverwrite;
public void Draw()
{
using var _ = ImRaii.TreeNode("Temporary");
if (!_)
return;
ImGui.InputTextWithHint("##tempCollection", "Collection Name...", ref _tempCollectionName, 128);
ImGuiUtil.GuidInput("##guid", "Collection GUID...", string.Empty, ref _tempGuid, ref _tempCollectionGuidName);
ImGui.InputInt("##tempActorIndex", ref _tempActorIndex, 0, 0);
ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32);
ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256);
ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256);
ImUtf8.InputText("##tempManip"u8, ref _tempManipulation, "Manipulation Base64 String..."u8);
ImGui.Checkbox("Force Character Collection Overwrite", ref _forceOverwrite);
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
IpcTester.DrawIntro("Last Error", _lastTempError.ToString());
ImGuiUtil.DrawTableColumn("Last Created Collection");
ImGui.TableNextColumn();
using (ImRaii.PushFont(UiBuilder.MonoFont))
{
ImGuiUtil.CopyOnClickSelectable(LastCreatedCollectionId.ToString());
}
IpcTester.DrawIntro(CreateTemporaryCollection.Label, "Create Temporary Collection");
if (ImGui.Button("Create##Collection"))
{
LastCreatedCollectionId = new CreateTemporaryCollection(pi).Invoke(_tempCollectionName);
if (_tempGuid == null)
{
_tempGuid = LastCreatedCollectionId;
_tempCollectionGuidName = LastCreatedCollectionId.ToString();
}
}
var guid = _tempGuid.GetValueOrDefault(Guid.Empty);
IpcTester.DrawIntro(DeleteTemporaryCollection.Label, "Delete Temporary Collection");
if (ImGui.Button("Delete##Collection"))
_lastTempError = new DeleteTemporaryCollection(pi).Invoke(guid);
ImGui.SameLine();
if (ImGui.Button("Delete Last##Collection"))
_lastTempError = new DeleteTemporaryCollection(pi).Invoke(LastCreatedCollectionId);
IpcTester.DrawIntro(AssignTemporaryCollection.Label, "Assign Temporary Collection");
if (ImGui.Button("Assign##NamedCollection"))
_lastTempError = new AssignTemporaryCollection(pi).Invoke(guid, _tempActorIndex, _forceOverwrite);
IpcTester.DrawIntro(AddTemporaryMod.Label, "Add Temporary Mod to specific Collection");
if (ImGui.Button("Add##Mod"))
_lastTempError = new AddTemporaryMod(pi).Invoke(_tempModName, guid,
new Dictionary<string, string> { { _tempGamePath, _tempFilePath } },
_tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue);
IpcTester.DrawIntro(CreateTemporaryCollection.Label, "Copy Existing Collection");
if (ImGuiUtil.DrawDisabledButton("Copy##Collection", Vector2.Zero,
"Copies the effective list from the collection named in Temporary Mod Name...",
!collections.Storage.ByName(_tempModName, out var copyCollection))
&& copyCollection is { HasCache: true })
{
var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString());
var manips = MetaApi.CompressMetaManipulations(copyCollection);
_lastTempError = new AddTemporaryMod(pi).Invoke(_tempModName, guid, files, manips, 999);
}
IpcTester.DrawIntro(AddTemporaryModAll.Label, "Add Temporary Mod to all Collections");
if (ImGui.Button("Add##All"))
_lastTempError = new AddTemporaryModAll(pi).Invoke(_tempModName,
new Dictionary<string, string> { { _tempGamePath, _tempFilePath } },
_tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue);
IpcTester.DrawIntro(RemoveTemporaryMod.Label, "Remove Temporary Mod from specific Collection");
if (ImGui.Button("Remove##Mod"))
_lastTempError = new RemoveTemporaryMod(pi).Invoke(_tempModName, guid, int.MaxValue);
IpcTester.DrawIntro(RemoveTemporaryModAll.Label, "Remove Temporary Mod from all Collections");
if (ImGui.Button("Remove##ModAll"))
_lastTempError = new RemoveTemporaryModAll(pi).Invoke(_tempModName, int.MaxValue);
}
public void DrawCollections()
{
using var collTree = ImRaii.TreeNode("Temporary Collections##TempCollections");
if (!collTree)
return;
using var table = ImRaii.Table("##collTree", 6, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
foreach (var (collection, idx) in tempCollections.Values.WithIndex())
{
using var id = ImRaii.PushId(idx);
ImGui.TableNextColumn();
var character = tempCollections.Collections.Where(p => p.Collection == collection).Select(p => p.DisplayName)
.FirstOrDefault()
?? "Unknown";
if (ImGui.Button("Save##Collection"))
TemporaryMod.SaveTempCollection(config, saveService, modManager, collection, character);
using (ImRaii.PushFont(UiBuilder.MonoFont))
{
ImGui.TableNextColumn();
ImGuiUtil.CopyOnClickSelectable(collection.Identifier);
}
ImGuiUtil.DrawTableColumn(collection.Name);
ImGuiUtil.DrawTableColumn(collection.ResolvedFiles.Count.ToString());
ImGuiUtil.DrawTableColumn(collection.MetaCache?.Count.ToString() ?? "0");
ImGuiUtil.DrawTableColumn(string.Join(", ",
tempCollections.Collections.Where(p => p.Collection == collection).Select(c => c.DisplayName)));
}
}
public void DrawMods()
{
using var modTree = ImRaii.TreeNode("Temporary Mods##TempMods");
if (!modTree)
return;
using var table = ImRaii.Table("##modTree", 5, ImGuiTableFlags.SizingFixedFit);
void PrintList(string collectionName, IReadOnlyList<TemporaryMod> list)
{
foreach (var mod in list)
{
ImGui.TableNextColumn();
ImGui.TextUnformatted(mod.Name);
ImGui.TableNextColumn();
ImGui.TextUnformatted(mod.Priority.ToString());
ImGui.TableNextColumn();
ImGui.TextUnformatted(collectionName);
ImGui.TableNextColumn();
ImGui.TextUnformatted(mod.Default.Files.Count.ToString());
if (ImGui.IsItemHovered())
{
using var tt = ImRaii.Tooltip();
foreach (var (path, file) in mod.Default.Files)
ImGui.TextUnformatted($"{path} -> {file}");
}
ImGui.TableNextColumn();
ImGui.TextUnformatted(mod.TotalManipulations.ToString());
if (ImGui.IsItemHovered())
{
using var tt = ImRaii.Tooltip();
foreach (var identifier in mod.Default.Manipulations.Identifiers)
ImGui.TextUnformatted(identifier.ToString());
}
}
}
if (table)
{
PrintList("All", tempMods.ModsForAllCollections);
foreach (var (collection, list) in tempMods.Mods)
PrintList(collection.Name, list);
}
}
}

View file

@ -0,0 +1,133 @@
using Dalamud.Plugin;
using ImGuiNET;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.Api.IpcSubscribers;
namespace Penumbra.Api.IpcTester;
public class UiIpcTester : IUiService, IDisposable
{
private readonly IDalamudPluginInterface _pi;
public readonly EventSubscriber<string, float, float> PreSettingsTabBar;
public readonly EventSubscriber<string> PreSettingsPanel;
public readonly EventSubscriber<string> PostEnabled;
public readonly EventSubscriber<string> PostSettingsPanelDraw;
public readonly EventSubscriber<ChangedItemType, uint> ChangedItemTooltip;
public readonly EventSubscriber<MouseButton, ChangedItemType, uint> ChangedItemClicked;
private string _lastDrawnMod = string.Empty;
private DateTimeOffset _lastDrawnModTime = DateTimeOffset.MinValue;
private bool _subscribedToTooltip;
private bool _subscribedToClick;
private string _lastClicked = string.Empty;
private string _lastHovered = string.Empty;
private TabType _selectTab = TabType.None;
private string _modName = string.Empty;
private PenumbraApiEc _ec = PenumbraApiEc.Success;
public UiIpcTester(IDalamudPluginInterface pi)
{
_pi = pi;
PreSettingsTabBar = IpcSubscribers.PreSettingsTabBarDraw.Subscriber(pi, UpdateLastDrawnMod);
PreSettingsPanel = IpcSubscribers.PreSettingsDraw.Subscriber(pi, UpdateLastDrawnMod);
PostEnabled = IpcSubscribers.PostEnabledDraw.Subscriber(pi, UpdateLastDrawnMod);
PostSettingsPanelDraw = IpcSubscribers.PostSettingsDraw.Subscriber(pi, UpdateLastDrawnMod);
ChangedItemTooltip = IpcSubscribers.ChangedItemTooltip.Subscriber(pi, AddedTooltip);
ChangedItemClicked = IpcSubscribers.ChangedItemClicked.Subscriber(pi, AddedClick);
PreSettingsTabBar.Disable();
PreSettingsPanel.Disable();
PostEnabled.Disable();
PostSettingsPanelDraw.Disable();
ChangedItemTooltip.Disable();
ChangedItemClicked.Disable();
}
public void Dispose()
{
PreSettingsTabBar.Dispose();
PreSettingsPanel.Dispose();
PostEnabled.Dispose();
PostSettingsPanelDraw.Dispose();
ChangedItemTooltip.Dispose();
ChangedItemClicked.Dispose();
}
public void Draw()
{
using var _ = ImRaii.TreeNode("UI");
if (!_)
return;
using (var combo = ImRaii.Combo("Tab to Open at", _selectTab.ToString()))
{
if (combo)
foreach (var val in Enum.GetValues<TabType>())
{
if (ImGui.Selectable(val.ToString(), _selectTab == val))
_selectTab = val;
}
}
ImGui.InputTextWithHint("##openMod", "Mod to Open at...", ref _modName, 256);
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
IpcTester.DrawIntro(IpcSubscribers.PostSettingsDraw.Label, "Last Drawn Mod");
ImGui.TextUnformatted(_lastDrawnMod.Length > 0 ? $"{_lastDrawnMod} at {_lastDrawnModTime}" : "None");
IpcTester.DrawIntro(IpcSubscribers.ChangedItemTooltip.Label, "Add Tooltip");
if (ImGui.Checkbox("##tooltip", ref _subscribedToTooltip))
{
if (_subscribedToTooltip)
ChangedItemTooltip.Enable();
else
ChangedItemTooltip.Disable();
}
ImGui.SameLine();
ImGui.TextUnformatted(_lastHovered);
IpcTester.DrawIntro(IpcSubscribers.ChangedItemClicked.Label, "Subscribe Click");
if (ImGui.Checkbox("##click", ref _subscribedToClick))
{
if (_subscribedToClick)
ChangedItemClicked.Enable();
else
ChangedItemClicked.Disable();
}
ImGui.SameLine();
ImGui.TextUnformatted(_lastClicked);
IpcTester.DrawIntro(OpenMainWindow.Label, "Open Mod Window");
if (ImGui.Button("Open##window"))
_ec = new OpenMainWindow(_pi).Invoke(_selectTab, _modName, _modName);
ImGui.SameLine();
ImGui.TextUnformatted(_ec.ToString());
IpcTester.DrawIntro(CloseMainWindow.Label, "Close Mod Window");
if (ImGui.Button("Close##window"))
new CloseMainWindow(_pi).Invoke();
}
private void UpdateLastDrawnMod(string name)
=> (_lastDrawnMod, _lastDrawnModTime) = (name, DateTimeOffset.Now);
private void UpdateLastDrawnMod(string name, float _1, float _2)
=> (_lastDrawnMod, _lastDrawnModTime) = (name, DateTimeOffset.Now);
private void AddedTooltip(ChangedItemType type, uint id)
{
_lastHovered = $"{type} {id} at {DateTime.UtcNow.ToLocalTime().ToString(CultureInfo.CurrentCulture)}";
ImGui.TextUnformatted("IPC Test Successful");
}
private void AddedClick(MouseButton button, ChangedItemType type, uint id)
{
_lastClicked = $"{button}-click on {type} {id} at {DateTime.UtcNow.ToLocalTime().ToString(CultureInfo.CurrentCulture)}";
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,427 +0,0 @@
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin;
using Penumbra.GameData.Enums;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.Collections.Manager;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.Util;
namespace Penumbra.Api;
using CurrentSettings = ValueTuple<PenumbraApiEc, (bool, int, IDictionary<string, IList<string>>, bool)?>;
public class PenumbraIpcProviders : IDisposable
{
internal readonly IPenumbraApi Api;
// Plugin State
internal readonly EventProvider Initialized;
internal readonly EventProvider Disposed;
internal readonly FuncProvider<int> ApiVersion;
internal readonly FuncProvider<(int Breaking, int Features)> ApiVersions;
internal readonly FuncProvider<bool> GetEnabledState;
internal readonly EventProvider<bool> EnabledChange;
// Configuration
internal readonly FuncProvider<string> GetModDirectory;
internal readonly FuncProvider<string> GetConfiguration;
internal readonly EventProvider<string, bool> ModDirectoryChanged;
// UI
internal readonly EventProvider<string> PreSettingsDraw;
internal readonly EventProvider<string> PostSettingsDraw;
internal readonly EventProvider<ChangedItemType, uint> ChangedItemTooltip;
internal readonly EventProvider<MouseButton, ChangedItemType, uint> ChangedItemClick;
internal readonly FuncProvider<TabType, string, string, PenumbraApiEc> OpenMainWindow;
internal readonly ActionProvider CloseMainWindow;
// Redrawing
internal readonly ActionProvider<RedrawType> RedrawAll;
internal readonly ActionProvider<GameObject, RedrawType> RedrawObject;
internal readonly ActionProvider<int, RedrawType> RedrawObjectByIndex;
internal readonly ActionProvider<string, RedrawType> RedrawObjectByName;
internal readonly EventProvider<nint, int> GameObjectRedrawn;
// Game State
internal readonly FuncProvider<nint, (nint, string)> GetDrawObjectInfo;
internal readonly FuncProvider<int, int> GetCutsceneParentIndex;
internal readonly FuncProvider<int, int, PenumbraApiEc> SetCutsceneParentIndex;
internal readonly EventProvider<nint, string, nint, nint, nint> CreatingCharacterBase;
internal readonly EventProvider<nint, string, nint> CreatedCharacterBase;
internal readonly EventProvider<nint, string, string> GameObjectResourcePathResolved;
// Resolve
internal readonly FuncProvider<string, string> ResolveDefaultPath;
internal readonly FuncProvider<string, string> ResolveInterfacePath;
internal readonly FuncProvider<string, string> ResolvePlayerPath;
internal readonly FuncProvider<string, int, string> ResolveGameObjectPath;
internal readonly FuncProvider<string, string, string> ResolveCharacterPath;
internal readonly FuncProvider<string, string, string[]> ReverseResolvePath;
internal readonly FuncProvider<string, int, string[]> ReverseResolveGameObjectPath;
internal readonly FuncProvider<string, string[]> ReverseResolvePlayerPath;
internal readonly FuncProvider<string[], string[], (string[], string[][])> ResolvePlayerPaths;
internal readonly FuncProvider<string[], string[], Task<(string[], string[][])>> ResolvePlayerPathsAsync;
// Collections
internal readonly FuncProvider<IList<string>> GetCollections;
internal readonly FuncProvider<string> GetCurrentCollectionName;
internal readonly FuncProvider<string> GetDefaultCollectionName;
internal readonly FuncProvider<string> GetInterfaceCollectionName;
internal readonly FuncProvider<string, (string, bool)> GetCharacterCollectionName;
internal readonly FuncProvider<ApiCollectionType, string> GetCollectionForType;
internal readonly FuncProvider<ApiCollectionType, string, bool, bool, (PenumbraApiEc, string)> SetCollectionForType;
internal readonly FuncProvider<int, (bool, bool, string)> GetCollectionForObject;
internal readonly FuncProvider<int, string, bool, bool, (PenumbraApiEc, string)> SetCollectionForObject;
internal readonly FuncProvider<string, IReadOnlyDictionary<string, object?>> GetChangedItems;
// Meta
internal readonly FuncProvider<string> GetPlayerMetaManipulations;
internal readonly FuncProvider<string, string> GetMetaManipulations;
internal readonly FuncProvider<int, string> GetGameObjectMetaManipulations;
// Mods
internal readonly FuncProvider<IList<(string, string)>> GetMods;
internal readonly FuncProvider<string, string, PenumbraApiEc> ReloadMod;
internal readonly FuncProvider<string, PenumbraApiEc> InstallMod;
internal readonly FuncProvider<string, PenumbraApiEc> AddMod;
internal readonly FuncProvider<string, string, PenumbraApiEc> DeleteMod;
internal readonly FuncProvider<string, string, (PenumbraApiEc, string, bool)> GetModPath;
internal readonly FuncProvider<string, string, string, PenumbraApiEc> SetModPath;
internal readonly EventProvider<string> ModDeleted;
internal readonly EventProvider<string> ModAdded;
internal readonly EventProvider<string, string> ModMoved;
// ModSettings
internal readonly FuncProvider<string, string, IDictionary<string, (IList<string>, GroupType)>?> GetAvailableModSettings;
internal readonly FuncProvider<string, string, string, bool, CurrentSettings> GetCurrentModSettings;
internal readonly FuncProvider<string, string, string, bool, PenumbraApiEc> TryInheritMod;
internal readonly FuncProvider<string, string, string, bool, PenumbraApiEc> TrySetMod;
internal readonly FuncProvider<string, string, string, int, PenumbraApiEc> TrySetModPriority;
internal readonly FuncProvider<string, string, string, string, string, PenumbraApiEc> TrySetModSetting;
internal readonly FuncProvider<string, string, string, string, IReadOnlyList<string>, PenumbraApiEc> TrySetModSettings;
internal readonly EventProvider<ModSettingChange, string, string, bool> ModSettingChanged;
internal readonly FuncProvider<string, string, string, PenumbraApiEc> CopyModSettings;
// Editing
internal readonly FuncProvider<string, string, TextureType, bool, Task> ConvertTextureFile;
internal readonly FuncProvider<byte[], int, string, TextureType, bool, Task> ConvertTextureData;
// Temporary
internal readonly FuncProvider<string, string, bool, (PenumbraApiEc, string)> CreateTemporaryCollection;
internal readonly FuncProvider<string, PenumbraApiEc> RemoveTemporaryCollection;
internal readonly FuncProvider<string, PenumbraApiEc> CreateNamedTemporaryCollection;
internal readonly FuncProvider<string, PenumbraApiEc> RemoveTemporaryCollectionByName;
internal readonly FuncProvider<string, int, bool, PenumbraApiEc> AssignTemporaryCollection;
internal readonly FuncProvider<string, Dictionary<string, string>, string, int, PenumbraApiEc> AddTemporaryModAll;
internal readonly FuncProvider<string, string, Dictionary<string, string>, string, int, PenumbraApiEc> AddTemporaryMod;
internal readonly FuncProvider<string, int, PenumbraApiEc> RemoveTemporaryModAll;
internal readonly FuncProvider<string, string, int, PenumbraApiEc> RemoveTemporaryMod;
// Resource Tree
internal readonly FuncProvider<ushort[], IReadOnlyDictionary<string, string[]>?[]> GetGameObjectResourcePaths;
internal readonly FuncProvider<IReadOnlyDictionary<ushort, IReadOnlyDictionary<string, string[]>>> GetPlayerResourcePaths;
internal readonly FuncProvider<ResourceType, bool, ushort[], IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>?[]>
GetGameObjectResourcesOfType;
internal readonly
FuncProvider<ResourceType, bool, IReadOnlyDictionary<ushort, IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>>>
GetPlayerResourcesOfType;
internal readonly FuncProvider<bool, ushort[], Ipc.ResourceTree?[]> GetGameObjectResourceTrees;
internal readonly FuncProvider<bool, IReadOnlyDictionary<ushort, Ipc.ResourceTree>> GetPlayerResourceTrees;
public PenumbraIpcProviders(DalamudPluginInterface pi, IPenumbraApi api, ModManager modManager, CollectionManager collections,
TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService, Configuration config)
{
Api = api;
// Plugin State
Initialized = Ipc.Initialized.Provider(pi);
Disposed = Ipc.Disposed.Provider(pi);
ApiVersion = Ipc.ApiVersion.Provider(pi, DeprecatedVersion);
ApiVersions = Ipc.ApiVersions.Provider(pi, () => Api.ApiVersion);
GetEnabledState = Ipc.GetEnabledState.Provider(pi, Api.GetEnabledState);
EnabledChange =
Ipc.EnabledChange.Provider(pi, () => Api.EnabledChange += EnabledChangeEvent, () => Api.EnabledChange -= EnabledChangeEvent);
// Configuration
GetModDirectory = Ipc.GetModDirectory.Provider(pi, Api.GetModDirectory);
GetConfiguration = Ipc.GetConfiguration.Provider(pi, Api.GetConfiguration);
ModDirectoryChanged = Ipc.ModDirectoryChanged.Provider(pi, a => Api.ModDirectoryChanged += a, a => Api.ModDirectoryChanged -= a);
// UI
PreSettingsDraw = Ipc.PreSettingsDraw.Provider(pi, a => Api.PreSettingsPanelDraw += a, a => Api.PreSettingsPanelDraw -= a);
PostSettingsDraw = Ipc.PostSettingsDraw.Provider(pi, a => Api.PostSettingsPanelDraw += a, a => Api.PostSettingsPanelDraw -= a);
ChangedItemTooltip =
Ipc.ChangedItemTooltip.Provider(pi, () => Api.ChangedItemTooltip += OnTooltip, () => Api.ChangedItemTooltip -= OnTooltip);
ChangedItemClick = Ipc.ChangedItemClick.Provider(pi, () => Api.ChangedItemClicked += OnClick, () => Api.ChangedItemClicked -= OnClick);
OpenMainWindow = Ipc.OpenMainWindow.Provider(pi, Api.OpenMainWindow);
CloseMainWindow = Ipc.CloseMainWindow.Provider(pi, Api.CloseMainWindow);
// Redrawing
RedrawAll = Ipc.RedrawAll.Provider(pi, Api.RedrawAll);
RedrawObject = Ipc.RedrawObject.Provider(pi, Api.RedrawObject);
RedrawObjectByIndex = Ipc.RedrawObjectByIndex.Provider(pi, Api.RedrawObject);
RedrawObjectByName = Ipc.RedrawObjectByName.Provider(pi, Api.RedrawObject);
GameObjectRedrawn = Ipc.GameObjectRedrawn.Provider(pi, () => Api.GameObjectRedrawn += OnGameObjectRedrawn,
() => Api.GameObjectRedrawn -= OnGameObjectRedrawn);
// Game State
GetDrawObjectInfo = Ipc.GetDrawObjectInfo.Provider(pi, Api.GetDrawObjectInfo);
GetCutsceneParentIndex = Ipc.GetCutsceneParentIndex.Provider(pi, Api.GetCutsceneParentIndex);
SetCutsceneParentIndex = Ipc.SetCutsceneParentIndex.Provider(pi, Api.SetCutsceneParentIndex);
CreatingCharacterBase = Ipc.CreatingCharacterBase.Provider(pi,
() => Api.CreatingCharacterBase += CreatingCharacterBaseEvent,
() => Api.CreatingCharacterBase -= CreatingCharacterBaseEvent);
CreatedCharacterBase = Ipc.CreatedCharacterBase.Provider(pi,
() => Api.CreatedCharacterBase += CreatedCharacterBaseEvent,
() => Api.CreatedCharacterBase -= CreatedCharacterBaseEvent);
GameObjectResourcePathResolved = Ipc.GameObjectResourcePathResolved.Provider(pi,
() => Api.GameObjectResourceResolved += GameObjectResourceResolvedEvent,
() => Api.GameObjectResourceResolved -= GameObjectResourceResolvedEvent);
// Resolve
ResolveDefaultPath = Ipc.ResolveDefaultPath.Provider(pi, Api.ResolveDefaultPath);
ResolveInterfacePath = Ipc.ResolveInterfacePath.Provider(pi, Api.ResolveInterfacePath);
ResolvePlayerPath = Ipc.ResolvePlayerPath.Provider(pi, Api.ResolvePlayerPath);
ResolveGameObjectPath = Ipc.ResolveGameObjectPath.Provider(pi, Api.ResolveGameObjectPath);
ResolveCharacterPath = Ipc.ResolveCharacterPath.Provider(pi, Api.ResolvePath);
ReverseResolvePath = Ipc.ReverseResolvePath.Provider(pi, Api.ReverseResolvePath);
ReverseResolveGameObjectPath = Ipc.ReverseResolveGameObjectPath.Provider(pi, Api.ReverseResolveGameObjectPath);
ReverseResolvePlayerPath = Ipc.ReverseResolvePlayerPath.Provider(pi, Api.ReverseResolvePlayerPath);
ResolvePlayerPaths = Ipc.ResolvePlayerPaths.Provider(pi, Api.ResolvePlayerPaths);
ResolvePlayerPathsAsync = Ipc.ResolvePlayerPathsAsync.Provider(pi, Api.ResolvePlayerPathsAsync);
// Collections
GetCollections = Ipc.GetCollections.Provider(pi, Api.GetCollections);
GetCurrentCollectionName = Ipc.GetCurrentCollectionName.Provider(pi, Api.GetCurrentCollection);
GetDefaultCollectionName = Ipc.GetDefaultCollectionName.Provider(pi, Api.GetDefaultCollection);
GetInterfaceCollectionName = Ipc.GetInterfaceCollectionName.Provider(pi, Api.GetInterfaceCollection);
GetCharacterCollectionName = Ipc.GetCharacterCollectionName.Provider(pi, Api.GetCharacterCollection);
GetCollectionForType = Ipc.GetCollectionForType.Provider(pi, Api.GetCollectionForType);
SetCollectionForType = Ipc.SetCollectionForType.Provider(pi, Api.SetCollectionForType);
GetCollectionForObject = Ipc.GetCollectionForObject.Provider(pi, Api.GetCollectionForObject);
SetCollectionForObject = Ipc.SetCollectionForObject.Provider(pi, Api.SetCollectionForObject);
GetChangedItems = Ipc.GetChangedItems.Provider(pi, Api.GetChangedItemsForCollection);
// Meta
GetPlayerMetaManipulations = Ipc.GetPlayerMetaManipulations.Provider(pi, Api.GetPlayerMetaManipulations);
GetMetaManipulations = Ipc.GetMetaManipulations.Provider(pi, Api.GetMetaManipulations);
GetGameObjectMetaManipulations = Ipc.GetGameObjectMetaManipulations.Provider(pi, Api.GetGameObjectMetaManipulations);
// Mods
GetMods = Ipc.GetMods.Provider(pi, Api.GetModList);
ReloadMod = Ipc.ReloadMod.Provider(pi, Api.ReloadMod);
InstallMod = Ipc.InstallMod.Provider(pi, Api.InstallMod);
AddMod = Ipc.AddMod.Provider(pi, Api.AddMod);
DeleteMod = Ipc.DeleteMod.Provider(pi, Api.DeleteMod);
GetModPath = Ipc.GetModPath.Provider(pi, Api.GetModPath);
SetModPath = Ipc.SetModPath.Provider(pi, Api.SetModPath);
ModDeleted = Ipc.ModDeleted.Provider(pi, () => Api.ModDeleted += ModDeletedEvent, () => Api.ModDeleted -= ModDeletedEvent);
ModAdded = Ipc.ModAdded.Provider(pi, () => Api.ModAdded += ModAddedEvent, () => Api.ModAdded -= ModAddedEvent);
ModMoved = Ipc.ModMoved.Provider(pi, () => Api.ModMoved += ModMovedEvent, () => Api.ModMoved -= ModMovedEvent);
// ModSettings
GetAvailableModSettings = Ipc.GetAvailableModSettings.Provider(pi, Api.GetAvailableModSettings);
GetCurrentModSettings = Ipc.GetCurrentModSettings.Provider(pi, Api.GetCurrentModSettings);
TryInheritMod = Ipc.TryInheritMod.Provider(pi, Api.TryInheritMod);
TrySetMod = Ipc.TrySetMod.Provider(pi, Api.TrySetMod);
TrySetModPriority = Ipc.TrySetModPriority.Provider(pi, Api.TrySetModPriority);
TrySetModSetting = Ipc.TrySetModSetting.Provider(pi, Api.TrySetModSetting);
TrySetModSettings = Ipc.TrySetModSettings.Provider(pi, Api.TrySetModSettings);
ModSettingChanged = Ipc.ModSettingChanged.Provider(pi,
() => Api.ModSettingChanged += ModSettingChangedEvent,
() => Api.ModSettingChanged -= ModSettingChangedEvent);
CopyModSettings = Ipc.CopyModSettings.Provider(pi, Api.CopyModSettings);
// Editing
ConvertTextureFile = Ipc.ConvertTextureFile.Provider(pi, Api.ConvertTextureFile);
ConvertTextureData = Ipc.ConvertTextureData.Provider(pi, Api.ConvertTextureData);
// Temporary
CreateTemporaryCollection = Ipc.CreateTemporaryCollection.Provider(pi, Api.CreateTemporaryCollection);
RemoveTemporaryCollection = Ipc.RemoveTemporaryCollection.Provider(pi, Api.RemoveTemporaryCollection);
CreateNamedTemporaryCollection = Ipc.CreateNamedTemporaryCollection.Provider(pi, Api.CreateNamedTemporaryCollection);
RemoveTemporaryCollectionByName = Ipc.RemoveTemporaryCollectionByName.Provider(pi, Api.RemoveTemporaryCollectionByName);
AssignTemporaryCollection = Ipc.AssignTemporaryCollection.Provider(pi, Api.AssignTemporaryCollection);
AddTemporaryModAll = Ipc.AddTemporaryModAll.Provider(pi, Api.AddTemporaryModAll);
AddTemporaryMod = Ipc.AddTemporaryMod.Provider(pi, Api.AddTemporaryMod);
RemoveTemporaryModAll = Ipc.RemoveTemporaryModAll.Provider(pi, Api.RemoveTemporaryModAll);
RemoveTemporaryMod = Ipc.RemoveTemporaryMod.Provider(pi, Api.RemoveTemporaryMod);
// ResourceTree
GetGameObjectResourcePaths = Ipc.GetGameObjectResourcePaths.Provider(pi, Api.GetGameObjectResourcePaths);
GetPlayerResourcePaths = Ipc.GetPlayerResourcePaths.Provider(pi, Api.GetPlayerResourcePaths);
GetGameObjectResourcesOfType = Ipc.GetGameObjectResourcesOfType.Provider(pi, Api.GetGameObjectResourcesOfType);
GetPlayerResourcesOfType = Ipc.GetPlayerResourcesOfType.Provider(pi, Api.GetPlayerResourcesOfType);
GetGameObjectResourceTrees = Ipc.GetGameObjectResourceTrees.Provider(pi, Api.GetGameObjectResourceTrees);
GetPlayerResourceTrees = Ipc.GetPlayerResourceTrees.Provider(pi, Api.GetPlayerResourceTrees);
Initialized.Invoke();
}
public void Dispose()
{
// Plugin State
Initialized.Dispose();
ApiVersion.Dispose();
ApiVersions.Dispose();
GetEnabledState.Dispose();
EnabledChange.Dispose();
// Configuration
GetModDirectory.Dispose();
GetConfiguration.Dispose();
ModDirectoryChanged.Dispose();
// UI
PreSettingsDraw.Dispose();
PostSettingsDraw.Dispose();
ChangedItemTooltip.Dispose();
ChangedItemClick.Dispose();
OpenMainWindow.Dispose();
CloseMainWindow.Dispose();
// Redrawing
RedrawAll.Dispose();
RedrawObject.Dispose();
RedrawObjectByIndex.Dispose();
RedrawObjectByName.Dispose();
GameObjectRedrawn.Dispose();
// Game State
GetDrawObjectInfo.Dispose();
GetCutsceneParentIndex.Dispose();
SetCutsceneParentIndex.Dispose();
CreatingCharacterBase.Dispose();
CreatedCharacterBase.Dispose();
GameObjectResourcePathResolved.Dispose();
// Resolve
ResolveDefaultPath.Dispose();
ResolveInterfacePath.Dispose();
ResolvePlayerPath.Dispose();
ResolveGameObjectPath.Dispose();
ResolveCharacterPath.Dispose();
ReverseResolvePath.Dispose();
ReverseResolveGameObjectPath.Dispose();
ReverseResolvePlayerPath.Dispose();
ResolvePlayerPaths.Dispose();
ResolvePlayerPathsAsync.Dispose();
// Collections
GetCollections.Dispose();
GetCurrentCollectionName.Dispose();
GetDefaultCollectionName.Dispose();
GetInterfaceCollectionName.Dispose();
GetCharacterCollectionName.Dispose();
GetCollectionForType.Dispose();
SetCollectionForType.Dispose();
GetCollectionForObject.Dispose();
SetCollectionForObject.Dispose();
GetChangedItems.Dispose();
// Meta
GetPlayerMetaManipulations.Dispose();
GetMetaManipulations.Dispose();
GetGameObjectMetaManipulations.Dispose();
// Mods
GetMods.Dispose();
ReloadMod.Dispose();
InstallMod.Dispose();
AddMod.Dispose();
DeleteMod.Dispose();
GetModPath.Dispose();
SetModPath.Dispose();
ModDeleted.Dispose();
ModAdded.Dispose();
ModMoved.Dispose();
// ModSettings
GetAvailableModSettings.Dispose();
GetCurrentModSettings.Dispose();
TryInheritMod.Dispose();
TrySetMod.Dispose();
TrySetModPriority.Dispose();
TrySetModSetting.Dispose();
TrySetModSettings.Dispose();
ModSettingChanged.Dispose();
CopyModSettings.Dispose();
// Temporary
CreateTemporaryCollection.Dispose();
RemoveTemporaryCollection.Dispose();
CreateNamedTemporaryCollection.Dispose();
RemoveTemporaryCollectionByName.Dispose();
AssignTemporaryCollection.Dispose();
AddTemporaryModAll.Dispose();
AddTemporaryMod.Dispose();
RemoveTemporaryModAll.Dispose();
RemoveTemporaryMod.Dispose();
// Editing
ConvertTextureFile.Dispose();
ConvertTextureData.Dispose();
// Resource Tree
GetGameObjectResourcePaths.Dispose();
GetPlayerResourcePaths.Dispose();
GetGameObjectResourcesOfType.Dispose();
GetPlayerResourcesOfType.Dispose();
GetGameObjectResourceTrees.Dispose();
GetPlayerResourceTrees.Dispose();
Disposed.Invoke();
Disposed.Dispose();
}
// Wrappers
private int DeprecatedVersion()
{
Penumbra.Log.Warning($"{Ipc.ApiVersion.Label} is outdated. Please use {Ipc.ApiVersions.Label} instead.");
return Api.ApiVersion.Breaking;
}
private void OnClick(MouseButton click, object? item)
{
var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(item);
ChangedItemClick.Invoke(click, type, id);
}
private void OnTooltip(object? item)
{
var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(item);
ChangedItemTooltip.Invoke(type, id);
}
private void EnabledChangeEvent(bool value)
=> EnabledChange.Invoke(value);
private void OnGameObjectRedrawn(IntPtr objectAddress, int objectTableIndex)
=> GameObjectRedrawn.Invoke(objectAddress, objectTableIndex);
private void CreatingCharacterBaseEvent(IntPtr gameObject, string collectionName, IntPtr modelId, IntPtr customize, IntPtr equipData)
=> CreatingCharacterBase.Invoke(gameObject, collectionName, modelId, customize, equipData);
private void CreatedCharacterBaseEvent(IntPtr gameObject, string collectionName, IntPtr drawObject)
=> CreatedCharacterBase.Invoke(gameObject, collectionName, drawObject);
private void GameObjectResourceResolvedEvent(IntPtr gameObject, string gamePath, string localPath)
=> GameObjectResourcePathResolved.Invoke(gameObject, gamePath, localPath);
private void ModSettingChangedEvent(ModSettingChange type, string collection, string mod, bool inherited)
=> ModSettingChanged.Invoke(type, collection, mod, inherited);
private void ModDeletedEvent(string name)
=> ModDeleted.Invoke(name);
private void ModAddedEvent(string name)
=> ModAdded.Invoke(name);
private void ModMovedEvent(string from, string to)
=> ModMoved.Invoke(from, to);
}

View file

@ -1,3 +1,5 @@
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
@ -5,6 +7,7 @@ using Penumbra.Services;
using Penumbra.String.Classes;
using Penumbra.Collections.Manager;
using Penumbra.Communication;
using Penumbra.Mods.Settings;
namespace Penumbra.Api;
@ -16,12 +19,12 @@ public enum RedirectResult
FilteredGamePath = 3,
}
public class TempModManager : IDisposable
public class TempModManager : IDisposable, IService
{
private readonly CommunicatorService _communicator;
private readonly Dictionary<ModCollection, List<TemporaryMod>> _mods = new();
private readonly List<TemporaryMod> _modsForAllCollections = new();
private readonly Dictionary<ModCollection, List<TemporaryMod>> _mods = [];
private readonly List<TemporaryMod> _modsForAllCollections = [];
public TempModManager(CommunicatorService communicator)
{
@ -41,7 +44,7 @@ public class TempModManager : IDisposable
=> _modsForAllCollections;
public RedirectResult Register(string tag, ModCollection? collection, Dictionary<Utf8GamePath, FullPath> dict,
HashSet<MetaManipulation> manips, int priority)
MetaDictionary manips, ModPriority priority)
{
var mod = GetOrCreateMod(tag, collection, priority, out var created);
Penumbra.Log.Verbose($"{(created ? "Created" : "Changed")} temporary Mod {mod.Name}.");
@ -50,10 +53,10 @@ public class TempModManager : IDisposable
return RedirectResult.Success;
}
public RedirectResult Unregister(string tag, ModCollection? collection, int? priority)
public RedirectResult Unregister(string tag, ModCollection? collection, ModPriority? priority)
{
Penumbra.Log.Verbose($"Removing temporary mod with tag {tag}...");
var list = collection == null ? _modsForAllCollections : _mods.TryGetValue(collection, out var l) ? l : null;
var list = collection == null ? _modsForAllCollections : _mods.GetValueOrDefault(collection);
if (list == null)
return RedirectResult.NotRegistered;
@ -84,11 +87,13 @@ public class TempModManager : IDisposable
{
Penumbra.Log.Verbose($"Removing temporary Mod {mod.Name} from {collection.AnonymizedName}.");
collection.Remove(mod);
_communicator.ModSettingChanged.Invoke(collection, ModSettingChange.TemporaryMod, null, Setting.False, 0, false);
}
else
{
Penumbra.Log.Verbose($"Adding {(created ? "new " : string.Empty)}temporary Mod {mod.Name} to {collection.AnonymizedName}.");
collection.Apply(mod, created);
_communicator.ModSettingChanged.Invoke(collection, ModSettingChange.TemporaryMod, null, Setting.True, 0, false);
}
}
else
@ -113,7 +118,7 @@ public class TempModManager : IDisposable
// Find or create a mod with the given tag as name and the given priority, for the given collection (or all collections).
// Returns the found or created mod and whether it was newly created.
private TemporaryMod GetOrCreateMod(string tag, ModCollection? collection, int priority, out bool created)
private TemporaryMod GetOrCreateMod(string tag, ModCollection? collection, ModPriority priority, out bool created)
{
List<TemporaryMod> list;
if (collection == null)
@ -126,14 +131,14 @@ public class TempModManager : IDisposable
}
else
{
list = new List<TemporaryMod>();
list = [];
_mods.Add(collection, list);
}
var mod = list.Find(m => m.Priority == priority && m.Name == tag);
if (mod == null)
{
mod = new TemporaryMod()
mod = new TemporaryMod
{
Name = tag,
Priority = priority,

View file

@ -1,56 +0,0 @@
using OtterGui.Filesystem;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public struct CmpCache : IDisposable
{
private CmpFile? _cmpFile = null;
private readonly List<RspManipulation> _cmpManipulations = new();
public CmpCache()
{ }
public void SetFiles(MetaFileManager manager)
=> manager.SetFile(_cmpFile, MetaIndex.HumanCmp);
public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager)
=> manager.TemporarilySetFile(_cmpFile, MetaIndex.HumanCmp);
public void Reset()
{
if (_cmpFile == null)
return;
_cmpFile.Reset(_cmpManipulations.Select(m => (m.SubRace, m.Attribute)));
_cmpManipulations.Clear();
}
public bool ApplyMod(MetaFileManager manager, RspManipulation manip)
{
_cmpManipulations.AddOrReplace(manip);
_cmpFile ??= new CmpFile(manager);
return manip.Apply(_cmpFile);
}
public bool RevertMod(MetaFileManager manager, RspManipulation manip)
{
if (!_cmpManipulations.Remove(manip))
return false;
var def = CmpFile.GetDefault(manager, manip.SubRace, manip.Attribute);
manip = new RspManipulation(manip.SubRace, manip.Attribute, def);
return manip.Apply(_cmpFile!);
}
public void Dispose()
{
_cmpFile?.Dispose();
_cmpFile = null;
_cmpManipulations.Clear();
}
}

View file

@ -2,12 +2,11 @@ using OtterGui;
using OtterGui.Classes;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
using Penumbra.Api.Enums;
using Penumbra.Communication;
using Penumbra.Mods.Editor;
using Penumbra.String.Classes;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Subclasses;
using Penumbra.Util;
using Penumbra.GameData.Data;
namespace Penumbra.Collections.Cache;
@ -18,15 +17,16 @@ public record ModConflicts(IMod Mod2, List<object> Conflicts, bool HasPriority,
/// The Cache contains all required temporary data to use a collection.
/// It will only be setup if a collection gets activated in any way.
/// </summary>
public class CollectionCache : IDisposable
public sealed class CollectionCache : IDisposable
{
private readonly CollectionCacheManager _manager;
private readonly ModCollection _collection;
public readonly CollectionModData ModData = new();
private readonly SortedList<string, (SingleArray<IMod>, object?)> _changedItems = [];
public readonly ConcurrentDictionary<Utf8GamePath, ModPath> ResolvedFiles = new();
public readonly MetaCache Meta;
public readonly Dictionary<IMod, SingleArray<ModConflicts>> ConflictDict = [];
private readonly CollectionCacheManager _manager;
private readonly ModCollection _collection;
public readonly CollectionModData ModData = new();
private readonly SortedList<string, (SingleArray<IMod>, IIdentifiedObjectData?)> _changedItems = [];
public readonly ConcurrentDictionary<Utf8GamePath, ModPath> ResolvedFiles = new();
public readonly CustomResourceCache CustomResources;
public readonly MetaCache Meta;
public readonly Dictionary<IMod, SingleArray<ModConflicts>> ConflictDict = [];
public int Calculating = -1;
@ -37,12 +37,12 @@ public class CollectionCache : IDisposable
=> ConflictDict.Values;
public SingleArray<ModConflicts> Conflicts(IMod mod)
=> ConflictDict.TryGetValue(mod, out SingleArray<ModConflicts> c) ? c : new SingleArray<ModConflicts>();
=> ConflictDict.TryGetValue(mod, out var c) ? c : new SingleArray<ModConflicts>();
private int _changedItemsSaveCounter = -1;
// Obtain currently changed items. Computes them if they haven't been computed before.
public IReadOnlyDictionary<string, (SingleArray<IMod>, object?)> ChangedItems
public IReadOnlyDictionary<string, (SingleArray<IMod>, IIdentifiedObjectData?)> ChangedItems
{
get
{
@ -54,16 +54,21 @@ public class CollectionCache : IDisposable
// The cache reacts through events on its collection changing.
public CollectionCache(CollectionCacheManager manager, ModCollection collection)
{
_manager = manager;
_collection = collection;
Meta = new MetaCache(manager.MetaFileManager, _collection);
_manager = manager;
_collection = collection;
Meta = new MetaCache(manager.MetaFileManager, _collection);
CustomResources = new CustomResourceCache(manager.ResourceLoader);
}
public void Dispose()
=> Meta.Dispose();
{
Meta.Dispose();
CustomResources.Dispose();
GC.SuppressFinalize(this);
}
~CollectionCache()
=> Meta.Dispose();
=> Dispose();
// Resolve a given game path according to this collection.
public FullPath? ResolvePath(Utf8GamePath gameResourcePath)
@ -72,7 +77,7 @@ public class CollectionCache : IDisposable
return null;
if (candidate.Path.InternalName.Length > Utf8GamePath.MaxGamePathLength
|| candidate.Path.IsRooted && !candidate.Path.Exists)
|| candidate.Path is { IsRooted: true, Exists: false })
return null;
return candidate.Path;
@ -100,7 +105,7 @@ public class CollectionCache : IDisposable
public HashSet<Utf8GamePath>[] ReverseResolvePaths(IReadOnlyCollection<string> fullPaths)
{
if (fullPaths.Count == 0)
return Array.Empty<HashSet<Utf8GamePath>>();
return [];
var ret = new HashSet<Utf8GamePath>[fullPaths.Count];
var dict = new Dictionary<FullPath, int>(fullPaths.Count);
@ -108,8 +113,8 @@ public class CollectionCache : IDisposable
{
dict[new FullPath(path)] = idx;
ret[idx] = !Path.IsPathRooted(path) && Utf8GamePath.FromString(path, out var utf8)
? new HashSet<Utf8GamePath> { utf8 }
: new HashSet<Utf8GamePath>();
? [utf8]
: [];
}
foreach (var (game, full) in ResolvedFiles)
@ -121,12 +126,6 @@ public class CollectionCache : IDisposable
return ret;
}
public void ForceFile(Utf8GamePath path, FullPath fullPath)
=> _manager.AddChange(ChangeData.ForcedFile(this, path, fullPath));
public void RemovePath(Utf8GamePath path)
=> _manager.AddChange(ChangeData.ForcedFile(this, path, FullPath.Empty));
public void ReloadMod(IMod mod, bool addMetaChanges)
=> _manager.AddChange(ChangeData.ModReload(this, mod, addMetaChanges));
@ -148,17 +147,20 @@ public class CollectionCache : IDisposable
if (fullPath.FullName.Length > 0)
{
ResolvedFiles.TryAdd(path, new ModPath(Mod.ForcedFiles, fullPath));
CustomResources.Invalidate(path);
InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Replaced, path, fullPath, modPath.Path,
Mod.ForcedFiles);
}
else
{
CustomResources.Invalidate(path);
InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Removed, path, FullPath.Empty, modPath.Path, null);
}
}
else if (fullPath.FullName.Length > 0)
{
ResolvedFiles.TryAdd(path, new ModPath(Mod.ForcedFiles, fullPath));
CustomResources.Invalidate(path);
InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Added, path, fullPath, FullPath.Empty, Mod.ForcedFiles);
}
}
@ -181,6 +183,7 @@ public class CollectionCache : IDisposable
{
if (ResolvedFiles.Remove(path, out var mp))
{
CustomResources.Invalidate(path);
if (mp.Mod != mod)
Penumbra.Log.Warning(
$"Invalid mod state, removing {mod.Name} and associated file {path} returned current mod {mp.Mod.Name}.");
@ -221,56 +224,41 @@ public class CollectionCache : IDisposable
/// <summary> Add all files and possibly manipulations of a given mod according to its settings in this collection. </summary>
internal void AddModSync(IMod mod, bool addMetaChanges)
{
if (mod.Index >= 0)
{
var settings = _collection[mod.Index].Settings;
if (settings is not { Enabled: true })
return;
var files = GetFiles(mod);
foreach (var (path, file) in files.FileRedirections)
AddFile(path, file, mod);
foreach (var (group, groupIndex) in mod.Groups.WithIndex().OrderByDescending(g => g.Item1.Priority))
{
if (group.Count == 0)
continue;
var config = settings.Settings[groupIndex];
switch (group.Type)
{
case GroupType.Single:
AddSubMod(group[(int)config], mod);
break;
case GroupType.Multi:
{
foreach (var (option, _) in group.WithIndex()
.Where(p => ((1 << p.Item2) & config) != 0)
.OrderByDescending(p => group.OptionPriority(p.Item2)))
AddSubMod(option, mod);
break;
}
}
}
}
AddSubMod(mod.Default, mod);
foreach (var (identifier, entry) in files.Manipulations.Eqp)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Eqdp)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Est)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Gmp)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Rsp)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Imc)
AddManipulation(mod, identifier, entry);
foreach (var identifier in files.Manipulations.GlobalEqp)
AddManipulation(mod, identifier, null!);
if (addMetaChanges)
{
_collection.IncrementCounter();
if (mod.TotalManipulations > 0)
AddMetaFiles(false);
_manager.MetaFileManager.ApplyDefaultFiles(_collection);
}
}
// Add all files and possibly manipulations of a specific submod
private void AddSubMod(ISubMod subMod, IMod parentMod)
private AppliedModData GetFiles(IMod mod)
{
foreach (var (path, file) in subMod.Files.Concat(subMod.FileSwaps))
AddFile(path, file, parentMod);
if (mod.Index < 0)
return mod.GetData();
foreach (var manip in subMod.Manipulations)
AddManipulation(manip, parentMod);
var settings = _collection[mod.Index].Settings;
return settings is not { Enabled: true }
? AppliedModData.Empty
: mod.GetData(settings);
}
/// <summary> Invoke only if not in a full recalculation. </summary>
@ -295,6 +283,7 @@ public class CollectionCache : IDisposable
if (ResolvedFiles.TryAdd(path, new ModPath(mod, file)))
{
ModData.AddPath(mod, path);
CustomResources.Invalidate(path);
InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Added, path, file, FullPath.Empty, mod);
return;
}
@ -309,13 +298,14 @@ public class CollectionCache : IDisposable
ModData.RemovePath(modPath.Mod, path);
ResolvedFiles[path] = new ModPath(mod, file);
ModData.AddPath(mod, path);
CustomResources.Invalidate(path);
InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Replaced, path, file, modPath.Path, mod);
}
}
catch (Exception ex)
{
Penumbra.Log.Error(
$"[{Thread.CurrentThread.ManagedThreadId}] Error adding redirection {file} -> {path} for mod {mod.Name} to collection cache {AnonymizedName}:\n{ex}");
$"[{Environment.CurrentManagedThreadId}] Error adding redirection {file} -> {path} for mod {mod.Name} to collection cache {AnonymizedName}:\n{ex}");
}
}
@ -356,7 +346,7 @@ public class CollectionCache : IDisposable
foreach (var conflict in tmpConflicts)
{
if (data is Utf8GamePath path && conflict.Conflicts.RemoveAll(p => p is Utf8GamePath x && x.Equals(path)) > 0
|| data is MetaManipulation meta && conflict.Conflicts.RemoveAll(m => m is MetaManipulation x && x.Equals(meta)) > 0)
|| data is IMetaIdentifier meta && conflict.Conflicts.RemoveAll(m => m.Equals(meta)) > 0)
AddConflict(data, addedMod, conflict.Mod2);
}
@ -388,12 +378,12 @@ public class CollectionCache : IDisposable
// For different mods, higher mod priority takes precedence before option group priority,
// which takes precedence before option priority, which takes precedence before ordering.
// Inside the same mod, conflicts are not recorded.
private void AddManipulation(MetaManipulation manip, IMod mod)
private void AddManipulation(IMod mod, IMetaIdentifier identifier, object entry)
{
if (!Meta.TryGetValue(manip, out var existingMod))
if (!Meta.TryGetMod(identifier, out var existingMod))
{
Meta.ApplyMod(manip, mod);
ModData.AddManip(mod, manip);
Meta.ApplyMod(mod, identifier, entry);
ModData.AddManip(mod, identifier);
return;
}
@ -401,20 +391,15 @@ public class CollectionCache : IDisposable
if (mod == existingMod)
return;
if (AddConflict(manip, mod, existingMod))
if (AddConflict(identifier, mod, existingMod))
{
ModData.RemoveManip(existingMod, manip);
Meta.ApplyMod(manip, mod);
ModData.AddManip(mod, manip);
ModData.RemoveManip(existingMod, identifier);
Meta.ApplyMod(mod, identifier, entry);
ModData.AddManip(mod, identifier);
}
}
// Add all necessary meta file redirects.
public void AddMetaFiles(bool fromFullCompute)
=> Meta.SetImcFiles(fromFullCompute);
// Identify and record all manipulated objects for this entire collection.
private void SetChangedItems()
{
@ -428,7 +413,7 @@ public class CollectionCache : IDisposable
// Skip IMCs because they would result in far too many false-positive items,
// since they are per set instead of per item-slot/item/variant.
var identifier = _manager.MetaFileManager.Identifier;
var items = new SortedList<string, object?>(512);
var items = new SortedList<string, IIdentifiedObjectData?>(512);
void AddItems(IMod mod)
{
@ -437,8 +422,8 @@ public class CollectionCache : IDisposable
if (!_changedItems.TryGetValue(name, out var data))
_changedItems.Add(name, (new SingleArray<IMod>(mod), obj));
else if (!data.Item1.Contains(mod))
_changedItems[name] = (data.Item1.Append(mod), obj is int x && data.Item2 is int y ? x + y : obj);
else if (obj is int x && data.Item2 is int y)
_changedItems[name] = (data.Item1.Append(mod), obj is IdentifiedCounter x && data.Item2 is IdentifiedCounter y ? x + y : obj);
else if (obj is IdentifiedCounter x && data.Item2 is IdentifiedCounter y)
_changedItems[name] = (data.Item1, x + y);
}
@ -451,11 +436,14 @@ public class CollectionCache : IDisposable
AddItems(modPath.Mod);
}
foreach (var (manip, mod) in Meta)
foreach (var (manip, mod) in Meta.IdentifierSources)
{
ModCacheManager.ComputeChangedItems(identifier, items, manip);
manip.AddChangedItems(identifier, items);
AddItems(mod);
}
if (_manager.Config.HideMachinistOffhandFromChangedItems)
_changedItems.RemoveMachinistOffhands();
}
catch (Exception e)
{

View file

@ -1,18 +1,24 @@
using Dalamud.Plugin.Services;
using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.Api;
using Penumbra.Api.Enums;
using Penumbra.Collections.Manager;
using Penumbra.Communication;
using Penumbra.Interop.Hooks.ResourceLoading;
using Penumbra.Meta;
using Penumbra.Mods;
using Penumbra.Mods.Groups;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Manager.OptionEditor;
using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods;
using Penumbra.Services;
using Penumbra.String.Classes;
namespace Penumbra.Collections.Cache;
public class CollectionCacheManager : IDisposable
public class CollectionCacheManager : IDisposable, IService
{
private readonly FrameworkManager _framework;
private readonly CommunicatorService _communicator;
@ -20,9 +26,10 @@ public class CollectionCacheManager : IDisposable
private readonly ModStorage _modStorage;
private readonly CollectionStorage _storage;
private readonly ActiveCollections _active;
internal readonly Configuration Config;
internal readonly ResolvedFileChanged ResolvedFileChanged;
internal readonly MetaFileManager MetaFileManager;
internal readonly MetaFileManager MetaFileManager;
internal readonly ResourceLoader ResourceLoader;
private readonly ConcurrentQueue<CollectionCache.ChangeData> _changeQueue = new();
@ -35,7 +42,8 @@ public class CollectionCacheManager : IDisposable
=> _storage.Where(c => c.HasCache);
public CollectionCacheManager(FrameworkManager framework, CommunicatorService communicator, TempModManager tempMods, ModStorage modStorage,
MetaFileManager metaFileManager, ActiveCollections active, CollectionStorage storage)
MetaFileManager metaFileManager, ActiveCollections active, CollectionStorage storage, ResourceLoader resourceLoader,
Configuration config)
{
_framework = framework;
_communicator = communicator;
@ -44,6 +52,8 @@ public class CollectionCacheManager : IDisposable
MetaFileManager = metaFileManager;
_active = active;
_storage = storage;
ResourceLoader = resourceLoader;
Config = config;
ResolvedFileChanged = _communicator.ResolvedFileChanged;
if (!_active.Individuals.IsLoaded)
@ -74,6 +84,12 @@ public class CollectionCacheManager : IDisposable
_communicator.ModSettingChanged.Unsubscribe(OnModSettingChange);
_communicator.CollectionInheritanceChanged.Unsubscribe(OnCollectionInheritanceChange);
MetaFileManager.CharacterUtility.LoadingFinished -= IncrementCounters;
foreach (var collection in _storage)
{
collection._cache?.Dispose();
collection._cache = null;
}
}
public void AddChange(CollectionCache.ChangeData data)
@ -116,7 +132,7 @@ public class CollectionCacheManager : IDisposable
/// Does not create caches.
/// </summary>
public void CalculateEffectiveFileList(ModCollection collection)
=> _framework.RegisterImportant(nameof(CalculateEffectiveFileList) + collection.Name,
=> _framework.RegisterImportant(nameof(CalculateEffectiveFileList) + collection.Identifier,
() => CalculateEffectiveFileListInternal(collection));
private void CalculateEffectiveFileListInternal(ModCollection collection)
@ -171,8 +187,6 @@ public class CollectionCacheManager : IDisposable
foreach (var mod in _modStorage)
cache.AddModSync(mod, false);
cache.AddMetaFiles(true);
collection.IncrementCounter();
MetaFileManager.ApplyDefaultFiles(collection);
@ -254,7 +268,8 @@ public class CollectionCacheManager : IDisposable
}
/// <summary> Prepare Changes by removing mods from caches with collections or add or reload mods. </summary>
private void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx)
private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container,
int movedToIdx)
{
if (type is ModOptionChangeType.PrepareChange)
{
@ -264,17 +279,17 @@ public class CollectionCacheManager : IDisposable
return;
}
type.HandlingInfo(out _, out var recomputeList, out var reload);
type.HandlingInfo(out _, out var recomputeList, out var justAdd);
if (!recomputeList)
return;
foreach (var collection in _storage.Where(collection => collection.HasCache && collection[mod.Index].Settings is { Enabled: true }))
{
if (reload)
collection._cache!.ReloadMod(mod, true);
else
if (justAdd)
collection._cache!.AddMod(mod, true);
else
collection._cache!.ReloadMod(mod, true);
}
}
@ -286,7 +301,7 @@ public class CollectionCacheManager : IDisposable
MetaFileManager.CharacterUtility.LoadingFinished -= IncrementCounters;
}
private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx, bool _)
private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx, bool _)
{
if (!collection.HasCache)
return;
@ -298,9 +313,9 @@ public class CollectionCacheManager : IDisposable
cache.ReloadMod(mod!, true);
break;
case ModSettingChange.EnableState:
if (oldValue == 0)
if (oldValue == Setting.False)
cache.AddMod(mod!, true);
else if (oldValue == 1)
else if (oldValue == Setting.True)
cache.RemoveMod(mod!, true);
else if (collection[mod!.Index].Settings?.Enabled == true)
cache.ReloadMod(mod!, true);
@ -322,6 +337,10 @@ public class CollectionCacheManager : IDisposable
case ModSettingChange.MultiEnableState:
FullRecalculation(collection);
break;
case ModSettingChange.TemporaryMod:
case ModSettingChange.Edited:
// handled otherwise
break;
}
}

View file

@ -1,23 +1,25 @@
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
using Penumbra.Mods.Editor;
using Penumbra.String.Classes;
namespace Penumbra.Collections.Cache;
/// <summary>
/// Contains associations between a mod and the paths and meta manipulations affected by that mod.
/// </summary>
public class CollectionModData
{
private readonly Dictionary<IMod, (HashSet<Utf8GamePath>, HashSet<MetaManipulation>)> _data = new();
private readonly Dictionary<IMod, (HashSet<Utf8GamePath>, HashSet<IMetaIdentifier>)> _data = new();
public IEnumerable<(IMod, IReadOnlySet<Utf8GamePath>, IReadOnlySet<MetaManipulation>)> Data
=> _data.Select(kvp => (kvp.Key, (IReadOnlySet<Utf8GamePath>)kvp.Value.Item1, (IReadOnlySet<MetaManipulation>)kvp.Value.Item2));
public IEnumerable<(IMod, IReadOnlySet<Utf8GamePath>, IReadOnlySet<IMetaIdentifier>)> Data
=> _data.Select(kvp => (kvp.Key, (IReadOnlySet<Utf8GamePath>)kvp.Value.Item1, (IReadOnlySet<IMetaIdentifier>)kvp.Value.Item2));
public (IReadOnlyCollection<Utf8GamePath> Paths, IReadOnlyCollection<MetaManipulation> Manipulations) RemoveMod(IMod mod)
public (IReadOnlyCollection<Utf8GamePath> Paths, IReadOnlyCollection<IMetaIdentifier> Manipulations) RemoveMod(IMod mod)
{
if (_data.Remove(mod, out var data))
return data;
return (Array.Empty<Utf8GamePath>(), Array.Empty<MetaManipulation>());
return ([], []);
}
public void AddPath(IMod mod, Utf8GamePath path)
@ -28,12 +30,12 @@ public class CollectionModData
}
else
{
data = (new HashSet<Utf8GamePath> { path }, new HashSet<MetaManipulation>());
data = ([path], []);
_data.Add(mod, data);
}
}
public void AddManip(IMod mod, MetaManipulation manipulation)
public void AddManip(IMod mod, IMetaIdentifier manipulation)
{
if (_data.TryGetValue(mod, out var data))
{
@ -41,7 +43,7 @@ public class CollectionModData
}
else
{
data = (new HashSet<Utf8GamePath>(), new HashSet<MetaManipulation> { manipulation });
data = ([], [manipulation]);
_data.Add(mod, data);
}
}
@ -52,7 +54,7 @@ public class CollectionModData
_data.Remove(mod);
}
public void RemoveManip(IMod mod, MetaManipulation manip)
public void RemoveManip(IMod mod, IMetaIdentifier manip)
{
if (_data.TryGetValue(mod, out var data) && data.Item2.Remove(manip) && data.Item1.Count == 0 && data.Item2.Count == 0)
_data.Remove(mod);

View file

@ -0,0 +1,49 @@
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.Api.Enums;
using Penumbra.Interop.Hooks.ResourceLoading;
using Penumbra.Interop.SafeHandles;
using Penumbra.String.Classes;
namespace Penumbra.Collections.Cache;
/// <summary> A cache for resources owned by a collection. </summary>
public sealed class CustomResourceCache(ResourceLoader loader)
: ConcurrentDictionary<Utf8GamePath, SafeResourceHandle>, IDisposable
{
/// <summary> Invalidate an existing resource by clearing it from the cache and disposing it. </summary>
public void Invalidate(Utf8GamePath path)
{
if (TryRemove(path, out var handle))
handle.Dispose();
}
public void Dispose()
{
foreach (var handle in Values)
handle.Dispose();
Clear();
}
/// <summary> Get the requested resource either from the cached resource, or load a new one if it does not exist. </summary>
public SafeResourceHandle Get(ResourceCategory category, ResourceType type, Utf8GamePath path, ResolveData data)
{
if (TryGetClonedValue(path, out var handle))
return handle;
handle = loader.LoadResolvedSafeResource(category, type, path.Path, data);
var clone = handle.Clone();
if (!TryAdd(path, clone))
clone.Dispose();
return handle;
}
/// <summary> Get a cloned cached resource if it exists. </summary>
private bool TryGetClonedValue(Utf8GamePath path, [NotNullWhen(true)] out SafeResourceHandle? handle)
{
if (!TryGetValue(path, out handle))
return false;
handle = handle.Clone();
return true;
}
}

View file

@ -1,97 +1,54 @@
using OtterGui;
using OtterGui.Filesystem;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public readonly struct EqdpCache : IDisposable
public sealed class EqdpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<EqdpIdentifier, EqdpEntry>(manager, collection)
{
private readonly ExpandedEqdpFile?[] _eqdpFiles = new ExpandedEqdpFile[CharacterUtilityData.EqdpIndices.Length]; // TODO: female Hrothgar
private readonly List<EqdpManipulation> _eqdpManipulations = new();
private readonly Dictionary<(PrimaryId Id, GenderRace GenderRace, bool Accessory), (EqdpEntry Entry, EqdpEntry InverseMask)> _fullEntries =
[];
public EqdpCache()
{ }
public void SetFiles(MetaFileManager manager)
{
for (var i = 0; i < CharacterUtilityData.EqdpIndices.Length; ++i)
manager.SetFile(_eqdpFiles[i], CharacterUtilityData.EqdpIndices[i]);
}
public void SetFile(MetaFileManager manager, MetaIndex index)
{
var i = CharacterUtilityData.EqdpIndices.IndexOf(index);
if (i != -1)
manager.SetFile(_eqdpFiles[i], index);
}
public MetaList.MetaReverter? TemporarilySetFiles(MetaFileManager manager, GenderRace genderRace, bool accessory)
{
var idx = CharacterUtilityData.EqdpIdx(genderRace, accessory);
if (idx < 0)
{
Penumbra.Log.Warning($"Invalid Gender, Race or Accessory for EQDP file {genderRace}, {accessory}.");
return null;
}
var i = CharacterUtilityData.EqdpIndices.IndexOf(idx);
if (i < 0)
{
Penumbra.Log.Warning($"Invalid Gender, Race or Accessory for EQDP file {genderRace}, {accessory}.");
return null;
}
return manager.TemporarilySetFile(_eqdpFiles[i], idx);
}
public EqdpEntry ApplyFullEntry(PrimaryId id, GenderRace genderRace, bool accessory, EqdpEntry originalEntry)
=> _fullEntries.TryGetValue((id, genderRace, accessory), out var pair)
? (originalEntry & pair.InverseMask) | pair.Entry
: originalEntry;
public void Reset()
{
foreach (var file in _eqdpFiles.OfType<ExpandedEqdpFile>())
{
var relevant = CharacterUtility.RelevantIndices[file.Index.Value];
file.Reset(_eqdpManipulations.Where(m => m.FileIndex() == relevant).Select(m => (PrimaryId)m.SetId));
}
_eqdpManipulations.Clear();
Clear();
_fullEntries.Clear();
}
public bool ApplyMod(MetaFileManager manager, EqdpManipulation manip)
protected override void ApplyModInternal(EqdpIdentifier identifier, EqdpEntry entry)
{
_eqdpManipulations.AddOrReplace(manip);
var file = _eqdpFiles[Array.IndexOf(CharacterUtilityData.EqdpIndices, manip.FileIndex())] ??=
new ExpandedEqdpFile(manager, Names.CombinedRace(manip.Gender, manip.Race), manip.Slot.IsAccessory()); // TODO: female Hrothgar
return manip.Apply(file);
var tuple = (identifier.SetId, identifier.GenderRace, identifier.Slot.IsAccessory());
var mask = Eqdp.Mask(identifier.Slot);
var inverseMask = ~mask;
if (_fullEntries.TryGetValue(tuple, out var pair))
pair = ((pair.Entry & inverseMask) | (entry & mask), pair.InverseMask & inverseMask);
else
pair = (entry & mask, inverseMask);
_fullEntries[tuple] = pair;
}
public bool RevertMod(MetaFileManager manager, EqdpManipulation manip)
protected override void RevertModInternal(EqdpIdentifier identifier)
{
if (!_eqdpManipulations.Remove(manip))
return false;
var tuple = (identifier.SetId, identifier.GenderRace, identifier.Slot.IsAccessory());
var def = ExpandedEqdpFile.GetDefault(manager, Names.CombinedRace(manip.Gender, manip.Race), manip.Slot.IsAccessory(), manip.SetId);
var file = _eqdpFiles[Array.IndexOf(CharacterUtilityData.EqdpIndices, manip.FileIndex())]!;
manip = new EqdpManipulation(def, manip.Slot, manip.Gender, manip.Race, manip.SetId);
return manip.Apply(file);
if (!_fullEntries.Remove(tuple, out var pair))
return;
var mask = Eqdp.Mask(identifier.Slot);
var newMask = pair.InverseMask | mask;
if (newMask is not EqdpEntry.FullMask)
_fullEntries[tuple] = (pair.Entry & ~mask, newMask);
}
public ExpandedEqdpFile? EqdpFile(GenderRace race, bool accessory)
=> _eqdpFiles
[Array.IndexOf(CharacterUtilityData.EqdpIndices, CharacterUtilityData.EqdpIdx(race, accessory))]; // TODO: female Hrothgar
public void Dispose()
protected override void Dispose(bool _)
{
for (var i = 0; i < _eqdpFiles.Length; ++i)
{
_eqdpFiles[i]?.Dispose();
_eqdpFiles[i] = null;
}
_eqdpManipulations.Clear();
Clear();
_fullEntries.Clear();
}
}

View file

@ -1,60 +1,66 @@
using OtterGui.Filesystem;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public struct EqpCache : IDisposable
public sealed class EqpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<EqpIdentifier, EqpEntry>(manager, collection)
{
private ExpandedEqpFile? _eqpFile = null;
private readonly List<EqpManipulation> _eqpManipulations = new();
public unsafe EqpEntry GetValues(CharacterArmor* armor)
{
var bodyEntry = GetSingleValue(armor[1].Set, EquipSlot.Body);
var headEntry = bodyEntry.HasFlag(EqpEntry.BodyShowHead)
? GetSingleValue(armor[0].Set, EquipSlot.Head)
: GetSingleValue(armor[1].Set, EquipSlot.Head);
var handEntry = bodyEntry.HasFlag(EqpEntry.BodyShowHand)
? GetSingleValue(armor[2].Set, EquipSlot.Hands)
: GetSingleValue(armor[1].Set, EquipSlot.Hands);
var (legsEntry, legsId) = bodyEntry.HasFlag(EqpEntry.BodyShowLeg)
? (GetSingleValue(armor[3].Set, EquipSlot.Legs), 3)
: (GetSingleValue(armor[1].Set, EquipSlot.Legs), 1);
var footEntry = legsEntry.HasFlag(EqpEntry.LegsShowFoot)
? GetSingleValue(armor[4].Set, EquipSlot.Feet)
: GetSingleValue(armor[legsId].Set, EquipSlot.Feet);
public EqpCache()
{ }
var combined = bodyEntry | headEntry | handEntry | legsEntry | footEntry;
return PostProcessFeet(PostProcessHands(combined));
}
public void SetFiles(MetaFileManager manager)
=> manager.SetFile(_eqpFile, MetaIndex.Eqp);
public static void ResetFiles(MetaFileManager manager)
=> manager.SetFile(null, MetaIndex.Eqp);
public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager)
=> manager.TemporarilySetFile(_eqpFile, MetaIndex.Eqp);
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private EqpEntry GetSingleValue(PrimaryId id, EquipSlot slot)
=> TryGetValue(new EqpIdentifier(id, slot), out var pair) ? pair.Entry : ExpandedEqpFile.GetDefault(Manager, id) & Eqp.Mask(slot);
public void Reset()
{
if (_eqpFile == null)
return;
=> Clear();
_eqpFile.Reset(_eqpManipulations.Select(m => m.SetId));
_eqpManipulations.Clear();
protected override void Dispose(bool _)
=> Clear();
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static EqpEntry PostProcessHands(EqpEntry entry)
{
if (!entry.HasFlag(EqpEntry.HandsHideForearm))
return entry;
var testFlag = entry.HasFlag(EqpEntry.HandsHideElbow)
? entry.HasFlag(EqpEntry.BodyHideGlovesL)
: entry.HasFlag(EqpEntry.BodyHideGlovesM);
return testFlag
? (entry | EqpEntry._4) & ~EqpEntry.BodyHideGlovesS
: entry & ~(EqpEntry._4 | EqpEntry.BodyHideGlovesS);
}
public bool ApplyMod(MetaFileManager manager, EqpManipulation manip)
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static EqpEntry PostProcessFeet(EqpEntry entry)
{
_eqpManipulations.AddOrReplace(manip);
_eqpFile ??= new ExpandedEqpFile(manager);
return manip.Apply(_eqpFile);
}
if (!entry.HasFlag(EqpEntry.FeetHideCalf))
return entry;
public bool RevertMod(MetaFileManager manager, EqpManipulation manip)
{
var idx = _eqpManipulations.FindIndex(manip.Equals);
if (idx < 0)
return false;
if (entry.HasFlag(EqpEntry.FeetHideKnee) || !entry.HasFlag(EqpEntry._20))
return entry & ~(EqpEntry.LegsHideBootsS | EqpEntry.LegsHideBootsM);
var def = ExpandedEqpFile.GetDefault(manager, manip.SetId);
manip = new EqpManipulation(def, manip.Slot, manip.SetId);
return manip.Apply(_eqpFile!);
}
public void Dispose()
{
_eqpFile?.Dispose();
_eqpFile = null;
_eqpManipulations.Clear();
return (entry | EqpEntry.LegsHideBootsM) & ~EqpEntry.LegsHideBootsS;
}
}

View file

@ -1,138 +1,19 @@
using OtterGui.Filesystem;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public struct EstCache : IDisposable
public sealed class EstCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<EstIdentifier, EstEntry>(manager, collection)
{
private EstFile? _estFaceFile = null;
private EstFile? _estHairFile = null;
private EstFile? _estBodyFile = null;
private EstFile? _estHeadFile = null;
private readonly List<EstManipulation> _estManipulations = new();
public EstCache()
{ }
public void SetFiles(MetaFileManager manager)
{
manager.SetFile(_estFaceFile, MetaIndex.FaceEst);
manager.SetFile(_estHairFile, MetaIndex.HairEst);
manager.SetFile(_estBodyFile, MetaIndex.BodyEst);
manager.SetFile(_estHeadFile, MetaIndex.HeadEst);
}
public void SetFile(MetaFileManager manager, MetaIndex index)
{
switch (index)
{
case MetaIndex.FaceEst:
manager.SetFile(_estFaceFile, MetaIndex.FaceEst);
break;
case MetaIndex.HairEst:
manager.SetFile(_estHairFile, MetaIndex.HairEst);
break;
case MetaIndex.BodyEst:
manager.SetFile(_estBodyFile, MetaIndex.BodyEst);
break;
case MetaIndex.HeadEst:
manager.SetFile(_estHeadFile, MetaIndex.HeadEst);
break;
}
}
public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager, EstManipulation.EstType type)
{
var (file, idx) = type switch
{
EstManipulation.EstType.Face => (_estFaceFile, MetaIndex.FaceEst),
EstManipulation.EstType.Hair => (_estHairFile, MetaIndex.HairEst),
EstManipulation.EstType.Body => (_estBodyFile, MetaIndex.BodyEst),
EstManipulation.EstType.Head => (_estHeadFile, MetaIndex.HeadEst),
_ => (null, 0),
};
return manager.TemporarilySetFile(file, idx);
}
private readonly EstFile? GetEstFile(EstManipulation.EstType type)
{
return type switch
{
EstManipulation.EstType.Face => _estFaceFile,
EstManipulation.EstType.Hair => _estHairFile,
EstManipulation.EstType.Body => _estBodyFile,
EstManipulation.EstType.Head => _estHeadFile,
_ => null,
};
}
internal ushort GetEstEntry(MetaFileManager manager, EstManipulation.EstType type, GenderRace genderRace, PrimaryId primaryId)
{
var file = GetEstFile(type);
return file != null
? file[genderRace, primaryId.Id]
: EstFile.GetDefault(manager, type, genderRace, primaryId);
}
public EstEntry GetEstEntry(EstIdentifier identifier)
=> TryGetValue(identifier, out var entry)
? entry.Entry
: EstFile.GetDefault(Manager, identifier);
public void Reset()
{
_estFaceFile?.Reset();
_estHairFile?.Reset();
_estBodyFile?.Reset();
_estHeadFile?.Reset();
_estManipulations.Clear();
}
=> Clear();
public bool ApplyMod(MetaFileManager manager, EstManipulation m)
{
_estManipulations.AddOrReplace(m);
var file = m.Slot switch
{
EstManipulation.EstType.Hair => _estHairFile ??= new EstFile(manager, EstManipulation.EstType.Hair),
EstManipulation.EstType.Face => _estFaceFile ??= new EstFile(manager, EstManipulation.EstType.Face),
EstManipulation.EstType.Body => _estBodyFile ??= new EstFile(manager, EstManipulation.EstType.Body),
EstManipulation.EstType.Head => _estHeadFile ??= new EstFile(manager, EstManipulation.EstType.Head),
_ => throw new ArgumentOutOfRangeException(),
};
return m.Apply(file);
}
public bool RevertMod(MetaFileManager manager, EstManipulation m)
{
if (!_estManipulations.Remove(m))
return false;
var def = EstFile.GetDefault(manager, m.Slot, Names.CombinedRace(m.Gender, m.Race), m.SetId);
var manip = new EstManipulation(m.Gender, m.Race, m.Slot, m.SetId, def);
var file = m.Slot switch
{
EstManipulation.EstType.Hair => _estHairFile!,
EstManipulation.EstType.Face => _estFaceFile!,
EstManipulation.EstType.Body => _estBodyFile!,
EstManipulation.EstType.Head => _estHeadFile!,
_ => throw new ArgumentOutOfRangeException(),
};
return manip.Apply(file);
}
public void Dispose()
{
_estFaceFile?.Dispose();
_estHairFile?.Dispose();
_estBodyFile?.Dispose();
_estHeadFile?.Dispose();
_estFaceFile = null;
_estHairFile = null;
_estBodyFile = null;
_estHeadFile = null;
_estManipulations.Clear();
}
protected override void Dispose(bool _)
=> Clear();
}

View file

@ -0,0 +1,96 @@
using OtterGui.Services;
using Penumbra.GameData.Structs;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
namespace Penumbra.Collections.Cache;
public class GlobalEqpCache : Dictionary<GlobalEqpManipulation, IMod>, IService
{
private readonly HashSet<PrimaryId> _doNotHideEarrings = [];
private readonly HashSet<PrimaryId> _doNotHideNecklace = [];
private readonly HashSet<PrimaryId> _doNotHideBracelets = [];
private readonly HashSet<PrimaryId> _doNotHideRingL = [];
private readonly HashSet<PrimaryId> _doNotHideRingR = [];
private bool _doNotHideVieraHats;
private bool _doNotHideHrothgarHats;
public new void Clear()
{
base.Clear();
_doNotHideEarrings.Clear();
_doNotHideNecklace.Clear();
_doNotHideBracelets.Clear();
_doNotHideRingL.Clear();
_doNotHideRingR.Clear();
_doNotHideHrothgarHats = false;
_doNotHideVieraHats = false;
}
public unsafe EqpEntry Apply(EqpEntry original, CharacterArmor* armor)
{
if (Count == 0)
return original;
if (_doNotHideVieraHats)
original |= EqpEntry.HeadShowVieraHat;
if (_doNotHideHrothgarHats)
original |= EqpEntry.HeadShowHrothgarHat;
if (_doNotHideEarrings.Contains(armor[5].Set))
original |= EqpEntry.HeadShowEarrings | EqpEntry.HeadShowEarringsAura | EqpEntry.HeadShowEarringsHuman;
if (_doNotHideNecklace.Contains(armor[6].Set))
original |= EqpEntry.BodyShowNecklace | EqpEntry.HeadShowNecklace;
if (_doNotHideBracelets.Contains(armor[7].Set))
original |= EqpEntry.BodyShowBracelet | EqpEntry.HandShowBracelet;
if (_doNotHideRingR.Contains(armor[8].Set))
original |= EqpEntry.HandShowRingR;
if (_doNotHideRingL.Contains(armor[9].Set))
original |= EqpEntry.HandShowRingL;
return original;
}
public bool ApplyMod(IMod mod, GlobalEqpManipulation manipulation)
{
if (Remove(manipulation, out var oldMod) && oldMod == mod)
return false;
this[manipulation] = mod;
_ = manipulation.Type switch
{
GlobalEqpType.DoNotHideEarrings => _doNotHideEarrings.Add(manipulation.Condition),
GlobalEqpType.DoNotHideNecklace => _doNotHideNecklace.Add(manipulation.Condition),
GlobalEqpType.DoNotHideBracelets => _doNotHideBracelets.Add(manipulation.Condition),
GlobalEqpType.DoNotHideRingR => _doNotHideRingR.Add(manipulation.Condition),
GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Add(manipulation.Condition),
GlobalEqpType.DoNotHideHrothgarHats => !_doNotHideHrothgarHats && (_doNotHideHrothgarHats = true),
GlobalEqpType.DoNotHideVieraHats => !_doNotHideVieraHats && (_doNotHideVieraHats = true),
_ => false,
};
return true;
}
public bool RevertMod(GlobalEqpManipulation manipulation, [NotNullWhen(true)] out IMod? mod)
{
if (!Remove(manipulation, out mod))
return false;
_ = manipulation.Type switch
{
GlobalEqpType.DoNotHideEarrings => _doNotHideEarrings.Remove(manipulation.Condition),
GlobalEqpType.DoNotHideNecklace => _doNotHideNecklace.Remove(manipulation.Condition),
GlobalEqpType.DoNotHideBracelets => _doNotHideBracelets.Remove(manipulation.Condition),
GlobalEqpType.DoNotHideRingR => _doNotHideRingR.Remove(manipulation.Condition),
GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Remove(manipulation.Condition),
GlobalEqpType.DoNotHideHrothgarHats => _doNotHideHrothgarHats && !(_doNotHideHrothgarHats = false),
GlobalEqpType.DoNotHideVieraHats => _doNotHideVieraHats && !(_doNotHideVieraHats = false),
_ => false,
};
return true;
}
}

View file

@ -1,56 +1,14 @@
using OtterGui.Filesystem;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.GameData.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public struct GmpCache : IDisposable
public sealed class GmpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<GmpIdentifier, GmpEntry>(manager, collection)
{
private ExpandedGmpFile? _gmpFile = null;
private readonly List<GmpManipulation> _gmpManipulations = new();
public GmpCache()
{ }
public void SetFiles(MetaFileManager manager)
=> manager.SetFile(_gmpFile, MetaIndex.Gmp);
public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager)
=> manager.TemporarilySetFile(_gmpFile, MetaIndex.Gmp);
public void Reset()
{
if (_gmpFile == null)
return;
=> Clear();
_gmpFile.Reset(_gmpManipulations.Select(m => m.SetId));
_gmpManipulations.Clear();
}
public bool ApplyMod(MetaFileManager manager, GmpManipulation manip)
{
_gmpManipulations.AddOrReplace(manip);
_gmpFile ??= new ExpandedGmpFile(manager);
return manip.Apply(_gmpFile);
}
public bool RevertMod(MetaFileManager manager, GmpManipulation manip)
{
if (!_gmpManipulations.Remove(manip))
return false;
var def = ExpandedGmpFile.GetDefault(manager, manip.SetId);
manip = new GmpManipulation(def, manip.SetId);
return manip.Apply(_gmpFile!);
}
public void Dispose()
{
_gmpFile?.Dispose();
_gmpFile = null;
_gmpManipulations.Clear();
}
protected override void Dispose(bool _)
=> Clear();
}

View file

@ -0,0 +1,60 @@
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
namespace Penumbra.Collections.Cache;
public abstract class MetaCacheBase<TIdentifier, TEntry>(MetaFileManager manager, ModCollection collection)
: Dictionary<TIdentifier, (IMod Source, TEntry Entry)>
where TIdentifier : unmanaged, IMetaIdentifier
where TEntry : unmanaged
{
protected readonly MetaFileManager Manager = manager;
protected readonly ModCollection Collection = collection;
public void Dispose()
{
Dispose(true);
}
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;
this[identifier] = (source, entry);
}
ApplyModInternal(identifier, entry);
return true;
}
public bool RevertMod(TIdentifier identifier, [NotNullWhen(true)] out IMod? mod)
{
lock (this)
{
if (!Remove(identifier, out var pair))
{
mod = null;
return false;
}
mod = pair.Source;
}
RevertModInternal(identifier);
return true;
}
protected virtual void ApplyModInternal(TIdentifier identifier, TEntry entry)
{ }
protected virtual void RevertModInternal(TIdentifier identifier)
{ }
protected virtual void Dispose(bool _)
{ }
}

View file

@ -1,123 +1,103 @@
using Penumbra.GameData.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes;
using Penumbra.String;
namespace Penumbra.Collections.Cache;
public readonly struct ImcCache : IDisposable
public sealed class ImcCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<ImcIdentifier, ImcEntry>(manager, collection)
{
private readonly Dictionary<Utf8GamePath, ImcFile> _imcFiles = new();
private readonly List<(ImcManipulation, ImcFile)> _imcManipulations = new();
private readonly Dictionary<CiByteString, (ImcFile, HashSet<ImcIdentifier>)> _imcFiles = [];
public ImcCache()
{ }
public bool HasFile(CiByteString path)
=> _imcFiles.ContainsKey(path);
public void SetFiles(ModCollection collection, bool fromFullCompute)
public bool GetFile(CiByteString path, [NotNullWhen(true)] out ImcFile? file)
{
if (fromFullCompute)
foreach (var path in _imcFiles.Keys)
collection._cache!.ForceFileSync(path, CreateImcPath(collection, path));
else
foreach (var path in _imcFiles.Keys)
collection._cache!.ForceFile(path, CreateImcPath(collection, path));
}
public void Reset(ModCollection collection)
{
foreach (var (path, file) in _imcFiles)
if (!_imcFiles.TryGetValue(path, out var p))
{
collection._cache!.RemovePath(path);
file.Reset();
}
_imcManipulations.Clear();
}
public bool ApplyMod(MetaFileManager manager, ModCollection collection, ImcManipulation manip)
{
if (!manip.Validate())
file = null;
return false;
var idx = _imcManipulations.FindIndex(p => p.Item1.Equals(manip));
if (idx < 0)
{
idx = _imcManipulations.Count;
_imcManipulations.Add((manip, null!));
}
var path = manip.GamePath();
file = p.Item1;
return true;
}
public void Reset()
{
foreach (var (_, (file, set)) in _imcFiles)
{
file.Reset();
set.Clear();
}
_imcFiles.Clear();
Clear();
}
protected override void ApplyModInternal(ImcIdentifier identifier, ImcEntry entry)
{
++Collection.ImcChangeCounter;
ApplyFile(identifier, entry);
}
private void ApplyFile(ImcIdentifier identifier, ImcEntry entry)
{
var path = identifier.GamePath().Path;
try
{
if (!_imcFiles.TryGetValue(path, out var file))
file = new ImcFile(manager, manip);
if (!_imcFiles.TryGetValue(path, out var pair))
pair = (new ImcFile(Manager, identifier), []);
_imcManipulations[idx] = (manip, file);
if (!manip.Apply(file))
return false;
_imcFiles[path] = file;
var fullPath = CreateImcPath(collection, path);
collection._cache!.ForceFile(path, fullPath);
if (!Apply(pair.Item1, identifier, entry))
return;
return true;
pair.Item2.Add(identifier);
_imcFiles[path] = pair;
}
catch (ImcException e)
{
manager.ValidityChecker.ImcExceptions.Add(e);
Manager.ValidityChecker.ImcExceptions.Add(e);
Penumbra.Log.Error(e.ToString());
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not apply IMC Manipulation {manip}:\n{e}");
Penumbra.Log.Error($"Could not apply IMC Manipulation {identifier}:\n{e}");
}
return false;
}
public bool RevertMod(MetaFileManager manager, ModCollection collection, ImcManipulation m)
protected override void RevertModInternal(ImcIdentifier identifier)
{
if (!m.Validate())
return false;
++Collection.ImcChangeCounter;
var path = identifier.GamePath().Path;
if (!_imcFiles.TryGetValue(path, out var pair))
return;
var idx = _imcManipulations.FindIndex(p => p.Item1.Equals(m));
if (idx < 0)
return false;
if (!pair.Item2.Remove(identifier))
return;
var (_, file) = _imcManipulations[idx];
_imcManipulations.RemoveAt(idx);
if (_imcManipulations.All(p => !ReferenceEquals(p.Item2, file)))
if (pair.Item2.Count == 0)
{
_imcFiles.Remove(file.Path);
collection._cache!.ForceFile(file.Path, FullPath.Empty);
file.Dispose();
return true;
_imcFiles.Remove(path);
pair.Item1.Dispose();
return;
}
var def = ImcFile.GetDefault(manager, file.Path, m.EquipSlot, m.Variant.Id, out _);
var manip = m.Copy(def);
if (!manip.Apply(file))
return false;
var fullPath = CreateImcPath(collection, file.Path);
collection._cache!.ForceFile(file.Path, fullPath);
return true;
var def = ImcFile.GetDefault(Manager, pair.Item1.Path, identifier.EquipSlot, identifier.Variant, out _);
Apply(pair.Item1, identifier, def);
}
public void Dispose()
public static bool Apply(ImcFile file, ImcIdentifier identifier, ImcEntry entry)
=> file.SetEntry(ImcFile.PartIndex(identifier.EquipSlot), identifier.Variant.Id, entry);
protected override void Dispose(bool _)
{
foreach (var file in _imcFiles.Values)
foreach (var (_, (file, _)) in _imcFiles)
file.Dispose();
Clear();
_imcFiles.Clear();
_imcManipulations.Clear();
}
private static FullPath CreateImcPath(ModCollection collection, Utf8GamePath path)
=> new($"|{collection.Name}_{collection.ChangeCounter}|{path}");
public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file)
=> _imcFiles.TryGetValue(path, out file);
}

View file

@ -1,234 +1,111 @@
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
using Penumbra.Mods.Editor;
using Penumbra.String.Classes;
namespace Penumbra.Collections.Cache;
public class MetaCache : IDisposable, IEnumerable<KeyValuePair<MetaManipulation, IMod>>
public class MetaCache(MetaFileManager manager, ModCollection collection)
{
private readonly MetaFileManager _manager;
private readonly ModCollection _collection;
private readonly Dictionary<MetaManipulation, IMod> _manipulations = new();
private EqpCache _eqpCache = new();
private readonly EqdpCache _eqdpCache = new();
private EstCache _estCache = new();
private GmpCache _gmpCache = new();
private CmpCache _cmpCache = new();
private readonly ImcCache _imcCache = new();
public bool TryGetValue(MetaManipulation manip, [NotNullWhen(true)] out IMod? mod)
{
lock (_manipulations)
{
return _manipulations.TryGetValue(manip, out mod);
}
}
public readonly EqpCache Eqp = new(manager, collection);
public readonly EqdpCache Eqdp = new(manager, collection);
public readonly EstCache Est = new(manager, collection);
public readonly GmpCache Gmp = new(manager, collection);
public readonly RspCache Rsp = new(manager, collection);
public readonly ImcCache Imc = new(manager, collection);
public readonly GlobalEqpCache GlobalEqp = new();
public int Count
=> _manipulations.Count;
=> Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + GlobalEqp.Count;
public IReadOnlyCollection<MetaManipulation> Manipulations
=> _manipulations.Keys;
public IEnumerator<KeyValuePair<MetaManipulation, IMod>> GetEnumerator()
=> _manipulations.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public MetaCache(MetaFileManager manager, ModCollection collection)
{
_manager = manager;
_collection = collection;
if (!_manager.CharacterUtility.Ready)
_manager.CharacterUtility.LoadingFinished += ApplyStoredManipulations;
}
public void SetFiles()
{
_eqpCache.SetFiles(_manager);
_eqdpCache.SetFiles(_manager);
_estCache.SetFiles(_manager);
_gmpCache.SetFiles(_manager);
_cmpCache.SetFiles(_manager);
_imcCache.SetFiles(_collection, false);
}
public IEnumerable<(IMetaIdentifier, IMod)> IdentifierSources
=> Eqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))
.Concat(Eqdp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(Est.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(Gmp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(Rsp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(Imc.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(GlobalEqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value)));
public void Reset()
{
_eqpCache.Reset();
_eqdpCache.Reset();
_estCache.Reset();
_gmpCache.Reset();
_cmpCache.Reset();
_imcCache.Reset(_collection);
_manipulations.Clear();
Eqp.Reset();
Eqdp.Reset();
Est.Reset();
Gmp.Reset();
Rsp.Reset();
Imc.Reset();
GlobalEqp.Clear();
}
public void Dispose()
{
_manager.CharacterUtility.LoadingFinished -= ApplyStoredManipulations;
_eqpCache.Dispose();
_eqdpCache.Dispose();
_estCache.Dispose();
_gmpCache.Dispose();
_cmpCache.Dispose();
_imcCache.Dispose();
_manipulations.Clear();
Eqp.Dispose();
Eqdp.Dispose();
Est.Dispose();
Gmp.Dispose();
Rsp.Dispose();
Imc.Dispose();
}
public bool TryGetMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod)
{
mod = null;
return identifier switch
{
EqdpIdentifier i => Eqdp.TryGetValue(i, out var p) && Convert(p, out mod),
EqpIdentifier i => Eqp.TryGetValue(i, out var p) && Convert(p, out mod),
EstIdentifier i => Est.TryGetValue(i, out var p) && Convert(p, out mod),
GmpIdentifier i => Gmp.TryGetValue(i, out var p) && Convert(p, out mod),
ImcIdentifier i => Imc.TryGetValue(i, out var p) && Convert(p, out mod),
RspIdentifier i => Rsp.TryGetValue(i, out var p) && Convert(p, out mod),
GlobalEqpManipulation i => GlobalEqp.TryGetValue(i, out mod),
_ => false,
};
static bool Convert<T>((IMod, T) pair, out IMod mod)
{
mod = pair.Item1;
return true;
}
}
public bool RevertMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod)
=> identifier switch
{
EqdpIdentifier i => Eqdp.RevertMod(i, out mod),
EqpIdentifier i => Eqp.RevertMod(i, out mod),
EstIdentifier i => Est.RevertMod(i, out mod),
GmpIdentifier i => Gmp.RevertMod(i, out mod),
ImcIdentifier i => Imc.RevertMod(i, out mod),
RspIdentifier i => Rsp.RevertMod(i, out mod),
GlobalEqpManipulation i => GlobalEqp.RevertMod(i, out mod),
_ => (mod = null) != null,
};
public bool ApplyMod(IMod mod, IMetaIdentifier identifier, object entry)
=> identifier switch
{
EqdpIdentifier i when entry is EqdpEntry e => Eqdp.ApplyMod(mod, i, e),
EqdpIdentifier i when entry is EqdpEntryInternal e => Eqdp.ApplyMod(mod, i, e.ToEntry(i.Slot)),
EqpIdentifier i when entry is EqpEntry e => Eqp.ApplyMod(mod, i, e),
EqpIdentifier i when entry is EqpEntryInternal e => Eqp.ApplyMod(mod, i, e.ToEntry(i.Slot)),
EstIdentifier i when entry is EstEntry e => Est.ApplyMod(mod, i, e),
GmpIdentifier i when entry is GmpEntry e => Gmp.ApplyMod(mod, i, e),
ImcIdentifier i when entry is ImcEntry e => Imc.ApplyMod(mod, i, e),
RspIdentifier i when entry is RspEntry e => Rsp.ApplyMod(mod, i, e),
GlobalEqpManipulation i => GlobalEqp.ApplyMod(mod, i),
_ => false,
};
~MetaCache()
=> Dispose();
public bool ApplyMod(MetaManipulation manip, IMod mod)
{
lock (_manipulations)
{
if (_manipulations.ContainsKey(manip))
_manipulations.Remove(manip);
_manipulations[manip] = mod;
}
if (!_manager.CharacterUtility.Ready)
return true;
// Imc manipulations do not require character utility,
// but they do require the file space to be ready.
return manip.ManipulationType switch
{
MetaManipulation.Type.Eqp => _eqpCache.ApplyMod(_manager, manip.Eqp),
MetaManipulation.Type.Eqdp => _eqdpCache.ApplyMod(_manager, manip.Eqdp),
MetaManipulation.Type.Est => _estCache.ApplyMod(_manager, manip.Est),
MetaManipulation.Type.Gmp => _gmpCache.ApplyMod(_manager, manip.Gmp),
MetaManipulation.Type.Rsp => _cmpCache.ApplyMod(_manager, manip.Rsp),
MetaManipulation.Type.Imc => _imcCache.ApplyMod(_manager, _collection, manip.Imc),
MetaManipulation.Type.Unknown => false,
_ => false,
};
}
public bool RevertMod(MetaManipulation manip, [NotNullWhen(true)] out IMod? mod)
{
lock (_manipulations)
{
var ret = _manipulations.Remove(manip, out mod);
if (!_manager.CharacterUtility.Ready)
return ret;
}
// Imc manipulations do not require character utility,
// but they do require the file space to be ready.
return manip.ManipulationType switch
{
MetaManipulation.Type.Eqp => _eqpCache.RevertMod(_manager, manip.Eqp),
MetaManipulation.Type.Eqdp => _eqdpCache.RevertMod(_manager, manip.Eqdp),
MetaManipulation.Type.Est => _estCache.RevertMod(_manager, manip.Est),
MetaManipulation.Type.Gmp => _gmpCache.RevertMod(_manager, manip.Gmp),
MetaManipulation.Type.Rsp => _cmpCache.RevertMod(_manager, manip.Rsp),
MetaManipulation.Type.Imc => _imcCache.RevertMod(_manager, _collection, manip.Imc),
MetaManipulation.Type.Unknown => false,
_ => false,
};
}
/// <summary> Set a single file. </summary>
public void SetFile(MetaIndex metaIndex)
{
switch (metaIndex)
{
case MetaIndex.Eqp:
_eqpCache.SetFiles(_manager);
break;
case MetaIndex.Gmp:
_gmpCache.SetFiles(_manager);
break;
case MetaIndex.HumanCmp:
_cmpCache.SetFiles(_manager);
break;
case MetaIndex.FaceEst:
case MetaIndex.HairEst:
case MetaIndex.HeadEst:
case MetaIndex.BodyEst:
_estCache.SetFile(_manager, metaIndex);
break;
default:
_eqdpCache.SetFile(_manager, metaIndex);
break;
}
}
/// <summary> Set the currently relevant IMC files for the collection cache. </summary>
public void SetImcFiles(bool fromFullCompute)
=> _imcCache.SetFiles(_collection, fromFullCompute);
public MetaList.MetaReverter TemporarilySetEqpFile()
=> _eqpCache.TemporarilySetFiles(_manager);
public MetaList.MetaReverter? TemporarilySetEqdpFile(GenderRace genderRace, bool accessory)
=> _eqdpCache.TemporarilySetFiles(_manager, genderRace, accessory);
public MetaList.MetaReverter TemporarilySetGmpFile()
=> _gmpCache.TemporarilySetFiles(_manager);
public MetaList.MetaReverter TemporarilySetCmpFile()
=> _cmpCache.TemporarilySetFiles(_manager);
public MetaList.MetaReverter TemporarilySetEstFile(EstManipulation.EstType type)
=> _estCache.TemporarilySetFiles(_manager, type);
/// <summary> Try to obtain a manipulated IMC file. </summary>
public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out Meta.Files.ImcFile? file)
=> _imcCache.GetImcFile(path, out file);
internal EqdpEntry GetEqdpEntry(GenderRace race, bool accessory, PrimaryId primaryId)
{
var eqdpFile = _eqdpCache.EqdpFile(race, accessory);
if (eqdpFile != null)
return primaryId.Id < eqdpFile.Count ? eqdpFile[primaryId] : default;
else
return Meta.Files.ExpandedEqdpFile.GetDefault(_manager, race, accessory, primaryId);
}
=> Eqdp.ApplyFullEntry(primaryId, race, accessory, Meta.Files.ExpandedEqdpFile.GetDefault(manager, race, accessory, primaryId));
internal ushort GetEstEntry(EstManipulation.EstType type, GenderRace genderRace, PrimaryId primaryId)
=> _estCache.GetEstEntry(_manager, type, genderRace, primaryId);
/// <summary> Use this when CharacterUtility becomes ready. </summary>
private void ApplyStoredManipulations()
{
if (!_manager.CharacterUtility.Ready)
return;
var loaded = 0;
lock (_manipulations)
{
foreach (var manip in Manipulations)
{
loaded += manip.ManipulationType switch
{
MetaManipulation.Type.Eqp => _eqpCache.ApplyMod(_manager, manip.Eqp),
MetaManipulation.Type.Eqdp => _eqdpCache.ApplyMod(_manager, manip.Eqdp),
MetaManipulation.Type.Est => _estCache.ApplyMod(_manager, manip.Est),
MetaManipulation.Type.Gmp => _gmpCache.ApplyMod(_manager, manip.Gmp),
MetaManipulation.Type.Rsp => _cmpCache.ApplyMod(_manager, manip.Rsp),
MetaManipulation.Type.Imc => _imcCache.ApplyMod(_manager, _collection, manip.Imc),
MetaManipulation.Type.Unknown => false,
_ => false,
}
? 1
: 0;
}
}
_manager.ApplyDefaultFiles(_collection);
_manager.CharacterUtility.LoadingFinished -= ApplyStoredManipulations;
Penumbra.Log.Debug($"{_collection.AnonymizedName}: Loaded {loaded} delayed meta manipulations.");
}
internal EstEntry GetEstEntry(EstType type, GenderRace genderRace, PrimaryId primaryId)
=> Est.GetEstEntry(new EstIdentifier(primaryId, type, genderRace));
}

View file

@ -0,0 +1,13 @@
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public sealed class RspCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<RspIdentifier, RspEntry>(manager, collection)
{
public void Reset()
=> Clear();
protected override void Dispose(bool _)
=> Clear();
}

View file

@ -1,4 +1,4 @@
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.ImGuiNotification;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;

View file

@ -1,8 +1,9 @@
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.ImGuiNotification;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.Communication;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
@ -11,7 +12,7 @@ using Penumbra.UI;
namespace Penumbra.Collections.Manager;
public class ActiveCollectionData
public class ActiveCollectionData : IService
{
public ModCollection Current { get; internal set; } = ModCollection.Empty;
public ModCollection Default { get; internal set; } = ModCollection.Empty;
@ -20,9 +21,9 @@ public class ActiveCollectionData
public readonly ModCollection?[] SpecialCollections = new ModCollection?[Enum.GetValues<Api.Enums.ApiCollectionType>().Length - 3];
}
public class ActiveCollections : ISavable, IDisposable
public class ActiveCollections : ISavable, IDisposable, IService
{
public const int Version = 1;
public const int Version = 2;
private readonly CollectionStorage _storage;
private readonly CommunicatorService _communicator;
@ -261,16 +262,17 @@ public class ActiveCollections : ISavable, IDisposable
var jObj = new JObject
{
{ nameof(Version), Version },
{ nameof(Default), Default.Name },
{ nameof(Interface), Interface.Name },
{ nameof(Current), Current.Name },
{ nameof(Default), Default.Id },
{ nameof(Interface), Interface.Id },
{ nameof(Current), Current.Id },
};
foreach (var (type, collection) in SpecialCollections.WithIndex().Where(p => p.Value != null)
.Select(p => ((CollectionType)p.Index, p.Value!)))
jObj.Add(type.ToString(), collection.Name);
jObj.Add(type.ToString(), collection.Id);
jObj.Add(nameof(Individuals), Individuals.ToJObject());
using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
using var j = new JsonTextWriter(writer);
j.Formatting = Formatting.Indented;
jObj.WriteTo(j);
}
@ -319,22 +321,16 @@ public class ActiveCollections : ISavable, IDisposable
}
}
/// <summary>
/// Load default, current, special, and character collections from config.
/// If a collection does not exist anymore, reset it to an appropriate default.
/// </summary>
private void LoadCollections()
private bool LoadCollectionsV1(JObject jObject)
{
Penumbra.Log.Debug("[Collections] Reading collection assignments...");
var configChanged = !Load(_saveService.FileNames, out var jObject);
// Load the default collection. If the string does not exist take the Default name if no file existed or the Empty name if one existed.
var defaultName = jObject[nameof(Default)]?.ToObject<string>()
?? (configChanged ? ModCollection.DefaultCollectionName : ModCollection.Empty.Name);
var configChanged = false;
// Load the default collection. If the name does not exist take the empty collection.
var defaultName = jObject[nameof(Default)]?.ToObject<string>() ?? ModCollection.Empty.Name;
if (!_storage.ByName(defaultName, out var defaultCollection))
{
Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Name}.", NotificationType.Warning);
$"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Name}.",
NotificationType.Warning);
Default = ModCollection.Empty;
configChanged = true;
}
@ -348,7 +344,8 @@ public class ActiveCollections : ISavable, IDisposable
if (!_storage.ByName(interfaceName, out var interfaceCollection))
{
Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Name}.", NotificationType.Warning);
$"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Name}.",
NotificationType.Warning);
Interface = ModCollection.Empty;
configChanged = true;
}
@ -362,7 +359,8 @@ public class ActiveCollections : ISavable, IDisposable
if (!_storage.ByName(currentName, out var currentCollection))
{
Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollection.DefaultCollectionName}.", NotificationType.Warning);
$"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollection.DefaultCollectionName}.",
NotificationType.Warning);
Current = _storage.DefaultNamed;
configChanged = true;
}
@ -393,11 +391,124 @@ public class ActiveCollections : ISavable, IDisposable
Penumbra.Log.Debug("[Collections] Loaded non-individual collection assignments.");
configChanged |= ActiveCollectionMigration.MigrateIndividualCollections(_storage, Individuals, jObject);
configChanged |= Individuals.ReadJObject(_saveService, this, jObject[nameof(Individuals)] as JArray, _storage);
configChanged |= Individuals.ReadJObject(_saveService, this, jObject[nameof(Individuals)] as JArray, _storage, 1);
// Save any changes.
if (configChanged)
_saveService.ImmediateSave(this);
return configChanged;
}
private bool LoadCollectionsV2(JObject jObject)
{
var configChanged = false;
// Load the default collection. If the guid does not exist take the empty collection.
var defaultId = jObject[nameof(Default)]?.ToObject<Guid>() ?? Guid.Empty;
if (!_storage.ById(defaultId, out var defaultCollection))
{
Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.DefaultCollection} {defaultId} is not available, reset to {ModCollection.Empty.Name}.",
NotificationType.Warning);
Default = ModCollection.Empty;
configChanged = true;
}
else
{
Default = defaultCollection;
}
// Load the interface collection. If no string is set, use the name of whatever was set as Default.
var interfaceId = jObject[nameof(Interface)]?.ToObject<Guid>() ?? Default.Id;
if (!_storage.ById(interfaceId, out var interfaceCollection))
{
Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.InterfaceCollection} {interfaceId} is not available, reset to {ModCollection.Empty.Name}.",
NotificationType.Warning);
Interface = ModCollection.Empty;
configChanged = true;
}
else
{
Interface = interfaceCollection;
}
// Load the current collection.
var currentId = jObject[nameof(Current)]?.ToObject<Guid>() ?? _storage.DefaultNamed.Id;
if (!_storage.ById(currentId, out var currentCollection))
{
Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.SelectedCollection} {currentId} is not available, reset to {ModCollection.DefaultCollectionName}.",
NotificationType.Warning);
Current = _storage.DefaultNamed;
configChanged = true;
}
else
{
Current = currentCollection;
}
// Load special collections.
foreach (var (type, name, _) in CollectionTypeExtensions.Special)
{
var typeId = jObject[type.ToString()]?.ToObject<Guid>();
if (typeId == null)
continue;
if (!_storage.ById(typeId.Value, out var typeCollection))
{
Penumbra.Messager.NotificationMessage($"Last choice of {name} Collection {typeId.Value} is not available, removed.",
NotificationType.Warning);
configChanged = true;
}
else
{
SpecialCollections[(int)type] = typeCollection;
}
}
Penumbra.Log.Debug("[Collections] Loaded non-individual collection assignments.");
configChanged |= ActiveCollectionMigration.MigrateIndividualCollections(_storage, Individuals, jObject);
configChanged |= Individuals.ReadJObject(_saveService, this, jObject[nameof(Individuals)] as JArray, _storage, 2);
return configChanged;
}
private bool LoadCollectionsNew()
{
Current = _storage.DefaultNamed;
Default = _storage.DefaultNamed;
Interface = _storage.DefaultNamed;
return true;
}
/// <summary>
/// Load default, current, special, and character collections from config.
/// If a collection does not exist anymore, reset it to an appropriate default.
/// </summary>
private void LoadCollections()
{
Penumbra.Log.Debug("[Collections] Reading collection assignments...");
var configChanged = !Load(_saveService.FileNames, out var jObject);
var version = jObject["Version"]?.ToObject<int>() ?? 0;
var changed = false;
switch (version)
{
case 1:
changed = LoadCollectionsV1(jObject);
break;
case 2:
changed = LoadCollectionsV2(jObject);
break;
case 0 when configChanged:
changed = LoadCollectionsNew();
break;
case 0:
Penumbra.Messager.NotificationMessage("Active Collections File has unknown version and will be reset.",
NotificationType.Warning);
changed = LoadCollectionsNew();
break;
}
if (changed)
_saveService.ImmediateSaveSync(this);
}
/// <summary>
@ -410,7 +521,7 @@ public class ActiveCollections : ISavable, IDisposable
var jObj = BackupService.GetJObjectForFile(fileNames, file);
if (jObj == null)
{
ret = new JObject();
ret = [];
return false;
}

View file

@ -1,32 +1,22 @@
using OtterGui;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Subclasses;
using Penumbra.Mods.Settings;
using Penumbra.Services;
namespace Penumbra.Collections.Manager;
public class CollectionEditor
public class CollectionEditor(SaveService saveService, CommunicatorService communicator, ModStorage modStorage) : IService
{
private readonly CommunicatorService _communicator;
private readonly SaveService _saveService;
private readonly ModStorage _modStorage;
public CollectionEditor(SaveService saveService, CommunicatorService communicator, ModStorage modStorage)
{
_saveService = saveService;
_communicator = communicator;
_modStorage = modStorage;
}
/// <summary> Enable or disable the mod inheritance of mod idx. </summary>
public bool SetModInheritance(ModCollection collection, Mod mod, bool inherit)
{
if (!FixInheritance(collection, mod, inherit))
return false;
InvokeChange(collection, ModSettingChange.Inheritance, mod, inherit ? 0 : 1, 0);
InvokeChange(collection, ModSettingChange.Inheritance, mod, inherit ? Setting.False : Setting.True, 0);
return true;
}
@ -42,7 +32,8 @@ public class CollectionEditor
var inheritance = FixInheritance(collection, mod, false);
((List<ModSettings?>)collection.Settings)[mod.Index]!.Enabled = newValue;
InvokeChange(collection, ModSettingChange.EnableState, mod, inheritance ? -1 : newValue ? 0 : 1, 0);
InvokeChange(collection, ModSettingChange.EnableState, mod, inheritance ? Setting.Indefinite : newValue ? Setting.False : Setting.True,
0);
return true;
}
@ -52,7 +43,7 @@ public class CollectionEditor
if (!mods.Aggregate(false, (current, mod) => current | FixInheritance(collection, mod, inherit)))
return;
InvokeChange(collection, ModSettingChange.MultiInheritance, null, -1, 0);
InvokeChange(collection, ModSettingChange.MultiInheritance, null, Setting.Indefinite, 0);
}
/// <summary>
@ -76,22 +67,22 @@ public class CollectionEditor
if (!changes)
return;
InvokeChange(collection, ModSettingChange.MultiEnableState, null, -1, 0);
InvokeChange(collection, ModSettingChange.MultiEnableState, null, Setting.Indefinite, 0);
}
/// <summary>
/// Set the priority of mod idx to newValue if it differs from the current priority.
/// If the mod is currently inherited, stop the inheritance.
/// </summary>
public bool SetModPriority(ModCollection collection, Mod mod, int newValue)
public bool SetModPriority(ModCollection collection, Mod mod, ModPriority newValue)
{
var oldValue = collection.Settings[mod.Index]?.Priority ?? collection[mod.Index].Settings?.Priority ?? 0;
var oldValue = collection.Settings[mod.Index]?.Priority ?? collection[mod.Index].Settings?.Priority ?? ModPriority.Default;
if (newValue == oldValue)
return false;
var inheritance = FixInheritance(collection, mod, false);
((List<ModSettings?>)collection.Settings)[mod.Index]!.Priority = newValue;
InvokeChange(collection, ModSettingChange.Priority, mod, inheritance ? -1 : oldValue, 0);
InvokeChange(collection, ModSettingChange.Priority, mod, inheritance ? Setting.Indefinite : oldValue.AsSetting, 0);
return true;
}
@ -99,7 +90,7 @@ public class CollectionEditor
/// Set a given setting group settingName of mod idx to newValue if it differs from the current value and fix it if necessary.
/// /// If the mod is currently inherited, stop the inheritance.
/// </summary>
public bool SetModSetting(ModCollection collection, Mod mod, int groupIdx, uint newValue)
public bool SetModSetting(ModCollection collection, Mod mod, int groupIdx, Setting newValue)
{
var settings = collection.Settings[mod.Index] != null
? collection.Settings[mod.Index]!.Settings
@ -110,7 +101,7 @@ public class CollectionEditor
var inheritance = FixInheritance(collection, mod, false);
((List<ModSettings?>)collection.Settings)[mod.Index]!.SetValue(mod, groupIdx, newValue);
InvokeChange(collection, ModSettingChange.Setting, mod, inheritance ? -1 : (int)oldValue, groupIdx);
InvokeChange(collection, ModSettingChange.Setting, mod, inheritance ? Setting.Indefinite : oldValue, groupIdx);
return true;
}
@ -158,35 +149,17 @@ public class CollectionEditor
if (savedSettings != null)
{
((Dictionary<string, ModSettings.SavedSettings>)collection.UnusedSettings)[targetName] = savedSettings.Value;
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
saveService.QueueSave(new ModCollectionSave(modStorage, collection));
}
else if (((Dictionary<string, ModSettings.SavedSettings>)collection.UnusedSettings).Remove(targetName))
{
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
saveService.QueueSave(new ModCollectionSave(modStorage, collection));
}
}
return true;
}
/// <summary>
/// Change one of the available mod settings for mod idx discerned by type.
/// If type == Setting, settingName should be a valid setting for that mod, otherwise it will be ignored.
/// The setting will also be automatically fixed if it is invalid for that setting group.
/// For boolean parameters, newValue == 0 will be treated as false and != 0 as true.
/// </summary>
public bool ChangeModSetting(ModCollection collection, ModSettingChange type, Mod mod, int newValue, int groupIdx)
{
return type switch
{
ModSettingChange.Inheritance => SetModInheritance(collection, mod, newValue != 0),
ModSettingChange.EnableState => SetModState(collection, mod, newValue != 0),
ModSettingChange.Priority => SetModPriority(collection, mod, newValue),
ModSettingChange.Setting => SetModSetting(collection, mod, groupIdx, (uint)newValue),
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null),
};
}
/// <summary>
/// Set inheritance of a mod without saving,
/// to be used as an intermediary.
@ -204,16 +177,16 @@ public class CollectionEditor
/// <summary> Queue saves and trigger changes for any non-inherited change in a collection, then trigger changes for all inheritors. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private void InvokeChange(ModCollection changedCollection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx)
private void InvokeChange(ModCollection changedCollection, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx)
{
_saveService.QueueSave(new ModCollectionSave(_modStorage, changedCollection));
_communicator.ModSettingChanged.Invoke(changedCollection, type, mod, oldValue, groupIdx, false);
saveService.QueueSave(new ModCollectionSave(modStorage, changedCollection));
communicator.ModSettingChanged.Invoke(changedCollection, type, mod, oldValue, groupIdx, false);
RecurseInheritors(changedCollection, type, mod, oldValue, groupIdx);
}
/// <summary> Trigger changes in all inherited collections. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private void RecurseInheritors(ModCollection directParent, ModSettingChange type, Mod? mod, int oldValue, int groupIdx)
private void RecurseInheritors(ModCollection directParent, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx)
{
foreach (var directInheritor in directParent.DirectParentOf)
{
@ -221,11 +194,11 @@ public class CollectionEditor
{
case ModSettingChange.MultiInheritance:
case ModSettingChange.MultiEnableState:
_communicator.ModSettingChanged.Invoke(directInheritor, type, null, oldValue, groupIdx, true);
communicator.ModSettingChanged.Invoke(directInheritor, type, null, oldValue, groupIdx, true);
break;
default:
if (directInheritor.Settings[mod!.Index] == null)
_communicator.ModSettingChanged.Invoke(directInheritor, type, mod, oldValue, groupIdx, true);
communicator.ModSettingChanged.Invoke(directInheritor, type, mod, oldValue, groupIdx, true);
break;
}

View file

@ -1,24 +1,20 @@
using OtterGui.Services;
using Penumbra.Collections.Cache;
namespace Penumbra.Collections.Manager;
public class CollectionManager
public class CollectionManager(
CollectionStorage storage,
ActiveCollections active,
InheritanceManager inheritances,
CollectionCacheManager caches,
TempCollectionManager temp,
CollectionEditor editor) : IService
{
public readonly CollectionStorage Storage;
public readonly ActiveCollections Active;
public readonly InheritanceManager Inheritances;
public readonly CollectionCacheManager Caches;
public readonly TempCollectionManager Temp;
public readonly CollectionEditor Editor;
public CollectionManager(CollectionStorage storage, ActiveCollections active, InheritanceManager inheritances,
CollectionCacheManager caches, TempCollectionManager temp, CollectionEditor editor)
{
Storage = storage;
Active = active;
Inheritances = inheritances;
Caches = caches;
Temp = temp;
Editor = editor;
}
public readonly CollectionStorage Storage = storage;
public readonly ActiveCollections Active = active;
public readonly InheritanceManager Inheritances = inheritances;
public readonly CollectionCacheManager Caches = caches;
public readonly TempCollectionManager Temp = temp;
public readonly CollectionEditor Editor = editor;
}

View file

@ -1,29 +1,80 @@
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.ImGuiNotification;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Filesystem;
using OtterGui.Services;
using Penumbra.Communication;
using Penumbra.Mods;
using Penumbra.Mods.Editor;
using Penumbra.Mods.Groups;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Subclasses;
using Penumbra.Mods.Manager.OptionEditor;
using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods;
using Penumbra.Services;
namespace Penumbra.Collections.Manager;
public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
/// <summary> A contiguously incrementing ID managed by the CollectionCreator. </summary>
public readonly record struct LocalCollectionId(int Id) : IAdditionOperators<LocalCollectionId, int, LocalCollectionId>
{
public static readonly LocalCollectionId Zero = new(0);
public static LocalCollectionId operator +(LocalCollectionId left, int right)
=> new(left.Id + right);
}
public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, IService
{
private readonly CommunicatorService _communicator;
private readonly SaveService _saveService;
private readonly ModStorage _modStorage;
public ModCollection Create(string name, int index, ModCollection? duplicate)
{
var newCollection = duplicate?.Duplicate(name, CurrentCollectionId, index)
?? ModCollection.CreateEmpty(name, CurrentCollectionId, index, _modStorage.Count);
_collectionsByLocal[CurrentCollectionId] = newCollection;
CurrentCollectionId += 1;
return newCollection;
}
public ModCollection CreateFromData(Guid id, string name, int version, Dictionary<string, ModSettings.SavedSettings> allSettings,
IReadOnlyList<string> inheritances)
{
var newCollection = ModCollection.CreateFromData(_saveService, _modStorage, id, name, CurrentCollectionId, version, Count, allSettings,
inheritances);
_collectionsByLocal[CurrentCollectionId] = newCollection;
CurrentCollectionId += 1;
return newCollection;
}
public ModCollection CreateTemporary(string name, int index, int globalChangeCounter)
{
var newCollection = ModCollection.CreateTemporary(name, CurrentCollectionId, index, globalChangeCounter);
_collectionsByLocal[CurrentCollectionId] = newCollection;
CurrentCollectionId += 1;
return newCollection;
}
public void Delete(ModCollection collection)
=> _collectionsByLocal.Remove(collection.LocalId);
/// <remarks> The empty collection is always available at Index 0. </remarks>
private readonly List<ModCollection> _collections =
[
ModCollection.Empty,
];
/// <remarks> A list of all collections ever created still existing by their local id. </remarks>
private readonly Dictionary<LocalCollectionId, ModCollection>
_collectionsByLocal = new() { [LocalCollectionId.Zero] = ModCollection.Empty };
public readonly ModCollection DefaultNamed;
/// <remarks> Incremented by 1 because the empty collection gets Zero. </remarks>
public LocalCollectionId CurrentCollectionId { get; private set; } = LocalCollectionId.Zero + 1;
/// <summary> Default enumeration skips the empty collection. </summary>
public IEnumerator<ModCollection> GetEnumerator()
=> _collections.Skip(1).GetEnumerator();
@ -47,6 +98,29 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
return true;
}
/// <summary> Find a collection by its id. If the GUID is empty, the empty collection is returned. </summary>
public bool ById(Guid id, [NotNullWhen(true)] out ModCollection? collection)
{
if (id != Guid.Empty)
return _collections.FindFirst(c => c.Id == id, out collection);
collection = ModCollection.Empty;
return true;
}
/// <summary> Find a collection by an identifier, which is interpreted as a GUID first and if it does not correspond to one, as a name. </summary>
public bool ByIdentifier(string identifier, [NotNullWhen(true)] out ModCollection? collection)
{
if (Guid.TryParse(identifier, out var guid))
return ById(guid, out collection);
return ByName(identifier, out collection);
}
/// <summary> Find a collection by its local ID if it still exists, otherwise returns the empty collection. </summary>
public ModCollection ByLocalId(LocalCollectionId localId)
=> _collectionsByLocal.TryGetValue(localId, out var coll) ? coll : ModCollection.Empty;
public CollectionStorage(CommunicatorService communicator, SaveService saveService, ModStorage modStorage)
{
_communicator = communicator;
@ -56,6 +130,7 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
_communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished, ModDiscoveryFinished.Priority.CollectionStorage);
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.CollectionStorage);
_communicator.ModOptionChanged.Subscribe(OnModOptionChange, ModOptionChanged.Priority.CollectionStorage);
_communicator.ModFileChanged.Subscribe(OnModFileChanged, ModFileChanged.Priority.CollectionStorage);
ReadCollections(out DefaultNamed);
}
@ -65,31 +140,7 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
_communicator.ModDiscoveryFinished.Unsubscribe(OnModDiscoveryFinished);
_communicator.ModPathChanged.Unsubscribe(OnModPathChange);
_communicator.ModOptionChanged.Unsubscribe(OnModOptionChange);
}
/// <summary>
/// Returns true if the name is not empty, it is not the name of the empty collection
/// and no existing collection results in the same filename as name. Also returns the fixed name.
/// </summary>
public bool CanAddCollection(string name, out string fixedName)
{
if (!IsValidName(name))
{
fixedName = string.Empty;
return false;
}
name = name.ToLowerInvariant();
if (name.Length == 0
|| name == ModCollection.Empty.Name.ToLowerInvariant()
|| _collections.Any(c => c.Name.ToLowerInvariant() == name))
{
fixedName = string.Empty;
return false;
}
fixedName = name;
return true;
_communicator.ModFileChanged.Unsubscribe(OnModFileChanged);
}
/// <summary>
@ -101,17 +152,11 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
/// </summary>
public bool AddCollection(string name, ModCollection? duplicate)
{
if (!CanAddCollection(name, out var fixedName))
{
Penumbra.Messager.NotificationMessage(
$"The new collection {name} would lead to the same path {fixedName} as one that already exists.", NotificationType.Warning, false);
if (name.Length == 0)
return false;
}
var newCollection = duplicate?.Duplicate(name, _collections.Count)
?? ModCollection.CreateEmpty(name, _collections.Count, _modStorage.Count);
var newCollection = Create(name, _collections.Count, duplicate);
_collections.Add(newCollection);
_saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection));
Penumbra.Messager.NotificationMessage($"Created new collection {newCollection.AnonymizedName}.", NotificationType.Success, false);
_communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty);
@ -135,11 +180,13 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
return false;
}
Delete(collection);
_saveService.ImmediateDelete(new ModCollectionSave(_modStorage, collection));
_collections.RemoveAt(collection.Index);
// Update indices.
for (var i = collection.Index; i < Count; ++i)
_collections[i].Index = i;
_collectionsByLocal.Remove(collection.LocalId);
Penumbra.Messager.NotificationMessage($"Deleted collection {collection.AnonymizedName}.", NotificationType.Success, false);
_communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty);
@ -162,16 +209,9 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
}
/// <summary>
/// Check if a name is valid to use for a collection.
/// Does not check for uniqueness.
/// </summary>
private static bool IsValidName(string name)
=> name.Length is > 0 and < 64 && name.All(c => !c.IsInvalidAscii() && c is not '|' && !c.IsInvalidInPath());
/// <summary>
/// Read all collection files in the Collection Directory.
/// Ensure that the default named collection exists, and apply inheritances afterwards.
/// Ensure that the default named collection exists, and apply inheritances afterward.
/// Duplicate collection files are not deleted, just not added here.
/// </summary>
private void ReadCollections(out ModCollection defaultNamedCollection)
@ -179,26 +219,64 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
Penumbra.Log.Debug("[Collections] Reading saved collections...");
foreach (var file in _saveService.FileNames.CollectionFiles)
{
if (!ModCollectionSave.LoadFromFile(file, out var name, out var version, out var settings, out var inheritance))
if (!ModCollectionSave.LoadFromFile(file, out var id, out var name, out var version, out var settings, out var inheritance))
continue;
if (!IsValidName(name))
if (id == Guid.Empty)
{
// TODO: handle better.
Penumbra.Messager.NotificationMessage($"Collection of unsupported name found: {name} is not a valid collection name.", NotificationType.Warning);
Penumbra.Messager.NotificationMessage("Collection without ID found.", NotificationType.Warning);
continue;
}
if (ByName(name, out _))
if (ById(id, out _))
{
Penumbra.Messager.NotificationMessage($"Duplicate collection found: {name} already exists. Import skipped.", NotificationType.Warning);
Penumbra.Messager.NotificationMessage($"Duplicate collection found: {id} already exists. Import skipped.",
NotificationType.Warning);
continue;
}
var collection = ModCollection.CreateFromData(_saveService, _modStorage, name, version, Count, settings, inheritance);
var collection = CreateFromData(id, name, version, settings, inheritance);
var correctName = _saveService.FileNames.CollectionFile(collection);
if (file.FullName != correctName)
Penumbra.Messager.NotificationMessage($"Collection {file.Name} does not correspond to {collection.Name}.", NotificationType.Warning);
try
{
if (version >= 2)
{
try
{
File.Move(file.FullName, correctName, false);
Penumbra.Messager.NotificationMessage(
$"Collection {file.Name} does not correspond to {collection.Identifier}, renamed.",
NotificationType.Warning);
}
catch (Exception ex)
{
Penumbra.Messager.NotificationMessage(
$"Collection {file.Name} does not correspond to {collection.Identifier}, rename failed:\n{ex}",
NotificationType.Warning);
}
}
else
{
_saveService.ImmediateSaveSync(new ModCollectionSave(_modStorage, collection));
try
{
File.Move(file.FullName, file.FullName + ".bak", true);
Penumbra.Log.Information($"Migrated collection {name} to Guid {id} with backup of old file.");
}
catch (Exception ex)
{
Penumbra.Log.Information($"Migrated collection {name} to Guid {id}, rename of old file failed:\n{ex}");
}
}
}
catch (Exception e)
{
Penumbra.Messager.NotificationMessage(e,
$"Collection {file.Name} does not correspond to {collection.Identifier}, but could not rename.",
NotificationType.Error);
}
_collections.Add(collection);
}
@ -220,7 +298,8 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
return _collections[^1];
Penumbra.Messager.NotificationMessage(
$"Unknown problem creating a collection with the name {ModCollection.DefaultCollectionName}, which is required to exist.", NotificationType.Error);
$"Unknown problem creating a collection with the name {ModCollection.DefaultCollectionName}, which is required to exist.",
NotificationType.Error);
return Count > 1 ? _collections[1] : _collections[0];
}
@ -257,11 +336,20 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null))
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
break;
case ModPathChangeType.Reloaded:
foreach (var collection in this)
{
if (collection.Settings[mod.Index]?.Settings.FixAll(mod) ?? false)
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
}
break;
}
}
/// <summary> Save all collections where the mod has settings and the change requires saving. </summary>
private void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx)
private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container,
int movedToIdx)
{
type.HandlingInfo(out var requiresSaving, out _, out _);
if (!requiresSaving)
@ -269,8 +357,22 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
foreach (var collection in this)
{
if (collection.Settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false)
if (collection.Settings[mod.Index]?.HandleChanges(type, mod, group, option, movedToIdx) ?? false)
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
}
}
/// <summary> Update change counters when changing files. </summary>
private void OnModFileChanged(Mod mod, FileRegistry file)
{
if (file.CurrentUsage == 0)
return;
foreach (var collection in this)
{
var (settings, _) = collection[mod.Index];
if (settings is { Enabled: true })
collection.IncrementCounter();
}
}
}

View file

@ -107,6 +107,9 @@ public static class CollectionTypeExtensions
public static bool IsSpecial(this CollectionType collectionType)
=> collectionType < CollectionType.Default;
public static bool CanBeRemoved(this CollectionType collectionType)
=> collectionType.IsSpecial() || collectionType is CollectionType.Individual;
public static readonly (CollectionType, string, string)[] Special = Enum.GetValues<CollectionType>()
.Where(IsSpecial)
.Select(s => (s, s.ToName(), s.ToDescription()))

View file

@ -127,7 +127,7 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa
}
}
public bool TryGetCollection(GameObject? gameObject, out ModCollection? collection)
public bool TryGetCollection(IGameObject? gameObject, out ModCollection? collection)
=> TryGetCollection(_actors.FromObject(gameObject, true, false, false), out collection);
public unsafe bool TryGetCollection(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject, out ModCollection? collection)

View file

@ -1,5 +1,5 @@
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.ImGuiNotification;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
using Penumbra.GameData.Actors;
@ -18,7 +18,7 @@ public partial class IndividualCollections
foreach (var (name, identifiers, collection) in Assignments)
{
var tmp = identifiers[0].ToJson();
tmp.Add("Collection", collection.Name);
tmp.Add("Collection", collection.Id);
tmp.Add("Display", name);
ret.Add(tmp);
}
@ -26,26 +26,84 @@ public partial class IndividualCollections
return ret;
}
public bool ReadJObject(SaveService saver, ActiveCollections parent, JArray? obj, CollectionStorage storage)
public bool ReadJObject(SaveService saver, ActiveCollections parent, JArray? obj, CollectionStorage storage, int version)
{
if (_actors.Awaiter.IsCompletedSuccessfully)
{
var ret = ReadJObjectInternal(obj, storage);
var ret = version switch
{
1 => ReadJObjectInternalV1(obj, storage),
2 => ReadJObjectInternalV2(obj, storage),
_ => true,
};
return ret;
}
Penumbra.Log.Debug("[Collections] Delayed reading individual assignments until actor service is ready...");
_actors.Awaiter.ContinueWith(_ =>
{
if (ReadJObjectInternal(obj, storage))
if (version switch
{
1 => ReadJObjectInternalV1(obj, storage),
2 => ReadJObjectInternalV2(obj, storage),
_ => true,
})
saver.ImmediateSave(parent);
IsLoaded = true;
Loaded.Invoke();
});
}, TaskScheduler.Default);
return false;
}
private bool ReadJObjectInternal(JArray? obj, CollectionStorage storage)
private bool ReadJObjectInternalV1(JArray? obj, CollectionStorage storage)
{
Penumbra.Log.Debug("[Collections] Reading individual assignments...");
if (obj == null)
{
Penumbra.Log.Debug($"[Collections] Finished reading {Count} individual assignments...");
return true;
}
foreach (var data in obj)
{
try
{
var identifier = _actors.FromJson(data as JObject);
var group = GetGroup(identifier);
if (group.Length == 0 || group.Any(i => !i.IsValid))
{
Penumbra.Messager.NotificationMessage("Could not load an unknown individual collection, removed.",
NotificationType.Error);
continue;
}
var collectionName = data["Collection"]?.ToObject<string>() ?? string.Empty;
if (collectionName.Length == 0 || !storage.ByName(collectionName, out var collection))
{
Penumbra.Messager.NotificationMessage(
$"Could not load the collection \"{collectionName}\" as individual collection for {identifier}, set to None.",
NotificationType.Warning);
continue;
}
if (!Add(group, collection))
{
Penumbra.Messager.NotificationMessage($"Could not add an individual collection for {identifier}, removed.",
NotificationType.Warning);
}
}
catch (Exception e)
{
Penumbra.Messager.NotificationMessage(e, $"Could not load an unknown individual collection, removed.", NotificationType.Error);
}
}
Penumbra.Log.Debug($"Finished reading {Count} individual assignments...");
return true;
}
private bool ReadJObjectInternalV2(JArray? obj, CollectionStorage storage)
{
Penumbra.Log.Debug("[Collections] Reading individual assignments...");
if (obj == null)
@ -64,17 +122,17 @@ public partial class IndividualCollections
if (group.Length == 0 || group.Any(i => !i.IsValid))
{
changes = true;
Penumbra.Messager.NotificationMessage("Could not load an unknown individual collection, removed.",
Penumbra.Messager.NotificationMessage("Could not load an unknown individual collection, removed assignment.",
NotificationType.Error);
continue;
}
var collectionName = data["Collection"]?.ToObject<string>() ?? string.Empty;
if (collectionName.Length == 0 || !storage.ByName(collectionName, out var collection))
var collectionId = data["Collection"]?.ToObject<Guid>();
if (!collectionId.HasValue || !storage.ById(collectionId.Value, out var collection))
{
changes = true;
Penumbra.Messager.NotificationMessage(
$"Could not load the collection \"{collectionName}\" as individual collection for {identifier}, set to None.",
$"Could not load the collection {collectionId} as individual collection for {identifier}, removed assignment.",
NotificationType.Warning);
continue;
}
@ -82,14 +140,14 @@ public partial class IndividualCollections
if (!Add(group, collection))
{
changes = true;
Penumbra.Messager.NotificationMessage($"Could not add an individual collection for {identifier}, removed.",
Penumbra.Messager.NotificationMessage($"Could not add an individual collection for {identifier}, removed assignment.",
NotificationType.Warning);
}
}
catch (Exception e)
{
changes = true;
Penumbra.Messager.NotificationMessage(e, $"Could not load an unknown individual collection, removed.", NotificationType.Error);
Penumbra.Messager.NotificationMessage(e, $"Could not load an unknown individual collection, removed assignment.", NotificationType.Error);
}
}
@ -100,14 +158,6 @@ public partial class IndividualCollections
internal void Migrate0To1(Dictionary<string, ModCollection> old)
{
static bool FindDataId(string name, NameDictionary data, out NpcId dataId)
{
var kvp = data.FirstOrDefault(kvp => kvp.Value.Equals(name, StringComparison.OrdinalIgnoreCase),
new KeyValuePair<NpcId, string>(uint.MaxValue, string.Empty));
dataId = kvp.Key;
return kvp.Value.Length > 0;
}
foreach (var (name, collection) in old)
{
var kind = ObjectKind.None;
@ -155,5 +205,15 @@ public partial class IndividualCollections
NotificationType.Error);
}
}
return;
static bool FindDataId(string name, NameDictionary data, out NpcId dataId)
{
var kvp = data.FirstOrDefault(kvp => kvp.Value.Equals(name, StringComparison.OrdinalIgnoreCase),
new KeyValuePair<NpcId, string>(uint.MaxValue, string.Empty));
dataId = kvp.Key;
return kvp.Value.Length > 0;
}
}
}

View file

@ -1,12 +1,11 @@
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.ImGuiNotification;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Filesystem;
using OtterGui.Services;
using Penumbra.Communication;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.UI.CollectionTab;
using Penumbra.Util;
namespace Penumbra.Collections.Manager;
@ -15,7 +14,7 @@ namespace Penumbra.Collections.Manager;
/// This is transitive, so a collection A inheriting from B also inherits from everything B inherits.
/// Circular dependencies are resolved by distinctness.
/// </summary>
public class InheritanceManager : IDisposable
public class InheritanceManager : IDisposable, IService
{
public enum ValidInheritance
{
@ -138,18 +137,30 @@ public class InheritanceManager : IDisposable
var changes = false;
foreach (var subCollectionName in collection.InheritanceByName)
{
if (_storage.ByName(subCollectionName, out var subCollection))
if (Guid.TryParse(subCollectionName, out var guid) && _storage.ById(guid, out var subCollection))
{
if (AddInheritance(collection, subCollection, false))
continue;
changes = true;
Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", NotificationType.Warning);
Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.",
NotificationType.Warning);
}
else if (_storage.ByName(subCollectionName, out subCollection))
{
changes = true;
Penumbra.Log.Information($"Migrating inheritance for {collection.AnonymizedName} from name to GUID.");
if (AddInheritance(collection, subCollection, false))
continue;
Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.",
NotificationType.Warning);
}
else
{
Penumbra.Messager.NotificationMessage(
$"Inherited collection {subCollectionName} for {collection.AnonymizedName} does not exist, it was removed.", NotificationType.Warning);
$"Inherited collection {subCollectionName} for {collection.AnonymizedName} does not exist, it was removed.",
NotificationType.Warning);
changes = true;
}
}

View file

@ -1,8 +1,6 @@
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Subclasses;
using Penumbra.Mods.Settings;
using Penumbra.Services;
using Penumbra.Util;
namespace Penumbra.Collections.Manager;
@ -40,9 +38,9 @@ internal static class ModCollectionMigration
/// <summary> We treat every completely defaulted setting as inheritance-ready. </summary>
private static bool SettingIsDefaultV0(ModSettings.SavedSettings setting)
=> setting is { Enabled: false, Priority: 0 } && setting.Settings.Values.All(s => s == 0);
=> setting is { Enabled: true, Priority.IsDefault: true } && setting.Settings.Values.All(s => s == Setting.Zero);
/// <inheritdoc cref="SettingIsDefaultV0(ModSettings.SavedSettings)"/>
private static bool SettingIsDefaultV0(ModSettings? setting)
=> setting is { Enabled: false, Priority: 0 } && setting.Settings.All(s => s == 0);
=> setting is { Enabled: true, Priority.IsDefault: true } && setting.Settings.All(s => s == Setting.Zero);
}

View file

@ -1,3 +1,5 @@
using OtterGui;
using OtterGui.Services;
using Penumbra.Api;
using Penumbra.Communication;
using Penumbra.GameData.Actors;
@ -7,15 +9,15 @@ using Penumbra.String;
namespace Penumbra.Collections.Manager;
public class TempCollectionManager : IDisposable
public class TempCollectionManager : IDisposable, IService
{
public int GlobalChangeCounter { get; private set; } = 0;
public int GlobalChangeCounter { get; private set; }
public readonly IndividualCollections Collections;
private readonly CommunicatorService _communicator;
private readonly CollectionStorage _storage;
private readonly ActorManager _actors;
private readonly Dictionary<string, ModCollection> _customCollections = new();
private readonly CommunicatorService _communicator;
private readonly CollectionStorage _storage;
private readonly ActorManager _actors;
private readonly Dictionary<Guid, ModCollection> _customCollections = [];
public TempCollectionManager(Configuration config, CommunicatorService communicator, ActorManager actors, CollectionStorage storage)
{
@ -42,36 +44,37 @@ public class TempCollectionManager : IDisposable
=> _customCollections.Values;
public bool CollectionByName(string name, [NotNullWhen(true)] out ModCollection? collection)
=> _customCollections.TryGetValue(name.ToLowerInvariant(), out collection);
=> _customCollections.Values.FindFirst(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase), out collection);
public string CreateTemporaryCollection(string name)
public bool CollectionById(Guid id, [NotNullWhen(true)] out ModCollection? collection)
=> _customCollections.TryGetValue(id, out collection);
public Guid CreateTemporaryCollection(string name)
{
if (_storage.ByName(name, out _))
return string.Empty;
if (GlobalChangeCounter == int.MaxValue)
GlobalChangeCounter = 0;
var collection = ModCollection.CreateTemporary(name, ~Count, GlobalChangeCounter++);
Penumbra.Log.Debug($"Creating temporary collection {collection.AnonymizedName}.");
if (_customCollections.TryAdd(collection.Name.ToLowerInvariant(), collection))
var collection = _storage.CreateTemporary(name, ~Count, GlobalChangeCounter++);
Penumbra.Log.Debug($"Creating temporary collection {collection.Name} with {collection.Id}.");
if (_customCollections.TryAdd(collection.Id, collection))
{
// Temporary collection created.
_communicator.CollectionChange.Invoke(CollectionType.Temporary, null, collection, string.Empty);
return collection.Name;
return collection.Id;
}
return string.Empty;
return Guid.Empty;
}
public bool RemoveTemporaryCollection(string collectionName)
public bool RemoveTemporaryCollection(Guid collectionId)
{
if (!_customCollections.Remove(collectionName.ToLowerInvariant(), out var collection))
if (!_customCollections.Remove(collectionId, out var collection))
{
Penumbra.Log.Debug($"Tried to delete temporary collection {collectionName.ToLowerInvariant()}, but did not exist.");
Penumbra.Log.Debug($"Tried to delete temporary collection {collectionId}, but did not exist.");
return false;
}
Penumbra.Log.Debug($"Deleted temporary collection {collection.AnonymizedName}.");
_storage.Delete(collection);
Penumbra.Log.Debug($"Deleted temporary collection {collection.Id}.");
GlobalChangeCounter += Math.Max(collection.ChangeCounter + 1 - GlobalChangeCounter, 0);
for (var i = 0; i < Collections.Count; ++i)
{
@ -80,7 +83,7 @@ public class TempCollectionManager : IDisposable
// Temporary collection assignment removed.
_communicator.CollectionChange.Invoke(CollectionType.Temporary, collection, null, Collections[i].DisplayName);
Penumbra.Log.Verbose($"Unassigned temporary collection {collection.AnonymizedName} from {Collections[i].DisplayName}.");
Penumbra.Log.Verbose($"Unassigned temporary collection {collection.Id} from {Collections[i].DisplayName}.");
Collections.Delete(i--);
}
@ -98,32 +101,32 @@ public class TempCollectionManager : IDisposable
return true;
}
public bool AddIdentifier(string collectionName, params ActorIdentifier[] identifiers)
public bool AddIdentifier(Guid collectionId, params ActorIdentifier[] identifiers)
{
if (!_customCollections.TryGetValue(collectionName.ToLowerInvariant(), out var collection))
if (!_customCollections.TryGetValue(collectionId, out var collection))
return false;
return AddIdentifier(collection, identifiers);
}
public bool AddIdentifier(string collectionName, string characterName, ushort worldId = ushort.MaxValue)
public bool AddIdentifier(Guid collectionId, string characterName, ushort worldId = ushort.MaxValue)
{
if (!ByteString.FromString(characterName, out var byteString, false))
if (!ByteString.FromString(characterName, out var byteString))
return false;
var identifier = _actors.CreatePlayer(byteString, worldId);
if (!identifier.IsValid)
return false;
return AddIdentifier(collectionName, identifier);
return AddIdentifier(collectionId, identifier);
}
internal bool RemoveByCharacterName(string characterName, ushort worldId = ushort.MaxValue)
{
if (!ByteString.FromString(characterName, out var byteString, false))
if (!ByteString.FromString(characterName, out var byteString))
return false;
var identifier = _actors.CreatePlayer(byteString, worldId);
return Collections.TryGetValue(identifier, out var collection) && RemoveTemporaryCollection(collection.Name);
return Collections.TryGetValue(identifier, out var collection) && RemoveTemporaryCollection(collection.Id);
}
}

View file

@ -1,12 +1,8 @@
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.Mods;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes;
using Penumbra.Collections.Cache;
using Penumbra.Interop.Services;
using Penumbra.GameData.Data;
using Penumbra.Mods.Editor;
namespace Penumbra.Collections;
@ -47,71 +43,15 @@ public partial class ModCollection
internal MetaCache? MetaCache
=> _cache?.Meta;
public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file)
{
if (_cache != null)
return _cache.Meta.GetImcFile(path, out file);
file = null;
return false;
}
internal IReadOnlyDictionary<Utf8GamePath, ModPath> ResolvedFiles
=> _cache?.ResolvedFiles ?? new ConcurrentDictionary<Utf8GamePath, ModPath>();
internal IReadOnlyDictionary<string, (SingleArray<IMod>, object?)> ChangedItems
=> _cache?.ChangedItems ?? new Dictionary<string, (SingleArray<IMod>, object?)>();
internal IReadOnlyDictionary<string, (SingleArray<IMod>, IIdentifiedObjectData?)> ChangedItems
=> _cache?.ChangedItems ?? new Dictionary<string, (SingleArray<IMod>, IIdentifiedObjectData?)>();
internal IEnumerable<SingleArray<ModConflicts>> AllConflicts
=> _cache?.AllConflicts ?? Array.Empty<SingleArray<ModConflicts>>();
internal SingleArray<ModConflicts> Conflicts(Mod mod)
=> _cache?.Conflicts(mod) ?? new SingleArray<ModConflicts>();
public void SetFiles(CharacterUtility utility)
{
if (_cache == null)
{
utility.ResetAll();
}
else
{
_cache.Meta.SetFiles();
Penumbra.Log.Debug($"Set CharacterUtility resources for collection {Name}.");
}
}
public void SetMetaFile(CharacterUtility utility, MetaIndex idx)
{
if (_cache == null)
utility.ResetResource(idx);
else
_cache.Meta.SetFile(idx);
}
// Used for short periods of changed files.
public MetaList.MetaReverter? TemporarilySetEqdpFile(CharacterUtility utility, GenderRace genderRace, bool accessory)
{
if (_cache != null)
return _cache?.Meta.TemporarilySetEqdpFile(genderRace, accessory);
var idx = CharacterUtilityData.EqdpIdx(genderRace, accessory);
return idx >= 0 ? utility.TemporarilyResetResource(idx) : null;
}
public MetaList.MetaReverter TemporarilySetEqpFile(CharacterUtility utility)
=> _cache?.Meta.TemporarilySetEqpFile()
?? utility.TemporarilyResetResource(MetaIndex.Eqp);
public MetaList.MetaReverter TemporarilySetGmpFile(CharacterUtility utility)
=> _cache?.Meta.TemporarilySetGmpFile()
?? utility.TemporarilyResetResource(MetaIndex.Gmp);
public MetaList.MetaReverter TemporarilySetCmpFile(CharacterUtility utility)
=> _cache?.Meta.TemporarilySetCmpFile()
?? utility.TemporarilyResetResource(MetaIndex.HumanCmp);
public MetaList.MetaReverter TemporarilySetEstFile(CharacterUtility utility, EstManipulation.EstType type)
=> _cache?.Meta.TemporarilySetEstFile(type)
?? utility.TemporarilyResetResource((MetaIndex)type);
}

View file

@ -1,7 +1,7 @@
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Collections.Manager;
using Penumbra.Mods.Subclasses;
using Penumbra.Mods.Settings;
using Penumbra.Services;
namespace Penumbra.Collections;
@ -17,7 +17,7 @@ namespace Penumbra.Collections;
/// </summary>
public partial class ModCollection
{
public const int CurrentVersion = 1;
public const int CurrentVersion = 2;
public const string DefaultCollectionName = "Default";
public const string EmptyCollectionName = "None";
@ -25,17 +25,27 @@ public partial class ModCollection
/// Create the always available Empty Collection that will always sit at index 0,
/// can not be deleted and does never create a cache.
/// </summary>
public static readonly ModCollection Empty = CreateEmpty(EmptyCollectionName, 0, 0);
public static readonly ModCollection Empty = new(Guid.Empty, EmptyCollectionName, LocalCollectionId.Zero, 0, 0, CurrentVersion, [], [], []);
/// <summary> The name of a collection can not contain characters invalid in a path. </summary>
public string Name { get; internal init; }
/// <summary> The name of a collection. </summary>
public string Name { get; set; }
public Guid Id { get; }
public LocalCollectionId LocalId { get; }
public string Identifier
=> Id.ToString();
public string ShortIdentifier
=> Identifier[..8];
public override string ToString()
=> Name;
=> Name.Length > 0 ? Name : ShortIdentifier;
/// <summary> Get the first two letters of a collection name and its Index (or None if it is the empty collection). </summary>
public string AnonymizedName
=> this == Empty ? Empty.Name : Name.Length > 2 ? $"{Name[..2]}... ({Index})" : $"{Name} ({Index})";
=> this == Empty ? Empty.Name : Name == DefaultCollectionName ? Name : ShortIdentifier;
/// <summary> The index of the collection is set and kept up-to-date by the CollectionManager. </summary>
public int Index { get; internal set; }
@ -46,6 +56,8 @@ public partial class ModCollection
/// </summary>
public int ChangeCounter { get; private set; }
public uint ImcChangeCounter { get; set; }
/// <summary> Increment the number of changes in the effective file list. </summary>
public int IncrementCounter()
=> ++ChangeCounter;
@ -109,19 +121,20 @@ public partial class ModCollection
/// <summary>
/// Constructor for duplication. Deep copies all settings and parent collections and adds the new collection to their children lists.
/// </summary>
public ModCollection Duplicate(string name, int index)
public ModCollection Duplicate(string name, LocalCollectionId localId, int index)
{
Debug.Assert(index > 0, "Collection duplicated with non-positive index.");
return new ModCollection(name, index, 0, CurrentVersion, Settings.Select(s => s?.DeepCopy()).ToList(),
return new ModCollection(Guid.NewGuid(), name, localId, index, 0, CurrentVersion, Settings.Select(s => s?.DeepCopy()).ToList(),
[.. DirectlyInheritsFrom], UnusedSettings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DeepCopy()));
}
/// <summary> Constructor for reading from files. </summary>
public static ModCollection CreateFromData(SaveService saver, ModStorage mods, string name, int version, int index,
public static ModCollection CreateFromData(SaveService saver, ModStorage mods, Guid id, string name, LocalCollectionId localId, int version,
int index,
Dictionary<string, ModSettings.SavedSettings> allSettings, IReadOnlyList<string> inheritances)
{
Debug.Assert(index > 0, "Collection read with non-positive index.");
var ret = new ModCollection(name, index, 0, version, new List<ModSettings?>(), new List<ModCollection>(), allSettings)
var ret = new ModCollection(id, name, localId, index, 0, version, [], [], allSettings)
{
InheritanceByName = inheritances,
};
@ -131,21 +144,20 @@ public partial class ModCollection
}
/// <summary> Constructor for temporary collections. </summary>
public static ModCollection CreateTemporary(string name, int index, int changeCounter)
public static ModCollection CreateTemporary(string name, LocalCollectionId localId, int index, int changeCounter)
{
Debug.Assert(index < 0, "Temporary collection created with non-negative index.");
var ret = new ModCollection(name, index, changeCounter, CurrentVersion, new List<ModSettings?>(), new List<ModCollection>(),
new Dictionary<string, ModSettings.SavedSettings>());
var ret = new ModCollection(Guid.NewGuid(), name, localId, index, changeCounter, CurrentVersion, [], [], []);
return ret;
}
/// <summary> Constructor for empty collections. </summary>
public static ModCollection CreateEmpty(string name, int index, int modCount)
public static ModCollection CreateEmpty(string name, LocalCollectionId localId, int index, int modCount)
{
Debug.Assert(index >= 0, "Empty collection created with negative index.");
return new ModCollection(name, index, 0, CurrentVersion, Enumerable.Repeat((ModSettings?)null, modCount).ToList(),
new List<ModCollection>(),
new Dictionary<string, ModSettings.SavedSettings>());
return new ModCollection(Guid.NewGuid(), name, localId, index, 0, CurrentVersion,
Enumerable.Repeat((ModSettings?)null, modCount).ToList(), [],
[]);
}
/// <summary> Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. </summary>
@ -193,10 +205,12 @@ public partial class ModCollection
saver.ImmediateSave(new ModCollectionSave(mods, this));
}
private ModCollection(string name, int index, int changeCounter, int version, List<ModSettings?> appliedSettings,
List<ModCollection> inheritsFrom, Dictionary<string, ModSettings.SavedSettings> settings)
private ModCollection(Guid id, string name, LocalCollectionId localId, int index, int changeCounter, int version,
List<ModSettings?> appliedSettings, List<ModCollection> inheritsFrom, Dictionary<string, ModSettings.SavedSettings> settings)
{
Name = name;
Id = id;
LocalId = localId;
Index = index;
ChangeCounter = changeCounter;
Settings = appliedSettings;

View file

@ -1,32 +1,21 @@
using Newtonsoft.Json.Linq;
using Penumbra.Mods;
using Penumbra.Services;
using Newtonsoft.Json;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Subclasses;
using Penumbra.Util;
using Penumbra.Mods.Settings;
namespace Penumbra.Collections;
/// <summary>
/// Handle saving and loading a collection.
/// </summary>
internal readonly struct ModCollectionSave : ISavable
internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection modCollection) : ISavable
{
private readonly ModStorage _modStorage;
private readonly ModCollection _modCollection;
public ModCollectionSave(ModStorage modStorage, ModCollection modCollection)
{
_modStorage = modStorage;
_modCollection = modCollection;
}
public string ToFilename(FilenameService fileNames)
=> fileNames.CollectionFile(_modCollection);
=> fileNames.CollectionFile(modCollection);
public string LogName(string _)
=> _modCollection.AnonymizedName;
=> modCollection.AnonymizedName;
public string TypeName
=> "Collection";
@ -39,21 +28,23 @@ internal readonly struct ModCollectionSave : ISavable
j.WriteStartObject();
j.WritePropertyName("Version");
j.WriteValue(ModCollection.CurrentVersion);
j.WritePropertyName(nameof(ModCollection.Id));
j.WriteValue(modCollection.Identifier);
j.WritePropertyName(nameof(ModCollection.Name));
j.WriteValue(_modCollection.Name);
j.WriteValue(modCollection.Name);
j.WritePropertyName(nameof(ModCollection.Settings));
// Write all used and unused settings by mod directory name.
j.WriteStartObject();
var list = new List<(string, ModSettings.SavedSettings)>(_modCollection.Settings.Count + _modCollection.UnusedSettings.Count);
for (var i = 0; i < _modCollection.Settings.Count; ++i)
var list = new List<(string, ModSettings.SavedSettings)>(modCollection.Settings.Count + modCollection.UnusedSettings.Count);
for (var i = 0; i < modCollection.Settings.Count; ++i)
{
var settings = _modCollection.Settings[i];
var settings = modCollection.Settings[i];
if (settings != null)
list.Add((_modStorage[i].ModPath.Name, new ModSettings.SavedSettings(settings, _modStorage[i])));
list.Add((modStorage[i].ModPath.Name, new ModSettings.SavedSettings(settings, modStorage[i])));
}
list.AddRange(_modCollection.UnusedSettings.Select(kvp => (kvp.Key, kvp.Value)));
list.AddRange(modCollection.UnusedSettings.Select(kvp => (kvp.Key, kvp.Value)));
list.Sort((a, b) => string.Compare(a.Item1, b.Item1, StringComparison.OrdinalIgnoreCase));
foreach (var (modDir, settings) in list)
@ -66,20 +57,20 @@ internal readonly struct ModCollectionSave : ISavable
// Inherit by collection name.
j.WritePropertyName("Inheritance");
x.Serialize(j, _modCollection.InheritanceByName ?? _modCollection.DirectlyInheritsFrom.Select(c => c.Name));
x.Serialize(j, modCollection.InheritanceByName ?? modCollection.DirectlyInheritsFrom.Select(c => c.Identifier));
j.WriteEndObject();
}
public static bool LoadFromFile(FileInfo file, out string name, out int version, out Dictionary<string, ModSettings.SavedSettings> settings,
public static bool LoadFromFile(FileInfo file, out Guid id, out string name, out int version, out Dictionary<string, ModSettings.SavedSettings> settings,
out IReadOnlyList<string> inheritance)
{
settings = new Dictionary<string, ModSettings.SavedSettings>();
inheritance = Array.Empty<string>();
settings = [];
inheritance = [];
if (!file.Exists)
{
Penumbra.Log.Error("Could not read collection because file does not exist.");
name = string.Empty;
name = string.Empty;
id = Guid.Empty;
version = 0;
return false;
}
@ -87,8 +78,9 @@ internal readonly struct ModCollectionSave : ISavable
try
{
var obj = JObject.Parse(File.ReadAllText(file.FullName));
name = obj[nameof(ModCollection.Name)]?.ToObject<string>() ?? string.Empty;
version = obj["Version"]?.ToObject<int>() ?? 0;
name = obj[nameof(ModCollection.Name)]?.ToObject<string>() ?? string.Empty;
id = obj[nameof(ModCollection.Id)]?.ToObject<Guid>() ?? (version == 1 ? Guid.NewGuid() : Guid.Empty);
// Custom deserialization that is converted with the constructor.
settings = obj[nameof(ModCollection.Settings)]?.ToObject<Dictionary<string, ModSettings.SavedSettings>>() ?? settings;
inheritance = obj["Inheritance"]?.ToObject<List<string>>() ?? inheritance;
@ -98,6 +90,7 @@ internal readonly struct ModCollectionSave : ISavable
{
name = string.Empty;
version = 0;
id = Guid.Empty;
Penumbra.Log.Error($"Could not read collection information from file:\n{e}");
return false;
}

View file

@ -1,15 +1,15 @@
namespace Penumbra.Collections;
public readonly struct ResolveData
public readonly struct ResolveData(ModCollection collection, nint gameObject)
{
public static readonly ResolveData Invalid = new();
private readonly ModCollection? _modCollection;
private readonly ModCollection? _modCollection = collection;
public ModCollection ModCollection
=> _modCollection ?? ModCollection.Empty;
public readonly nint AssociatedGameObject;
public readonly nint AssociatedGameObject = gameObject;
public bool Valid
=> _modCollection != null;
@ -18,12 +18,6 @@ public readonly struct ResolveData
: this(null!, nint.Zero)
{ }
public ResolveData(ModCollection collection, nint gameObject)
{
_modCollection = collection;
AssociatedGameObject = gameObject;
}
public ResolveData(ModCollection collection)
: this(collection, nint.Zero)
{ }

View file

@ -3,6 +3,7 @@ using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Services;
using ImGuiNET;
using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
@ -10,12 +11,12 @@ using Penumbra.GameData.Actors;
using Penumbra.Interop.Services;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.UI;
using Penumbra.UI.Knowledge;
namespace Penumbra;
public class CommandHandler : IDisposable
public class CommandHandler : IDisposable, IApiService
{
private const string CommandName = "/penumbra";
@ -29,11 +30,12 @@ public class CommandHandler : IDisposable
private readonly CollectionManager _collectionManager;
private readonly Penumbra _penumbra;
private readonly CollectionEditor _collectionEditor;
private readonly KnowledgeWindow _knowledgeWindow;
public CommandHandler(IFramework framework, ICommandManager commandManager, IChatGui chat, RedrawService redrawService,
Configuration config,
ConfigWindow configWindow, ModManager modManager, CollectionManager collectionManager, ActorManager actors, Penumbra penumbra,
CollectionEditor collectionEditor)
Configuration config, ConfigWindow configWindow, ModManager modManager, CollectionManager collectionManager, ActorManager actors,
Penumbra penumbra,
CollectionEditor collectionEditor, KnowledgeWindow knowledgeWindow)
{
_commandManager = commandManager;
_redrawService = redrawService;
@ -45,6 +47,7 @@ public class CommandHandler : IDisposable
_chat = chat;
_penumbra = penumbra;
_collectionEditor = collectionEditor;
_knowledgeWindow = knowledgeWindow;
framework.RunOnFrameworkThread(() =>
{
if (_commandManager.Commands.ContainsKey(CommandName))
@ -69,7 +72,7 @@ public class CommandHandler : IDisposable
var argumentList = arguments.Split(' ', 2);
arguments = argumentList.Length == 2 ? argumentList[1] : string.Empty;
var _ = argumentList[0].ToLowerInvariant() switch
_ = argumentList[0].ToLowerInvariant() switch
{
"window" => ToggleWindow(arguments),
"enable" => SetPenumbraState(arguments, true),
@ -83,6 +86,7 @@ public class CommandHandler : IDisposable
"collection" => SetCollection(arguments),
"mod" => SetMod(arguments),
"bulktag" => SetTag(arguments),
"knowledge" => HandleKnowledge(arguments),
_ => PrintHelp(argumentList[0]),
};
}
@ -304,7 +308,7 @@ public class CommandHandler : IDisposable
identifiers = _actors.FromUserString(split[2], false);
}
}
catch (ActorManager.IdentifierParseError e)
catch (ActorIdentifierFactory.IdentifierParseError e)
{
_chat.Print(new SeStringBuilder().AddText("The argument ").AddRed(split[2], true)
.AddText($" could not be converted to an identifier. {e.Message}")
@ -513,7 +517,7 @@ public class CommandHandler : IDisposable
collection = string.Equals(lowerName, ModCollection.Empty.Name, StringComparison.OrdinalIgnoreCase)
? ModCollection.Empty
: _collectionManager.Storage.ByName(lowerName, out var c)
: _collectionManager.Storage.ByIdentifier(lowerName, out var c)
? c
: null;
if (collection != null)
@ -619,4 +623,10 @@ public class CommandHandler : IDisposable
if (_config.PrintSuccessfulCommandsToChat)
_chat.Print(text());
}
private bool HandleKnowledge(string arguments)
{
_knowledgeWindow.Toggle();
return true;
}
}

View file

@ -1,5 +1,7 @@
using OtterGui.Classes;
using Penumbra.Api.Api;
using Penumbra.Api.Enums;
using Penumbra.GameData.Data;
namespace Penumbra.Communication;
@ -10,11 +12,11 @@ namespace Penumbra.Communication;
/// <item>Parameter is the clicked object data if any. </item>
/// </list>
/// </summary>
public sealed class ChangedItemClick() : EventWrapper<MouseButton, object?, ChangedItemClick.Priority>(nameof(ChangedItemClick))
public sealed class ChangedItemClick() : EventWrapper<MouseButton, IIdentifiedObjectData?, ChangedItemClick.Priority>(nameof(ChangedItemClick))
{
public enum Priority
{
/// <seealso cref="Api.PenumbraApi.ChangedItemClicked"/>
/// <seealso cref="UiApi.OnChangedItemClick"/>
Default = 0,
/// <seealso cref="Penumbra.SetupApi"/>

View file

@ -1,4 +1,6 @@
using OtterGui.Classes;
using Penumbra.Api.Api;
using Penumbra.GameData.Data;
namespace Penumbra.Communication;
@ -8,11 +10,11 @@ namespace Penumbra.Communication;
/// <item>Parameter is the hovered object data if any. </item>
/// </list>
/// </summary>
public sealed class ChangedItemHover() : EventWrapper<object?, ChangedItemHover.Priority>(nameof(ChangedItemHover))
public sealed class ChangedItemHover() : EventWrapper<IIdentifiedObjectData?, ChangedItemHover.Priority>(nameof(ChangedItemHover))
{
public enum Priority
{
/// <seealso cref="Api.PenumbraApi.ChangedItemTooltip"/>
/// <seealso cref="UiApi.OnChangedItemHover"/>
Default = 0,
/// <seealso cref="Penumbra.SetupApi"/>

View file

@ -1,5 +1,6 @@
using OtterGui.Classes;
using Penumbra.Api;
using Penumbra.Api.Api;
using Penumbra.Collections;
namespace Penumbra.Communication;

View file

@ -1,5 +1,6 @@
using OtterGui.Classes;
using Penumbra.Api;
using Penumbra.Api.Api;
using Penumbra.Services;
namespace Penumbra.Communication;
@ -13,11 +14,14 @@ namespace Penumbra.Communication;
/// <item>Parameter is a pointer to the equip data array. </item>
/// </list> </summary>
public sealed class CreatingCharacterBase()
: EventWrapper<nint, string, nint, nint, nint, CreatingCharacterBase.Priority>(nameof(CreatingCharacterBase))
: EventWrapper<nint, Guid, nint, nint, nint, CreatingCharacterBase.Priority>(nameof(CreatingCharacterBase))
{
public enum Priority
{
/// <seealso cref="PenumbraApi.CreatingCharacterBase"/>
/// <seealso cref="GameStateApi.CreatingCharacterBase"/>
Api = 0,
/// <seealso cref="CrashHandlerService.OnCreatingCharacterBase"/>
CrashHandler = 0,
}
}

View file

@ -1,5 +1,6 @@
using OtterGui.Classes;
using Penumbra.Api;
using Penumbra.Api.IpcSubscribers;
namespace Penumbra.Communication;
@ -13,7 +14,7 @@ public sealed class EnabledChanged() : EventWrapper<bool, EnabledChanged.Priorit
{
public enum Priority
{
/// <seealso cref="Ipc.EnabledChange"/>
/// <seealso cref="Api.IpcSubscribers.Ipc.EnabledChange"/>
Api = int.MinValue,
/// <seealso cref="Api.DalamudSubstitutionProvider.OnEnabledChange"/>

View file

@ -1,5 +1,5 @@
using OtterGui.Classes;
using Penumbra.Api;
using Penumbra.Api.Api;
namespace Penumbra.Communication;
@ -14,7 +14,7 @@ public sealed class ModDirectoryChanged() : EventWrapper<string, bool, ModDirect
{
public enum Priority
{
/// <seealso cref="PenumbraApi.ModDirectoryChanged"/>
/// <seealso cref="PluginStateApi.ModDirectoryChanged"/>
Api = 0,
/// <seealso cref="UI.FileDialogService.OnModDirectoryChange"/>

View file

@ -0,0 +1,29 @@
using OtterGui.Classes;
using Penumbra.Api;
using Penumbra.Api.Api;
using Penumbra.Mods;
using Penumbra.Mods.Editor;
namespace Penumbra.Communication;
/// <summary>
/// Triggered whenever an existing file in a mod is overwritten by Penumbra.
/// <list type="number">
/// <item>Parameter is the changed mod. </item>
/// <item>Parameter file registry of the changed file. </item>
/// </list> </summary>
public sealed class ModFileChanged()
: EventWrapper<Mod, FileRegistry, ModFileChanged.Priority>(nameof(ModFileChanged))
{
public enum Priority
{
/// <seealso cref="PenumbraApi.OnModFileChanged"/>
Api = int.MinValue,
/// <seealso cref="Interop.Services.RedrawService.OnModFileChanged"/>
RedrawService = -50,
/// <seealso cref="Collections.Manager.CollectionStorage.OnModFileChanged"/>
CollectionStorage = 0,
}
}

View file

@ -1,6 +1,11 @@
using OtterGui.Classes;
using Penumbra.Api.Api;
using Penumbra.Mods;
using Penumbra.Mods.Groups;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Manager.OptionEditor;
using Penumbra.Mods.SubMods;
using static Penumbra.Communication.ModOptionChanged;
namespace Penumbra.Communication;
@ -9,19 +14,23 @@ namespace Penumbra.Communication;
/// <list type="number">
/// <item>Parameter is the type option change. </item>
/// <item>Parameter is the changed mod. </item>
/// <item>Parameter is the index of the changed group inside the mod. </item>
/// <item>Parameter is the index of the changed option inside the group or -1 if it does not concern a specific option. </item>
/// <item>Parameter is the index of the group an option was moved to. </item>
/// <item>Parameter is the changed group inside the mod. </item>
/// <item>Parameter is the changed option inside the group or null if it does not concern a specific option. </item>
/// <item>Parameter is the changed data container inside the group or null if it does not concern a specific data container. </item>
/// <item>Parameter is the index of the group or option moved or deleted from. </item>
/// </list> </summary>
public sealed class ModOptionChanged()
: EventWrapper<ModOptionChangeType, Mod, int, int, int, ModOptionChanged.Priority>(nameof(ModOptionChanged))
: EventWrapper<ModOptionChangeType, Mod, IModGroup?, IModOption?, IModDataContainer?, int, Priority>(nameof(ModOptionChanged))
{
public enum Priority
{
/// <seealso cref="ModSettingsApi.OnModOptionEdited"/>
Api = int.MinValue,
/// <seealso cref="Collections.Cache.CollectionCacheManager.OnModOptionChange"/>
CollectionCacheManager = -100,
/// <seealso cref="Mods.Manager.ModCacheManager.OnModOptionChange"/>
/// <seealso cref="ModCacheManager.OnModOptionChange"/>
ModCacheManager = 0,
/// <seealso cref="UI.AdvancedWindow.ItemSwapTab.OnModOptionChange"/>

View file

@ -1,5 +1,6 @@
using OtterGui.Classes;
using Penumbra.Api;
using Penumbra.Api.Api;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
@ -19,15 +20,18 @@ public sealed class ModPathChanged()
{
public enum Priority
{
/// <seealso cref="ModsApi.OnModPathChange"/>
ApiMods = int.MinValue,
/// <seealso cref="ModSettingsApi.OnModPathChange"/>
ApiModSettings = int.MinValue,
/// <seealso cref="EphemeralConfig.OnModPathChanged"/>
EphemeralConfig = -500,
/// <seealso cref="Collections.Cache.CollectionCacheManager.OnModChangeAddition"/>
CollectionCacheManagerAddition = -100,
/// <seealso cref="PenumbraApi.ModPathChangeSubscriber"/>
Api = 0,
/// <seealso cref="Mods.Manager.ModCacheManager.OnModPathChange"/>
ModCacheManager = 0,

View file

@ -1,8 +1,10 @@
using OtterGui.Classes;
using Penumbra.Api;
using Penumbra.Api.Api;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Mods;
using Penumbra.Mods.Settings;
namespace Penumbra.Communication;
@ -12,17 +14,17 @@ namespace Penumbra.Communication;
/// <item>Parameter is the collection in which the setting was changed. </item>
/// <item>Parameter is the type of change. </item>
/// <item>Parameter is the mod the setting was changed for, unless it was a multi-change. </item>
/// <item>Parameter is the old value of the setting before the change as int. </item>
/// <item>Parameter is the old value of the setting before the change as Setting. </item>
/// <item>Parameter is the index of the changed group if the change type is Setting. </item>
/// <item>Parameter is whether the change was inherited from another collection. </item>
/// </list>
/// </summary>
public sealed class ModSettingChanged()
: EventWrapper<ModCollection, ModSettingChange, Mod?, int, int, bool, ModSettingChanged.Priority>(nameof(ModSettingChanged))
: EventWrapper<ModCollection, ModSettingChange, Mod?, Setting, int, bool, ModSettingChanged.Priority>(nameof(ModSettingChanged))
{
public enum Priority
{
/// <seealso cref="PenumbraApi.OnModSettingChange"/>
/// <seealso cref="ModSettingsApi.OnModSettingChange"/>
Api = int.MinValue,
/// <seealso cref="Collections.Cache.CollectionCacheManager.OnModSettingChange"/>

View file

@ -6,11 +6,11 @@ namespace Penumbra.Communication;
/// <item>Parameter is the material resource handle for which the shader package has been loaded. </item>
/// <item>Parameter is the associated game object. </item>
/// </list> </summary>
public sealed class MtrlShpkLoaded() : EventWrapper<nint, nint, MtrlShpkLoaded.Priority>(nameof(MtrlShpkLoaded))
public sealed class MtrlLoaded() : EventWrapper<nint, nint, MtrlLoaded.Priority>(nameof(MtrlLoaded))
{
public enum Priority
{
/// <seealso cref="Interop.Services.SkinFixer.OnMtrlShpkLoaded"/>
SkinFixer = 0,
/// <seealso cref="Interop.Hooks.PostProcessing.ShaderReplacementFixer.OnMtrlLoaded"/>
ShaderReplacementFixer = 0,
}
}

View file

@ -0,0 +1,19 @@
using OtterGui.Classes;
using Penumbra.Api.Api;
namespace Penumbra.Communication;
/// <summary>
/// Triggered after the Enabled Checkbox line in settings is drawn, but before options are drawn.
/// <list type="number">
/// <item>Parameter is the identifier (directory name) of the currently selected mod. </item>
/// </list>
/// </summary>
public sealed class PostEnabledDraw() : EventWrapper<string, PostEnabledDraw.Priority>(nameof(PostEnabledDraw))
{
public enum Priority
{
/// <seealso cref="PenumbraApi.PostEnabledDraw"/>
Default = 0,
}
}

View file

@ -1,4 +1,5 @@
using OtterGui.Classes;
using Penumbra.Api.Api;
namespace Penumbra.Communication;
@ -12,7 +13,7 @@ public sealed class PostSettingsPanelDraw() : EventWrapper<string, PostSettingsP
{
public enum Priority
{
/// <seealso cref="Api.PenumbraApi.PostSettingsPanelDraw"/>
/// <seealso cref="PenumbraApi.PostSettingsPanelDraw"/>
Default = 0,
}
}

View file

@ -1,4 +1,5 @@
using OtterGui.Classes;
using Penumbra.Api.Api;
namespace Penumbra.Communication;
@ -12,7 +13,7 @@ public sealed class PreSettingsPanelDraw() : EventWrapper<string, PreSettingsPan
{
public enum Priority
{
/// <seealso cref="Api.PenumbraApi.PreSettingsPanelDraw"/>
/// <seealso cref="PenumbraApi.PreSettingsPanelDraw"/>
Default = 0,
}
}

View file

@ -0,0 +1,22 @@
using OtterGui.Classes;
using Penumbra.Api.Api;
using Penumbra.Api.IpcSubscribers;
namespace Penumbra.Communication;
/// <summary>
/// Triggered before the settings tab bar for a mod is drawn, after the title group is drawn.
/// <list type="number">
/// <item>Parameter is the identifier (directory name) of the currently selected mod. </item>
/// <item>is the total width of the header group. </item>
/// <item>is the width of the title box. </item>
/// </list>
/// </summary>
public sealed class PreSettingsTabBarDraw() : EventWrapper<string, float, float, PreSettingsTabBarDraw.Priority>(nameof(PreSettingsTabBarDraw))
{
public enum Priority
{
/// <seealso cref="Api.IpcSubscribers.PreSettingsTabBarDraw"/>
Default = 0,
}
}

View file

@ -1,9 +1,10 @@
using Dalamud.Configuration;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.ImGuiNotification;
using Newtonsoft.Json;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Filesystem;
using OtterGui.Services;
using OtterGui.Widgets;
using Penumbra.Import.Structs;
using Penumbra.Interop.Services;
@ -11,13 +12,14 @@ using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.UI.Classes;
using Penumbra.UI.ModsTab;
using Penumbra.UI.ResourceWatcher;
using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
namespace Penumbra;
[Serializable]
public class Configuration : IPluginConfiguration, ISavable
public class Configuration : IPluginConfiguration, ISavable, IService
{
[JsonIgnore]
private readonly SaveService _saveService;
@ -29,28 +31,45 @@ public class Configuration : IPluginConfiguration, ISavable
public ChangeLogDisplayType ChangeLogDisplayType { get; set; } = ChangeLogDisplayType.New;
public bool EnableMods { get; set; } = true;
public event Action<bool>? ModsEnabled;
[JsonIgnore]
private bool _enableMods = true;
public bool EnableMods
{
get => _enableMods;
set
{
_enableMods = value;
ModsEnabled?.Invoke(value);
}
}
public string ModDirectory { get; set; } = string.Empty;
public string ExportDirectory { get; set; } = string.Empty;
public bool OpenWindowAtStart { get; set; } = false;
public bool HideUiInGPose { get; set; } = false;
public bool HideUiInCutscenes { get; set; } = true;
public bool HideUiWhenUiHidden { get; set; } = false;
public bool UseDalamudUiTextureRedirection { get; set; } = true;
public bool? UseCrashHandler { get; set; } = null;
public bool OpenWindowAtStart { get; set; } = false;
public bool HideUiInGPose { get; set; } = false;
public bool HideUiInCutscenes { get; set; } = true;
public bool HideUiWhenUiHidden { get; set; } = false;
public bool UseDalamudUiTextureRedirection { get; set; } = true;
public bool UseCharacterCollectionInMainWindow { get; set; } = true;
public bool UseCharacterCollectionsInCards { get; set; } = true;
public bool UseCharacterCollectionInInspect { get; set; } = true;
public bool UseCharacterCollectionInTryOn { get; set; } = true;
public bool UseOwnerNameForCharacterCollection { get; set; } = true;
public bool UseNoModsInInspect { get; set; } = false;
public bool HideChangedItemFilters { get; set; } = false;
public bool ReplaceNonAsciiOnImport { get; set; } = false;
public bool HidePrioritiesInSelector { get; set; } = false;
public bool HideRedrawBar { get; set; } = false;
public int OptionGroupCollapsibleMin { get; set; } = 5;
public bool ShowModsInLobby { get; set; } = true;
public bool UseCharacterCollectionInMainWindow { get; set; } = true;
public bool UseCharacterCollectionsInCards { get; set; } = true;
public bool UseCharacterCollectionInInspect { get; set; } = true;
public bool UseCharacterCollectionInTryOn { get; set; } = true;
public bool UseOwnerNameForCharacterCollection { get; set; } = true;
public bool UseNoModsInInspect { get; set; } = false;
public bool HideChangedItemFilters { get; set; } = false;
public bool ReplaceNonAsciiOnImport { get; set; } = false;
public bool HidePrioritiesInSelector { get; set; } = false;
public bool HideRedrawBar { get; set; } = false;
public bool HideMachinistOffhandFromChangedItems { get; set; } = true;
public RenameField ShowRename { get; set; } = RenameField.BothDataPrio;
public int OptionGroupCollapsibleMin { get; set; } = 5;
public Vector2 MinimumSize = new(Constants.MinimumSizeX, Constants.MinimumSizeY);
@ -77,13 +96,18 @@ public class Configuration : IPluginConfiguration, ISavable
public DoubleModifier DeleteModModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift);
public bool PrintSuccessfulCommandsToChat { get; set; } = true;
public bool AutoDeduplicateOnImport { get; set; } = true;
public bool AutoReduplicateUiOnImport { get; set; } = true;
public bool UseFileSystemCompression { get; set; } = true;
public bool EnableHttpApi { get; set; } = true;
public bool MigrateImportedModelsToV6 { get; set; } = true;
public bool MigrateImportedMaterialsToLegacy { get; set; } = true;
public string DefaultModImportPath { get; set; } = string.Empty;
public bool AlwaysOpenDefaultImport { get; set; } = false;
public bool KeepDefaultMetaChanges { get; set; } = false;
public string DefaultModAuthor { get; set; } = DefaultTexToolsData.Author;
public bool EditRawTileTransforms { get; set; } = false;
public Dictionary<ColorId, uint> Colors { get; set; }
= Enum.GetValues<ColorId>().ToDictionary(c => c, c => c.Data().DefaultColor);
@ -134,7 +158,7 @@ public class Configuration : IPluginConfiguration, ISavable
/// <summary> Contains some default values or boundaries for config values. </summary>
public static class Constants
{
public const int CurrentVersion = 8;
public const int CurrentVersion = 9;
public const float MaxAbsoluteSize = 600;
public const int DefaultAbsoluteSize = 250;
public const float MinAbsoluteSize = 50;

View file

@ -60,6 +60,13 @@ public enum ResourceTypeFlag : ulong
Uld = 0x0002_0000_0000_0000,
Waoe = 0x0004_0000_0000_0000,
Wtd = 0x0008_0000_0000_0000,
Bklb = 0x0010_0000_0000_0000,
Cutb = 0x0020_0000_0000_0000,
Eanb = 0x0040_0000_0000_0000,
Eslb = 0x0080_0000_0000_0000,
Fpeb = 0x0100_0000_0000_0000,
Kdb = 0x0200_0000_0000_0000,
Kdlb = 0x0400_0000_0000_0000,
}
[Flags]
@ -141,6 +148,13 @@ public static class ResourceExtensions
ResourceType.Uld => ResourceTypeFlag.Uld,
ResourceType.Waoe => ResourceTypeFlag.Waoe,
ResourceType.Wtd => ResourceTypeFlag.Wtd,
ResourceType.Bklb => ResourceTypeFlag.Bklb,
ResourceType.Cutb => ResourceTypeFlag.Cutb,
ResourceType.Eanb => ResourceTypeFlag.Eanb,
ResourceType.Eslb => ResourceTypeFlag.Eslb,
ResourceType.Fpeb => ResourceTypeFlag.Fpeb,
ResourceType.Kdb => ResourceTypeFlag.Kdb ,
ResourceType.Kdlb => ResourceTypeFlag.Kdlb,
_ => 0,
};
@ -148,7 +162,7 @@ public static class ResourceExtensions
=> (type.ToFlag() & flags) != 0;
public static ResourceCategoryFlag ToFlag(this ResourceCategory type)
=> type switch
=> (ResourceCategory)((uint) type & 0x00FFFFFF) switch
{
ResourceCategory.Common => ResourceCategoryFlag.Common,
ResourceCategory.BgCommon => ResourceCategoryFlag.BgCommon,
@ -202,10 +216,10 @@ public static class ResourceExtensions
};
}
public static ResourceType Type(ByteString path)
public static ResourceType Type(CiByteString path)
{
var extIdx = path.LastIndexOf((byte)'.');
var ext = extIdx == -1 ? path : extIdx == path.Length - 1 ? ByteString.Empty : path.Substring(extIdx + 1);
var ext = extIdx == -1 ? path : extIdx == path.Length - 1 ? CiByteString.Empty : path.Substring(extIdx + 1);
return ext.Length switch
{
@ -217,7 +231,7 @@ public static class ResourceExtensions
};
}
public static ResourceCategory Category(ByteString path)
public static ResourceCategory Category(CiByteString path)
{
if (path.Length < 3)
return ResourceCategory.Debug;

View file

@ -1,6 +1,7 @@
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.ImGuiNotification;
using Newtonsoft.Json;
using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Communication;
using Penumbra.Enums;
@ -14,7 +15,7 @@ using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
namespace Penumbra;
public class EphemeralConfig : ISavable, IDisposable
public class EphemeralConfig : ISavable, IDisposable, IService
{
[JsonIgnore]
private readonly SaveService _saveService;
@ -22,23 +23,24 @@ public class EphemeralConfig : ISavable, IDisposable
[JsonIgnore]
private readonly ModPathChanged _modPathChanged;
public int Version { get; set; } = Configuration.Constants.CurrentVersion;
public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion;
public bool DebugSeparateWindow { get; set; } = false;
public int TutorialStep { get; set; } = 0;
public bool EnableResourceLogging { get; set; } = false;
public string ResourceLoggingFilter { get; set; } = string.Empty;
public bool EnableResourceWatcher { get; set; } = false;
public bool OnlyAddMatchingResources { get; set; } = true;
public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes;
public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories;
public RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords;
public CollectionsTab.PanelMode CollectionPanel { get; set; } = CollectionsTab.PanelMode.SimpleAssignment;
public TabType SelectedTab { get; set; } = TabType.Settings;
public ChangedItemDrawer.ChangedItemIcon ChangedItemFilter { get; set; } = ChangedItemDrawer.DefaultFlags;
public bool FixMainWindow { get; set; } = false;
public string LastModPath { get; set; } = string.Empty;
public bool AdvancedEditingOpen { get; set; } = false;
public int Version { get; set; } = Configuration.Constants.CurrentVersion;
public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion;
public bool DebugSeparateWindow { get; set; } = false;
public int TutorialStep { get; set; } = 0;
public bool EnableResourceLogging { get; set; } = false;
public string ResourceLoggingFilter { get; set; } = string.Empty;
public bool EnableResourceWatcher { get; set; } = false;
public bool OnlyAddMatchingResources { get; set; } = true;
public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes;
public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories;
public RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords;
public CollectionsTab.PanelMode CollectionPanel { get; set; } = CollectionsTab.PanelMode.SimpleAssignment;
public TabType SelectedTab { get; set; } = TabType.Settings;
public ChangedItemIconFlag ChangedItemFilter { get; set; } = ChangedItemFlagExtensions.DefaultFlags;
public bool FixMainWindow { get; set; } = false;
public string LastModPath { get; set; } = string.Empty;
public bool AdvancedEditingOpen { get; set; } = false;
public bool ForceRedrawOnFileChange { get; set; } = false;
/// <summary>
/// Load the current configuration.
@ -46,7 +48,7 @@ public class EphemeralConfig : ISavable, IDisposable
/// </summary>
public EphemeralConfig(SaveService saveService, ModPathChanged modPathChanged)
{
_saveService = saveService;
_saveService = saveService;
_modPathChanged = modPathChanged;
Load();
_modPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.EphemeralConfig);
@ -93,13 +95,13 @@ public class EphemeralConfig : ISavable, IDisposable
public void Save(StreamWriter writer)
{
using var jWriter = new JsonTextWriter(writer);
using var jWriter = new JsonTextWriter(writer);
jWriter.Formatting = Formatting.Indented;
var serializer = new JsonSerializer { Formatting = Formatting.Indented };
var serializer = new JsonSerializer { Formatting = Formatting.Indented };
serializer.Serialize(jWriter, this);
}
/// <summary> Overwrite the last saved mod path if it changes. </summary>
/// <summary> Overwrite the last saved mod path if it changes. </summary>
private void OnModPathChanged(ModPathChangeType type, Mod mod, DirectoryInfo? old, DirectoryInfo? _)
{
if (type is not ModPathChangeType.Moved || !string.Equals(old?.Name, LastModPath, StringComparison.OrdinalIgnoreCase))

View file

@ -1,5 +1,6 @@
using Lumina.Data.Parsing;
using Penumbra.GameData.Files;
using Penumbra.GameData.Files.MaterialStructs;
using SharpGLTF.Materials;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Advanced;
@ -48,12 +49,12 @@ public class MaterialExporter
private static MaterialBuilder BuildCharacter(Material material, string name)
{
// Build the textures from the color table.
var table = material.Mtrl.Table;
var table = new LegacyColorTable(material.Mtrl.Table!);
var normal = material.Textures[TextureUsage.SamplerNormal];
var operation = new ProcessCharacterNormalOperation(normal, table);
ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds(), in operation);
ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds, in operation);
// Check if full textures are provided, and merge in if available.
var baseColor = operation.BaseColor;
@ -102,7 +103,8 @@ public class MaterialExporter
// TODO: It feels a little silly to request the entire normal here when extracting the normal only needs some of the components.
// As a future refactor, it would be neat to accept a single-channel field here, and then do composition of other stuff later.
private readonly struct ProcessCharacterNormalOperation(Image<Rgba32> normal, MtrlFile.ColorTable table) : IRowOperation
// 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(normal.Width, normal.Height);
@ -138,18 +140,17 @@ public class MaterialExporter
var nextRow = table[tableRow.Next];
// Base colour (table, .b)
var lerpedDiffuse = Vector3.Lerp(prevRow.Diffuse, nextRow.Diffuse, tableRow.Weight);
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(prevRow.Specular, nextRow.Specular, tableRow.Weight);
// float.Lerp is .NET8 ;-; #TODO
var lerpedSpecularFactor = prevRow.SpecularStrength * (1.0f - tableRow.Weight) + nextRow.SpecularStrength * tableRow.Weight;
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(prevRow.Emissive, nextRow.Emissive, tableRow.Weight);
var lerpedEmissive = Vector3.Lerp((Vector3)prevRow.EmissiveColor, (Vector3)nextRow.EmissiveColor, tableRow.Weight);
emissiveSpan[x].FromVector4(new Vector4(lerpedEmissive, 1));
// Normal (.rg)
@ -199,7 +200,7 @@ public class MaterialExporter
small.Mutate(context => context.Resize(large.Width, large.Height));
var operation = new MultiplyOperation<TPixel1, TPixel2>(target, multiplier);
ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, target.Bounds(), in operation);
ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, target.Bounds, in operation);
}
}

Some files were not shown because too many files have changed in this diff Show more