diff --git a/ZuneModCore/Mods/WebservicesMod.cs b/ZuneModCore/Mods/WebservicesMod.cs index a9615bd..3ba8a70 100644 --- a/ZuneModCore/Mods/WebservicesMod.cs +++ b/ZuneModCore/Mods/WebservicesMod.cs @@ -1,4 +1,5 @@ -using OwlCore.AbstractUI.Models; +using Microsoft.Extensions.Logging; +using OwlCore.AbstractUI.Models; using OwlCore.ComponentModel; using System; using System.Collections.Generic; @@ -28,7 +29,7 @@ public class WebservicesModFactory : DIModFactoryBase Description, Author, new(1, 2)); } -public partial class WebservicesMod(ModMetadata metadata, IModCoreConfig modConfig) : Mod(metadata), IAsyncInit +public partial class WebservicesMod(ModMetadata metadata, IModCoreConfig modConfig, ILogger log) : Mod(metadata), IAsyncInit { private const int ZUNESERVICES_ENDPOINTS_BLOCK_OFFSET = 0x14D60; private const int ZUNESERVICES_ENDPOINTS_BLOCK_LENGTH = 0x884; @@ -130,6 +131,8 @@ public partial class WebservicesMod(ModMetadata metadata, IModCoreConfig modConf if (errorMessage is not null) { + log.LogWarning("Binary patching failed, falling back to hosts file modification: {ErrorMessage}", errorMessage); + // Fallback to editing hosts file errorMessage = await AddEntriesToHostsFile(newHost); diff --git a/ZuneModCore/Services/FileLogger.cs b/ZuneModCore/Services/FileLogger.cs new file mode 100644 index 0000000..551da61 --- /dev/null +++ b/ZuneModCore/Services/FileLogger.cs @@ -0,0 +1,77 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Diagnostics; +using System.IO; + +namespace ZuneModCore.Services; + +public class FileLoggerProvider(string logDirectory) : ILoggerProvider +{ + private StreamWriter? _writer; + + public ILogger CreateLogger(string categoryName) + { + _writer ??= CreateLogFile(); + + return new FileLogger(_writer, categoryName); + } + + private StreamWriter CreateLogFile() + { + var logFilePath = Path.Combine(logDirectory, $"{DateTime.Now:yyyyMMdd_HHmmss}.log"); + var logFile = File.Open(logFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.Read); + + return new(logFile) { AutoFlush = true }; + } + + public void Dispose() => _writer?.Dispose(); +} + +/// +/// Initializes a new instance of the class. +/// +/// The name of the logger. +internal sealed class FileLogger(TextWriter writer, string name) : ILogger +{ + /// + public IDisposable BeginScope(TState state) where TState : notnull => null!; + + /// + public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None; + + /// + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + ArgumentNullException.ThrowIfNull(formatter); + + string formatted = formatter(state, exception); + + if (string.IsNullOrEmpty(formatted) && exception == null) + { + // With no formatted message or exception, there's nothing to print. + return; + } + + var message = $"{logLevel} [{name}]{Environment.NewLine}"; + if (string.IsNullOrEmpty(formatted)) + { + Debug.Assert(exception != null); + message += $"{exception}"; + } + else if (exception == null) + { + message += $"{formatted}"; + } + else + { + message += $"{formatted}{Environment.NewLine}{Environment.NewLine}{exception}"; + } + + writer.WriteLine(message); + } +} diff --git a/ZuneModCore/ZuneModCore.csproj b/ZuneModCore/ZuneModCore.csproj index 2de2ee6..5ea306f 100644 --- a/ZuneModCore/ZuneModCore.csproj +++ b/ZuneModCore/ZuneModCore.csproj @@ -9,6 +9,7 @@ + diff --git a/ZuneModdingHelper/App.xaml.cs b/ZuneModdingHelper/App.xaml.cs index 4f36159..9eee6d8 100644 --- a/ZuneModdingHelper/App.xaml.cs +++ b/ZuneModdingHelper/App.xaml.cs @@ -1,8 +1,10 @@ using System; +using System.IO; using System.Text; using System.Windows; using CommunityToolkit.Mvvm.DependencyInjection; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using ZuneModCore; using ZuneModCore.Services; using ZuneModdingHelper.Services; @@ -57,7 +59,14 @@ namespace ZuneModdingHelper private static void ConfigureServices() { - ServiceCollection services = new(); + ServiceCollection services = []; + + services.AddLogging(builder => + { + var logDir = Path.Combine(Settings.AppDataDir, "Logs"); + Directory.CreateDirectory(logDir); + builder.AddProvider(new FileLoggerProvider(logDir)); + }); Octokit.IGitHubClient github = new Octokit.GitHubClient(new Octokit.ProductHeaderValue(Title.Replace(" ", ""), VersionStr)); services.AddSingleton(github); diff --git a/ZuneModdingHelper/Pages/ModsPage.xaml.cs b/ZuneModdingHelper/Pages/ModsPage.xaml.cs index 5dc3777..b4849f9 100644 --- a/ZuneModdingHelper/Pages/ModsPage.xaml.cs +++ b/ZuneModdingHelper/Pages/ModsPage.xaml.cs @@ -1,8 +1,10 @@ using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; +using Microsoft.Extensions.Logging; using OwlCore.AbstractUI.Models; using OwlCore.ComponentModel; +using System; using System.Net.NetworkInformation; using System.Threading.Tasks; using System.Windows; @@ -21,10 +23,12 @@ namespace ZuneModdingHelper.Pages private const string MOD_MANAGER_TITLE = "MOD MANAGER"; private readonly Settings _settings; + private readonly ILogger _log; - public ModsPage(Settings settings) + public ModsPage(Settings settings, ILogger log) { _settings = settings; + _log = log; InitializeComponent(); ModList.ItemsSource = ModManager.ModFactories; @@ -84,6 +88,8 @@ namespace ZuneModdingHelper.Pages Description = $"Successfully applied '{modTitle}'", OnAction = new AsyncRelayCommand(ShowDonationRequestDialog) })); + + _log.LogInformation("Mod {ModId} applied", mod.Metadata.Id); } private async void ResetButton_Click(object sender, RoutedEventArgs e) @@ -130,6 +136,8 @@ namespace ZuneModdingHelper.Pages Title = MOD_MANAGER_TITLE, Description = $"Successfully reset '{modTitle}'", })); + + _log.LogError("Mod {ModId} reset", mod.Metadata.Id); } private static bool TryGetModFromControl(object sender, out IModFactory modFactory) @@ -148,8 +156,10 @@ namespace ZuneModdingHelper.Pages return mod; } - private static void ShowErrorDialog(Mod mod, string action, string errorMessage) + private void ShowErrorDialog(Mod mod, string action, string errorMessage) { + _log.LogError("Mod {ModId} failed to {Action}: {ErrorMessage}", mod.Metadata.Id, action, errorMessage); + DialogViewModel errorDialog = new() { Title = MOD_MANAGER_TITLE, diff --git a/ZuneModdingHelper/Services/Settings.cs b/ZuneModdingHelper/Services/Settings.cs index bb5cf06..3659a0b 100644 --- a/ZuneModdingHelper/Services/Settings.cs +++ b/ZuneModdingHelper/Services/Settings.cs @@ -2,9 +2,7 @@ using OwlCore.Storage; using System.IO; using System; -using ZuneModCore; using ZuneModCore.Services; -using System.Collections.Generic; namespace ZuneModdingHelper.Services; @@ -12,13 +10,15 @@ public class Settings(IModifiableFolder folder) : SettingsBase(folder, SystemTex { static Settings() { - string dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ZuneModCore", "Settings"); + string dir = Path.Combine(AppDataDir, "Settings"); Directory.CreateDirectory(dir); OwlCore.Storage.SystemIO.SystemFolder settingsFolder = new(dir); Default = new(settingsFolder); } + public static string AppDataDir => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ZuneModCore"); + public static Settings Default { get; } public string ZuneInstallDir