diff --git a/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs b/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs new file mode 100644 index 00000000..dd966542 --- /dev/null +++ b/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs @@ -0,0 +1,119 @@ +using System.Text.Json.Nodes; + +namespace Penumbra.CrashHandler.Buffers; + +/// The types of currently hooked and relevant animation loading functions. +public enum AnimationInvocationType : int +{ + PapLoad, + ActionLoad, + ScheduleClipUpdate, + LoadTimelineResources, + LoadCharacterVfx, + LoadCharacterSound, + ApricotSoundPlay, + LoadAreaVfx, + CharacterBaseLoadAnimation, +} + +/// The full crash entry for an invoked vfx function. +public record struct VfxFuncInvokedEntry( + double Age, + DateTimeOffset Timestamp, + int ThreadId, + string InvocationType, + string CharacterName, + string CharacterAddress, + string CollectionName) : ICrashDataEntry; + +/// Only expose the write interface for the buffer. +public interface IAnimationInvocationBufferWriter +{ + /// Write a line into the buffer with the given data. + /// The address of the related character, if known. + /// The name of the related character, anonymized or relying on index if unavailable, if known. + /// The name of the associated collection. Not anonymized. + /// The type of VFX func called. + public void WriteLine(nint characterAddress, ReadOnlySpan 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 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 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})", + }; +} diff --git a/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs b/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs new file mode 100644 index 00000000..1fe5d7ba --- /dev/null +++ b/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs @@ -0,0 +1,85 @@ +using System.Text.Json.Nodes; + +namespace Penumbra.CrashHandler.Buffers; + +/// Only expose the write interface for the buffer. +public interface ICharacterBaseBufferWriter +{ + /// Write a line into the buffer with the given data. + /// The address of the related character, if known. + /// The name of the related character, anonymized or relying on index if unavailable, if known. + /// The name of the associated collection. Not anonymized. + public void WriteLine(nint characterAddress, ReadOnlySpan characterName, string collectionName); +} + +/// The full crash entry for a loaded character base. +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 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 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) + { } +} diff --git a/Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs b/Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs new file mode 100644 index 00000000..35055864 --- /dev/null +++ b/Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs @@ -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 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 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 input, Span 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 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 GetSpan(MemoryMappedViewAccessor accessor, int offset = 0) + => GetSpan(accessor, offset, (int)accessor.Capacity - offset); + + protected static unsafe Span 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(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); +} diff --git a/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs b/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs new file mode 100644 index 00000000..b472d413 --- /dev/null +++ b/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs @@ -0,0 +1,99 @@ +using System.Text.Json.Nodes; + +namespace Penumbra.CrashHandler.Buffers; + +/// Only expose the write interface for the buffer. +public interface IModdedFileBufferWriter +{ + /// Write a line into the buffer with the given data. + /// The address of the related character, if known. + /// The name of the related character, anonymized or relying on index if unavailable, if known. + /// The name of the associated collection. Not anonymized. + /// The file name as requested by the game. + /// The actual modded file name loaded. + public void WriteLine(nint characterAddress, ReadOnlySpan characterName, string collectionName, ReadOnlySpan requestedFileName, + ReadOnlySpan actualFileName); +} + +/// The full crash entry for a loaded modded file. +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 characterName, string collectionName, ReadOnlySpan requestedFileName, + ReadOnlySpan 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 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) + { } +} diff --git a/Penumbra.CrashHandler/CrashData.cs b/Penumbra.CrashHandler/CrashData.cs new file mode 100644 index 00000000..956a3db7 --- /dev/null +++ b/Penumbra.CrashHandler/CrashData.cs @@ -0,0 +1,62 @@ +using Penumbra.CrashHandler.Buffers; + +namespace Penumbra.CrashHandler; + +/// A base entry for crash data. +public interface ICrashDataEntry +{ + /// The timestamp of the event. + DateTimeOffset Timestamp { get; } + + /// The thread invoking the event. + int ThreadId { get; } + + /// The age of the event compared to the crash. (Redundantly with the timestamp) + double Age { get; } +} + +/// A full set of crash data. +public class CrashData +{ + /// The mode this data was obtained - manually or from a crash. + public string Mode { get; set; } = "Unknown"; + + /// The time this crash data was generated. + public DateTimeOffset CrashTime { get; set; } = DateTimeOffset.UnixEpoch; + + /// The FFXIV process ID when this data was generated. + public int ProcessId { get; set; } = 0; + + /// The FFXIV Exit Code (if any) when this data was generated. + public int ExitCode { get; set; } = 0; + + /// The total amount of characters loaded during this session. + public int TotalCharactersLoaded { get; set; } = 0; + + /// The total amount of modded files loaded during this session. + public int TotalModdedFilesLoaded { get; set; } = 0; + + /// The total amount of vfx functions invoked during this session. + public int TotalVFXFuncsInvoked { get; set; } = 0; + + /// The last character loaded before this crash data was generated. + public CharacterLoadedEntry? LastCharacterLoaded + => LastCharactersLoaded.Count == 0 ? default : LastCharactersLoaded[0]; + + /// The last modded file loaded before this crash data was generated. + public ModdedFileLoadedEntry? LastModdedFileLoaded + => LastModdedFilesLoaded.Count == 0 ? default : LastModdedFilesLoaded[0]; + + /// The last vfx function invoked before this crash data was generated. + public VfxFuncInvokedEntry? LastVfxFuncInvoked + => LastVfxFuncsInvoked.Count == 0 ? default : LastVfxFuncsInvoked[0]; + + /// A collection of the last few characters loaded before this crash data was generated. + public List LastCharactersLoaded { get; } = []; + + /// A collection of the last few modded files loaded before this crash data was generated. + public List LastModdedFilesLoaded { get; } = []; + + /// A collection of the last few vfx functions invoked before this crash data was generated. + public List LastVfxFuncsInvoked { get; } = []; +} diff --git a/Penumbra.CrashHandler/GameEventLogReader.cs b/Penumbra.CrashHandler/GameEventLogReader.cs new file mode 100644 index 00000000..283be526 --- /dev/null +++ b/Penumbra.CrashHandler/GameEventLogReader.cs @@ -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 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; + } +} diff --git a/Penumbra.CrashHandler/GameEventLogWriter.cs b/Penumbra.CrashHandler/GameEventLogWriter.cs new file mode 100644 index 00000000..8e809cec --- /dev/null +++ b/Penumbra.CrashHandler/GameEventLogWriter.cs @@ -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(); + } +} diff --git a/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj b/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj new file mode 100644 index 00000000..ea61b968 --- /dev/null +++ b/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj @@ -0,0 +1,28 @@ + + + + Exe + net7.0-windows + preview + enable + x64 + enable + true + false + + + + $(appdata)\XIVLauncher\addon\Hooks\dev\ + $(HOME)/.xlcore/dalamud/Hooks/dev/ + $(DALAMUD_HOME)/ + + + + embedded + + + + embedded + + + diff --git a/Penumbra.CrashHandler/Program.cs b/Penumbra.CrashHandler/Program.cs new file mode 100644 index 00000000..e4a46348 --- /dev/null +++ b/Penumbra.CrashHandler/Program.cs @@ -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}"); + } + } +} diff --git a/Penumbra.sln b/Penumbra.sln index 5c11aaea..78fa1543 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -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 diff --git a/Penumbra/Communication/CreatingCharacterBase.cs b/Penumbra/Communication/CreatingCharacterBase.cs index 1e232761..2f249c14 100644 --- a/Penumbra/Communication/CreatingCharacterBase.cs +++ b/Penumbra/Communication/CreatingCharacterBase.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using Penumbra.Api; +using Penumbra.Services; namespace Penumbra.Communication; @@ -19,5 +20,8 @@ public sealed class CreatingCharacterBase() { /// Api = 0, + + /// + CrashHandler = 0, } } diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 188be65d..9c0b4f2d 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -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; diff --git a/Penumbra/EphemeralConfig.cs b/Penumbra/EphemeralConfig.cs index 98b1a5d6..0a542d04 100644 --- a/Penumbra/EphemeralConfig.cs +++ b/Penumbra/EphemeralConfig.cs @@ -47,7 +47,7 @@ public class EphemeralConfig : ISavable, IDisposable /// 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); } - /// Overwrite the last saved mod path if it changes. + /// Overwrite the last saved mod path if it changes. private void OnModPathChanged(ModPathChangeType type, Mod mod, DirectoryInfo? old, DirectoryInfo? _) { if (type is not ModPathChangeType.Moved || !string.Equals(old?.Name, LastModPath, StringComparison.OrdinalIgnoreCase)) diff --git a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs index b91c5375..77927593 100644 --- a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs +++ b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs @@ -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; /// Called for some sound effects caused by animations or VFX. public sealed unsafe class ApricotListenerSoundPlay : FastHook { - 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("Apricot Listener Sound Play", Sigs.ApricotListenerSoundPlay, Detour, true); } @@ -46,6 +50,7 @@ public sealed unsafe class ApricotListenerSoundPlay : FastHook public sealed unsafe class CharacterBaseLoadAnimation : FastHook { - 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("CharacterBase Load Animation", Sigs.CharacterBaseLoadAnimation, Detour, true); } @@ -33,7 +37,9 @@ public sealed unsafe class CharacterBaseLoadAnimation : FastHook Load a ground-based area VFX. public sealed unsafe class LoadAreaVfx : FastHook { - 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("Load Area VFX", Sigs.LoadAreaVfx, Detour, true); } @@ -25,10 +29,11 @@ public sealed unsafe class LoadAreaVfx : FastHook 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}."); diff --git a/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs b/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs index af13805d..98454a77 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs @@ -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; /// Characters load some of their voice lines or whatever with this function. public sealed unsafe class LoadCharacterSound : FastHook { - 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("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 {ret}."); _state.RestoreSoundData(last); diff --git a/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs b/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs index 240c062e..69c22773 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs @@ -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; /// Load a VFX specifically for a character. public sealed unsafe class LoadCharacterVfx : FastHook { - 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("Load Character VFX", Sigs.LoadCharacterVfx, Detour, true); } @@ -45,6 +50,7 @@ public sealed unsafe class LoadCharacterVfx : FastHookGameObjectId:X}, {vfxParams->TargetCount}, {unk1}, {unk2}, {unk3}, {unk4} -> 0x{ret:X}."); diff --git a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs index 2ca8ffe7..ade957b9 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs @@ -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; /// public sealed unsafe class LoadTimelineResources : FastHook { - 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("Load Timeline Resources", Sigs.LoadTimelineResources, Detour, true); + _crashHandler = crashHandler; + Task = hooks.CreateHook("Load Timeline Resources", Sigs.LoadTimelineResources, Detour, true); } public delegate ulong Delegate(nint timeline); @@ -39,7 +43,13 @@ public sealed unsafe class LoadTimelineResources : FastHook Called when some action timelines update. public sealed unsafe class ScheduleClipUpdate : FastHook { - 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("Schedule Clip Update", Sigs.ScheduleClipUpdate, Detour, true); } @@ -27,8 +32,9 @@ public sealed unsafe class ScheduleClipUpdate : FastHookSchedulerTimeline)); + 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); } diff --git a/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs b/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs index 48931d73..6de3aeb0 100644 --- a/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs +++ b/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs @@ -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; - /// Seems to load character actions when zoning or changing class, maybe. public sealed unsafe class SomeActionLoad : FastHook { - 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("Some Action Load", Sigs.LoadSomeAction, Detour, true); } @@ -24,8 +28,10 @@ public sealed unsafe class SomeActionLoad : FastHook [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); } diff --git a/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs b/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs index 75caacee..3e60e62f 100644 --- a/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs +++ b/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs @@ -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; /// Unknown what exactly this is, but it seems to load a bunch of paps. public sealed unsafe class SomePapLoad : FastHook { - 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("Some PAP Load", Sigs.LoadSomePap, Detour, true); } @@ -33,8 +38,9 @@ public sealed unsafe class SomePapLoad : FastHook 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; diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 01b3d680..796eae01 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -1,4 +1,4 @@ - + net7.0-windows preview @@ -69,8 +69,7 @@ - - + @@ -79,8 +78,10 @@ + + @@ -103,4 +104,4 @@ $(GitCommitHash) - \ No newline at end of file + diff --git a/Penumbra/Services/CrashHandlerService.cs b/Penumbra/Services/CrashHandlerService.cs new file mode 100644 index 00000000..4e2bce0f --- /dev/null +++ b/Penumbra/Services/CrashHandlerService.cs @@ -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; + } +} diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs index 5f918a90..49b4cb42 100644 --- a/Penumbra/Services/FilenameService.cs +++ b/Penumbra/Services/FilenameService.cs @@ -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"); + /// Obtain the path of a collection file given its name. public string CollectionFile(ModCollection collection) => CollectionFile(collection.Name); diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index 76fb8c96..ab0badf4 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -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 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 /// Apply the current filters. private bool FilterChangedItem(KeyValuePair, 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))); /// Draw a full column for a changed item. private void DrawChangedItemColumn(KeyValuePair, 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 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()) { diff --git a/Penumbra/UI/Tabs/ConfigTabBar.cs b/Penumbra/UI/Tabs/ConfigTabBar.cs index ad3fdb3d..9fd07f27 100644 --- a/Penumbra/UI/Tabs/ConfigTabBar.cs +++ b/Penumbra/UI/Tabs/ConfigTabBar.cs @@ -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); } diff --git a/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs b/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs new file mode 100644 index 00000000..3d32d267 --- /dev/null +++ b/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs @@ -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()); + } +} diff --git a/Penumbra/UI/Tabs/Debug/CrashHandlerPanel.cs b/Penumbra/UI/Tabs/Debug/CrashHandlerPanel.cs new file mode 100644 index 00000000..c8e7f001 --- /dev/null +++ b/Penumbra/UI/Tabs/Debug/CrashHandlerPanel.cs @@ -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(); + + 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(); + _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(string label, in T data) + { + ImGuiUtil.DrawTableColumn(label); + ImGuiUtil.DrawTableColumn(data?.ToString() ?? "NULL"); + } +} diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index f4ddbe31..ab7ccf0c 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -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)) diff --git a/Penumbra/UI/Tabs/EffectiveTab.cs b/Penumbra/UI/Tabs/EffectiveTab.cs index 8ef6c30e..37561000 100644 --- a/Penumbra/UI/Tabs/EffectiveTab.cs +++ b/Penumbra/UI/Tabs/EffectiveTab.cs @@ -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 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); } diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index 9f070d35..d111c465 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -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 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); } } diff --git a/Penumbra/UI/Tabs/OnScreenTab.cs b/Penumbra/UI/Tabs/OnScreenTab.cs index 8d323baf..09772d8e 100644 --- a/Penumbra/UI/Tabs/OnScreenTab.cs +++ b/Penumbra/UI/Tabs/OnScreenTab.cs @@ -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) { diff --git a/Penumbra/UI/Tabs/ResourceTab.cs b/Penumbra/UI/Tabs/ResourceTab.cs index 6f3dec30..bbb0561b 100644 --- a/Penumbra/UI/Tabs/ResourceTab.cs +++ b/Penumbra/UI/Tabs/ResourceTab.cs @@ -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 Label => "Resource Manager"u8; public bool IsVisible - => _config.DebugMode; + => config.DebugMode; /// Draw a tab to iterate over the main resource maps and see what resources are currently loaded. 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)); } } diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index cb873592..d0e8fa1a 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -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, )" } },