Add crash handler stuff.

This commit is contained in:
Ottermandias 2024-03-16 16:20:34 +01:00
parent 9ba6e4d0af
commit e08e9c4d13
35 changed files with 1472 additions and 237 deletions

View 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})",
};
}

View 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)
{ }
}

View 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);
}

View 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)
{ }
}

View 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; } = [];
}

View 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;
}
}

View 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();
}
}

View 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>

View 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}");
}
}
}

View file

@ -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

View file

@ -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,
}
}

View file

@ -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;

View file

@ -2,8 +2,10 @@ 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;
@ -12,11 +14,13 @@ public sealed unsafe class ApricotListenerSoundPlay : FastHook<ApricotListenerSo
{
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);

View file

@ -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;
@ -15,13 +17,15 @@ public sealed unsafe class CharacterBaseLoadAnimation : FastHook<CharacterBaseLo
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);

View file

@ -1,8 +1,10 @@
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;
@ -11,11 +13,13 @@ public sealed unsafe class LoadAreaVfx : FastHook<LoadAreaVfx.Delegate>
{
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);
}
@ -29,6 +33,7 @@ public sealed unsafe class LoadAreaVfx : FastHook<LoadAreaVfx.Delegate>
: 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}.");

View file

@ -1,6 +1,8 @@
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;
@ -9,11 +11,13 @@ public sealed unsafe class LoadCharacterSound : FastHook<LoadCharacterSound.Dele
{
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;
_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);

View file

@ -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;
@ -15,12 +17,15 @@ public sealed unsafe class LoadCharacterVfx : FastHook<LoadCharacterVfx.Delegate
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}.");

View file

@ -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;
@ -18,14 +20,16 @@ public sealed unsafe class LoadTimelineResources : FastHook<LoadTimelineResource
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;
_crashHandler = crashHandler;
Task = hooks.CreateHook<Delegate>("Load Timeline Resources", Sigs.LoadTimelineResources, Detour, true);
}
@ -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;

View file

@ -1,8 +1,10 @@
using Dalamud.Plugin.Services;
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;
@ -12,12 +14,15 @@ public sealed unsafe class ScheduleClipUpdate : FastHook<ScheduleClipUpdate.Dele
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);
}

View file

@ -1,8 +1,10 @@
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;
@ -11,11 +13,13 @@ public sealed unsafe class SomeActionLoad : FastHook<SomeActionLoad.Delegate>
{
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);
}

View file

@ -1,8 +1,10 @@
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;
@ -12,12 +14,15 @@ public sealed unsafe class SomePapLoad : FastHook<SomePapLoad.Delegate>
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;

View file

@ -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>

View 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;
}
}

View file

@ -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);

View file

@ -12,22 +12,13 @@ using Penumbra.UI.Classes;
namespace Penumbra.UI.Tabs;
public class ChangedItemsTab : 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,
public class ChangedItemsTab(
CollectionManager collectionManager,
CollectionSelectHeader collectionHeader,
ChangedItemDrawer drawer,
CommunicatorService communicator)
{
_collectionManager = collectionManager;
_collectionHeader = collectionHeader;
_drawer = drawer;
_communicator = communicator;
}
: ITab
{
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())
{

View file

@ -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);
}

View 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());
}
}

View 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");
}
}

View file

@ -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));
@ -93,13 +93,17 @@ public class DebugTab : Window, ITab
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;
@ -135,6 +139,7 @@ public class DebugTab : Window, ITab
_emotes = emotes;
_diagnostics = diagnostics;
_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))

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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)
{

View file

@ -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));
}
}

View file

@ -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, )"
}
},