diff --git a/src/Features/Console/Godot/ConsoleController.cs b/src/Features/Console/Godot/ConsoleController.cs index aa319d7..aaa685f 100644 --- a/src/Features/Console/Godot/ConsoleController.cs +++ b/src/Features/Console/Godot/ConsoleController.cs @@ -1,6 +1,7 @@ using Game.Features.Console.Application; using Game.Features.Console.Contracts; using Game.Features.Console.Infrastructure; +using Game.Features.Logging.Contracts; using Game.Godot; using Game.Godot.Extensions; using Game.Godot.Mappings; @@ -8,7 +9,7 @@ using Godot; namespace Game.Features.Console.Godot; -public partial class ConsoleController : CanvasLayer +public partial class ConsoleController : CanvasLayer, ILogTarget { [Export] public LineEdit CommandInput; [Export] public RichTextLabel OutputLog; @@ -32,16 +33,6 @@ public partial class ConsoleController : CanvasLayer _mouseMemento.RestoreCaptureMode(); } } - - public void AppendLine(string line) - { - OutputLog.AppendText(line + "\n"); - } - - public void UseCommandRegistry(ICommandRegistry registry) - { - _commandExecute = new CommandExecute(registry); - } private void OnInputSubmitted(string text) { @@ -64,6 +55,21 @@ public partial class ConsoleController : CanvasLayer } } + private void AppendLine(string line) + { + OutputLog.AppendText(line + "\n"); + } + + public void UseCommandRegistry(ICommandRegistry registry) + { + _commandExecute = new CommandExecute(registry); + } + + public void WriteLogLine(string logLine) + { + AppendLine(logLine); + } + public override void _Ready() { this.AssertRequiredExports(); diff --git a/src/Features/Logging/Contracts/ILogTarget.cs b/src/Features/Logging/Contracts/ILogTarget.cs new file mode 100644 index 0000000..233ea0a --- /dev/null +++ b/src/Features/Logging/Contracts/ILogTarget.cs @@ -0,0 +1,6 @@ +namespace Game.Features.Logging.Contracts; + +public interface ILogTarget +{ + void WriteLogLine(string logLine); +} diff --git a/src/Features/Logging/Contracts/ILogTarget.cs.uid b/src/Features/Logging/Contracts/ILogTarget.cs.uid new file mode 100644 index 0000000..1437cfb --- /dev/null +++ b/src/Features/Logging/Contracts/ILogTarget.cs.uid @@ -0,0 +1 @@ +uid://cm0ugu0l8td0e diff --git a/src/Features/Logging/Contracts/ILogger.cs b/src/Features/Logging/Contracts/ILogger.cs new file mode 100644 index 0000000..a9c038b --- /dev/null +++ b/src/Features/Logging/Contracts/ILogger.cs @@ -0,0 +1,9 @@ +namespace Game.Features.Logging.Contracts; + +public interface ILogger +{ + void Debug(string category, string message); + void Info(string category, string message); + void Warning(string category, string message); + void Error(string category, string message); +} diff --git a/src/Features/Logging/Contracts/ILogger.cs.uid b/src/Features/Logging/Contracts/ILogger.cs.uid new file mode 100644 index 0000000..1596543 --- /dev/null +++ b/src/Features/Logging/Contracts/ILogger.cs.uid @@ -0,0 +1 @@ +uid://0hjq84kko3td diff --git a/src/Features/Logging/Contracts/LogLevel.cs b/src/Features/Logging/Contracts/LogLevel.cs new file mode 100644 index 0000000..703da66 --- /dev/null +++ b/src/Features/Logging/Contracts/LogLevel.cs @@ -0,0 +1,9 @@ +namespace Game.Features.Logging.Contracts; + +public enum LogLevel +{ + Debug, + Info, + Warning, + Error +} diff --git a/src/Features/Logging/Contracts/LogLevel.cs.uid b/src/Features/Logging/Contracts/LogLevel.cs.uid new file mode 100644 index 0000000..ee7c49b --- /dev/null +++ b/src/Features/Logging/Contracts/LogLevel.cs.uid @@ -0,0 +1 @@ +uid://2ee58okj156n diff --git a/src/Features/Logging/Godot/FileLogger.cs b/src/Features/Logging/Godot/FileLogger.cs new file mode 100644 index 0000000..6092859 --- /dev/null +++ b/src/Features/Logging/Godot/FileLogger.cs @@ -0,0 +1,48 @@ +using System; +using Game.Features.Logging.Contracts; +using Godot; + +namespace Game.Features.Logging.Godot; + +public sealed class FileLogger : ILogger, IDisposable +{ + private const string LogPath = "user://logs/latest.log"; + + private readonly FileAccess _file; + + public FileLogger() + { + DirAccess.MakeDirRecursiveAbsolute(LogPath.GetBaseDir()); + _file = FileAccess.Open(LogPath, FileAccess.ModeFlags.Write); + + if (_file is null) return; + + _file.StoreLine($"=== Log started at {DateTime.Now:yyyy-MM-dd HH:mm:ss} ==="); + _file.Flush(); + } + + private void Write(LogLevel level, string category, string message) + { + if (_file is null) return; + + _file.StoreLine($"{DateTime.Now:HH:mm:ss} [{level}] [{category}] {message}"); + _file.Flush(); + } + + public void Debug(string category, string message) + => Write(LogLevel.Debug, category, message); + + public void Info(string category, string message) + => Write(LogLevel.Info, category, message); + + public void Warning(string category, string message) + => Write(LogLevel.Warning, category, message); + + public void Error(string category, string message) + => Write(LogLevel.Error, category, message); + + public void Dispose() + { + _file?.Dispose(); + } +} diff --git a/src/Features/Logging/Godot/FileLogger.cs.uid b/src/Features/Logging/Godot/FileLogger.cs.uid new file mode 100644 index 0000000..d7d20fe --- /dev/null +++ b/src/Features/Logging/Godot/FileLogger.cs.uid @@ -0,0 +1 @@ +uid://f45qb03i1dua diff --git a/src/Features/Logging/Godot/GodotConsoleLogger.cs b/src/Features/Logging/Godot/GodotConsoleLogger.cs new file mode 100644 index 0000000..4019666 --- /dev/null +++ b/src/Features/Logging/Godot/GodotConsoleLogger.cs @@ -0,0 +1,22 @@ +using Game.Features.Logging.Contracts; +using Godot; + +namespace Game.Features.Logging.Godot; + +public sealed class GodotConsoleLogger : ILogger +{ + private static string Format(LogLevel level, string category, string message) + => $"[{level}] [{category}] {message}"; + + public void Debug(string category, string message) + => GD.Print(Format(LogLevel.Debug, category, message)); + + public void Info(string category, string message) + => GD.Print(Format(LogLevel.Info, category, message)); + + public void Warning(string category, string message) + => GD.PushWarning(Format(LogLevel.Warning, category, message)); + + public void Error(string category, string message) + => GD.PushError(Format(LogLevel.Error, category, message)); +} diff --git a/src/Features/Logging/Godot/GodotConsoleLogger.cs.uid b/src/Features/Logging/Godot/GodotConsoleLogger.cs.uid new file mode 100644 index 0000000..2ab88e5 --- /dev/null +++ b/src/Features/Logging/Godot/GodotConsoleLogger.cs.uid @@ -0,0 +1 @@ +uid://jyq0in2wtq02 diff --git a/src/Features/Logging/Infrastructure/CompositeLogger.cs b/src/Features/Logging/Infrastructure/CompositeLogger.cs new file mode 100644 index 0000000..bc90d47 --- /dev/null +++ b/src/Features/Logging/Infrastructure/CompositeLogger.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using Game.Features.Logging.Contracts; + +namespace Game.Features.Logging.Infrastructure; + +public sealed class CompositeLogger(IReadOnlyList loggers) : ILogger +{ + public void Debug(string category, string message) + { + foreach (ILogger logger in loggers) + { + logger.Debug(category, message); + } + } + + public void Info(string category, string message) + { + foreach (ILogger logger in loggers) + { + logger.Info(category, message); + } + } + + public void Warning(string category, string message) + { + foreach (ILogger logger in loggers) + { + logger.Warning(category, message); + } + } + + public void Error(string category, string message) + { + foreach (ILogger logger in loggers) + { + logger.Error(category, message); + } + } +} diff --git a/src/Features/Logging/Infrastructure/CompositeLogger.cs.uid b/src/Features/Logging/Infrastructure/CompositeLogger.cs.uid new file mode 100644 index 0000000..0eecb7e --- /dev/null +++ b/src/Features/Logging/Infrastructure/CompositeLogger.cs.uid @@ -0,0 +1 @@ +uid://cumunwgy6i71s diff --git a/src/Features/Logging/Infrastructure/LogTargetLogger.cs b/src/Features/Logging/Infrastructure/LogTargetLogger.cs new file mode 100644 index 0000000..f5ee689 --- /dev/null +++ b/src/Features/Logging/Infrastructure/LogTargetLogger.cs @@ -0,0 +1,21 @@ +using Game.Features.Logging.Contracts; + +namespace Game.Features.Logging.Infrastructure; + +/// +/// Forwards log entries to an ILogTarget. +/// +public sealed class LogTargetLogger(ILogTarget target) : ILogger +{ + public void Debug(string category, string message) + => target.WriteLogLine(message); + + public void Info(string category, string message) + => target.WriteLogLine(message); + + public void Warning(string category, string message) + => target.WriteLogLine(message); + + public void Error(string category, string message) + => target.WriteLogLine(message); +} diff --git a/src/Features/Logging/Infrastructure/LogTargetLogger.cs.uid b/src/Features/Logging/Infrastructure/LogTargetLogger.cs.uid new file mode 100644 index 0000000..941d4a6 --- /dev/null +++ b/src/Features/Logging/Infrastructure/LogTargetLogger.cs.uid @@ -0,0 +1 @@ +uid://bxnu6wg0qe03r diff --git a/src/Godot/GameBootstrap.cs b/src/Godot/GameBootstrap.cs index 1fb5148..78cf737 100644 --- a/src/Godot/GameBootstrap.cs +++ b/src/Godot/GameBootstrap.cs @@ -1,6 +1,8 @@ using Game.Features.Console.Application.Commands; using Game.Features.Console.Godot; using Game.Features.Console.Infrastructure; +using Game.Features.Logging.Godot; +using Game.Features.Logging.Infrastructure; using Game.Features.Multiplayer.Godot; using Game.Features.Players.Godot; using Game.Godot.Extensions; @@ -15,6 +17,19 @@ public partial class GameBootstrap : Node [Export] public PlayerController Player; [Export] public MultiplayerController MultiplayerController; + private FileLogger _fileLogger; + private CompositeLogger _logger; + + private void InitializeLogging() + { + _fileLogger = new FileLogger(); + _logger = new CompositeLogger([ + new GodotConsoleLogger(), + new LogTargetLogger(Console), + _fileLogger + ]); + } + private void RegisterConsoleCommands() { CommandRegistry registry = new(); @@ -31,11 +46,11 @@ public partial class GameBootstrap : Node private void InitializeMultiplayer() { MultiplayerController.UsePeerFactory(new EnetMultiplayerPeerFactory()); - MultiplayerController.Connected += () => Console.AppendLine("Connected successfully."); - MultiplayerController.ConnectionFailed += () => Console.AppendLine("Connection failed."); - MultiplayerController.Disconnected += () => Console.AppendLine("Server disconnected."); - MultiplayerController.PeerJoined += id => Console.AppendLine($"Peer {id} joined."); - MultiplayerController.PeerLeft += id => Console.AppendLine($"Peer {id} left."); + MultiplayerController.Connected += () => _logger.Info("Multiplayer", "Connected successfully."); + MultiplayerController.ConnectionFailed += () => _logger.Warning("Multiplayer", "Connection failed."); + MultiplayerController.Disconnected += () => _logger.Warning("Multiplayer", "Server disconnected."); + MultiplayerController.PeerJoined += id => _logger.Info("Multiplayer", $"Peer {id} joined."); + MultiplayerController.PeerLeft += id => _logger.Info("Multiplayer", $"Peer {id} left."); } public override void _Ready() @@ -43,6 +58,8 @@ public partial class GameBootstrap : Node this.AssertRequiredExports(); MouseCapture.Capture(); + + InitializeLogging(); InitializeMultiplayer(); RegisterConsoleCommands(); } @@ -54,4 +71,9 @@ public partial class GameBootstrap : Node MouseCapture.Toggle(); } } + + public override void _ExitTree() + { + _fileLogger?.Dispose(); + } }