// Copyright (c) 2025-2026 GeWuYou // SPDX-License-Identifier: Apache-2.0 using System; using System.Threading; using GFramework.Core.Abstractions.Logging; namespace GFramework.Godot.Logging; /// /// Static Godot logging entry point with auto-discovered configuration and deferred logger creation. /// public static class GodotLog { #if NET9_0_OR_GREATER private static readonly System.Threading.Lock ConfigureLock = new(); #else private static readonly object ConfigureLock = new(); #endif private static Action? _configure; private static readonly Lazy LazyConfigurationSource = new( CreateConfigurationSource, LazyThreadSafetyMode.ExecutionAndPublication); private static readonly Lazy LazyProvider = new( static () => new GodotLoggerFactoryProvider(() => LazyConfigurationSource.Value.CurrentSettings), LazyThreadSafetyMode.ExecutionAndPublication); /// /// Applies imperative option overrides before the global Godot logger provider is materialized. /// /// The options mutator. public static void Configure(Action configure) { ArgumentNullException.ThrowIfNull(configure); lock (ConfigureLock) { if (LazyProvider.IsValueCreated || LazyConfigurationSource.IsValueCreated) { throw new InvalidOperationException( "GodotLog.Configure must be called before any GodotLog provider or configuration source is materialized."); } _configure = configure; } } /// /// Gets the lazily-configured Godot logger provider. /// public static ILoggerFactoryProvider Provider { get { lock (ConfigureLock) { return LazyProvider.Value; } } } /// /// Gets the discovered configuration file path, if any, without materializing the global configuration source. /// /// /// This property is safe for diagnostics before runs. When the source is not created /// yet, it performs discovery directly instead of touching LazyConfigurationSource.Value, so callers do /// not accidentally lock in the default options before configuring . /// public static string? ConfigurationPath => LazyConfigurationSource.IsValueCreated ? LazyConfigurationSource.Value.ConfigurationPath : GodotLoggerSettingsLoader.DiscoverConfigurationPath(); /// /// Creates a logger for the specified category without materializing the provider until first use. /// public static ILogger CreateLogger(string category) { ArgumentException.ThrowIfNullOrWhiteSpace(category); return new DeferredLogger(category, static () => Provider); } /// /// Creates a logger for the specified type without materializing the provider until first use. /// public static ILogger CreateLogger() { return CreateLogger(GetCategoryName(typeof(T))); } /// /// Installs the Godot provider as the current global resolver provider. /// public static void UseAsDefaultProvider() { LoggerFactoryResolver.Provider = Provider; } /// /// Stops the file watcher owned by the materialized configuration source, if the source has been created. /// /// /// Godot hosts often keep process-wide logging for the whole game lifetime. Dedicated servers and tests can call /// this method during teardown to release the watcher handle deterministically. The static lazy source is not /// reset; later logger usage continues with the last published settings snapshot but no longer receives reload /// notifications from the disposed watcher. /// public static void Shutdown() { if (LazyConfigurationSource.IsValueCreated) { LazyConfigurationSource.Value.Dispose(); } } private static GodotLogConfigurationSource CreateConfigurationSource() { lock (ConfigureLock) { return new GodotLogConfigurationSource(_configure); } } private static string GetCategoryName(Type type) { return type.FullName ?? type.Name; } }