mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 10:17:22 +01:00
Add crash handler stuff.
This commit is contained in:
parent
9ba6e4d0af
commit
e08e9c4d13
35 changed files with 1472 additions and 237 deletions
119
Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs
Normal file
119
Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
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,
|
||||
string CollectionName) : 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="collectionName"> The name of the associated collection. Not anonymized. </param>
|
||||
/// <param name="type"> The type of VFX func called. </param>
|
||||
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, string collectionName, AnimationInvocationType type);
|
||||
}
|
||||
|
||||
internal sealed class AnimationInvocationBuffer : MemoryMappedBuffer, IAnimationInvocationBufferWriter, IBufferReader
|
||||
{
|
||||
private const int _version = 1;
|
||||
private const int _lineCount = 64;
|
||||
private const int _lineCapacity = 256;
|
||||
private const string _name = "Penumbra.AnimationInvocation";
|
||||
|
||||
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, string collectionName, 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, 104);
|
||||
WriteSpan(characterName, span);
|
||||
span = GetSpan(accessor, 128);
|
||||
WriteString(collectionName, span);
|
||||
}
|
||||
}
|
||||
|
||||
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 characterName = ReadString(line[24..]);
|
||||
var collectionName = ReadString(line[128..]);
|
||||
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.CollectionName)] = collectionName,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static IBufferReader CreateReader()
|
||||
=> new AnimationInvocationBuffer(false);
|
||||
|
||||
public static IAnimationInvocationBufferWriter CreateWriter()
|
||||
=> new AnimationInvocationBuffer();
|
||||
|
||||
private AnimationInvocationBuffer(bool writer)
|
||||
: base(_name, _version)
|
||||
{ }
|
||||
|
||||
private AnimationInvocationBuffer()
|
||||
: base(_name, _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})",
|
||||
};
|
||||
}
|
||||
85
Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs
Normal file
85
Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
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="collectionName"> The name of the associated collection. Not anonymized. </param>
|
||||
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, string collectionName);
|
||||
}
|
||||
|
||||
/// <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,
|
||||
string CollectionName) : ICrashDataEntry;
|
||||
|
||||
internal sealed class CharacterBaseBuffer : MemoryMappedBuffer, ICharacterBaseBufferWriter, IBufferReader
|
||||
{
|
||||
private const int _version = 1;
|
||||
private const int _lineCount = 10;
|
||||
private const int _lineCapacity = 256;
|
||||
private const string _name = "Penumbra.CharacterBase";
|
||||
|
||||
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, string collectionName)
|
||||
{
|
||||
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, 108);
|
||||
WriteSpan(characterName, span);
|
||||
span = GetSpan(accessor, 128);
|
||||
WriteString(collectionName, span);
|
||||
}
|
||||
}
|
||||
|
||||
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 characterName = ReadString(line[20..]);
|
||||
var collectionName = ReadString(line[128..]);
|
||||
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.CollectionName)] = collectionName,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public uint TotalCount
|
||||
=> TotalWrittenLines;
|
||||
|
||||
public static IBufferReader CreateReader()
|
||||
=> new CharacterBaseBuffer(false);
|
||||
|
||||
public static ICharacterBaseBufferWriter CreateWriter()
|
||||
=> new CharacterBaseBuffer();
|
||||
|
||||
private CharacterBaseBuffer(bool writer)
|
||||
: base(_name, _version)
|
||||
{ }
|
||||
|
||||
private CharacterBaseBuffer()
|
||||
: base(_name, _version, _lineCount, _lineCapacity)
|
||||
{ }
|
||||
}
|
||||
215
Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs
Normal file
215
Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
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)
|
||||
{
|
||||
_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();
|
||||
_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 length = bytes.Length + 1;
|
||||
if (length > span.Length)
|
||||
throw new Exception($"String {text} is too long to write into span.");
|
||||
|
||||
bytes.CopyTo(span);
|
||||
span[bytes.Length] = 0;
|
||||
return length;
|
||||
}
|
||||
|
||||
protected static int WriteSpan(ReadOnlySpan<byte> input, Span<byte> span)
|
||||
{
|
||||
var length = input.Length + 1;
|
||||
if (length > span.Length)
|
||||
throw new Exception("Byte array is too long to write into span.");
|
||||
|
||||
input.CopyTo(span);
|
||||
span[input.Length] = 0;
|
||||
return length;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
99
Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs
Normal file
99
Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
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="collectionName"> The name of the associated collection. Not anonymized. </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, string collectionName, 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,
|
||||
string CollectionName,
|
||||
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, string collectionName, 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, 80);
|
||||
WriteSpan(characterName, span);
|
||||
span = GetSpan(accessor, 92, 80);
|
||||
WriteString(collectionName, span);
|
||||
span = GetSpan(accessor, 172, 260);
|
||||
WriteSpan(requestedFileName, span);
|
||||
span = GetSpan(accessor, 432);
|
||||
WriteSpan(actualFileName, span);
|
||||
}
|
||||
}
|
||||
|
||||
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 characterName = ReadString(line[20..]);
|
||||
var collectionName = ReadString(line[92..]);
|
||||
var requestedFileName = ReadString(line[172..]);
|
||||
var actualFileName = ReadString(line[432..]);
|
||||
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.CollectionName)] = collectionName,
|
||||
[nameof(ModdedFileLoadedEntry.RequestedFileName)] = requestedFileName,
|
||||
[nameof(ModdedFileLoadedEntry.ActualFileName)] = actualFileName,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static IBufferReader CreateReader()
|
||||
=> new ModdedFileBuffer(false);
|
||||
|
||||
public static IModdedFileBufferWriter CreateWriter()
|
||||
=> new ModdedFileBuffer();
|
||||
|
||||
private ModdedFileBuffer(bool writer)
|
||||
: base(_name, _version)
|
||||
{ }
|
||||
|
||||
private ModdedFileBuffer()
|
||||
: base(_name, _version, _lineCount, _lineCapacity)
|
||||
{ }
|
||||
}
|
||||
62
Penumbra.CrashHandler/CrashData.cs
Normal file
62
Penumbra.CrashHandler/CrashData.cs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
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> 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; } = [];
|
||||
|
||||
/// <summary> A collection of the last few modded files loaded before this crash data was generated. </summary>
|
||||
public List<ModdedFileLoadedEntry> LastModdedFilesLoaded { get; } = [];
|
||||
|
||||
/// <summary> A collection of the last few vfx functions invoked before this crash data was generated. </summary>
|
||||
public List<VfxFuncInvokedEntry> LastVfxFuncsInvoked { get; } = [];
|
||||
}
|
||||
53
Penumbra.CrashHandler/GameEventLogReader.cs
Normal file
53
Penumbra.CrashHandler/GameEventLogReader.cs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
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 : IDisposable
|
||||
{
|
||||
public readonly (IBufferReader Reader, string TypeSingular, string TypePlural)[] Readers =
|
||||
[
|
||||
(CharacterBaseBuffer.CreateReader(), "CharacterLoaded", "CharactersLoaded"),
|
||||
(ModdedFileBuffer.CreateReader(), "ModdedFileLoaded", "ModdedFilesLoaded"),
|
||||
(AnimationInvocationBuffer.CreateReader(), "VFXFuncInvoked", "VFXFuncsInvoked"),
|
||||
];
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var (reader, _, _) in Readers)
|
||||
(reader as IDisposable)?.Dispose();
|
||||
}
|
||||
|
||||
|
||||
public JsonObject Dump(string mode, int processId, int exitCode)
|
||||
{
|
||||
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,
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
17
Penumbra.CrashHandler/GameEventLogWriter.cs
Normal file
17
Penumbra.CrashHandler/GameEventLogWriter.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
using Penumbra.CrashHandler.Buffers;
|
||||
|
||||
namespace Penumbra.CrashHandler;
|
||||
|
||||
public sealed class GameEventLogWriter : IDisposable
|
||||
{
|
||||
public readonly ICharacterBaseBufferWriter CharacterBase = CharacterBaseBuffer.CreateWriter();
|
||||
public readonly IModdedFileBufferWriter FileLoaded = ModdedFileBuffer.CreateWriter();
|
||||
public readonly IAnimationInvocationBufferWriter AnimationFuncInvoked = AnimationInvocationBuffer.CreateWriter();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
(CharacterBase as IDisposable)?.Dispose();
|
||||
(FileLoaded as IDisposable)?.Dispose();
|
||||
(AnimationFuncInvoked as IDisposable)?.Dispose();
|
||||
}
|
||||
}
|
||||
28
Penumbra.CrashHandler/Penumbra.CrashHandler.csproj
Normal file
28
Penumbra.CrashHandler/Penumbra.CrashHandler.csproj
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.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>
|
||||
30
Penumbra.CrashHandler/Program.cs
Normal file
30
Penumbra.CrashHandler/Program.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Penumbra.CrashHandler;
|
||||
|
||||
public class CrashHandler
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
if (args.Length < 2 || !int.TryParse(args[1], out var pid))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
using var reader = new GameEventLogReader();
|
||||
var parent = Process.GetProcessById(pid);
|
||||
|
||||
parent.WaitForExit();
|
||||
var exitCode = parent.ExitCode;
|
||||
var obj = reader.Dump("Crash", pid, exitCode);
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "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 +46,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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using OtterGui.Classes;
|
||||
using Penumbra.Api;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Communication;
|
||||
|
||||
|
|
@ -19,5 +20,8 @@ public sealed class CreatingCharacterBase()
|
|||
{
|
||||
/// <seealso cref="PenumbraApi.CreatingCharacterBase"/>
|
||||
Api = 0,
|
||||
|
||||
/// <seealso cref="CrashHandlerService.OnCreatingCharacterBase"/>
|
||||
CrashHandler = 0,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ public class Configuration : IPluginConfiguration, ISavable
|
|||
public string ModDirectory { get; set; } = string.Empty;
|
||||
public string ExportDirectory { get; set; } = string.Empty;
|
||||
|
||||
public bool UseCrashHandler { get; set; } = true;
|
||||
public bool OpenWindowAtStart { get; set; } = false;
|
||||
public bool HideUiInGPose { get; set; } = false;
|
||||
public bool HideUiInCutscenes { get; set; } = true;
|
||||
|
|
|
|||
|
|
@ -47,7 +47,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);
|
||||
|
|
@ -94,13 +94,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))
|
||||
|
|
|
|||
|
|
@ -2,21 +2,25 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
|||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.CrashHandler.Buffers;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.Interop.PathResolving;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Interop.Hooks.Animation;
|
||||
|
||||
/// <summary> Called for some sound effects caused by animations or VFX. </summary>
|
||||
public sealed unsafe class ApricotListenerSoundPlay : FastHook<ApricotListenerSoundPlay.Delegate>
|
||||
{
|
||||
private readonly GameState _state;
|
||||
private readonly CollectionResolver _collectionResolver;
|
||||
private readonly GameState _state;
|
||||
private readonly CollectionResolver _collectionResolver;
|
||||
private readonly CrashHandlerService _crashHandler;
|
||||
|
||||
public ApricotListenerSoundPlay(HookManager hooks, GameState state, CollectionResolver collectionResolver)
|
||||
public ApricotListenerSoundPlay(HookManager hooks, GameState state, CollectionResolver collectionResolver, CrashHandlerService crashHandler)
|
||||
{
|
||||
_state = state;
|
||||
_collectionResolver = collectionResolver;
|
||||
_crashHandler = crashHandler;
|
||||
Task = hooks.CreateHook<Delegate>("Apricot Listener Sound Play", Sigs.ApricotListenerSoundPlay, Detour, true);
|
||||
}
|
||||
|
||||
|
|
@ -46,6 +50,7 @@ public sealed unsafe class ApricotListenerSoundPlay : FastHook<ApricotListenerSo
|
|||
newData = _collectionResolver.IdentifyCollection(drawObject, true);
|
||||
}
|
||||
|
||||
_crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.ApricotSoundPlay);
|
||||
var last = _state.SetAnimationData(newData);
|
||||
var ret = Task.Result.Original(a1, a2, a3, a4, a5, a6);
|
||||
_state.RestoreAnimationData(last);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.CrashHandler.Buffers;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.Interop.PathResolving;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Interop.Hooks.Animation;
|
||||
|
||||
|
|
@ -12,16 +14,18 @@ namespace Penumbra.Interop.Hooks.Animation;
|
|||
/// </summary>
|
||||
public sealed unsafe class CharacterBaseLoadAnimation : FastHook<CharacterBaseLoadAnimation.Delegate>
|
||||
{
|
||||
private readonly GameState _state;
|
||||
private readonly CollectionResolver _collectionResolver;
|
||||
private readonly DrawObjectState _drawObjectState;
|
||||
private readonly GameState _state;
|
||||
private readonly CollectionResolver _collectionResolver;
|
||||
private readonly DrawObjectState _drawObjectState;
|
||||
private readonly CrashHandlerService _crashHandler;
|
||||
|
||||
public CharacterBaseLoadAnimation(HookManager hooks, GameState state, CollectionResolver collectionResolver,
|
||||
DrawObjectState drawObjectState)
|
||||
DrawObjectState drawObjectState, CrashHandlerService crashHandler)
|
||||
{
|
||||
_state = state;
|
||||
_collectionResolver = collectionResolver;
|
||||
_drawObjectState = drawObjectState;
|
||||
_crashHandler = crashHandler;
|
||||
Task = hooks.CreateHook<Delegate>("CharacterBase Load Animation", Sigs.CharacterBaseLoadAnimation, Detour, true);
|
||||
}
|
||||
|
||||
|
|
@ -33,7 +37,9 @@ public sealed unsafe class CharacterBaseLoadAnimation : FastHook<CharacterBaseLo
|
|||
var lastObj = _state.LastGameObject;
|
||||
if (lastObj == nint.Zero && _drawObjectState.TryGetValue((nint)drawObject, out var p))
|
||||
lastObj = p.Item1;
|
||||
var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection((GameObject*)lastObj, true));
|
||||
var data = _collectionResolver.IdentifyCollection((GameObject*)lastObj, true);
|
||||
var last = _state.SetAnimationData(data);
|
||||
_crashHandler.LogAnimation(data.AssociatedGameObject, data.ModCollection, AnimationInvocationType.CharacterBaseLoadAnimation);
|
||||
Penumbra.Log.Excessive($"[CharacterBase Load Animation] Invoked on {(nint)drawObject:X}");
|
||||
Task.Result.Original(drawObject);
|
||||
_state.RestoreAnimationData(last);
|
||||
|
|
|
|||
|
|
@ -1,21 +1,25 @@
|
|||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.CrashHandler.Buffers;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.Interop.PathResolving;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Interop.Hooks.Animation;
|
||||
|
||||
/// <summary> Load a ground-based area VFX. </summary>
|
||||
public sealed unsafe class LoadAreaVfx : FastHook<LoadAreaVfx.Delegate>
|
||||
{
|
||||
private readonly GameState _state;
|
||||
private readonly CollectionResolver _collectionResolver;
|
||||
private readonly GameState _state;
|
||||
private readonly CollectionResolver _collectionResolver;
|
||||
private readonly CrashHandlerService _crashHandler;
|
||||
|
||||
public LoadAreaVfx(HookManager hooks, GameState state, CollectionResolver collectionResolver)
|
||||
public LoadAreaVfx(HookManager hooks, GameState state, CollectionResolver collectionResolver, CrashHandlerService crashHandler)
|
||||
{
|
||||
_state = state;
|
||||
_collectionResolver = collectionResolver;
|
||||
_crashHandler = crashHandler;
|
||||
Task = hooks.CreateHook<Delegate>("Load Area VFX", Sigs.LoadAreaVfx, Detour, true);
|
||||
}
|
||||
|
||||
|
|
@ -25,10 +29,11 @@ public sealed unsafe class LoadAreaVfx : FastHook<LoadAreaVfx.Delegate>
|
|||
private nint Detour(uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3)
|
||||
{
|
||||
var newData = caster != null
|
||||
? _collectionResolver.IdentifyCollection(caster, true)
|
||||
: ResolveData.Invalid;
|
||||
? _collectionResolver.IdentifyCollection(caster, true)
|
||||
: ResolveData.Invalid;
|
||||
|
||||
var last = _state.SetAnimationData(newData);
|
||||
_crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.LoadAreaVfx);
|
||||
var ret = Task.Result.Original(vfxId, pos, caster, unk1, unk2, unk3);
|
||||
Penumbra.Log.Excessive(
|
||||
$"[Load Area VFX] Invoked with {vfxId}, [{pos[0]} {pos[1]} {pos[2]}], 0x{(nint)caster:X}, {unk1}, {unk2}, {unk3} -> 0x{ret:X}.");
|
||||
|
|
|
|||
|
|
@ -1,19 +1,23 @@
|
|||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.CrashHandler.Buffers;
|
||||
using Penumbra.Interop.PathResolving;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Interop.Hooks.Animation;
|
||||
|
||||
/// <summary> Characters load some of their voice lines or whatever with this function. </summary>
|
||||
public sealed unsafe class LoadCharacterSound : FastHook<LoadCharacterSound.Delegate>
|
||||
{
|
||||
private readonly GameState _state;
|
||||
private readonly CollectionResolver _collectionResolver;
|
||||
private readonly GameState _state;
|
||||
private readonly CollectionResolver _collectionResolver;
|
||||
private readonly CrashHandlerService _crashHandler;
|
||||
|
||||
public LoadCharacterSound(HookManager hooks, GameState state, CollectionResolver collectionResolver)
|
||||
public LoadCharacterSound(HookManager hooks, GameState state, CollectionResolver collectionResolver, CrashHandlerService crashHandler)
|
||||
{
|
||||
_state = state;
|
||||
_state = state;
|
||||
_collectionResolver = collectionResolver;
|
||||
_crashHandler = crashHandler;
|
||||
Task = hooks.CreateHook<Delegate>("Load Character Sound",
|
||||
(nint)FFXIVClientStructs.FFXIV.Client.Game.Character.Character.VfxContainer.MemberFunctionPointers.LoadCharacterSound, Detour,
|
||||
true);
|
||||
|
|
@ -25,7 +29,9 @@ public sealed unsafe class LoadCharacterSound : FastHook<LoadCharacterSound.Dele
|
|||
private nint Detour(nint container, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7)
|
||||
{
|
||||
var character = *(GameObject**)(container + 8);
|
||||
var last = _state.SetSoundData(_collectionResolver.IdentifyCollection(character, true));
|
||||
var newData = _collectionResolver.IdentifyCollection(character, true);
|
||||
var last = _state.SetSoundData(newData);
|
||||
_crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.LoadCharacterSound);
|
||||
var ret = Task.Result.Original(container, unk1, unk2, unk3, unk4, unk5, unk6, unk7);
|
||||
Penumbra.Log.Excessive($"[Load Character Sound] Invoked with {container:X} {unk1} {unk2} {unk3} {unk4} {unk5} {unk6} {unk7} -> {ret}.");
|
||||
_state.RestoreSoundData(last);
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ using Dalamud.Plugin.Services;
|
|||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.CrashHandler.Buffers;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.Interop.PathResolving;
|
||||
using Penumbra.Interop.Structs;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.String;
|
||||
|
||||
namespace Penumbra.Interop.Hooks.Animation;
|
||||
|
|
@ -12,15 +14,18 @@ namespace Penumbra.Interop.Hooks.Animation;
|
|||
/// <summary> Load a VFX specifically for a character. </summary>
|
||||
public sealed unsafe class LoadCharacterVfx : FastHook<LoadCharacterVfx.Delegate>
|
||||
{
|
||||
private readonly GameState _state;
|
||||
private readonly CollectionResolver _collectionResolver;
|
||||
private readonly IObjectTable _objects;
|
||||
private readonly GameState _state;
|
||||
private readonly CollectionResolver _collectionResolver;
|
||||
private readonly IObjectTable _objects;
|
||||
private readonly CrashHandlerService _crashHandler;
|
||||
|
||||
public LoadCharacterVfx(HookManager hooks, GameState state, CollectionResolver collectionResolver, IObjectTable objects)
|
||||
public LoadCharacterVfx(HookManager hooks, GameState state, CollectionResolver collectionResolver, IObjectTable objects,
|
||||
CrashHandlerService crashHandler)
|
||||
{
|
||||
_state = state;
|
||||
_collectionResolver = collectionResolver;
|
||||
_objects = objects;
|
||||
_crashHandler = crashHandler;
|
||||
Task = hooks.CreateHook<Delegate>("Load Character VFX", Sigs.LoadCharacterVfx, Detour, true);
|
||||
}
|
||||
|
||||
|
|
@ -45,6 +50,7 @@ public sealed unsafe class LoadCharacterVfx : FastHook<LoadCharacterVfx.Delegate
|
|||
}
|
||||
|
||||
var last = _state.SetAnimationData(newData);
|
||||
_crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.LoadCharacterVfx);
|
||||
var ret = Task.Result.Original(vfxPath, vfxParams, unk1, unk2, unk3, unk4);
|
||||
Penumbra.Log.Excessive(
|
||||
$"[Load Character VFX] Invoked with {new ByteString(vfxPath)}, 0x{vfxParams->GameObjectId:X}, {vfxParams->TargetCount}, {unk1}, {unk2}, {unk3}, {unk4} -> 0x{ret:X}.");
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ using Dalamud.Plugin.Services;
|
|||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.CrashHandler;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.Interop.PathResolving;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Interop.Hooks.Animation;
|
||||
|
||||
|
|
@ -14,19 +16,21 @@ namespace Penumbra.Interop.Hooks.Animation;
|
|||
/// </summary>
|
||||
public sealed unsafe class LoadTimelineResources : FastHook<LoadTimelineResources.Delegate>
|
||||
{
|
||||
private readonly GameState _state;
|
||||
private readonly CollectionResolver _collectionResolver;
|
||||
private readonly ICondition _conditions;
|
||||
private readonly IObjectTable _objects;
|
||||
private readonly GameState _state;
|
||||
private readonly CollectionResolver _collectionResolver;
|
||||
private readonly ICondition _conditions;
|
||||
private readonly IObjectTable _objects;
|
||||
private readonly CrashHandlerService _crashHandler;
|
||||
|
||||
public LoadTimelineResources(HookManager hooks, GameState state, CollectionResolver collectionResolver, ICondition conditions,
|
||||
IObjectTable objects)
|
||||
IObjectTable objects, CrashHandlerService crashHandler)
|
||||
{
|
||||
_state = state;
|
||||
_collectionResolver = collectionResolver;
|
||||
_conditions = conditions;
|
||||
_objects = objects;
|
||||
Task = hooks.CreateHook<Delegate>("Load Timeline Resources", Sigs.LoadTimelineResources, Detour, true);
|
||||
_crashHandler = crashHandler;
|
||||
Task = hooks.CreateHook<Delegate>("Load Timeline Resources", Sigs.LoadTimelineResources, Detour, true);
|
||||
}
|
||||
|
||||
public delegate ulong Delegate(nint timeline);
|
||||
|
|
@ -39,7 +43,13 @@ public sealed unsafe class LoadTimelineResources : FastHook<LoadTimelineResource
|
|||
if (_conditions[ConditionFlag.OccupiedInCutSceneEvent] || _conditions[ConditionFlag.WatchingCutscene78])
|
||||
return Task.Result.Original(timeline);
|
||||
|
||||
var last = _state.SetAnimationData(GetDataFromTimeline(_objects, _collectionResolver, timeline));
|
||||
var newData = GetDataFromTimeline(_objects, _collectionResolver, timeline);
|
||||
var last = _state.SetAnimationData(newData);
|
||||
|
||||
#if false
|
||||
// This is called far too often and spams the log too much.
|
||||
_crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.LoadTimelineResources);
|
||||
#endif
|
||||
var ret = Task.Result.Original(timeline);
|
||||
_state.RestoreAnimationData(last);
|
||||
return ret;
|
||||
|
|
|
|||
|
|
@ -1,23 +1,28 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.Interop.PathResolving;
|
||||
using Penumbra.Interop.Structs;
|
||||
|
||||
namespace Penumbra.Interop.Hooks.Animation;
|
||||
|
||||
using OtterGui.Services;
|
||||
using Penumbra.CrashHandler.Buffers;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.Interop.PathResolving;
|
||||
using Penumbra.Interop.Structs;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Interop.Hooks.Animation;
|
||||
|
||||
/// <summary> Called when some action timelines update. </summary>
|
||||
public sealed unsafe class ScheduleClipUpdate : FastHook<ScheduleClipUpdate.Delegate>
|
||||
{
|
||||
private readonly GameState _state;
|
||||
private readonly CollectionResolver _collectionResolver;
|
||||
private readonly IObjectTable _objects;
|
||||
private readonly GameState _state;
|
||||
private readonly CollectionResolver _collectionResolver;
|
||||
private readonly IObjectTable _objects;
|
||||
private readonly CrashHandlerService _crashHandler;
|
||||
|
||||
public ScheduleClipUpdate(HookManager hooks, GameState state, CollectionResolver collectionResolver, IObjectTable objects)
|
||||
public ScheduleClipUpdate(HookManager hooks, GameState state, CollectionResolver collectionResolver, IObjectTable objects,
|
||||
CrashHandlerService crashHandler)
|
||||
{
|
||||
_state = state;
|
||||
_collectionResolver = collectionResolver;
|
||||
_objects = objects;
|
||||
_crashHandler = crashHandler;
|
||||
Task = hooks.CreateHook<Delegate>("Schedule Clip Update", Sigs.ScheduleClipUpdate, Detour, true);
|
||||
}
|
||||
|
||||
|
|
@ -27,8 +32,9 @@ public sealed unsafe class ScheduleClipUpdate : FastHook<ScheduleClipUpdate.Dele
|
|||
private void Detour(ClipScheduler* clipScheduler)
|
||||
{
|
||||
Penumbra.Log.Excessive($"[Schedule Clip Update] Invoked on {(nint)clipScheduler:X}.");
|
||||
var last = _state.SetAnimationData(
|
||||
LoadTimelineResources.GetDataFromTimeline(_objects, _collectionResolver, clipScheduler->SchedulerTimeline));
|
||||
var newData = LoadTimelineResources.GetDataFromTimeline(_objects, _collectionResolver, clipScheduler->SchedulerTimeline);
|
||||
var last = _state.SetAnimationData(newData);
|
||||
_crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.ScheduleClipUpdate);
|
||||
Task.Result.Original(clipScheduler);
|
||||
_state.RestoreAnimationData(last);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,25 @@
|
|||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.GameData;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.CrashHandler.Buffers;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.Interop.PathResolving;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Interop.Hooks.Animation;
|
||||
|
||||
namespace Penumbra.Interop.Hooks.Animation;
|
||||
|
||||
/// <summary> Seems to load character actions when zoning or changing class, maybe. </summary>
|
||||
public sealed unsafe class SomeActionLoad : FastHook<SomeActionLoad.Delegate>
|
||||
{
|
||||
private readonly GameState _state;
|
||||
private readonly CollectionResolver _collectionResolver;
|
||||
private readonly GameState _state;
|
||||
private readonly CollectionResolver _collectionResolver;
|
||||
private readonly CrashHandlerService _crashHandler;
|
||||
|
||||
public SomeActionLoad(HookManager hooks, GameState state, CollectionResolver collectionResolver)
|
||||
public SomeActionLoad(HookManager hooks, GameState state, CollectionResolver collectionResolver, CrashHandlerService crashHandler)
|
||||
{
|
||||
_state = state;
|
||||
_collectionResolver = collectionResolver;
|
||||
_crashHandler = crashHandler;
|
||||
Task = hooks.CreateHook<Delegate>("Some Action Load", Sigs.LoadSomeAction, Detour, true);
|
||||
}
|
||||
|
||||
|
|
@ -24,8 +28,10 @@ public sealed unsafe class SomeActionLoad : FastHook<SomeActionLoad.Delegate>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private void Detour(ActionTimelineManager* timelineManager)
|
||||
{
|
||||
var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection((GameObject*)timelineManager->Parent, true));
|
||||
var newData = _collectionResolver.IdentifyCollection((GameObject*)timelineManager->Parent, true);
|
||||
var last = _state.SetAnimationData(newData);
|
||||
Penumbra.Log.Excessive($"[Some Action Load] Invoked on 0x{(nint)timelineManager:X}.");
|
||||
_crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.ActionLoad);
|
||||
Task.Result.Original(timelineManager);
|
||||
_state.RestoreAnimationData(last);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,28 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.CrashHandler.Buffers;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.Interop.PathResolving;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Interop.Hooks.Animation;
|
||||
|
||||
/// <summary> Unknown what exactly this is, but it seems to load a bunch of paps. </summary>
|
||||
public sealed unsafe class SomePapLoad : FastHook<SomePapLoad.Delegate>
|
||||
{
|
||||
private readonly GameState _state;
|
||||
private readonly CollectionResolver _collectionResolver;
|
||||
private readonly IObjectTable _objects;
|
||||
private readonly GameState _state;
|
||||
private readonly CollectionResolver _collectionResolver;
|
||||
private readonly IObjectTable _objects;
|
||||
private readonly CrashHandlerService _crashHandler;
|
||||
|
||||
public SomePapLoad(HookManager hooks, GameState state, CollectionResolver collectionResolver, IObjectTable objects)
|
||||
public SomePapLoad(HookManager hooks, GameState state, CollectionResolver collectionResolver, IObjectTable objects,
|
||||
CrashHandlerService crashHandler)
|
||||
{
|
||||
_state = state;
|
||||
_collectionResolver = collectionResolver;
|
||||
_objects = objects;
|
||||
_crashHandler = crashHandler;
|
||||
Task = hooks.CreateHook<Delegate>("Some PAP Load", Sigs.LoadSomePap, Detour, true);
|
||||
}
|
||||
|
||||
|
|
@ -33,8 +38,9 @@ public sealed unsafe class SomePapLoad : FastHook<SomePapLoad.Delegate>
|
|||
var actorIdx = (int)(*(*(ulong**)timelinePtr + 1) >> 3);
|
||||
if (actorIdx >= 0 && actorIdx < _objects.Length)
|
||||
{
|
||||
var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection((GameObject*)_objects.GetObjectAddress(actorIdx),
|
||||
true));
|
||||
var newData = _collectionResolver.IdentifyCollection((GameObject*)_objects.GetObjectAddress(actorIdx), true);
|
||||
var last = _state.SetAnimationData(newData);
|
||||
_crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.PapLoad);
|
||||
Task.Result.Original(a1, a2, a3, a4);
|
||||
_state.RestoreAnimationData(last);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
|
|
@ -69,8 +69,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="EmbedIO" Version="3.4.3" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.8.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.2" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.3" />
|
||||
<PackageReference Include="SharpCompress" Version="0.33.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||
<PackageReference Include="SharpGLTF.Core" Version="1.0.0-alpha0030" />
|
||||
|
|
@ -79,8 +78,10 @@
|
|||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OtterGui\OtterGui.csproj" />
|
||||
<ProjectReference Include="..\Penumbra.CrashHandler\Penumbra.CrashHandler.csproj" />
|
||||
<ProjectReference Include="..\Penumbra.GameData\Penumbra.GameData.csproj" />
|
||||
<ProjectReference Include="..\Penumbra.Api\Penumbra.Api.csproj" />
|
||||
<ProjectReference Include="..\Penumbra.String\Penumbra.String.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
@ -103,4 +104,4 @@
|
|||
<InformationalVersion>$(GitCommitHash)</InformationalVersion>
|
||||
</PropertyGroup>
|
||||
</Target>
|
||||
</Project>
|
||||
</Project>
|
||||
|
|
|
|||
296
Penumbra/Services/CrashHandlerService.cs
Normal file
296
Penumbra/Services/CrashHandlerService.cs
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Communication;
|
||||
using Penumbra.CrashHandler;
|
||||
using Penumbra.CrashHandler.Buffers;
|
||||
using Penumbra.GameData.Actors;
|
||||
using Penumbra.Interop.ResourceLoading;
|
||||
using Penumbra.Interop.Structs;
|
||||
using Penumbra.String;
|
||||
using Penumbra.String.Classes;
|
||||
using FileMode = System.IO.FileMode;
|
||||
|
||||
namespace Penumbra.Services;
|
||||
|
||||
public sealed class CrashHandlerService : IDisposable, IService
|
||||
{
|
||||
private readonly FilenameService _files;
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly ActorManager _actors;
|
||||
private readonly ResourceLoader _resourceLoader;
|
||||
private readonly Configuration _config;
|
||||
|
||||
public CrashHandlerService(FilenameService files, CommunicatorService communicator, ActorManager actors, ResourceLoader resourceLoader,
|
||||
Configuration config)
|
||||
{
|
||||
_files = files;
|
||||
_communicator = communicator;
|
||||
_actors = actors;
|
||||
_resourceLoader = resourceLoader;
|
||||
_config = config;
|
||||
|
||||
if (!_config.UseCrashHandler)
|
||||
return;
|
||||
|
||||
OpenEventWriter();
|
||||
LaunchCrashHandler();
|
||||
if (_eventWriter != null)
|
||||
Subscribe();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
CloseEventWriter();
|
||||
_eventWriter?.Dispose();
|
||||
if (_child != null)
|
||||
{
|
||||
_child.Kill();
|
||||
Penumbra.Log.Debug($"Killed crash handler child process {_child.Id}.");
|
||||
}
|
||||
|
||||
Unsubscribe();
|
||||
}
|
||||
|
||||
private Process? _child;
|
||||
private GameEventLogWriter? _eventWriter;
|
||||
|
||||
public string CopiedExe = string.Empty;
|
||||
|
||||
public string OriginalExe
|
||||
=> _files.CrashHandlerExe;
|
||||
|
||||
public string LogPath
|
||||
=> _files.LogFileName;
|
||||
|
||||
public int ChildProcessId
|
||||
=> _child?.Id ?? -1;
|
||||
|
||||
public int ProcessId
|
||||
=> Environment.ProcessId;
|
||||
|
||||
public bool IsRunning
|
||||
=> _eventWriter != null && _child is { HasExited: false };
|
||||
|
||||
public int ChildExitCode
|
||||
=> IsRunning ? 0 : _child?.ExitCode ?? 0;
|
||||
|
||||
public void Enable()
|
||||
{
|
||||
if (_config.UseCrashHandler)
|
||||
return;
|
||||
|
||||
_config.UseCrashHandler = true;
|
||||
_config.Save();
|
||||
OpenEventWriter();
|
||||
LaunchCrashHandler();
|
||||
if (_eventWriter != null)
|
||||
Subscribe();
|
||||
}
|
||||
|
||||
public void Disable()
|
||||
{
|
||||
if (!_config.UseCrashHandler)
|
||||
return;
|
||||
|
||||
_config.UseCrashHandler = false;
|
||||
_config.Save();
|
||||
CloseEventWriter();
|
||||
CloseCrashHandler();
|
||||
Unsubscribe();
|
||||
}
|
||||
|
||||
public JsonObject? Load(string fileName)
|
||||
{
|
||||
if (!File.Exists(fileName))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
var data = File.ReadAllText(fileName);
|
||||
return JsonNode.Parse(data) as JsonObject;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Penumbra.Log.Error($"Could not parse crash dump at {fileName}:\n{ex}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void CloseCrashHandler()
|
||||
{
|
||||
if (_child == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
if (_child.HasExited)
|
||||
return;
|
||||
|
||||
_child.Kill();
|
||||
Penumbra.Log.Debug($"Closed Crash Handler at {CopiedExe}.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_child = null;
|
||||
Penumbra.Log.Debug($"Closed not close Crash Handler at {CopiedExe}:\n{ex}.");
|
||||
}
|
||||
}
|
||||
|
||||
public void LaunchCrashHandler()
|
||||
{
|
||||
try
|
||||
{
|
||||
CloseCrashHandler();
|
||||
CopiedExe = CopyExecutables();
|
||||
var info = new ProcessStartInfo()
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
FileName = CopiedExe,
|
||||
};
|
||||
info.ArgumentList.Add(_files.LogFileName);
|
||||
info.ArgumentList.Add(Environment.ProcessId.ToString());
|
||||
_child = Process.Start(info);
|
||||
if (_child == null)
|
||||
throw new Exception("Child Process could not be created.");
|
||||
|
||||
Penumbra.Log.Information($"Opened Crash Handler at {CopiedExe}, PID {_child.Id}.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Penumbra.Log.Error($"Could not launch crash handler process:\n{ex}");
|
||||
CloseCrashHandler();
|
||||
_child = null;
|
||||
}
|
||||
}
|
||||
|
||||
public JsonObject? Dump()
|
||||
{
|
||||
if (_eventWriter == null)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
using var reader = new GameEventLogReader();
|
||||
JsonObject jObj;
|
||||
lock (_eventWriter)
|
||||
{
|
||||
jObj = reader.Dump("Manual Dump", Environment.ProcessId, 0);
|
||||
}
|
||||
|
||||
var logFile = _files.LogFileName;
|
||||
using var s = File.Open(logFile, FileMode.Create);
|
||||
using var jw = new Utf8JsonWriter(s, new JsonWriterOptions() { Indented = true });
|
||||
jObj.WriteTo(jw);
|
||||
Penumbra.Log.Information($"Dumped crash handler memory to {logFile}.");
|
||||
return jObj;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Penumbra.Log.Error($"Error dumping crash handler memory to file:\n{ex}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private string CopyExecutables()
|
||||
{
|
||||
var parent = Path.GetDirectoryName(_files.CrashHandlerExe)!;
|
||||
var folder = Path.Combine(parent, "temp");
|
||||
Directory.CreateDirectory(folder);
|
||||
foreach (var file in Directory.EnumerateFiles(parent, "Penumbra.CrashHandler.*"))
|
||||
File.Copy(file, Path.Combine(folder, Path.GetFileName(file)), true);
|
||||
return Path.Combine(folder, Path.GetFileName(_files.CrashHandlerExe));
|
||||
}
|
||||
|
||||
public void LogAnimation(nint character, ModCollection collection, AnimationInvocationType type)
|
||||
{
|
||||
if (_eventWriter == null)
|
||||
return;
|
||||
|
||||
var name = GetActorName(character);
|
||||
lock (_eventWriter)
|
||||
{
|
||||
_eventWriter?.AnimationFuncInvoked.WriteLine(character, name.Span, collection.Name, type);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCreatingCharacterBase(nint address, string collection, nint _1, nint _2, nint _3)
|
||||
{
|
||||
if (_eventWriter == null)
|
||||
return;
|
||||
|
||||
var name = GetActorName(address);
|
||||
|
||||
lock (_eventWriter)
|
||||
{
|
||||
_eventWriter?.CharacterBase.WriteLine(address, name.Span, collection);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe ByteString GetActorName(nint address)
|
||||
{
|
||||
var obj = (GameObject*)address;
|
||||
if (obj == null)
|
||||
return ByteString.FromSpanUnsafe("Unknown"u8, true, false, true);
|
||||
|
||||
var id = _actors.FromObject(obj, out _, false, true, false);
|
||||
return id.IsValid ? ByteString.FromStringUnsafe(id.Incognito(null), false) :
|
||||
obj->Name[0] != 0 ? new ByteString(obj->Name) : ByteString.FromStringUnsafe($"Actor #{obj->ObjectIndex}", false);
|
||||
}
|
||||
|
||||
private unsafe void OnResourceLoaded(ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, ResolveData resolveData)
|
||||
{
|
||||
if (manipulatedPath == null || _eventWriter == null)
|
||||
return;
|
||||
|
||||
var dashIdx = manipulatedPath.Value.InternalName[0] == (byte)'|' ? manipulatedPath.Value.InternalName.IndexOf((byte)'|', 1) : -1;
|
||||
if (dashIdx >= 0 && !Utf8GamePath.IsRooted(manipulatedPath.Value.InternalName.Substring(dashIdx + 1)))
|
||||
return;
|
||||
|
||||
var name = GetActorName(resolveData.AssociatedGameObject);
|
||||
lock (_eventWriter)
|
||||
{
|
||||
_eventWriter!.FileLoaded.WriteLine(resolveData.AssociatedGameObject, name.Span, resolveData.ModCollection.Name,
|
||||
manipulatedPath.Value.InternalName.Span, originalPath.Path.Span);
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseEventWriter()
|
||||
{
|
||||
if (_eventWriter == null)
|
||||
return;
|
||||
|
||||
_eventWriter.Dispose();
|
||||
_eventWriter = null;
|
||||
Penumbra.Log.Debug("Closed Event Writer for crash handler.");
|
||||
}
|
||||
|
||||
private void OpenEventWriter()
|
||||
{
|
||||
try
|
||||
{
|
||||
CloseEventWriter();
|
||||
_eventWriter = new GameEventLogWriter();
|
||||
Penumbra.Log.Debug("Opened new Event Writer for crash handler.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Penumbra.Log.Error($"Could not open Event Writer:\n{ex}");
|
||||
CloseEventWriter();
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void Subscribe()
|
||||
{
|
||||
_communicator.CreatingCharacterBase.Subscribe(OnCreatingCharacterBase, CreatingCharacterBase.Priority.CrashHandler);
|
||||
_resourceLoader.ResourceLoaded += OnResourceLoaded;
|
||||
}
|
||||
|
||||
private unsafe void Unsubscribe()
|
||||
{
|
||||
_communicator.CreatingCharacterBase.Unsubscribe(OnCreatingCharacterBase);
|
||||
_resourceLoader.ResourceLoaded -= OnResourceLoaded;
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,12 @@ public class FilenameService(DalamudPluginInterface pi) : IService
|
|||
public readonly string FilesystemFile = Path.Combine(pi.ConfigDirectory.FullName, "sort_order.json");
|
||||
public readonly string ActiveCollectionsFile = Path.Combine(pi.ConfigDirectory.FullName, "active_collections.json");
|
||||
|
||||
public readonly string CrashHandlerExe =
|
||||
Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "Penumbra.CrashHandler.exe");
|
||||
|
||||
public readonly string LogFileName =
|
||||
Path.Combine(Path.GetDirectoryName(Path.GetDirectoryName(pi.ConfigDirectory.FullName)!)!, "Penumbra.log");
|
||||
|
||||
/// <summary> Obtain the path of a collection file given its name.</summary>
|
||||
public string CollectionFile(ModCollection collection)
|
||||
=> CollectionFile(collection.Name);
|
||||
|
|
|
|||
|
|
@ -12,22 +12,13 @@ using Penumbra.UI.Classes;
|
|||
|
||||
namespace Penumbra.UI.Tabs;
|
||||
|
||||
public class ChangedItemsTab : ITab
|
||||
public class ChangedItemsTab(
|
||||
CollectionManager collectionManager,
|
||||
CollectionSelectHeader collectionHeader,
|
||||
ChangedItemDrawer drawer,
|
||||
CommunicatorService communicator)
|
||||
: ITab
|
||||
{
|
||||
private readonly CollectionManager _collectionManager;
|
||||
private readonly ChangedItemDrawer _drawer;
|
||||
private readonly CollectionSelectHeader _collectionHeader;
|
||||
private readonly CommunicatorService _communicator;
|
||||
|
||||
public ChangedItemsTab(CollectionManager collectionManager, CollectionSelectHeader collectionHeader, ChangedItemDrawer drawer,
|
||||
CommunicatorService communicator)
|
||||
{
|
||||
_collectionManager = collectionManager;
|
||||
_collectionHeader = collectionHeader;
|
||||
_drawer = drawer;
|
||||
_communicator = communicator;
|
||||
}
|
||||
|
||||
public ReadOnlySpan<byte> Label
|
||||
=> "Changed Items"u8;
|
||||
|
||||
|
|
@ -36,8 +27,8 @@ public class ChangedItemsTab : ITab
|
|||
|
||||
public void DrawContent()
|
||||
{
|
||||
_collectionHeader.Draw(true);
|
||||
_drawer.DrawTypeFilter();
|
||||
collectionHeader.Draw(true);
|
||||
drawer.DrawTypeFilter();
|
||||
var varWidth = DrawFilters();
|
||||
using var child = ImRaii.Child("##changedItemsChild", -Vector2.One);
|
||||
if (!child)
|
||||
|
|
@ -54,7 +45,7 @@ public class ChangedItemsTab : ITab
|
|||
ImGui.TableSetupColumn("mods", flags, varWidth - 130 * UiHelpers.Scale);
|
||||
ImGui.TableSetupColumn("id", flags, 130 * UiHelpers.Scale);
|
||||
|
||||
var items = _collectionManager.Active.Current.ChangedItems;
|
||||
var items = collectionManager.Active.Current.ChangedItems;
|
||||
var rest = ImGuiClip.FilteredClippedDraw(items, skips, FilterChangedItem, DrawChangedItemColumn);
|
||||
ImGuiClip.DrawEndDummy(rest, height);
|
||||
}
|
||||
|
|
@ -75,21 +66,21 @@ public class ChangedItemsTab : ITab
|
|||
|
||||
/// <summary> Apply the current filters. </summary>
|
||||
private bool FilterChangedItem(KeyValuePair<string, (SingleArray<IMod>, object?)> item)
|
||||
=> _drawer.FilterChangedItem(item.Key, item.Value.Item2, _changedItemFilter)
|
||||
=> drawer.FilterChangedItem(item.Key, item.Value.Item2, _changedItemFilter)
|
||||
&& (_changedItemModFilter.IsEmpty || item.Value.Item1.Any(m => m.Name.Contains(_changedItemModFilter)));
|
||||
|
||||
/// <summary> Draw a full column for a changed item. </summary>
|
||||
private void DrawChangedItemColumn(KeyValuePair<string, (SingleArray<IMod>, object?)> item)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
_drawer.DrawCategoryIcon(item.Key, item.Value.Item2);
|
||||
drawer.DrawCategoryIcon(item.Key, item.Value.Item2);
|
||||
ImGui.SameLine();
|
||||
_drawer.DrawChangedItem(item.Key, item.Value.Item2);
|
||||
drawer.DrawChangedItem(item.Key, item.Value.Item2);
|
||||
ImGui.TableNextColumn();
|
||||
DrawModColumn(item.Value.Item1);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
_drawer.DrawModelData(item.Value.Item2);
|
||||
drawer.DrawModelData(item.Value.Item2);
|
||||
}
|
||||
|
||||
private void DrawModColumn(SingleArray<IMod> mods)
|
||||
|
|
@ -102,7 +93,7 @@ public class ChangedItemsTab : ITab
|
|||
if (ImGui.Selectable(first.Name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight()))
|
||||
&& ImGui.GetIO().KeyCtrl
|
||||
&& first is Mod mod)
|
||||
_communicator.SelectTab.Invoke(TabType.Mods, mod);
|
||||
communicator.SelectTab.Invoke(TabType.Mods, mod);
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
|
|
|
|||
|
|
@ -44,8 +44,8 @@ public class ConfigTabBar : IDisposable
|
|||
Watcher = watcher;
|
||||
OnScreen = onScreen;
|
||||
Messages = messages;
|
||||
Tabs = new ITab[]
|
||||
{
|
||||
Tabs =
|
||||
[
|
||||
Settings,
|
||||
Collections,
|
||||
Mods,
|
||||
|
|
@ -56,7 +56,7 @@ public class ConfigTabBar : IDisposable
|
|||
Resource,
|
||||
Watcher,
|
||||
Messages,
|
||||
};
|
||||
];
|
||||
_communicator.SelectTab.Subscribe(OnSelectTab, Communication.SelectTab.Priority.ConfigTabBar);
|
||||
}
|
||||
|
||||
|
|
|
|||
104
Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs
Normal file
104
Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using Penumbra.CrashHandler;
|
||||
|
||||
namespace Penumbra.UI.Tabs.Debug;
|
||||
|
||||
public static class CrashDataExtensions
|
||||
{
|
||||
public static void DrawMeta(this CrashData data)
|
||||
{
|
||||
using (ImRaii.Group())
|
||||
{
|
||||
ImGui.TextUnformatted(nameof(data.Mode));
|
||||
ImGui.TextUnformatted(nameof(data.CrashTime));
|
||||
ImGui.TextUnformatted(nameof(data.ExitCode));
|
||||
ImGui.TextUnformatted(nameof(data.ProcessId));
|
||||
ImGui.TextUnformatted(nameof(data.TotalModdedFilesLoaded));
|
||||
ImGui.TextUnformatted(nameof(data.TotalCharactersLoaded));
|
||||
ImGui.TextUnformatted(nameof(data.TotalVFXFuncsInvoked));
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
using (ImRaii.Group())
|
||||
{
|
||||
ImGui.TextUnformatted(data.Mode);
|
||||
ImGui.TextUnformatted(data.CrashTime.ToString());
|
||||
ImGui.TextUnformatted(data.ExitCode.ToString());
|
||||
ImGui.TextUnformatted(data.ProcessId.ToString());
|
||||
ImGui.TextUnformatted(data.TotalModdedFilesLoaded.ToString());
|
||||
ImGui.TextUnformatted(data.TotalCharactersLoaded.ToString());
|
||||
ImGui.TextUnformatted(data.TotalVFXFuncsInvoked.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public static void DrawCharacters(this CrashData data)
|
||||
{
|
||||
using var tree = ImRaii.TreeNode("Last Characters");
|
||||
if (!tree)
|
||||
return;
|
||||
|
||||
using var table = ImRaii.Table("##characterTable", 6,
|
||||
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInner);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
ImGuiClip.ClippedDraw(data.LastCharactersLoaded, character =>
|
||||
{
|
||||
ImGuiUtil.DrawTableColumn(character.Age.ToString(CultureInfo.InvariantCulture));
|
||||
ImGuiUtil.DrawTableColumn(character.ThreadId.ToString());
|
||||
ImGuiUtil.DrawTableColumn(character.CharacterName);
|
||||
ImGuiUtil.DrawTableColumn(character.CollectionName);
|
||||
ImGuiUtil.DrawTableColumn(character.CharacterAddress);
|
||||
ImGuiUtil.DrawTableColumn(character.Timestamp.ToString());
|
||||
}, ImGui.GetTextLineHeightWithSpacing());
|
||||
}
|
||||
|
||||
public static void DrawFiles(this CrashData data)
|
||||
{
|
||||
using var tree = ImRaii.TreeNode("Last Files");
|
||||
if (!tree)
|
||||
return;
|
||||
|
||||
using var table = ImRaii.Table("##filesTable", 8,
|
||||
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInner);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
ImGuiClip.ClippedDraw(data.LastModdedFilesLoaded, file =>
|
||||
{
|
||||
ImGuiUtil.DrawTableColumn(file.Age.ToString(CultureInfo.InvariantCulture));
|
||||
ImGuiUtil.DrawTableColumn(file.ThreadId.ToString());
|
||||
ImGuiUtil.DrawTableColumn(file.ActualFileName);
|
||||
ImGuiUtil.DrawTableColumn(file.RequestedFileName);
|
||||
ImGuiUtil.DrawTableColumn(file.CharacterName);
|
||||
ImGuiUtil.DrawTableColumn(file.CollectionName);
|
||||
ImGuiUtil.DrawTableColumn(file.CharacterAddress);
|
||||
ImGuiUtil.DrawTableColumn(file.Timestamp.ToString());
|
||||
}, ImGui.GetTextLineHeightWithSpacing());
|
||||
}
|
||||
|
||||
public static void DrawVfxInvocations(this CrashData data)
|
||||
{
|
||||
using var tree = ImRaii.TreeNode("Last VFX Invocations");
|
||||
if (!tree)
|
||||
return;
|
||||
|
||||
using var table = ImRaii.Table("##vfxTable", 7,
|
||||
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInner);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
ImGuiClip.ClippedDraw(data.LastVfxFuncsInvoked, vfx =>
|
||||
{
|
||||
ImGuiUtil.DrawTableColumn(vfx.Age.ToString(CultureInfo.InvariantCulture));
|
||||
ImGuiUtil.DrawTableColumn(vfx.ThreadId.ToString());
|
||||
ImGuiUtil.DrawTableColumn(vfx.InvocationType);
|
||||
ImGuiUtil.DrawTableColumn(vfx.CharacterName);
|
||||
ImGuiUtil.DrawTableColumn(vfx.CollectionName);
|
||||
ImGuiUtil.DrawTableColumn(vfx.CharacterAddress);
|
||||
ImGuiUtil.DrawTableColumn(vfx.Timestamp.ToString());
|
||||
}, ImGui.GetTextLineHeightWithSpacing());
|
||||
}
|
||||
}
|
||||
136
Penumbra/UI/Tabs/Debug/CrashHandlerPanel.cs
Normal file
136
Penumbra/UI/Tabs/Debug/CrashHandlerPanel.cs
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
using System.Text.Json;
|
||||
using Dalamud.Interface.DragDrop;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.CrashHandler;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.UI.Tabs.Debug;
|
||||
|
||||
public class CrashHandlerPanel(CrashHandlerService _service, Configuration _config, IDragDropManager _dragDrop) : IService
|
||||
{
|
||||
private CrashData? _lastDump;
|
||||
private string _lastLoadedFile = string.Empty;
|
||||
private CrashData? _lastLoad;
|
||||
private Exception? _lastLoadException;
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
DrawDropSource();
|
||||
DrawData();
|
||||
DrawDropTarget();
|
||||
}
|
||||
|
||||
private void DrawData()
|
||||
{
|
||||
using var _ = ImRaii.Group();
|
||||
using var header = ImRaii.CollapsingHeader("Crash Handler");
|
||||
if (!header)
|
||||
return;
|
||||
|
||||
DrawButtons();
|
||||
DrawMainData();
|
||||
DrawObject("Last Manual Dump", _lastDump, null);
|
||||
DrawObject(_lastLoadedFile.Length > 0 ? $"Loaded File ({_lastLoadedFile})###Loaded File" : "Loaded File", _lastLoad,
|
||||
_lastLoadException);
|
||||
}
|
||||
|
||||
private void DrawMainData()
|
||||
{
|
||||
using var table = ImRaii.Table("##CrashHandlerTable", 2, ImGuiTableFlags.SizingFixedFit);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
PrintValue("Enabled", _config.UseCrashHandler);
|
||||
PrintValue("Copied Executable Path", _service.CopiedExe);
|
||||
PrintValue("Original Executable Path", _service.OriginalExe);
|
||||
PrintValue("Log File Path", _service.LogPath);
|
||||
PrintValue("XIV Process ID", _service.ProcessId.ToString());
|
||||
PrintValue("Crash Handler Running", _service.IsRunning.ToString());
|
||||
PrintValue("Crash Handler Process ID", _service.ChildProcessId.ToString());
|
||||
PrintValue("Crash Handler Exit Code", _service.ChildExitCode.ToString());
|
||||
}
|
||||
|
||||
private void DrawButtons()
|
||||
{
|
||||
if (ImGui.Button("Dump Crash Handler Memory"))
|
||||
_lastDump = _service.Dump()?.Deserialize<CrashData>();
|
||||
|
||||
if (ImGui.Button("Enable"))
|
||||
_service.Enable();
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Disable"))
|
||||
_service.Disable();
|
||||
|
||||
if (ImGui.Button("Shutdown Crash Handler"))
|
||||
_service.CloseCrashHandler();
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Relaunch Crash Handler"))
|
||||
_service.LaunchCrashHandler();
|
||||
}
|
||||
|
||||
private void DrawDropSource()
|
||||
{
|
||||
_dragDrop.CreateImGuiSource("LogDragDrop", m => m.Files.Any(f => f.EndsWith("Penumbra.log")), m =>
|
||||
{
|
||||
ImGui.TextUnformatted("Dragging Penumbra.log for import.");
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private void DrawDropTarget()
|
||||
{
|
||||
if (!_dragDrop.CreateImGuiTarget("LogDragDrop", out var files, out _))
|
||||
return;
|
||||
|
||||
var file = files.FirstOrDefault(f => f.EndsWith("Penumbra.log"));
|
||||
if (file == null)
|
||||
return;
|
||||
|
||||
_lastLoadedFile = file;
|
||||
try
|
||||
{
|
||||
var jObj = _service.Load(file);
|
||||
_lastLoad = jObj?.Deserialize<CrashData>();
|
||||
_lastLoadException = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_lastLoad = null;
|
||||
_lastLoadException = ex;
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawObject(string name, CrashData? data, Exception? ex)
|
||||
{
|
||||
using var tree = ImRaii.TreeNode(name);
|
||||
if (!tree)
|
||||
return;
|
||||
|
||||
if (ex != null)
|
||||
{
|
||||
ImGuiUtil.TextWrapped(ex.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
if (data == null)
|
||||
{
|
||||
ImGui.TextUnformatted("Nothing loaded.");
|
||||
return;
|
||||
}
|
||||
|
||||
data.DrawMeta();
|
||||
data.DrawFiles();
|
||||
data.DrawCharacters();
|
||||
data.DrawVfxInvocations();
|
||||
}
|
||||
|
||||
private static void PrintValue<T>(string label, in T data)
|
||||
{
|
||||
ImGuiUtil.DrawTableColumn(label);
|
||||
ImGuiUtil.DrawTableColumn(data?.ToString() ?? "NULL");
|
||||
}
|
||||
}
|
||||
|
|
@ -53,7 +53,7 @@ public class Diagnostics(IServiceProvider provider)
|
|||
foreach (var type in typeof(ActorManager).Assembly.GetTypes()
|
||||
.Where(t => t is { IsAbstract: false, IsInterface: false } && t.IsAssignableTo(typeof(IAsyncDataContainer))))
|
||||
{
|
||||
var container = (IAsyncDataContainer) provider.GetRequiredService(type);
|
||||
var container = (IAsyncDataContainer)provider.GetRequiredService(type);
|
||||
ImGuiUtil.DrawTableColumn(container.Name);
|
||||
ImGuiUtil.DrawTableColumn(container.Time.ToString());
|
||||
ImGuiUtil.DrawTableColumn(Functions.HumanReadableSize(container.Memory));
|
||||
|
|
@ -88,18 +88,22 @@ public class DebugTab : Window, ITab
|
|||
private readonly TextureManager _textureManager;
|
||||
private readonly ShaderReplacementFixer _shaderReplacementFixer;
|
||||
private readonly RedrawService _redraws;
|
||||
private readonly DictEmote _emotes;
|
||||
private readonly DictEmote _emotes;
|
||||
private readonly Diagnostics _diagnostics;
|
||||
private readonly IObjectTable _objects;
|
||||
private readonly IClientState _clientState;
|
||||
private readonly IpcTester _ipcTester;
|
||||
private readonly CrashHandlerPanel _crashHandlerPanel;
|
||||
|
||||
public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, IObjectTable objects, IClientState clientState,
|
||||
ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains, CharacterUtility characterUtility, ResidentResourceManager residentResources,
|
||||
public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, IObjectTable objects,
|
||||
IClientState clientState,
|
||||
ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains,
|
||||
CharacterUtility characterUtility, ResidentResourceManager residentResources,
|
||||
ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver,
|
||||
DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache,
|
||||
CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework,
|
||||
TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, Diagnostics diagnostics, IpcTester ipcTester)
|
||||
TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes,
|
||||
Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel)
|
||||
: base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse)
|
||||
{
|
||||
IsOpen = true;
|
||||
|
|
@ -134,7 +138,8 @@ public class DebugTab : Window, ITab
|
|||
_redraws = redraws;
|
||||
_emotes = emotes;
|
||||
_diagnostics = diagnostics;
|
||||
_ipcTester = ipcTester;
|
||||
_ipcTester = ipcTester;
|
||||
_crashHandlerPanel = crashHandlerPanel;
|
||||
_objects = objects;
|
||||
_clientState = clientState;
|
||||
}
|
||||
|
|
@ -158,6 +163,9 @@ public class DebugTab : Window, ITab
|
|||
return;
|
||||
|
||||
DrawDebugTabGeneral();
|
||||
ImGui.NewLine();
|
||||
_crashHandlerPanel.Draw();
|
||||
ImGui.NewLine();
|
||||
_diagnostics.DrawDiagnostics();
|
||||
DrawPerformanceTab();
|
||||
ImGui.NewLine();
|
||||
|
|
@ -257,6 +265,7 @@ public class DebugTab : Window, ITab
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
var issues = _modManager.WithIndex().Count(p => p.Index != p.Value.Index);
|
||||
using (var tree = TreeNode($"Mods ({issues} Issues)###Mods"))
|
||||
{
|
||||
|
|
@ -394,7 +403,7 @@ public class DebugTab : Window, ITab
|
|||
private void DrawPerformanceTab()
|
||||
{
|
||||
ImGui.NewLine();
|
||||
if (ImGui.CollapsingHeader("Performance"))
|
||||
if (!ImGui.CollapsingHeader("Performance"))
|
||||
return;
|
||||
|
||||
using (var start = TreeNode("Startup Performance", ImGuiTreeNodeFlags.DefaultOpen))
|
||||
|
|
|
|||
|
|
@ -8,31 +8,22 @@ using Penumbra.Collections;
|
|||
using Penumbra.Collections.Cache;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Mods.Editor;
|
||||
using Penumbra.String.Classes;
|
||||
using Penumbra.UI.Classes;
|
||||
|
||||
namespace Penumbra.UI.Tabs;
|
||||
|
||||
public class EffectiveTab : ITab
|
||||
public class EffectiveTab(CollectionManager collectionManager, CollectionSelectHeader collectionHeader)
|
||||
: ITab
|
||||
{
|
||||
private readonly CollectionManager _collectionManager;
|
||||
private readonly CollectionSelectHeader _collectionHeader;
|
||||
|
||||
public EffectiveTab(CollectionManager collectionManager, CollectionSelectHeader collectionHeader)
|
||||
{
|
||||
_collectionManager = collectionManager;
|
||||
_collectionHeader = collectionHeader;
|
||||
}
|
||||
|
||||
public ReadOnlySpan<byte> Label
|
||||
=> "Effective Changes"u8;
|
||||
|
||||
public void DrawContent()
|
||||
{
|
||||
SetupEffectiveSizes();
|
||||
_collectionHeader.Draw(true);
|
||||
collectionHeader.Draw(true);
|
||||
DrawFilters();
|
||||
using var child = ImRaii.Child("##EffectiveChangesTab", -Vector2.One, false);
|
||||
if (!child)
|
||||
|
|
@ -48,7 +39,7 @@ public class EffectiveTab : ITab
|
|||
ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, _effectiveArrowLength);
|
||||
ImGui.TableSetupColumn("##file", ImGuiTableColumnFlags.WidthFixed, _effectiveRightTextLength);
|
||||
|
||||
DrawEffectiveRows(_collectionManager.Active.Current, skips, height,
|
||||
DrawEffectiveRows(collectionManager.Active.Current, skips, height,
|
||||
_effectiveFilePathFilter.Length > 0 || _effectiveGamePathFilter.Length > 0);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,68 +17,53 @@ using Penumbra.Collections.Manager;
|
|||
|
||||
namespace Penumbra.UI.Tabs;
|
||||
|
||||
public class ModsTab : ITab
|
||||
public class ModsTab(
|
||||
ModManager modManager,
|
||||
CollectionManager collectionManager,
|
||||
ModFileSystemSelector selector,
|
||||
ModPanel panel,
|
||||
TutorialService tutorial,
|
||||
RedrawService redrawService,
|
||||
Configuration config,
|
||||
IClientState clientState,
|
||||
CollectionSelectHeader collectionHeader,
|
||||
ITargetManager targets,
|
||||
IObjectTable objectTable)
|
||||
: ITab
|
||||
{
|
||||
private readonly ModFileSystemSelector _selector;
|
||||
private readonly ModPanel _panel;
|
||||
private readonly TutorialService _tutorial;
|
||||
private readonly ModManager _modManager;
|
||||
private readonly ActiveCollections _activeCollections;
|
||||
private readonly RedrawService _redrawService;
|
||||
private readonly Configuration _config;
|
||||
private readonly IClientState _clientState;
|
||||
private readonly CollectionSelectHeader _collectionHeader;
|
||||
private readonly ITargetManager _targets;
|
||||
private readonly IObjectTable _objectTable;
|
||||
|
||||
public ModsTab(ModManager modManager, CollectionManager collectionManager, ModFileSystemSelector selector, ModPanel panel,
|
||||
TutorialService tutorial, RedrawService redrawService, Configuration config, IClientState clientState,
|
||||
CollectionSelectHeader collectionHeader, ITargetManager targets, IObjectTable objectTable)
|
||||
{
|
||||
_modManager = modManager;
|
||||
_activeCollections = collectionManager.Active;
|
||||
_selector = selector;
|
||||
_panel = panel;
|
||||
_tutorial = tutorial;
|
||||
_redrawService = redrawService;
|
||||
_config = config;
|
||||
_clientState = clientState;
|
||||
_collectionHeader = collectionHeader;
|
||||
_targets = targets;
|
||||
_objectTable = objectTable;
|
||||
}
|
||||
private readonly ActiveCollections _activeCollections = collectionManager.Active;
|
||||
|
||||
public bool IsVisible
|
||||
=> _modManager.Valid;
|
||||
=> modManager.Valid;
|
||||
|
||||
public ReadOnlySpan<byte> Label
|
||||
=> "Mods"u8;
|
||||
|
||||
public void DrawHeader()
|
||||
=> _tutorial.OpenTutorial(BasicTutorialSteps.Mods);
|
||||
=> tutorial.OpenTutorial(BasicTutorialSteps.Mods);
|
||||
|
||||
public Mod SelectMod
|
||||
{
|
||||
set => _selector.SelectByValue(value);
|
||||
set => selector.SelectByValue(value);
|
||||
}
|
||||
|
||||
public void DrawContent()
|
||||
{
|
||||
try
|
||||
{
|
||||
_selector.Draw(GetModSelectorSize(_config));
|
||||
selector.Draw(GetModSelectorSize(config));
|
||||
ImGui.SameLine();
|
||||
using var group = ImRaii.Group();
|
||||
_collectionHeader.Draw(false);
|
||||
collectionHeader.Draw(false);
|
||||
|
||||
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
|
||||
|
||||
using (var child = ImRaii.Child("##ModsTabMod", new Vector2(-1, _config.HideRedrawBar ? 0 : -ImGui.GetFrameHeight()),
|
||||
using (var child = ImRaii.Child("##ModsTabMod", new Vector2(-1, config.HideRedrawBar ? 0 : -ImGui.GetFrameHeight()),
|
||||
true, ImGuiWindowFlags.HorizontalScrollbar))
|
||||
{
|
||||
style.Pop();
|
||||
if (child)
|
||||
_panel.Draw();
|
||||
panel.Draw();
|
||||
|
||||
style.Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
|
||||
}
|
||||
|
|
@ -89,14 +74,14 @@ public class ModsTab : ITab
|
|||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Log.Error($"Exception thrown during ModPanel Render:\n{e}");
|
||||
Penumbra.Log.Error($"{_modManager.Count} Mods\n"
|
||||
Penumbra.Log.Error($"{modManager.Count} Mods\n"
|
||||
+ $"{_activeCollections.Current.AnonymizedName} Current Collection\n"
|
||||
+ $"{_activeCollections.Current.Settings.Count} Settings\n"
|
||||
+ $"{_selector.SortMode.Name} Sort Mode\n"
|
||||
+ $"{_selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n"
|
||||
+ $"{_selector.Selected?.Name ?? "NULL"} Selected Mod\n"
|
||||
+ $"{selector.SortMode.Name} Sort Mode\n"
|
||||
+ $"{selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n"
|
||||
+ $"{selector.Selected?.Name ?? "NULL"} Selected Mod\n"
|
||||
+ $"{string.Join(", ", _activeCollections.Current.DirectlyInheritsFrom.Select(c => c.AnonymizedName))} Inheritances\n"
|
||||
+ $"{_selector.SelectedSettingCollection.AnonymizedName} Collection\n");
|
||||
+ $"{selector.SelectedSettingCollection.AnonymizedName} Collection\n");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -115,9 +100,9 @@ public class ModsTab : ITab
|
|||
|
||||
private void DrawRedrawLine()
|
||||
{
|
||||
if (_config.HideRedrawBar)
|
||||
if (config.HideRedrawBar)
|
||||
{
|
||||
_tutorial.SkipTutorial(BasicTutorialSteps.Redrawing);
|
||||
tutorial.SkipTutorial(BasicTutorialSteps.Redrawing);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -135,15 +120,15 @@ public class ModsTab : ITab
|
|||
}
|
||||
|
||||
var hovered = ImGui.IsItemHovered();
|
||||
_tutorial.OpenTutorial(BasicTutorialSteps.Redrawing);
|
||||
tutorial.OpenTutorial(BasicTutorialSteps.Redrawing);
|
||||
if (hovered)
|
||||
ImGui.SetTooltip($"The supported modifiers for '/penumbra redraw' are:\n{TutorialService.SupportedRedrawModifiers}");
|
||||
|
||||
using var id = ImRaii.PushId("Redraw");
|
||||
using var disabled = ImRaii.Disabled(_clientState.LocalPlayer == null);
|
||||
using var disabled = ImRaii.Disabled(clientState.LocalPlayer == null);
|
||||
ImGui.SameLine();
|
||||
var buttonWidth = frameHeight with { X = ImGui.GetContentRegionAvail().X / 5 };
|
||||
var tt = _objectTable.GetObjectAddress(0) == nint.Zero
|
||||
var tt = objectTable.GetObjectAddress(0) == nint.Zero
|
||||
? "\nCan only be used when you are logged in and your character is available."
|
||||
: string.Empty;
|
||||
DrawButton(buttonWidth, "All", string.Empty, tt);
|
||||
|
|
@ -151,13 +136,13 @@ public class ModsTab : ITab
|
|||
DrawButton(buttonWidth, "Self", "self", tt);
|
||||
ImGui.SameLine();
|
||||
|
||||
tt = _targets.Target == null && _targets.GPoseTarget == null
|
||||
tt = targets.Target == null && targets.GPoseTarget == null
|
||||
? "\nCan only be used when you have a target."
|
||||
: string.Empty;
|
||||
DrawButton(buttonWidth, "Target", "target", tt);
|
||||
ImGui.SameLine();
|
||||
|
||||
tt = _targets.FocusTarget == null
|
||||
tt = targets.FocusTarget == null
|
||||
? "\nCan only be used when you have a focus target."
|
||||
: string.Empty;
|
||||
DrawButton(buttonWidth, "Focus", "focus", tt);
|
||||
|
|
@ -176,9 +161,9 @@ public class ModsTab : ITab
|
|||
if (ImGui.Button(label, size))
|
||||
{
|
||||
if (lower.Length > 0)
|
||||
_redrawService.RedrawObject(lower, RedrawType.Redraw);
|
||||
redrawService.RedrawObject(lower, RedrawType.Redraw);
|
||||
else
|
||||
_redrawService.RedrawAll(RedrawType.Redraw);
|
||||
redrawService.RedrawAll(RedrawType.Redraw);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ namespace Penumbra.UI.Tabs;
|
|||
public class OnScreenTab : ITab
|
||||
{
|
||||
private readonly Configuration _config;
|
||||
private ResourceTreeViewer _viewer;
|
||||
private readonly ResourceTreeViewer _viewer;
|
||||
|
||||
public OnScreenTab(Configuration config, ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -12,24 +12,14 @@ using Penumbra.String.Classes;
|
|||
|
||||
namespace Penumbra.UI.Tabs;
|
||||
|
||||
public class ResourceTab : ITab
|
||||
public class ResourceTab(Configuration config, ResourceManagerService resourceManager, ISigScanner sigScanner)
|
||||
: ITab
|
||||
{
|
||||
private readonly Configuration _config;
|
||||
private readonly ResourceManagerService _resourceManager;
|
||||
private readonly ISigScanner _sigScanner;
|
||||
|
||||
public ResourceTab(Configuration config, ResourceManagerService resourceManager, ISigScanner sigScanner)
|
||||
{
|
||||
_config = config;
|
||||
_resourceManager = resourceManager;
|
||||
_sigScanner = sigScanner;
|
||||
}
|
||||
|
||||
public ReadOnlySpan<byte> Label
|
||||
=> "Resource Manager"u8;
|
||||
|
||||
public bool IsVisible
|
||||
=> _config.DebugMode;
|
||||
=> config.DebugMode;
|
||||
|
||||
/// <summary> Draw a tab to iterate over the main resource maps and see what resources are currently loaded. </summary>
|
||||
public void DrawContent()
|
||||
|
|
@ -44,15 +34,15 @@ public class ResourceTab : ITab
|
|||
|
||||
unsafe
|
||||
{
|
||||
_resourceManager.IterateGraphs(DrawCategoryContainer);
|
||||
resourceManager.IterateGraphs(DrawCategoryContainer);
|
||||
}
|
||||
|
||||
ImGui.NewLine();
|
||||
unsafe
|
||||
{
|
||||
ImGui.TextUnformatted(
|
||||
$"Static Address: 0x{(ulong)_resourceManager.ResourceManagerAddress:X} (+0x{(ulong)_resourceManager.ResourceManagerAddress - (ulong)_sigScanner.Module.BaseAddress:X})");
|
||||
ImGui.TextUnformatted($"Actual Address: 0x{(ulong)_resourceManager.ResourceManager:X}");
|
||||
$"Static Address: 0x{(ulong)resourceManager.ResourceManagerAddress:X} (+0x{(ulong)resourceManager.ResourceManagerAddress - (ulong)sigScanner.Module.BaseAddress:X})");
|
||||
ImGui.TextUnformatted($"Actual Address: 0x{(ulong)resourceManager.ResourceManager:X}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -82,7 +72,7 @@ public class ResourceTab : ITab
|
|||
ImGui.TableSetupColumn("Refs", ImGuiTableColumnFlags.WidthFixed, _refsColumnWidth);
|
||||
ImGui.TableHeadersRow();
|
||||
|
||||
_resourceManager.IterateResourceMap(map, (hash, r) =>
|
||||
resourceManager.IterateResourceMap(map, (hash, r) =>
|
||||
{
|
||||
// Filter unwanted names.
|
||||
if (_resourceManagerFilter.Length != 0
|
||||
|
|
@ -125,7 +115,7 @@ public class ResourceTab : ITab
|
|||
if (tree)
|
||||
{
|
||||
SetTableWidths();
|
||||
_resourceManager.IterateExtMap(map, (ext, m) => DrawResourceMap(category, ext, m));
|
||||
resourceManager.IterateExtMap(map, (ext, m) => DrawResourceMap(category, ext, m));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,18 +11,6 @@
|
|||
"Unosquare.Swan.Lite": "3.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.CodeAnalysis.Common": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.8.0, )",
|
||||
"resolved": "4.8.0",
|
||||
"contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==",
|
||||
"dependencies": {
|
||||
"Microsoft.CodeAnalysis.Analyzers": "3.3.4",
|
||||
"System.Collections.Immutable": "7.0.0",
|
||||
"System.Reflection.Metadata": "7.0.0",
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection": {
|
||||
"type": "Direct",
|
||||
"requested": "[7.0.0, )",
|
||||
|
|
@ -55,29 +43,15 @@
|
|||
},
|
||||
"SixLabors.ImageSharp": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.1.2, )",
|
||||
"resolved": "2.1.2",
|
||||
"contentHash": "In0pC521LqJXJXZgFVHegvSzES10KkKRN31McxqA1+fKtKsNe+EShWavBFQnKRlXCdeAmfx/wDjLILbvCaq+8Q==",
|
||||
"dependencies": {
|
||||
"System.Runtime.CompilerServices.Unsafe": "5.0.0",
|
||||
"System.Text.Encoding.CodePages": "5.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.CodeAnalysis.Analyzers": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.3.4",
|
||||
"contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g=="
|
||||
"requested": "[3.1.3, )",
|
||||
"resolved": "3.1.3",
|
||||
"contentHash": "wybtaqZQ1ZRZ4ZeU+9h+PaSeV14nyiGKIy7qRbDfSHzHq4ybqyOcjoifeaYbiKLO1u+PVxLBuy7MF/DMmwwbfg=="
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "7.0.0",
|
||||
"contentHash": "h3j/QfmFN4S0w4C2A6X7arXij/M/OVw3uQHSOFxnND4DyAzO1F9eMX7Eti7lU/OkSthEE0WzRsfT/Dmx86jzCw=="
|
||||
},
|
||||
"Microsoft.NETCore.Platforms": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ=="
|
||||
},
|
||||
"SharpGLTF.Runtime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.0.0-alpha0030",
|
||||
|
|
@ -86,32 +60,6 @@
|
|||
"SharpGLTF.Core": "1.0.0-alpha0030"
|
||||
}
|
||||
},
|
||||
"System.Collections.Immutable": {
|
||||
"type": "Transitive",
|
||||
"resolved": "7.0.0",
|
||||
"contentHash": "dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ=="
|
||||
},
|
||||
"System.Reflection.Metadata": {
|
||||
"type": "Transitive",
|
||||
"resolved": "7.0.0",
|
||||
"contentHash": "MclTG61lsD9sYdpNz9xsKBzjsmsfCtcMZYXz/IUr2zlhaTaABonlr1ESeompTgM+Xk+IwtGYU7/voh3YWB/fWw==",
|
||||
"dependencies": {
|
||||
"System.Collections.Immutable": "7.0.0"
|
||||
}
|
||||
},
|
||||
"System.Runtime.CompilerServices.Unsafe": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
|
||||
},
|
||||
"System.Text.Encoding.CodePages": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "NyscU59xX6Uo91qvhOs2Ccho3AR2TnZPomo1Z0K6YpyztBPM/A5VbkzOO19sy3A3i1TtEnTxA7bCe3Us+r5MWg==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "5.0.0"
|
||||
}
|
||||
},
|
||||
"System.ValueTuple": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.0",
|
||||
|
|
@ -134,11 +82,14 @@
|
|||
"penumbra.api": {
|
||||
"type": "Project"
|
||||
},
|
||||
"penumbra.crashhandler": {
|
||||
"type": "Project"
|
||||
},
|
||||
"penumbra.gamedata": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"OtterGui": "[1.0.0, )",
|
||||
"Penumbra.Api": "[1.0.13, )",
|
||||
"Penumbra.Api": "[1.0.14, )",
|
||||
"Penumbra.String": "[1.0.4, )"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue