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 EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.String", "Penumbra.String\Penumbra.String.csproj", "{5549BAFD-6357-4B1A-800C-75AC36E5B76D}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.String", "Penumbra.String\Penumbra.String.csproj", "{5549BAFD-6357-4B1A-800C-75AC36E5B76D}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Penumbra.CrashHandler", "Penumbra.CrashHandler\Penumbra.CrashHandler.csproj", "{EE834491-A98F-4395-BE0D-6861AE5AD953}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View file

@ -1,5 +1,6 @@
using OtterGui.Classes; using OtterGui.Classes;
using Penumbra.Api; using Penumbra.Api;
using Penumbra.Services;
namespace Penumbra.Communication; namespace Penumbra.Communication;
@ -19,5 +20,8 @@ public sealed class CreatingCharacterBase()
{ {
/// <seealso cref="PenumbraApi.CreatingCharacterBase"/> /// <seealso cref="PenumbraApi.CreatingCharacterBase"/>
Api = 0, 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 ModDirectory { get; set; } = string.Empty;
public string ExportDirectory { 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 OpenWindowAtStart { get; set; } = false;
public bool HideUiInGPose { get; set; } = false; public bool HideUiInGPose { get; set; } = false;
public bool HideUiInCutscenes { get; set; } = true; public bool HideUiInCutscenes { get; set; } = true;

View file

@ -47,7 +47,7 @@ public class EphemeralConfig : ISavable, IDisposable
/// </summary> /// </summary>
public EphemeralConfig(SaveService saveService, ModPathChanged modPathChanged) public EphemeralConfig(SaveService saveService, ModPathChanged modPathChanged)
{ {
_saveService = saveService; _saveService = saveService;
_modPathChanged = modPathChanged; _modPathChanged = modPathChanged;
Load(); Load();
_modPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.EphemeralConfig); _modPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.EphemeralConfig);
@ -94,9 +94,9 @@ public class EphemeralConfig : ISavable, IDisposable
public void Save(StreamWriter writer) public void Save(StreamWriter writer)
{ {
using var jWriter = new JsonTextWriter(writer); using var jWriter = new JsonTextWriter(writer);
jWriter.Formatting = Formatting.Indented; jWriter.Formatting = Formatting.Indented;
var serializer = new JsonSerializer { Formatting = Formatting.Indented }; var serializer = new JsonSerializer { Formatting = Formatting.Indented };
serializer.Serialize(jWriter, this); serializer.Serialize(jWriter, this);
} }

View file

@ -2,21 +2,25 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using OtterGui.Services; using OtterGui.Services;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.CrashHandler.Buffers;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.Interop.PathResolving; using Penumbra.Interop.PathResolving;
using Penumbra.Services;
namespace Penumbra.Interop.Hooks.Animation; namespace Penumbra.Interop.Hooks.Animation;
/// <summary> Called for some sound effects caused by animations or VFX. </summary> /// <summary> Called for some sound effects caused by animations or VFX. </summary>
public sealed unsafe class ApricotListenerSoundPlay : FastHook<ApricotListenerSoundPlay.Delegate> public sealed unsafe class ApricotListenerSoundPlay : FastHook<ApricotListenerSoundPlay.Delegate>
{ {
private readonly GameState _state; private readonly GameState _state;
private readonly CollectionResolver _collectionResolver; 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; _state = state;
_collectionResolver = collectionResolver; _collectionResolver = collectionResolver;
_crashHandler = crashHandler;
Task = hooks.CreateHook<Delegate>("Apricot Listener Sound Play", Sigs.ApricotListenerSoundPlay, Detour, true); 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); newData = _collectionResolver.IdentifyCollection(drawObject, true);
} }
_crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.ApricotSoundPlay);
var last = _state.SetAnimationData(newData); var last = _state.SetAnimationData(newData);
var ret = Task.Result.Original(a1, a2, a3, a4, a5, a6); var ret = Task.Result.Original(a1, a2, a3, a4, a5, a6);
_state.RestoreAnimationData(last); _state.RestoreAnimationData(last);

View file

@ -1,8 +1,10 @@
using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using OtterGui.Services; using OtterGui.Services;
using Penumbra.CrashHandler.Buffers;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.Interop.PathResolving; using Penumbra.Interop.PathResolving;
using Penumbra.Services;
namespace Penumbra.Interop.Hooks.Animation; namespace Penumbra.Interop.Hooks.Animation;
@ -12,16 +14,18 @@ namespace Penumbra.Interop.Hooks.Animation;
/// </summary> /// </summary>
public sealed unsafe class CharacterBaseLoadAnimation : FastHook<CharacterBaseLoadAnimation.Delegate> public sealed unsafe class CharacterBaseLoadAnimation : FastHook<CharacterBaseLoadAnimation.Delegate>
{ {
private readonly GameState _state; private readonly GameState _state;
private readonly CollectionResolver _collectionResolver; private readonly CollectionResolver _collectionResolver;
private readonly DrawObjectState _drawObjectState; private readonly DrawObjectState _drawObjectState;
private readonly CrashHandlerService _crashHandler;
public CharacterBaseLoadAnimation(HookManager hooks, GameState state, CollectionResolver collectionResolver, public CharacterBaseLoadAnimation(HookManager hooks, GameState state, CollectionResolver collectionResolver,
DrawObjectState drawObjectState) DrawObjectState drawObjectState, CrashHandlerService crashHandler)
{ {
_state = state; _state = state;
_collectionResolver = collectionResolver; _collectionResolver = collectionResolver;
_drawObjectState = drawObjectState; _drawObjectState = drawObjectState;
_crashHandler = crashHandler;
Task = hooks.CreateHook<Delegate>("CharacterBase Load Animation", Sigs.CharacterBaseLoadAnimation, Detour, true); 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; var lastObj = _state.LastGameObject;
if (lastObj == nint.Zero && _drawObjectState.TryGetValue((nint)drawObject, out var p)) if (lastObj == nint.Zero && _drawObjectState.TryGetValue((nint)drawObject, out var p))
lastObj = p.Item1; 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}"); Penumbra.Log.Excessive($"[CharacterBase Load Animation] Invoked on {(nint)drawObject:X}");
Task.Result.Original(drawObject); Task.Result.Original(drawObject);
_state.RestoreAnimationData(last); _state.RestoreAnimationData(last);

View file

@ -1,21 +1,25 @@
using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.Object;
using OtterGui.Services; using OtterGui.Services;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.CrashHandler.Buffers;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.Interop.PathResolving; using Penumbra.Interop.PathResolving;
using Penumbra.Services;
namespace Penumbra.Interop.Hooks.Animation; namespace Penumbra.Interop.Hooks.Animation;
/// <summary> Load a ground-based area VFX. </summary> /// <summary> Load a ground-based area VFX. </summary>
public sealed unsafe class LoadAreaVfx : FastHook<LoadAreaVfx.Delegate> public sealed unsafe class LoadAreaVfx : FastHook<LoadAreaVfx.Delegate>
{ {
private readonly GameState _state; private readonly GameState _state;
private readonly CollectionResolver _collectionResolver; 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; _state = state;
_collectionResolver = collectionResolver; _collectionResolver = collectionResolver;
_crashHandler = crashHandler;
Task = hooks.CreateHook<Delegate>("Load Area VFX", Sigs.LoadAreaVfx, Detour, true); Task = hooks.CreateHook<Delegate>("Load Area VFX", Sigs.LoadAreaVfx, Detour, true);
} }
@ -26,9 +30,10 @@ public sealed unsafe class LoadAreaVfx : FastHook<LoadAreaVfx.Delegate>
{ {
var newData = caster != null var newData = caster != null
? _collectionResolver.IdentifyCollection(caster, true) ? _collectionResolver.IdentifyCollection(caster, true)
: ResolveData.Invalid; : ResolveData.Invalid;
var last = _state.SetAnimationData(newData); var last = _state.SetAnimationData(newData);
_crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.LoadAreaVfx);
var ret = Task.Result.Original(vfxId, pos, caster, unk1, unk2, unk3); var ret = Task.Result.Original(vfxId, pos, caster, unk1, unk2, unk3);
Penumbra.Log.Excessive( 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}."); $"[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,19 +1,23 @@
using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.Object;
using OtterGui.Services; using OtterGui.Services;
using Penumbra.CrashHandler.Buffers;
using Penumbra.Interop.PathResolving; using Penumbra.Interop.PathResolving;
using Penumbra.Services;
namespace Penumbra.Interop.Hooks.Animation; namespace Penumbra.Interop.Hooks.Animation;
/// <summary> Characters load some of their voice lines or whatever with this function. </summary> /// <summary> Characters load some of their voice lines or whatever with this function. </summary>
public sealed unsafe class LoadCharacterSound : FastHook<LoadCharacterSound.Delegate> public sealed unsafe class LoadCharacterSound : FastHook<LoadCharacterSound.Delegate>
{ {
private readonly GameState _state; private readonly GameState _state;
private readonly CollectionResolver _collectionResolver; 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; _collectionResolver = collectionResolver;
_crashHandler = crashHandler;
Task = hooks.CreateHook<Delegate>("Load Character Sound", Task = hooks.CreateHook<Delegate>("Load Character Sound",
(nint)FFXIVClientStructs.FFXIV.Client.Game.Character.Character.VfxContainer.MemberFunctionPointers.LoadCharacterSound, Detour, (nint)FFXIVClientStructs.FFXIV.Client.Game.Character.Character.VfxContainer.MemberFunctionPointers.LoadCharacterSound, Detour,
true); 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) 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 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); 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}."); Penumbra.Log.Excessive($"[Load Character Sound] Invoked with {container:X} {unk1} {unk2} {unk3} {unk4} {unk5} {unk6} {unk7} -> {ret}.");
_state.RestoreSoundData(last); _state.RestoreSoundData(last);

View file

@ -2,9 +2,11 @@ using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.Object;
using OtterGui.Services; using OtterGui.Services;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.CrashHandler.Buffers;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.Interop.PathResolving; using Penumbra.Interop.PathResolving;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.Services;
using Penumbra.String; using Penumbra.String;
namespace Penumbra.Interop.Hooks.Animation; namespace Penumbra.Interop.Hooks.Animation;
@ -12,15 +14,18 @@ namespace Penumbra.Interop.Hooks.Animation;
/// <summary> Load a VFX specifically for a character. </summary> /// <summary> Load a VFX specifically for a character. </summary>
public sealed unsafe class LoadCharacterVfx : FastHook<LoadCharacterVfx.Delegate> public sealed unsafe class LoadCharacterVfx : FastHook<LoadCharacterVfx.Delegate>
{ {
private readonly GameState _state; private readonly GameState _state;
private readonly CollectionResolver _collectionResolver; private readonly CollectionResolver _collectionResolver;
private readonly IObjectTable _objects; 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; _state = state;
_collectionResolver = collectionResolver; _collectionResolver = collectionResolver;
_objects = objects; _objects = objects;
_crashHandler = crashHandler;
Task = hooks.CreateHook<Delegate>("Load Character VFX", Sigs.LoadCharacterVfx, Detour, true); 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); var last = _state.SetAnimationData(newData);
_crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.LoadCharacterVfx);
var ret = Task.Result.Original(vfxPath, vfxParams, unk1, unk2, unk3, unk4); var ret = Task.Result.Original(vfxPath, vfxParams, unk1, unk2, unk3, unk4);
Penumbra.Log.Excessive( Penumbra.Log.Excessive(
$"[Load Character VFX] Invoked with {new ByteString(vfxPath)}, 0x{vfxParams->GameObjectId:X}, {vfxParams->TargetCount}, {unk1}, {unk2}, {unk3}, {unk4} -> 0x{ret:X}."); $"[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 FFXIVClientStructs.FFXIV.Client.Game.Object;
using OtterGui.Services; using OtterGui.Services;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.CrashHandler;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.Interop.PathResolving; using Penumbra.Interop.PathResolving;
using Penumbra.Services;
namespace Penumbra.Interop.Hooks.Animation; namespace Penumbra.Interop.Hooks.Animation;
@ -14,19 +16,21 @@ namespace Penumbra.Interop.Hooks.Animation;
/// </summary> /// </summary>
public sealed unsafe class LoadTimelineResources : FastHook<LoadTimelineResources.Delegate> public sealed unsafe class LoadTimelineResources : FastHook<LoadTimelineResources.Delegate>
{ {
private readonly GameState _state; private readonly GameState _state;
private readonly CollectionResolver _collectionResolver; private readonly CollectionResolver _collectionResolver;
private readonly ICondition _conditions; private readonly ICondition _conditions;
private readonly IObjectTable _objects; private readonly IObjectTable _objects;
private readonly CrashHandlerService _crashHandler;
public LoadTimelineResources(HookManager hooks, GameState state, CollectionResolver collectionResolver, ICondition conditions, public LoadTimelineResources(HookManager hooks, GameState state, CollectionResolver collectionResolver, ICondition conditions,
IObjectTable objects) IObjectTable objects, CrashHandlerService crashHandler)
{ {
_state = state; _state = state;
_collectionResolver = collectionResolver; _collectionResolver = collectionResolver;
_conditions = conditions; _conditions = conditions;
_objects = objects; _objects = objects;
Task = hooks.CreateHook<Delegate>("Load Timeline Resources", Sigs.LoadTimelineResources, Detour, true); _crashHandler = crashHandler;
Task = hooks.CreateHook<Delegate>("Load Timeline Resources", Sigs.LoadTimelineResources, Detour, true);
} }
public delegate ulong Delegate(nint timeline); public delegate ulong Delegate(nint timeline);
@ -39,7 +43,13 @@ public sealed unsafe class LoadTimelineResources : FastHook<LoadTimelineResource
if (_conditions[ConditionFlag.OccupiedInCutSceneEvent] || _conditions[ConditionFlag.WatchingCutscene78]) if (_conditions[ConditionFlag.OccupiedInCutSceneEvent] || _conditions[ConditionFlag.WatchingCutscene78])
return Task.Result.Original(timeline); 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); var ret = Task.Result.Original(timeline);
_state.RestoreAnimationData(last); _state.RestoreAnimationData(last);
return ret; return ret;

View file

@ -1,23 +1,28 @@
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using OtterGui.Services; using OtterGui.Services;
using Penumbra.CrashHandler.Buffers;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.Interop.PathResolving; using Penumbra.Interop.PathResolving;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.Services;
namespace Penumbra.Interop.Hooks.Animation; namespace Penumbra.Interop.Hooks.Animation;
/// <summary> Called when some action timelines update. </summary> /// <summary> Called when some action timelines update. </summary>
public sealed unsafe class ScheduleClipUpdate : FastHook<ScheduleClipUpdate.Delegate> public sealed unsafe class ScheduleClipUpdate : FastHook<ScheduleClipUpdate.Delegate>
{ {
private readonly GameState _state; private readonly GameState _state;
private readonly CollectionResolver _collectionResolver; private readonly CollectionResolver _collectionResolver;
private readonly IObjectTable _objects; 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; _state = state;
_collectionResolver = collectionResolver; _collectionResolver = collectionResolver;
_objects = objects; _objects = objects;
_crashHandler = crashHandler;
Task = hooks.CreateHook<Delegate>("Schedule Clip Update", Sigs.ScheduleClipUpdate, Detour, true); 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) private void Detour(ClipScheduler* clipScheduler)
{ {
Penumbra.Log.Excessive($"[Schedule Clip Update] Invoked on {(nint)clipScheduler:X}."); Penumbra.Log.Excessive($"[Schedule Clip Update] Invoked on {(nint)clipScheduler:X}.");
var last = _state.SetAnimationData( var newData = LoadTimelineResources.GetDataFromTimeline(_objects, _collectionResolver, clipScheduler->SchedulerTimeline);
LoadTimelineResources.GetDataFromTimeline(_objects, _collectionResolver, clipScheduler->SchedulerTimeline)); var last = _state.SetAnimationData(newData);
_crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.ScheduleClipUpdate);
Task.Result.Original(clipScheduler); Task.Result.Original(clipScheduler);
_state.RestoreAnimationData(last); _state.RestoreAnimationData(last);
} }

View file

@ -1,21 +1,25 @@
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.Object;
using OtterGui.Services; using OtterGui.Services;
using Penumbra.CrashHandler.Buffers;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.Interop.PathResolving; using Penumbra.Interop.PathResolving;
using Penumbra.Services;
namespace Penumbra.Interop.Hooks.Animation; namespace Penumbra.Interop.Hooks.Animation;
/// <summary> Seems to load character actions when zoning or changing class, maybe. </summary> /// <summary> Seems to load character actions when zoning or changing class, maybe. </summary>
public sealed unsafe class SomeActionLoad : FastHook<SomeActionLoad.Delegate> public sealed unsafe class SomeActionLoad : FastHook<SomeActionLoad.Delegate>
{ {
private readonly GameState _state; private readonly GameState _state;
private readonly CollectionResolver _collectionResolver; 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; _state = state;
_collectionResolver = collectionResolver; _collectionResolver = collectionResolver;
_crashHandler = crashHandler;
Task = hooks.CreateHook<Delegate>("Some Action Load", Sigs.LoadSomeAction, Detour, true); 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)] [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private void Detour(ActionTimelineManager* timelineManager) 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}."); Penumbra.Log.Excessive($"[Some Action Load] Invoked on 0x{(nint)timelineManager:X}.");
_crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.ActionLoad);
Task.Result.Original(timelineManager); Task.Result.Original(timelineManager);
_state.RestoreAnimationData(last); _state.RestoreAnimationData(last);
} }

View file

@ -1,23 +1,28 @@
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.Object;
using OtterGui.Services; using OtterGui.Services;
using Penumbra.CrashHandler.Buffers;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.Interop.PathResolving; using Penumbra.Interop.PathResolving;
using Penumbra.Services;
namespace Penumbra.Interop.Hooks.Animation; namespace Penumbra.Interop.Hooks.Animation;
/// <summary> Unknown what exactly this is, but it seems to load a bunch of paps. </summary> /// <summary> Unknown what exactly this is, but it seems to load a bunch of paps. </summary>
public sealed unsafe class SomePapLoad : FastHook<SomePapLoad.Delegate> public sealed unsafe class SomePapLoad : FastHook<SomePapLoad.Delegate>
{ {
private readonly GameState _state; private readonly GameState _state;
private readonly CollectionResolver _collectionResolver; private readonly CollectionResolver _collectionResolver;
private readonly IObjectTable _objects; 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; _state = state;
_collectionResolver = collectionResolver; _collectionResolver = collectionResolver;
_objects = objects; _objects = objects;
_crashHandler = crashHandler;
Task = hooks.CreateHook<Delegate>("Some PAP Load", Sigs.LoadSomePap, Detour, true); 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); var actorIdx = (int)(*(*(ulong**)timelinePtr + 1) >> 3);
if (actorIdx >= 0 && actorIdx < _objects.Length) if (actorIdx >= 0 && actorIdx < _objects.Length)
{ {
var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection((GameObject*)_objects.GetObjectAddress(actorIdx), var newData = _collectionResolver.IdentifyCollection((GameObject*)_objects.GetObjectAddress(actorIdx), true);
true)); var last = _state.SetAnimationData(newData);
_crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.PapLoad);
Task.Result.Original(a1, a2, a3, a4); Task.Result.Original(a1, a2, a3, a4);
_state.RestoreAnimationData(last); _state.RestoreAnimationData(last);
return; return;

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework> <TargetFramework>net7.0-windows</TargetFramework>
<LangVersion>preview</LangVersion> <LangVersion>preview</LangVersion>
@ -69,8 +69,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="EmbedIO" Version="3.4.3" /> <PackageReference Include="EmbedIO" Version="3.4.3" />
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.8.0" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.3" />
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.2" />
<PackageReference Include="SharpCompress" Version="0.33.0" /> <PackageReference Include="SharpCompress" Version="0.33.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="SharpGLTF.Core" Version="1.0.0-alpha0030" /> <PackageReference Include="SharpGLTF.Core" Version="1.0.0-alpha0030" />
@ -79,8 +78,10 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\OtterGui\OtterGui.csproj" /> <ProjectReference Include="..\OtterGui\OtterGui.csproj" />
<ProjectReference Include="..\Penumbra.CrashHandler\Penumbra.CrashHandler.csproj" />
<ProjectReference Include="..\Penumbra.GameData\Penumbra.GameData.csproj" /> <ProjectReference Include="..\Penumbra.GameData\Penumbra.GameData.csproj" />
<ProjectReference Include="..\Penumbra.Api\Penumbra.Api.csproj" /> <ProjectReference Include="..\Penumbra.Api\Penumbra.Api.csproj" />
<ProjectReference Include="..\Penumbra.String\Penumbra.String.csproj" />
</ItemGroup> </ItemGroup>
<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 FilesystemFile = Path.Combine(pi.ConfigDirectory.FullName, "sort_order.json");
public readonly string ActiveCollectionsFile = Path.Combine(pi.ConfigDirectory.FullName, "active_collections.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> /// <summary> Obtain the path of a collection file given its name.</summary>
public string CollectionFile(ModCollection collection) public string CollectionFile(ModCollection collection)
=> CollectionFile(collection.Name); => CollectionFile(collection.Name);

View file

@ -12,22 +12,13 @@ using Penumbra.UI.Classes;
namespace Penumbra.UI.Tabs; namespace Penumbra.UI.Tabs;
public class ChangedItemsTab : ITab public class ChangedItemsTab(
CollectionManager collectionManager,
CollectionSelectHeader collectionHeader,
ChangedItemDrawer drawer,
CommunicatorService communicator)
: ITab
{ {
private readonly CollectionManager _collectionManager;
private readonly ChangedItemDrawer _drawer;
private readonly CollectionSelectHeader _collectionHeader;
private readonly CommunicatorService _communicator;
public ChangedItemsTab(CollectionManager collectionManager, CollectionSelectHeader collectionHeader, ChangedItemDrawer drawer,
CommunicatorService communicator)
{
_collectionManager = collectionManager;
_collectionHeader = collectionHeader;
_drawer = drawer;
_communicator = communicator;
}
public ReadOnlySpan<byte> Label public ReadOnlySpan<byte> Label
=> "Changed Items"u8; => "Changed Items"u8;
@ -36,8 +27,8 @@ public class ChangedItemsTab : ITab
public void DrawContent() public void DrawContent()
{ {
_collectionHeader.Draw(true); collectionHeader.Draw(true);
_drawer.DrawTypeFilter(); drawer.DrawTypeFilter();
var varWidth = DrawFilters(); var varWidth = DrawFilters();
using var child = ImRaii.Child("##changedItemsChild", -Vector2.One); using var child = ImRaii.Child("##changedItemsChild", -Vector2.One);
if (!child) if (!child)
@ -54,7 +45,7 @@ public class ChangedItemsTab : ITab
ImGui.TableSetupColumn("mods", flags, varWidth - 130 * UiHelpers.Scale); ImGui.TableSetupColumn("mods", flags, varWidth - 130 * UiHelpers.Scale);
ImGui.TableSetupColumn("id", flags, 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); var rest = ImGuiClip.FilteredClippedDraw(items, skips, FilterChangedItem, DrawChangedItemColumn);
ImGuiClip.DrawEndDummy(rest, height); ImGuiClip.DrawEndDummy(rest, height);
} }
@ -75,21 +66,21 @@ public class ChangedItemsTab : ITab
/// <summary> Apply the current filters. </summary> /// <summary> Apply the current filters. </summary>
private bool FilterChangedItem(KeyValuePair<string, (SingleArray<IMod>, object?)> item) 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))); && (_changedItemModFilter.IsEmpty || item.Value.Item1.Any(m => m.Name.Contains(_changedItemModFilter)));
/// <summary> Draw a full column for a changed item. </summary> /// <summary> Draw a full column for a changed item. </summary>
private void DrawChangedItemColumn(KeyValuePair<string, (SingleArray<IMod>, object?)> item) private void DrawChangedItemColumn(KeyValuePair<string, (SingleArray<IMod>, object?)> item)
{ {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
_drawer.DrawCategoryIcon(item.Key, item.Value.Item2); drawer.DrawCategoryIcon(item.Key, item.Value.Item2);
ImGui.SameLine(); ImGui.SameLine();
_drawer.DrawChangedItem(item.Key, item.Value.Item2); drawer.DrawChangedItem(item.Key, item.Value.Item2);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
DrawModColumn(item.Value.Item1); DrawModColumn(item.Value.Item1);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
_drawer.DrawModelData(item.Value.Item2); drawer.DrawModelData(item.Value.Item2);
} }
private void DrawModColumn(SingleArray<IMod> mods) 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())) if (ImGui.Selectable(first.Name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight()))
&& ImGui.GetIO().KeyCtrl && ImGui.GetIO().KeyCtrl
&& first is Mod mod) && first is Mod mod)
_communicator.SelectTab.Invoke(TabType.Mods, mod); communicator.SelectTab.Invoke(TabType.Mods, mod);
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
{ {

View file

@ -44,8 +44,8 @@ public class ConfigTabBar : IDisposable
Watcher = watcher; Watcher = watcher;
OnScreen = onScreen; OnScreen = onScreen;
Messages = messages; Messages = messages;
Tabs = new ITab[] Tabs =
{ [
Settings, Settings,
Collections, Collections,
Mods, Mods,
@ -56,7 +56,7 @@ public class ConfigTabBar : IDisposable
Resource, Resource,
Watcher, Watcher,
Messages, Messages,
}; ];
_communicator.SelectTab.Subscribe(OnSelectTab, Communication.SelectTab.Priority.ConfigTabBar); _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() foreach (var type in typeof(ActorManager).Assembly.GetTypes()
.Where(t => t is { IsAbstract: false, IsInterface: false } && t.IsAssignableTo(typeof(IAsyncDataContainer)))) .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.Name);
ImGuiUtil.DrawTableColumn(container.Time.ToString()); ImGuiUtil.DrawTableColumn(container.Time.ToString());
ImGuiUtil.DrawTableColumn(Functions.HumanReadableSize(container.Memory)); ImGuiUtil.DrawTableColumn(Functions.HumanReadableSize(container.Memory));
@ -88,18 +88,22 @@ public class DebugTab : Window, ITab
private readonly TextureManager _textureManager; private readonly TextureManager _textureManager;
private readonly ShaderReplacementFixer _shaderReplacementFixer; private readonly ShaderReplacementFixer _shaderReplacementFixer;
private readonly RedrawService _redraws; private readonly RedrawService _redraws;
private readonly DictEmote _emotes; private readonly DictEmote _emotes;
private readonly Diagnostics _diagnostics; private readonly Diagnostics _diagnostics;
private readonly IObjectTable _objects; private readonly IObjectTable _objects;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly IpcTester _ipcTester; private readonly IpcTester _ipcTester;
private readonly CrashHandlerPanel _crashHandlerPanel;
public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, IObjectTable objects, IClientState clientState, public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, IObjectTable objects,
ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains, CharacterUtility characterUtility, ResidentResourceManager residentResources, IClientState clientState,
ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains,
CharacterUtility characterUtility, ResidentResourceManager residentResources,
ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver, ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver,
DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache,
CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, 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) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse)
{ {
IsOpen = true; IsOpen = true;
@ -134,7 +138,8 @@ public class DebugTab : Window, ITab
_redraws = redraws; _redraws = redraws;
_emotes = emotes; _emotes = emotes;
_diagnostics = diagnostics; _diagnostics = diagnostics;
_ipcTester = ipcTester; _ipcTester = ipcTester;
_crashHandlerPanel = crashHandlerPanel;
_objects = objects; _objects = objects;
_clientState = clientState; _clientState = clientState;
} }
@ -158,6 +163,9 @@ public class DebugTab : Window, ITab
return; return;
DrawDebugTabGeneral(); DrawDebugTabGeneral();
ImGui.NewLine();
_crashHandlerPanel.Draw();
ImGui.NewLine();
_diagnostics.DrawDiagnostics(); _diagnostics.DrawDiagnostics();
DrawPerformanceTab(); DrawPerformanceTab();
ImGui.NewLine(); ImGui.NewLine();
@ -257,6 +265,7 @@ public class DebugTab : Window, ITab
} }
} }
var issues = _modManager.WithIndex().Count(p => p.Index != p.Value.Index); var issues = _modManager.WithIndex().Count(p => p.Index != p.Value.Index);
using (var tree = TreeNode($"Mods ({issues} Issues)###Mods")) using (var tree = TreeNode($"Mods ({issues} Issues)###Mods"))
{ {
@ -394,7 +403,7 @@ public class DebugTab : Window, ITab
private void DrawPerformanceTab() private void DrawPerformanceTab()
{ {
ImGui.NewLine(); ImGui.NewLine();
if (ImGui.CollapsingHeader("Performance")) if (!ImGui.CollapsingHeader("Performance"))
return; return;
using (var start = TreeNode("Startup Performance", ImGuiTreeNodeFlags.DefaultOpen)) using (var start = TreeNode("Startup Performance", ImGuiTreeNodeFlags.DefaultOpen))

View file

@ -8,31 +8,22 @@ using Penumbra.Collections;
using Penumbra.Collections.Cache; using Penumbra.Collections.Cache;
using Penumbra.Collections.Manager; using Penumbra.Collections.Manager;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
using Penumbra.Mods.Editor; using Penumbra.Mods.Editor;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
namespace Penumbra.UI.Tabs; 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 public ReadOnlySpan<byte> Label
=> "Effective Changes"u8; => "Effective Changes"u8;
public void DrawContent() public void DrawContent()
{ {
SetupEffectiveSizes(); SetupEffectiveSizes();
_collectionHeader.Draw(true); collectionHeader.Draw(true);
DrawFilters(); DrawFilters();
using var child = ImRaii.Child("##EffectiveChangesTab", -Vector2.One, false); using var child = ImRaii.Child("##EffectiveChangesTab", -Vector2.One, false);
if (!child) if (!child)
@ -48,7 +39,7 @@ public class EffectiveTab : ITab
ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, _effectiveArrowLength); ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, _effectiveArrowLength);
ImGui.TableSetupColumn("##file", ImGuiTableColumnFlags.WidthFixed, _effectiveRightTextLength); ImGui.TableSetupColumn("##file", ImGuiTableColumnFlags.WidthFixed, _effectiveRightTextLength);
DrawEffectiveRows(_collectionManager.Active.Current, skips, height, DrawEffectiveRows(collectionManager.Active.Current, skips, height,
_effectiveFilePathFilter.Length > 0 || _effectiveGamePathFilter.Length > 0); _effectiveFilePathFilter.Length > 0 || _effectiveGamePathFilter.Length > 0);
} }

View file

@ -17,68 +17,53 @@ using Penumbra.Collections.Manager;
namespace Penumbra.UI.Tabs; 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 ActiveCollections _activeCollections = collectionManager.Active;
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;
}
public bool IsVisible public bool IsVisible
=> _modManager.Valid; => modManager.Valid;
public ReadOnlySpan<byte> Label public ReadOnlySpan<byte> Label
=> "Mods"u8; => "Mods"u8;
public void DrawHeader() public void DrawHeader()
=> _tutorial.OpenTutorial(BasicTutorialSteps.Mods); => tutorial.OpenTutorial(BasicTutorialSteps.Mods);
public Mod SelectMod public Mod SelectMod
{ {
set => _selector.SelectByValue(value); set => selector.SelectByValue(value);
} }
public void DrawContent() public void DrawContent()
{ {
try try
{ {
_selector.Draw(GetModSelectorSize(_config)); selector.Draw(GetModSelectorSize(config));
ImGui.SameLine(); ImGui.SameLine();
using var group = ImRaii.Group(); using var group = ImRaii.Group();
_collectionHeader.Draw(false); collectionHeader.Draw(false);
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); 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)) true, ImGuiWindowFlags.HorizontalScrollbar))
{ {
style.Pop(); style.Pop();
if (child) if (child)
_panel.Draw(); panel.Draw();
style.Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero); style.Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
} }
@ -89,14 +74,14 @@ public class ModsTab : ITab
catch (Exception e) catch (Exception e)
{ {
Penumbra.Log.Error($"Exception thrown during ModPanel Render:\n{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.AnonymizedName} Current Collection\n"
+ $"{_activeCollections.Current.Settings.Count} Settings\n" + $"{_activeCollections.Current.Settings.Count} Settings\n"
+ $"{_selector.SortMode.Name} Sort Mode\n" + $"{selector.SortMode.Name} Sort Mode\n"
+ $"{_selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n" + $"{selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n"
+ $"{_selector.Selected?.Name ?? "NULL"} Selected Mod\n" + $"{selector.Selected?.Name ?? "NULL"} Selected Mod\n"
+ $"{string.Join(", ", _activeCollections.Current.DirectlyInheritsFrom.Select(c => c.AnonymizedName))} Inheritances\n" + $"{string.Join(", ", _activeCollections.Current.DirectlyInheritsFrom.Select(c => c.AnonymizedName))} Inheritances\n"
+ $"{_selector.SelectedSettingCollection.AnonymizedName} Collection\n"); + $"{selector.SelectedSettingCollection.AnonymizedName} Collection\n");
} }
} }
@ -115,9 +100,9 @@ public class ModsTab : ITab
private void DrawRedrawLine() private void DrawRedrawLine()
{ {
if (_config.HideRedrawBar) if (config.HideRedrawBar)
{ {
_tutorial.SkipTutorial(BasicTutorialSteps.Redrawing); tutorial.SkipTutorial(BasicTutorialSteps.Redrawing);
return; return;
} }
@ -135,15 +120,15 @@ public class ModsTab : ITab
} }
var hovered = ImGui.IsItemHovered(); var hovered = ImGui.IsItemHovered();
_tutorial.OpenTutorial(BasicTutorialSteps.Redrawing); tutorial.OpenTutorial(BasicTutorialSteps.Redrawing);
if (hovered) if (hovered)
ImGui.SetTooltip($"The supported modifiers for '/penumbra redraw' are:\n{TutorialService.SupportedRedrawModifiers}"); ImGui.SetTooltip($"The supported modifiers for '/penumbra redraw' are:\n{TutorialService.SupportedRedrawModifiers}");
using var id = ImRaii.PushId("Redraw"); using var id = ImRaii.PushId("Redraw");
using var disabled = ImRaii.Disabled(_clientState.LocalPlayer == null); using var disabled = ImRaii.Disabled(clientState.LocalPlayer == null);
ImGui.SameLine(); ImGui.SameLine();
var buttonWidth = frameHeight with { X = ImGui.GetContentRegionAvail().X / 5 }; 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." ? "\nCan only be used when you are logged in and your character is available."
: string.Empty; : string.Empty;
DrawButton(buttonWidth, "All", string.Empty, tt); DrawButton(buttonWidth, "All", string.Empty, tt);
@ -151,13 +136,13 @@ public class ModsTab : ITab
DrawButton(buttonWidth, "Self", "self", tt); DrawButton(buttonWidth, "Self", "self", tt);
ImGui.SameLine(); 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." ? "\nCan only be used when you have a target."
: string.Empty; : string.Empty;
DrawButton(buttonWidth, "Target", "target", tt); DrawButton(buttonWidth, "Target", "target", tt);
ImGui.SameLine(); ImGui.SameLine();
tt = _targets.FocusTarget == null tt = targets.FocusTarget == null
? "\nCan only be used when you have a focus target." ? "\nCan only be used when you have a focus target."
: string.Empty; : string.Empty;
DrawButton(buttonWidth, "Focus", "focus", tt); DrawButton(buttonWidth, "Focus", "focus", tt);
@ -176,9 +161,9 @@ public class ModsTab : ITab
if (ImGui.Button(label, size)) if (ImGui.Button(label, size))
{ {
if (lower.Length > 0) if (lower.Length > 0)
_redrawService.RedrawObject(lower, RedrawType.Redraw); redrawService.RedrawObject(lower, RedrawType.Redraw);
else else
_redrawService.RedrawAll(RedrawType.Redraw); redrawService.RedrawAll(RedrawType.Redraw);
} }
} }

