// Copyright (c) 2023 bradson // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. using System.Collections.Concurrent; using System.Linq; using System.Runtime.InteropServices; using JetBrains.Annotations; using PerformanceFish.Pools; using PerformanceFish.Prepatching; namespace PerformanceFish; public static class Log { public static void Message(string message) => Config.Message(message); public static void Message(PooledStringHandler message) => Config.Message(message.GetFormattedText()); public static void Warning(string message) => Config.Warning(message); public static void Warning(PooledStringHandler message) => Config.Warning(message.GetFormattedText()); public static void Error(string message) => Config.Error(message); public static void Error(PooledStringHandler message) => Config.Error(message.GetFormattedText()); [PublicAPI] public static class Config { public static Action Message { get; set; } = static text => { Verse.Log.logLock ??= new(); // in case this runs from the Log cctor, and in turn before it finishes setup Verse.Log.Message(text); HandleEarlyMessage(text, LogMessageType.Message); }; public static Action Warning { get; set; } = static text => { Verse.Log.logLock ??= new(); Verse.Log.Warning(text); HandleEarlyMessage(text, LogMessageType.Warning); }; public static Action Error { get; set; } = static text => { Verse.Log.logLock ??= new(); Verse.Log.Error(text); HandleEarlyMessage(text, LogMessageType.Error); }; } private static void HandleEarlyMessage(string text, LogMessageType messageType) { if (Ready) return; FindAndEnqueueUnhandledMessage(text, messageType); } private static void FindAndEnqueueUnhandledMessage(string text, LogMessageType messageType) { var foundMessage = false; var lastMessage = Verse.Log.messageQueue.lastMessage; if (!TryEnqueueUnhandledMessage(text, messageType, lastMessage, ref foundMessage)) { foreach (var message in Verse.Log.messageQueue.messages) { if (TryEnqueueUnhandledMessage(text, messageType, message, ref foundMessage)) break; } } if (!foundMessage) { var message = Verse.Log.messageQueue.messages.FirstOrDefault(message => message.text.Contains(text) || text.Contains(message.text)); if (message != null) UnhandledMessages!.Enqueue(message); } } private static bool TryEnqueueUnhandledMessage(string text, LogMessageType messageType, LogMessage message, ref bool foundMessage) { if (message.text != text || message.type != messageType) return false; UnhandledMessages!.Enqueue(message); foundMessage = true; return true; } public static UnhandledMessageQueue? UnhandledMessages { get; } = SetupUnhandledMessageQueue(); private static UnhandledMessageQueue? SetupUnhandledMessageQueue() { if (Ready) return null; ref var stash = ref FishStash.Get; if (stash.UnhandledMessages is { } stashedMessages) { stash.MessageQueueGCHandle.Free(); return stashedMessages; } stash.UnhandledMessages = stashedMessages = new(); stash.MessageQueueGCHandle = GCHandle.Alloc(stashedMessages, GCHandleType.Pinned); return stash.UnhandledMessages; } public static bool Ready { get; set; } = PerformanceFishMod.Mod != null; } [StructLayout(LayoutKind.Sequential)] public struct LogMessageData : IDisposable { public IntPtr Text, StackTrace; public int Type, Repeats; public LogMessage ToLogMessage() => new((LogMessageType)Type, Marshal.PtrToStringUni(Text), Marshal.PtrToStringUni(StackTrace)) { repeats = Repeats }; public LogMessageData(LogMessage logMessage) { Text = Marshal.StringToHGlobalUni(logMessage.text); Type = (int)logMessage.type; Repeats = logMessage.repeats; StackTrace = Marshal.StringToHGlobalUni(logMessage.stackTrace); } public void Dispose() { Marshal.FreeHGlobal(Text); Marshal.FreeHGlobal(StackTrace); } } [StructLayout(LayoutKind.Sequential)] public unsafe class UnhandledMessageQueue : IDisposable { public int Count { get; private set; } private int _arrayLength; private LogMessageData* _messages; public void Enqueue(LogMessageData message) { lock (this) { if (Count >= _arrayLength) Expand(); _messages[Count++] = message; } } public void Enqueue(LogMessage message) => Enqueue(new LogMessageData(message)); public bool TryDequeue(out LogMessageData message) { lock (this) { if (--Count >= 0) { message = _messages[Count]; _messages[Count] = default; return true; } message = default; return false; } } private void Expand() { if (_arrayLength == 0) { _arrayLength = 1; _messages = (LogMessageData*)Marshal.AllocHGlobal(sizeof(LogMessageData)); } else { _arrayLength <<= 1; _messages = (LogMessageData*)Marshal.ReAllocHGlobal((IntPtr)_messages, (IntPtr)_arrayLength); } } // ReSharper disable once InconsistentlySynchronizedField private void ReleaseUnmanagedResources() => Marshal.FreeHGlobal((IntPtr)_messages); public void Dispose() { ReleaseUnmanagedResources(); GC.SuppressFinalize(this); } ~UnhandledMessageQueue() => ReleaseUnmanagedResources(); }