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