View file

@ -7,7 +7,7 @@ namespace Penumbra.UI.Tabs;
public class OnScreenTab : ITab public class OnScreenTab : ITab
{ {
private readonly Configuration _config; private readonly Configuration _config;
private ResourceTreeViewer _viewer; private readonly ResourceTreeViewer _viewer;
public OnScreenTab(Configuration config, ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer) public OnScreenTab(Configuration config, ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer)
{ {

View file

@ -12,24 +12,14 @@ using Penumbra.String.Classes;
namespace Penumbra.UI.Tabs; 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 public ReadOnlySpan<byte> Label
=> "Resource Manager"u8; => "Resource Manager"u8;
public bool IsVisible 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> /// <summary> Draw a tab to iterate over the main resource maps and see what resources are currently loaded. </summary>
public void DrawContent() public void DrawContent()
@ -44,15 +34,15 @@ public class ResourceTab : ITab
unsafe unsafe
{ {
_resourceManager.IterateGraphs(DrawCategoryContainer); resourceManager.IterateGraphs(DrawCategoryContainer);
} }
ImGui.NewLine(); ImGui.NewLine();
unsafe unsafe
{ {
ImGui.TextUnformatted( ImGui.TextUnformatted(
$"Static Address: 0x{(ulong)_resourceManager.ResourceManagerAddress:X} (+0x{(ulong)_resourceManager.ResourceManagerAddress - (ulong)_sigScanner.Module.BaseAddress: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}"); ImGui.TextUnformatted($"Actual Address: 0x{(ulong)resourceManager.ResourceManager:X}");
} }
} }
@ -82,7 +72,7 @@ public class ResourceTab : ITab
ImGui.TableSetupColumn("Refs", ImGuiTableColumnFlags.WidthFixed, _refsColumnWidth); ImGui.TableSetupColumn("Refs", ImGuiTableColumnFlags.WidthFixed, _refsColumnWidth);
ImGui.TableHeadersRow(); ImGui.TableHeadersRow();
_resourceManager.IterateResourceMap(map, (hash, r) => resourceManager.IterateResourceMap(map, (hash, r) =>
{ {
// Filter unwanted names. // Filter unwanted names.
if (_resourceManagerFilter.Length != 0 if (_resourceManagerFilter.Length != 0
@ -125,7 +115,7 @@ public class ResourceTab : ITab
if (tree) if (tree)
{ {
SetTableWidths(); 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" "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": { "Microsoft.Extensions.DependencyInjection": {
"type": "Direct", "type": "Direct",
"requested": "[7.0.0, )", "requested": "[7.0.0, )",
@ -55,29 +43,15 @@
}, },
"SixLabors.ImageSharp": { "SixLabors.ImageSharp": {
"type": "Direct", "type": "Direct",
"requested": "[2.1.2, )", "requested": "[3.1.3, )",
"resolved": "2.1.2", "resolved": "3.1.3",
"contentHash": "In0pC521LqJXJXZgFVHegvSzES10KkKRN31McxqA1+fKtKsNe+EShWavBFQnKRlXCdeAmfx/wDjLILbvCaq+8Q==", "contentHash": "wybtaqZQ1ZRZ4ZeU+9h+PaSeV14nyiGKIy7qRbDfSHzHq4ybqyOcjoifeaYbiKLO1u+PVxLBuy7MF/DMmwwbfg=="
"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=="
}, },
"Microsoft.Extensions.DependencyInjection.Abstractions": { "Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive", "type": "Transitive",
"resolved": "7.0.0", "resolved": "7.0.0",
"contentHash": "h3j/QfmFN4S0w4C2A6X7arXij/M/OVw3uQHSOFxnND4DyAzO1F9eMX7Eti7lU/OkSthEE0WzRsfT/Dmx86jzCw==" "contentHash": "h3j/QfmFN4S0w4C2A6X7arXij/M/OVw3uQHSOFxnND4DyAzO1F9eMX7Eti7lU/OkSthEE0WzRsfT/Dmx86jzCw=="
}, },
"Microsoft.NETCore.Platforms": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ=="
},
"SharpGLTF.Runtime": { "SharpGLTF.Runtime": {
"type": "Transitive", "type": "Transitive",
"resolved": "1.0.0-alpha0030", "resolved": "1.0.0-alpha0030",
@ -86,32 +60,6 @@
"SharpGLTF.Core": "1.0.0-alpha0030" "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": { "System.ValueTuple": {
"type": "Transitive", "type": "Transitive",
"resolved": "4.5.0", "resolved": "4.5.0",
@ -134,11 +82,14 @@
"penumbra.api": { "penumbra.api": {
"type": "Project" "type": "Project"
}, },
"penumbra.crashhandler": {
"type": "Project"
},
"penumbra.gamedata": { "penumbra.gamedata": {
"type": "Project", "type": "Project",
"dependencies": { "dependencies": {
"OtterGui": "[1.0.0, )", "OtterGui": "[1.0.0, )",
"Penumbra.Api": "[1.0.13, )", "Penumbra.Api": "[1.0.14, )",
"Penumbra.String": "[1.0.4, )" "Penumbra.String": "[1.0.4, )"
} }
}, },