From d10e3d4c862d1d6ca5cb37fb40bb4b3f8f2a449a Mon Sep 17 00:00:00 2001 From: MattEqualsCoder Date: Mon, 16 Dec 2024 23:04:36 -0500 Subject: [PATCH 1/2] Split tracker speech sprites from regular sprites and configs --- .../Configuration/ConfigProvider.cs | 9 + .../Options/GeneralOptions.cs | 2 + .../Options/RandomizerOptions.cs | 9 + .../Options/Sprite.cs | 2 + .../Options/TrackerSpeechReactionImages.cs | 55 +++ .../RandomizerDirectories.cs | 38 +- .../GitHubFileDownloadUpdateEventArgs.cs | 21 + .../Services/GitHubFileSynchronizerService.cs | 362 ++++++++++++++++++ .../Services/GitHubSpriteDownloaderService.cs | 276 ------------- .../IGitHubFileSynchronizerService.cs | 40 ++ .../IGitHubSpriteDownloaderService.cs | 41 -- .../Services/OptionsWindowService.cs | 42 +- .../Services/SpriteDownloadUpdateEventArgs.cs | 9 - .../Services/TrackerSpriteService.cs | 77 ++++ .../ViewModels/OptionsWindowTrackerOptions.cs | 5 + .../ViewModels/OptionsWindowViewModel.cs | 7 +- .../Services/IUIService.cs | 8 - .../Services/UIService.cs | 31 -- .../TrackerSpeechImages.cs | 8 - .../ServiceCollectionExtensions.cs | 3 +- .../Services/MainWindowService.cs | 48 ++- .../Services/SpriteDownloadWindowService.cs | 6 +- .../Services/TrackerSpeechWindowService.cs | 31 +- 23 files changed, 712 insertions(+), 418 deletions(-) create mode 100644 src/TrackerCouncil.Smz3.Data/Options/TrackerSpeechReactionImages.cs create mode 100644 src/TrackerCouncil.Smz3.Data/Services/GitHubFileDownloadUpdateEventArgs.cs create mode 100644 src/TrackerCouncil.Smz3.Data/Services/GitHubFileSynchronizerService.cs delete mode 100644 src/TrackerCouncil.Smz3.Data/Services/GitHubSpriteDownloaderService.cs create mode 100644 src/TrackerCouncil.Smz3.Data/Services/IGitHubFileSynchronizerService.cs delete mode 100644 src/TrackerCouncil.Smz3.Data/Services/IGitHubSpriteDownloaderService.cs delete mode 100644 src/TrackerCouncil.Smz3.Data/Services/SpriteDownloadUpdateEventArgs.cs create mode 100644 src/TrackerCouncil.Smz3.Data/Services/TrackerSpriteService.cs delete mode 100644 src/TrackerCouncil.Smz3.Tracking/TrackerSpeechImages.cs diff --git a/src/TrackerCouncil.Smz3.Data/Configuration/ConfigProvider.cs b/src/TrackerCouncil.Smz3.Data/Configuration/ConfigProvider.cs index 51b571879..2d0956a56 100644 --- a/src/TrackerCouncil.Smz3.Data/Configuration/ConfigProvider.cs +++ b/src/TrackerCouncil.Smz3.Data/Configuration/ConfigProvider.cs @@ -22,6 +22,8 @@ namespace TrackerCouncil.Smz3.Data.Configuration; /// public partial class ConfigProvider { + public static HashSet DeprecatedConfigProfiles = ["Halloween Tracker Sprites", "Plain Tracker Sprites"]; + private static readonly JsonSerializerOptions s_options = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault, @@ -40,6 +42,13 @@ public ConfigProvider(ILogger? logger) { _basePath = RandomizerDirectories.ConfigPath; _logger = logger; + + var toDelete = Directory.EnumerateDirectories(_basePath) + .Where(directory => DeprecatedConfigProfiles.Contains(Path.GetFileName(directory))).ToList(); + foreach (var directory in toDelete) + { + Directory.Delete(directory, true); + } } /// diff --git a/src/TrackerCouncil.Smz3.Data/Options/GeneralOptions.cs b/src/TrackerCouncil.Smz3.Data/Options/GeneralOptions.cs index de4e8bbb9..4e0097a94 100644 --- a/src/TrackerCouncil.Smz3.Data/Options/GeneralOptions.cs +++ b/src/TrackerCouncil.Smz3.Data/Options/GeneralOptions.cs @@ -51,6 +51,8 @@ public class GeneralOptions : INotifyPropertyChanged public bool TrackerShadows { get; set; } = true; + public string TrackerSpeechImagePack { get; set; } = "Default"; + public byte[] TrackerSpeechBGColor { get; set; } = [0xFF, 0x48, 0x3D, 0x8B]; public bool TrackerSpeechEnableBounce { get; set; } = true; diff --git a/src/TrackerCouncil.Smz3.Data/Options/RandomizerOptions.cs b/src/TrackerCouncil.Smz3.Data/Options/RandomizerOptions.cs index 5792067dd..8a16c271a 100644 --- a/src/TrackerCouncil.Smz3.Data/Options/RandomizerOptions.cs +++ b/src/TrackerCouncil.Smz3.Data/Options/RandomizerOptions.cs @@ -1,11 +1,13 @@ using System; using System.ComponentModel; using System.IO; +using System.Linq; using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization; using MSURandomizerLibrary; using SnesConnectorLibrary; +using TrackerCouncil.Smz3.Data.Configuration; using TrackerCouncil.Smz3.Data.Logic; using TrackerCouncil.Smz3.Shared.Enums; using YamlDotNet.Serialization; @@ -130,6 +132,13 @@ public static RandomizerOptions Load(string loadPath, string savePath, bool isYa : AutoMapUpdateBehavior.Disabled; } + // Remove deprecated config profiles + if (options.GeneralOptions.SelectedProfiles.Any(p => p != null && ConfigProvider.DeprecatedConfigProfiles.Contains(p))) + { + options.GeneralOptions.SelectedProfiles = options.GeneralOptions.SelectedProfiles + .Where(p => p != null && !ConfigProvider.DeprecatedConfigProfiles.Contains(p)).ToList(); + } + return options; } else diff --git a/src/TrackerCouncil.Smz3.Data/Options/Sprite.cs b/src/TrackerCouncil.Smz3.Data/Options/Sprite.cs index 0ad7b4d38..67277ed81 100644 --- a/src/TrackerCouncil.Smz3.Data/Options/Sprite.cs +++ b/src/TrackerCouncil.Smz3.Data/Options/Sprite.cs @@ -8,6 +8,8 @@ namespace TrackerCouncil.Smz3.Data.Options; public class Sprite : IEquatable { + public static HashSet ValidDownloadExtensions = [".png", ".rdc", ".ips", ".gif"]; + private static readonly Dictionary s_folderNames = new() { { SpriteType.Samus, "Samus" }, diff --git a/src/TrackerCouncil.Smz3.Data/Options/TrackerSpeechReactionImages.cs b/src/TrackerCouncil.Smz3.Data/Options/TrackerSpeechReactionImages.cs new file mode 100644 index 000000000..fe270dbe3 --- /dev/null +++ b/src/TrackerCouncil.Smz3.Data/Options/TrackerSpeechReactionImages.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; + +namespace TrackerCouncil.Smz3.Data.Options; + +/// +/// Images for a single reaction +/// +public class TrackerSpeechReactionImages +{ + /// + /// Image for when tracker is not talking + /// + public required string IdleImage { get; set; } + + /// + /// Image for when tracker is saying something + /// + public required string TalkingImage { get; set; } +} + +/// +/// A package of different sets of reaction images for Tracker +/// +public class TrackerSpeechImagePack +{ + /// + /// The name of the pack + /// + public required string Name { get; set; } + + /// + /// The default reaction images for the speech image pack + /// + public required TrackerSpeechReactionImages Default { get; set; } + + /// + /// A dictionary of all of the different reaction types for this pack + /// + public required Dictionary Reactions { get; set; } + + /// + /// Gets the reaction images for a given reaction type. Will return the default reaction type if not specified + /// or the requested reaction type is not present in this pack. + /// + /// The name of the reaction + /// The appropriate images to use for the reaction + public TrackerSpeechReactionImages GetReactionImages(string? reactionName = null) + { + if (reactionName == null) + { + return Default; + } + return Reactions.TryGetValue(reactionName.ToLower(), out var reaction) ? reaction : Default; + } +} diff --git a/src/TrackerCouncil.Smz3.Data/RandomizerDirectories.cs b/src/TrackerCouncil.Smz3.Data/RandomizerDirectories.cs index 625ccd3dc..5cd6502da 100644 --- a/src/TrackerCouncil.Smz3.Data/RandomizerDirectories.cs +++ b/src/TrackerCouncil.Smz3.Data/RandomizerDirectories.cs @@ -4,7 +4,7 @@ namespace TrackerCouncil.Smz3.Data; -public class RandomizerDirectories +public static class RandomizerDirectories { public static string SolutionPath { @@ -66,4 +66,40 @@ public static string SpritePath #endif } } + +#if DEBUG + public static string SpriteHashYamlFilePath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "SMZ3CasRandomizer", "sprite-hashes-debug.yml"); +#else + public static string SpriteHashYamlFilePath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "SMZ3CasRandomizer", "sprite-hashes.yml"); +#endif + + public static string SpriteInitialJsonFilePath => Path.Combine(SpritePath, "sprites.json"); + + public static string TrackerSpritePath + { + get + { +#if DEBUG + var parentDir = new DirectoryInfo(SolutionPath).Parent; + var spriteRepo = parentDir?.GetDirectories().FirstOrDefault(x => x.Name == "TrackerSprites"); + + if (spriteRepo?.Exists != true) + { + return Path.Combine(AppContext.BaseDirectory, "TrackerSprites"); + } + + return spriteRepo.FullName; +#else + return Path.Combine(AppContext.BaseDirectory, "TrackerSprites"); +#endif + } + } + +#if DEBUG + public static string TrackerSpriteHashYamlFilePath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "SMZ3CasRandomizer", "tracker-sprite-hashes-debug.yml"); +#else + public static string TrackerSpriteHashYamlFilePath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "SMZ3CasRandomizer", "tracker-sprite-hashes.yml"); +#endif + + public static string TrackerSpriteInitialJsonFilePath => Path.Combine(SpritePath, "tracker-sprites.json"); } diff --git a/src/TrackerCouncil.Smz3.Data/Services/GitHubFileDownloadUpdateEventArgs.cs b/src/TrackerCouncil.Smz3.Data/Services/GitHubFileDownloadUpdateEventArgs.cs new file mode 100644 index 000000000..564467105 --- /dev/null +++ b/src/TrackerCouncil.Smz3.Data/Services/GitHubFileDownloadUpdateEventArgs.cs @@ -0,0 +1,21 @@ +using System; + +namespace TrackerCouncil.Smz3.Data.Services; + +/// +/// Event for the progress of a download of files from GitHub +/// +/// +/// +public class GitHubFileDownloadUpdateEventArgs(int completed, int total) : EventArgs +{ + /// + /// How many files have been finished (either successful or failed) + /// + public int Completed => completed; + + /// + /// The total number of files to process + /// + public int Total => total; +} diff --git a/src/TrackerCouncil.Smz3.Data/Services/GitHubFileSynchronizerService.cs b/src/TrackerCouncil.Smz3.Data/Services/GitHubFileSynchronizerService.cs new file mode 100644 index 000000000..5b256e436 --- /dev/null +++ b/src/TrackerCouncil.Smz3.Data/Services/GitHubFileSynchronizerService.cs @@ -0,0 +1,362 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using TrackerCouncil.Smz3.Data.Options; +using YamlDotNet.Serialization; + +namespace TrackerCouncil.Smz3.Data.Services; + +public delegate bool PathCheck(string path); + +public delegate string PathConversion(string path); + +/// +/// Request for synchronizing a folder with GitHub +/// +public class GitHubFileDownloaderRequest +{ + /// + /// The user/team name that owns the repository + /// + public required string RepoOwner { get; set; } + + /// + /// The name of the repository + /// + public required string RepoName { get; set; } + + /// + /// The destination folder to synchronize GitHub files to + /// + public required string DestinationFolder { get; set; } + + /// + /// File to save the GitHub hashes to to prevent redownloading a file if it hasn't changed + /// + public required string HashPath { get; set; } + + /// + /// Path to a JSON file which contains the GitHub path and hash information. Used to prevent downloading files + /// when first installing the application. + /// + public string? InitialJsonPath { get; set; } + + /// + /// Delegate to check if a given file should be synchronized + /// + public PathCheck? ValidPathCheck { get; set; } + + /// + /// Delegate to convert the relative path on GitHub to the relative path to save the file locally + /// + public PathConversion? ConvertGitHubPathToLocalPath { get; set; } + + /// + /// Timeout value for each individual request + /// + public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(5); +} + +/// +/// Class that houses data of a file to synchronize +/// +public class GitHubFileDetails +{ + /// + /// The relative path of the file + /// + public required string Path { get; set; } + + /// + /// The expected path of the file on the local computer + /// + public required string LocalPath { get; set; } + + /// + /// The url to use for downloading the file + /// + public required string DownloadUrl { get; set; } + + /// + /// The hash of the file on GitHub to track if the file was previously downloaded + /// + public required string RemoteHash { get; set; } + + /// + /// If the file is found on the local computer + /// + public required bool FileExistsLocally { get; set; } + + /// + /// If there is a difference in the file between the local computer and Github + /// + public required bool FileMatchesLocally { get; set; } + + /// + /// The path to the file to save the GitHub hash to + /// + public required string HashPath { get; set; } +} + +/// +/// Service to synchronize files between a local folder and a GitHub repo +/// +public class GitHubFileSynchronizerService : IGitHubFileSynchronizerService +{ + private ILogger _logger; + private CancellationTokenSource _cts = new(); + + public GitHubFileSynchronizerService(ILogger logger) + { + _logger = logger; + } + + public void CancelDownload() => _cts.Cancel(); + + public event EventHandler? SynchronizeUpdate; + + public async Task> GetGitHubFileDetailsAsync(GitHubFileDownloaderRequest request) + { + var files = await GetGitHubFilesAsync(request); + if (files == null) + { + return []; + } + + var previousHashes = GetPreviousHashes(request); + + var fileList = new ConcurrentBag(); + + Parallel.ForEach(files, parallelOptions: new ParallelOptions { MaxDegreeOfParallelism = 4 }, + fileData => + { + var localPath = ConvertToLocalPath(request, fileData.Key); + var currentHash = fileData.Value; + previousHashes.TryGetValue(localPath, out var prevHash); + + fileList.Add(new GitHubFileDetails() + { + Path = fileData.Key, + LocalPath = localPath, + DownloadUrl = $"https://raw.githubusercontent.com/{request.RepoOwner}/{request.RepoName}/main/{fileData.Key}", + RemoteHash = fileData.Value, + FileExistsLocally = File.Exists(localPath), + FileMatchesLocally = File.Exists(localPath) && prevHash == currentHash, + HashPath = request.HashPath + }); + }); + + var foundLocalFiles = fileList.Select(x => x.LocalPath).ToHashSet(); + + foreach (var file in Directory.EnumerateFiles(request.DestinationFolder, "*", SearchOption.AllDirectories).Where(x => !foundLocalFiles.Contains(x) && IsValidPath(request, x))) + { + fileList.Add(new GitHubFileDetails() + { + Path = file, + LocalPath = file, + DownloadUrl = "", + RemoteHash = "", + FileExistsLocally = true, + FileMatchesLocally = false, + HashPath = request.HashPath + }); + } + + return fileList.ToList(); + } + + public async Task SyncGitHubFilesAsync(GitHubFileDownloaderRequest request) + { + var fileDetails = await GetGitHubFileDetailsAsync(request); + await SyncGitHubFilesAsync(fileDetails); + } + + + public async Task SyncGitHubFilesAsync(List fileDetails) + { + if (fileDetails.Count == 0) + { + return; + } + + var filesToProcess = fileDetails.Where(x => !x.FileMatchesLocally).ToList(); + var total = filesToProcess.Count; + var completed = 0; + + if (filesToProcess.Any()) + { + await Parallel.ForEachAsync(filesToProcess, parallelOptions: new ParallelOptions() { MaxDegreeOfParallelism = 4, CancellationToken = _cts.Token}, + async (fileData, _) => + { + if (!string.IsNullOrEmpty(fileData.DownloadUrl)) + { + await DownloadFileAsync(fileData.LocalPath, fileData.DownloadUrl); + } + else + { + File.Delete(fileData.LocalPath); + _logger.LogInformation("Deleted {Path}", fileData.LocalPath); + } + + completed++; + SynchronizeUpdate?.Invoke(this, new GitHubFileDownloadUpdateEventArgs(completed, total)); + }); + } + + foreach (var hashPath in fileDetails.Select(x => x.HashPath).Distinct()) + { + SaveFileHashYaml(hashPath, + fileDetails.Where(x => x.HashPath == hashPath && !string.IsNullOrEmpty(x.DownloadUrl)) + .ToDictionary(x => x.LocalPath, x => x.RemoteHash)); + } + } + + private Dictionary GetPreviousHashes(GitHubFileDownloaderRequest request) + { + var initialJsonPath = request.InitialJsonPath; + var hashYamlPath = request.HashPath; + ; + if (!string.IsNullOrEmpty(initialJsonPath) && File.Exists(initialJsonPath)) + { + var initialJson = File.ReadAllText(initialJsonPath); + var tree = JsonSerializer.Deserialize(initialJson); + + if (tree?.tree == null || tree.tree.Count == 0) + { + File.Delete(initialJsonPath); + return []; + } + + _logger.LogInformation("Loading previous file hashes from {Path}", initialJsonPath); + + var toReturn = tree.tree + .Where(p => IsValidPath(request, p.path)) + .ToDictionary(x => ConvertToLocalPath(request, x.path), x => x.sha); + + SaveFileHashYaml(request.HashPath, toReturn); + File.Delete(initialJsonPath); + + return toReturn; + } + else if (File.Exists(hashYamlPath)) + { + var yamlText = File.ReadAllText(hashYamlPath); + var serializer = new DeserializerBuilder() + .IgnoreUnmatchedProperties() + .Build(); + try + { + return serializer.Deserialize>(yamlText); + } + catch (Exception e) + { + _logger.LogError(e, "Error deserializing file hash yaml file {Path}", hashYamlPath); + } + } + + return []; + } + + private void SaveFileHashYaml(string hashPath, Dictionary hashes) + { + var serializer = new Serializer(); + var yamlText = serializer.Serialize(hashes); + File.WriteAllText(hashPath, yamlText); + } + + private async Task DownloadFileAsync(string destination, string url, int attempts = 2) + { + try + { + var destinationFile = new FileInfo(destination); + Directory.CreateDirectory(destinationFile.DirectoryName ?? ""); + using var client = new HttpClient(); + var response = await client.GetAsync(new Uri(url)); + await using var fs = new FileStream(destination, FileMode.Create); + await response.Content.CopyToAsync(fs); + _logger.LogInformation("Downloaded {Url} to {Path}", url, destination); + return true; + } + catch (Exception e) + { + if (attempts == 0) + { + _logger.LogError(e, "Unable to download {Url} to {Path}", url, destination); + return false; + } + else + { + return await DownloadFileAsync(destination, url, attempts-1); + } + } + } + + private async Task?> GetGitHubFilesAsync(GitHubFileDownloaderRequest request) + { + var apiUrl = $"https://api.github.com/repos/{request.RepoOwner}/{request.RepoName}/git/trees/main?recursive=1"; + + string response; + + try + { + var client = new HttpClient(); + client.DefaultRequestHeaders.Add("Accept", "application/json"); + client.DefaultRequestHeaders.Add("User-Agent", "GitHubReleaseChecker"); + client.Timeout = request.Timeout; + response = await client.GetStringAsync(apiUrl); + } + catch (Exception e) + { + _logger.LogError(e, "Unable to call GitHub Release API"); + return null; + } + + var tree = JsonSerializer.Deserialize(response); + + if (tree?.tree?.Any() != true) + { + _logger.LogWarning("Unable to parse GitHub JSON"); + return null; + } + + _logger.LogInformation("Retrieved {Count} file data from GitHub", tree.tree.Count); + + return tree.tree + .Where(x => IsValidPath(request, x.path)) + .ToDictionary(x => x.path, x => x.sha); + } + + private bool IsValidPath(GitHubFileDownloaderRequest request, string path) + { + return request.ValidPathCheck == null || request.ValidPathCheck(path); + } + + private string ConvertToLocalPath(GitHubFileDownloaderRequest request, string path) + { + return Path.Combine(request.DestinationFolder, + request.ConvertGitHubPathToLocalPath == null ? path : request.ConvertGitHubPathToLocalPath(path)); + } + + private class GitHubTree + { + public string sha { get; set; } = ""; + public string url { get; set; } = ""; + public List? tree { get; set; } + } + + private class GitHubFile + { + public required string path { get; set; } + public required string mode { get; set; } + public required string type { get; set; } + public required string sha { get; set; } + public required string url { get; set; } + } +} diff --git a/src/TrackerCouncil.Smz3.Data/Services/GitHubSpriteDownloaderService.cs b/src/TrackerCouncil.Smz3.Data/Services/GitHubSpriteDownloaderService.cs deleted file mode 100644 index ebae06f0b..000000000 --- a/src/TrackerCouncil.Smz3.Data/Services/GitHubSpriteDownloaderService.cs +++ /dev/null @@ -1,276 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using TrackerCouncil.Smz3.Data.Options; -using YamlDotNet.Serialization; - -namespace TrackerCouncil.Smz3.Data.Services; - -/// -/// -/// -public class GitHubSpriteDownloaderService : IGitHubSpriteDownloaderService -{ - private ILogger _logger; - private string _spriteFolder; - private CancellationTokenSource _cts = new(); - - public GitHubSpriteDownloaderService(ILogger logger) - { - _logger = logger; - _spriteFolder = Sprite.SpritePath; - _logger.LogInformation("Sprite path: {Path} | {OtherPath}", Sprite.SpritePath, AppContext.BaseDirectory); - } - - public void CancelDownload() => _cts.Cancel(); - - public event EventHandler? SpriteDownloadUpdate; - - public async Task?> GetSpritesToDownloadAsync(string owner, string repo, TimeSpan? timeout = null, bool ignoreNoPreviousHashes = false) - { - var sprites = await GetGitHubSpritesAsync(owner, repo, timeout); - if (sprites == null) - { - return new Dictionary(); - } - - var previousHashes = GetPreviousSpriteHashes(); - - // If there are no previous hashes, don't download any sprites - if (!ignoreNoPreviousHashes && previousHashes.Count == 0) - { - return new Dictionary(); - } - - var toDownload = new ConcurrentDictionary(); - - Parallel.ForEach(sprites, parallelOptions: new ParallelOptions() { MaxDegreeOfParallelism = 4 }, - spriteData => - { - var localPath = ConvertGitHubPath(spriteData.Key); - var currentHash = spriteData.Value; - previousHashes.TryGetValue(localPath, out var prevHash); - - if (currentHash == prevHash && File.Exists(localPath)) - { - return; - } - - toDownload[spriteData.Key] = spriteData.Value; - }); - - return toDownload; - } - - public async Task DownloadSpritesAsync(string owner, string repo, IDictionary? spritesToDownload = null, TimeSpan? timeout = null) - { - spritesToDownload ??= await GetSpritesToDownloadAsync(owner, repo, timeout); - if (spritesToDownload == null || spritesToDownload.Count == 0) - { - return; - } - - _cts.TryReset(); - - if (!Directory.Exists(_spriteFolder)) - { - Directory.CreateDirectory(_spriteFolder); - } - - var spriteHashes = GetPreviousSpriteHashes(); - var addedHashes = new ConcurrentDictionary(); - - var total = spritesToDownload.Count; - var completed = 0; - - if (spritesToDownload.Any()) - { - await Parallel.ForEachAsync(spritesToDownload, parallelOptions: new ParallelOptions() { MaxDegreeOfParallelism = 4, CancellationToken = _cts.Token}, - async (spriteData, _) => - { - var localPath = ConvertGitHubPath(spriteData.Key); - var currentHash = spriteData.Value; - var downloadUrl = GetGitHubRawUrl(spriteData.Key, owner, repo); - var successful = await DownloadFileAsync(localPath, downloadUrl); - - if (successful) - { - addedHashes[localPath] = currentHash; - } - - completed++; - SpriteDownloadUpdate?.Invoke(this, new SpriteDownloadUpdateEventArgs(completed, total)); - }); - } - - foreach (var addedSprite in addedHashes) - { - spriteHashes[addedSprite.Key] = addedSprite.Value; - } - - SaveSpriteHashYaml(spriteHashes); - } - - private Dictionary GetPreviousSpriteHashes() - { - if (File.Exists(GitHubSpriteJsonFilePath)) - { - var spriteJson = File.ReadAllText(GitHubSpriteJsonFilePath); - var tree = JsonSerializer.Deserialize(spriteJson); - - if (tree?.tree == null || tree.tree.Count == 0) - { - File.Delete(GitHubSpriteJsonFilePath); - return []; - } - - _logger.LogInformation("Loading previous sprite hashes from {Path}", GitHubSpriteJsonFilePath); - - var toReturn = tree.tree - .Where(IsValidSpriteFile) - .ToDictionary(x => ConvertGitHubPath(x.path), x => x.sha); - - SaveSpriteHashYaml(toReturn); - File.Delete(GitHubSpriteJsonFilePath); - - return toReturn; - } - else if (File.Exists(SpriteHashYamlFilePath)) - { - var yamlText = File.ReadAllText(SpriteHashYamlFilePath); - var serializer = new DeserializerBuilder() - .IgnoreUnmatchedProperties() - .Build(); - try - { - return serializer.Deserialize>(yamlText); - } - catch (Exception e) - { - _logger.LogError(e, "Error deserializing sprite hash yaml file {Path}", SpriteHashYamlFilePath); - } - } - - return []; - } - - private void SaveSpriteHashYaml(Dictionary hashes) - { - var serializer = new Serializer(); - var yamlText = serializer.Serialize(hashes); - File.WriteAllText(SpriteHashYamlFilePath, yamlText); - } - - private async Task DownloadFileAsync(string destination, string url, int attempts = 2) - { - try - { - var destinationFile = new FileInfo(destination); - if (destinationFile.Directory?.Exists == false) - { - destinationFile.Directory.Create(); - } - using var client = new HttpClient(); - var response = await client.GetAsync(new Uri(url)); - await using var fs = new FileStream(destination, FileMode.Create); - await response.Content.CopyToAsync(fs); - _logger.LogInformation("Downloaded {Url} to {Path}", url, destination); - return true; - } - catch (Exception e) - { - if (attempts == 0) - { - _logger.LogError(e, "Unable to download {Url} to {Path}", url, destination); - return false; - } - else - { - return await DownloadFileAsync(destination, url, attempts-1); - } - } - } - - private async Task?> GetGitHubSpritesAsync(string owner, string repo, TimeSpan? timeout = null) - { - var apiUrl = $"https://api.github.com/repos/{owner}/{repo}/git/trees/main?recursive=1"; - - string response; - - try - { - var client = new HttpClient(); - client.DefaultRequestHeaders.Add("Accept", "application/json"); - client.DefaultRequestHeaders.Add("User-Agent", "GitHubReleaseChecker"); - client.Timeout = timeout ?? TimeSpan.FromSeconds(5); - response = await client.GetStringAsync(apiUrl); - } - catch (Exception e) - { - _logger.LogError(e, "Unable to call GitHub Release API"); - return null; - } - - var tree = JsonSerializer.Deserialize(response); - - if (tree?.tree?.Any() != true) - { - _logger.LogWarning("Unable to parse GitHub JSON"); - return null; - } - - _logger.LogInformation("Retrieved {Count} file data from GitHub", tree.tree.Count); - - return tree.tree - .Where(IsValidSpriteFile) - .ToDictionary(x => x.path, x => x.sha); - } - - private string ConvertGitHubPath(string path) - { - var pathParts = new List() { _spriteFolder }; - pathParts.AddRange(path.Replace("Sprites/", "").Split("/")); - return Path.Combine(pathParts.ToArray()); - } - - private string GetGitHubRawUrl(string path, string owner, string repo) - { - return $"https://raw.githubusercontent.com/{owner}/{repo}/main/{path}"; - } - - private bool IsValidSpriteFile(GitHubFile file) - { - return file.path.StartsWith("Sprites/") && file.path.Contains("."); - } - - private class GitHubTree - { - public string sha { get; set; } = ""; - public string url { get; set; } = ""; - public List? tree { get; set; } - } - - private class GitHubFile - { - public required string path { get; set; } - public required string mode { get; set; } - public required string type { get; set; } - public required string sha { get; set; } - public required string url { get; set; } - } - -#if DEBUG - private string SpriteHashYamlFilePath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "SMZ3CasRandomizer", "sprite-hashes-debug.yml"); -#else - private string SpriteHashYamlFilePath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "SMZ3CasRandomizer", "sprite-hashes.yml"); -#endif - - private string GitHubSpriteJsonFilePath => Path.Combine(_spriteFolder, "sprites.json"); -} diff --git a/src/TrackerCouncil.Smz3.Data/Services/IGitHubFileSynchronizerService.cs b/src/TrackerCouncil.Smz3.Data/Services/IGitHubFileSynchronizerService.cs new file mode 100644 index 000000000..c468f9733 --- /dev/null +++ b/src/TrackerCouncil.Smz3.Data/Services/IGitHubFileSynchronizerService.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace TrackerCouncil.Smz3.Data.Services; + +/// +/// Service for synchronizing a GitHub repository and a local folder +/// +public interface IGitHubFileSynchronizerService +{ + /// + /// Retrieves the list of files to synchronize between GitHub and the local folder + /// + /// Request with details about the GitHub repository to sync + /// A list of all files located either on GitHub or in the local folder + public Task> GetGitHubFileDetailsAsync(GitHubFileDownloaderRequest request); + + /// + /// Synchronizes the provided list of GitHubFileDetails + /// + /// Request with details about the GitHub repository to sync + public Task SyncGitHubFilesAsync(GitHubFileDownloaderRequest request); + + /// + /// Synchronizes the provided list of GitHubFileDetails + /// + /// List of files to download + public Task SyncGitHubFilesAsync(List fileDetails); + + /// + /// Cancels the current sprite download + /// + public void CancelDownload(); + + /// + /// Event that fires off after each completed sprite download + /// + public event EventHandler SynchronizeUpdate; +} diff --git a/src/TrackerCouncil.Smz3.Data/Services/IGitHubSpriteDownloaderService.cs b/src/TrackerCouncil.Smz3.Data/Services/IGitHubSpriteDownloaderService.cs deleted file mode 100644 index 6da252330..000000000 --- a/src/TrackerCouncil.Smz3.Data/Services/IGitHubSpriteDownloaderService.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace TrackerCouncil.Smz3.Data.Services; - -/// -/// Service for downloading sprites from GitHub -/// -public interface IGitHubSpriteDownloaderService -{ - /// - /// Compares the previously downloaded sprites with what was found on GitHub and returns the sprites to download - /// - /// The GitHub repository owner - /// The GitHub repository to download - /// How long to timeout from the api call - /// If downloads should be triggered, even if no previous hashes exist - /// A dictionary of paths on GitHub and their git hashes - public Task?> GetSpritesToDownloadAsync(string owner, string repo, TimeSpan? timeout = null, bool ignoreNoPreviousHashes = false); - - /// - /// Downloads sprites from GitHub to the folder - /// - /// The GitHub repository owner - /// The GitHub repository to download - /// A dictionary for sprites that need to be downloaded from calling GetSpritesToDownloadAsync - /// How long to timeout from the api call - /// - public Task DownloadSpritesAsync(string owner, string repo, IDictionary? spritesToDownload = null, TimeSpan? timeout = null); - - /// - /// Cancels the current sprite download - /// - public void CancelDownload(); - - /// - /// Event that fires off after each completed sprite download - /// - public event EventHandler SpriteDownloadUpdate; -} diff --git a/src/TrackerCouncil.Smz3.Data/Services/OptionsWindowService.cs b/src/TrackerCouncil.Smz3.Data/Services/OptionsWindowService.cs index 9031550c8..9ea5bb1a9 100644 --- a/src/TrackerCouncil.Smz3.Data/Services/OptionsWindowService.cs +++ b/src/TrackerCouncil.Smz3.Data/Services/OptionsWindowService.cs @@ -17,7 +17,15 @@ public class TwitchErrorEventHandler(string error) : EventArgs public string Error => error; } -public class OptionsWindowService(ConfigProvider configProvider, IMicrophoneService microphoneService, OptionsFactory optionsFactory, IChatAuthenticationService chatAuthenticationService, ILogger logger, IGitHubConfigDownloaderService gitHubConfigDownloaderService, IGitHubSpriteDownloaderService gitHubSpriteDownloaderService) +public class OptionsWindowService( + ConfigProvider configProvider, + IMicrophoneService microphoneService, + OptionsFactory optionsFactory, + IChatAuthenticationService chatAuthenticationService, + ILogger logger, + IGitHubConfigDownloaderService gitHubConfigDownloaderService, + IGitHubFileSynchronizerService gitHubFileSynchronizerService, + TrackerSpriteService trackerSpriteService) { private Dictionary _availableInputDevices = new() { { "Default", "Default" } }; private OptionsWindowViewModel _model = new(); @@ -35,7 +43,8 @@ public OptionsWindowViewModel GetViewModel() _availableInputDevices[device.Key] = device.Value; } - _model = new OptionsWindowViewModel(optionsFactory.Create().GeneralOptions, _availableInputDevices, + _model = new OptionsWindowViewModel(optionsFactory.Create().GeneralOptions, + trackerSpriteService.GetPackOptions(), _availableInputDevices, configProvider.GetAvailableProfiles().ToList()); _model.RandomizerOptions.UpdateConfigButtonPressed += (sender, args) => @@ -192,12 +201,35 @@ private async Task UpdateConfigsAsync() private async Task UpdateSpritesAsync() { - var sprites = await gitHubSpriteDownloaderService.GetSpritesToDownloadAsync("TheTrackerCouncil", "SMZ3CasSprites", null, true); + var spriteDownloadRequest = new GitHubFileDownloaderRequest + { + RepoOwner = "TheTrackerCouncil", + RepoName = "SMZ3CasSprites", + DestinationFolder = RandomizerDirectories.SpritePath, + HashPath = RandomizerDirectories.SpriteHashYamlFilePath, + InitialJsonPath = RandomizerDirectories.SpriteInitialJsonFilePath, + ValidPathCheck = p => p.StartsWith("Sprites/") && p.Contains('.'), + ConvertGitHubPathToLocalPath = p => p.Replace("Sprites/", ""), + }; + + var sprites = await gitHubFileSynchronizerService.GetGitHubFileDetailsAsync(spriteDownloadRequest); + + spriteDownloadRequest = new GitHubFileDownloaderRequest + { + RepoOwner = "TheTrackerCouncil", + RepoName = "TrackerSprites", + DestinationFolder = RandomizerDirectories.TrackerSpritePath, + HashPath = RandomizerDirectories.TrackerSpritePath, + InitialJsonPath = RandomizerDirectories.TrackerSpritePath, + ValidPathCheck = p => p.EndsWith(".png", StringComparison.OrdinalIgnoreCase) || p.EndsWith(".gif", StringComparison.OrdinalIgnoreCase), + }; + + sprites.AddRange(await gitHubFileSynchronizerService.GetGitHubFileDetailsAsync(spriteDownloadRequest)); - if (sprites?.Any() == true) + if (sprites.Count > 0) { SpriteDownloadStarted?.Invoke(this, EventArgs.Empty); - await gitHubSpriteDownloaderService.DownloadSpritesAsync("TheTrackerCouncil", "SMZ3CasSprites", sprites); + await gitHubFileSynchronizerService.SyncGitHubFilesAsync(sprites); SpriteDownloadEnded?.Invoke(this, EventArgs.Empty); } } diff --git a/src/TrackerCouncil.Smz3.Data/Services/SpriteDownloadUpdateEventArgs.cs b/src/TrackerCouncil.Smz3.Data/Services/SpriteDownloadUpdateEventArgs.cs deleted file mode 100644 index b3d75f589..000000000 --- a/src/TrackerCouncil.Smz3.Data/Services/SpriteDownloadUpdateEventArgs.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace TrackerCouncil.Smz3.Data.Services; - -public class SpriteDownloadUpdateEventArgs(int completed, int total) : EventArgs -{ - public int Completed => completed; - public int Total => total; -} diff --git a/src/TrackerCouncil.Smz3.Data/Services/TrackerSpriteService.cs b/src/TrackerCouncil.Smz3.Data/Services/TrackerSpriteService.cs new file mode 100644 index 000000000..a416c1be0 --- /dev/null +++ b/src/TrackerCouncil.Smz3.Data/Services/TrackerSpriteService.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Extensions.Logging; +using TrackerCouncil.Smz3.Data.Options; + +namespace TrackerCouncil.Smz3.Data.Services; + +/// +/// Service for loading tracker speech sprites +/// +/// +/// +public class TrackerSpriteService(ILogger logger, OptionsFactory optionsFactory) +{ + private List _packs = []; + + public void LoadSprites() + { + _packs = []; + var path = RandomizerDirectories.TrackerSpritePath; + var packFolders = Directory.EnumerateDirectories(RandomizerDirectories.TrackerSpritePath); + foreach (var folder in packFolders) + { + var packName = Path.GetFileName(folder); + + if (string.IsNullOrEmpty(packName) || !File.Exists(Path.Combine(folder, "default_idle.png")) || + !File.Exists(Path.Combine(folder, "default_talk.png"))) + { + continue; + } + + Dictionary _reactions = []; + + foreach (var idleImage in Directory.GetFiles(folder, "*_idle.png")) + { + var talkingImage = idleImage.Replace("_idle.png", "_talk.png"); + if (!File.Exists(talkingImage)) + { + continue; + } + + var reactionName = Path.GetFileName(idleImage).Replace("_idle.png", "").ToLower(); + _reactions[reactionName] = new TrackerSpeechReactionImages + { + IdleImage = idleImage, TalkingImage = talkingImage, + }; + } + + _packs.Add(new TrackerSpeechImagePack + { + Name = packName, + Default = new TrackerSpeechReactionImages + { + IdleImage = Path.Combine(folder, "default_idle.png"), + TalkingImage = Path.Combine(folder, "default_talk.png"), + }, + Reactions = _reactions + }); + } + } + + public Dictionary GetPackOptions() + { + return _packs.OrderByDescending(x => x.Name == "Default") + .ThenByDescending(x => x.Name == "Vanilla") + .ThenBy(x => x.Name) + .ToDictionary(x => x.Name, x => x.Name); + } + + public TrackerSpeechImagePack GetPack(string? packName = null) + { + packName ??= optionsFactory.Create().GeneralOptions.TrackerSpeechImagePack; + return _packs.FirstOrDefault(x => x.Name == packName) ?? + _packs.FirstOrDefault(x => x.Name == "Default") ?? _packs.First(); + } +} diff --git a/src/TrackerCouncil.Smz3.Data/ViewModels/OptionsWindowTrackerOptions.cs b/src/TrackerCouncil.Smz3.Data/ViewModels/OptionsWindowTrackerOptions.cs index e1c4a9f15..8eaf44902 100644 --- a/src/TrackerCouncil.Smz3.Data/ViewModels/OptionsWindowTrackerOptions.cs +++ b/src/TrackerCouncil.Smz3.Data/ViewModels/OptionsWindowTrackerOptions.cs @@ -18,6 +18,10 @@ public class OptionsWindowTrackerOptions [DynamicFormFieldCheckBox(checkBoxText: "Render shadows", alignment: DynamicFormAlignment.Right)] public bool TrackerShadows { get; set; } = true; + [DynamicFormFieldComboBox(label: "Tracker speech window image pack:", + comboBoxOptionsProperty: nameof(TrackerSpeechImagePacks))] + public string TrackerSpeechImagePack { get; set; } = "Default"; + [DynamicFormFieldColorPicker(label: "Tracker speech window color:")] public byte[] TrackerSpeechBGColor { get; set; } = [0xFF, 0x48, 0x3D, 0x8B]; @@ -85,4 +89,5 @@ public class OptionsWindowTrackerOptions public bool MsuMessageReceiverEnabled { get; set; } = true; public Dictionary AudioDevices { get; set; } = new(); + public Dictionary TrackerSpeechImagePacks { get; set; } = []; } diff --git a/src/TrackerCouncil.Smz3.Data/ViewModels/OptionsWindowViewModel.cs b/src/TrackerCouncil.Smz3.Data/ViewModels/OptionsWindowViewModel.cs index ba49c9db6..6c0d9da6f 100644 --- a/src/TrackerCouncil.Smz3.Data/ViewModels/OptionsWindowViewModel.cs +++ b/src/TrackerCouncil.Smz3.Data/ViewModels/OptionsWindowViewModel.cs @@ -17,7 +17,7 @@ public OptionsWindowViewModel() } - public OptionsWindowViewModel(GeneralOptions options, Dictionary audioInputDevices, + public OptionsWindowViewModel(GeneralOptions options, Dictionary trackerImagePacks, Dictionary audioInputDevices, List availableProfiles) { RandomizerOptions.Z3RomPath = options.Z3RomPath; @@ -34,6 +34,7 @@ public OptionsWindowViewModel(GeneralOptions options, Dictionary TrackerOptions.TrackerBGColor = options.TrackerBGColor; TrackerOptions.TrackerShadows = options.TrackerShadows; + TrackerOptions.TrackerSpeechImagePack = options.TrackerSpeechImagePack; TrackerOptions.TrackerSpeechBGColor = options.TrackerSpeechBGColor; TrackerOptions.TrackerSpeechEnableBounce = options.TrackerSpeechEnableBounce; TrackerOptions.TrackerRecognitionThreshold = options.TrackerRecognitionThreshold * 100; @@ -54,9 +55,10 @@ public OptionsWindowViewModel(GeneralOptions options, Dictionary TrackerOptions.AutoSaveLookAtEvents = options.AutoSaveLookAtEvents; TrackerOptions.TrackerHintsEnabled = options.TrackerHintsEnabled; TrackerOptions.TrackerSpoilersEnabled = options.TrackerSpoilersEnabled; - TrackerOptions.AudioDevices = audioInputDevices; TrackerOptions.TrackerTimerEnabled = options.TrackerTimerEnabled; TrackerOptions.MsuMessageReceiverEnabled = options.MsuMessageReceiverEnabled; + TrackerOptions.TrackerSpeechImagePacks = trackerImagePacks; + TrackerOptions.AudioDevices = audioInputDevices; TwitchIntegration.TwitchUserName = options.TwitchUserName; TwitchIntegration.TwitchChannel = options.TwitchChannel; @@ -87,6 +89,7 @@ public void UpdateOptions(GeneralOptions options) options.TrackerBGColor = TrackerOptions.TrackerBGColor; options.TrackerShadows = TrackerOptions.TrackerShadows; + options.TrackerSpeechImagePack = TrackerOptions.TrackerSpeechImagePack; options.TrackerSpeechBGColor = TrackerOptions.TrackerSpeechBGColor; options.TrackerSpeechEnableBounce = TrackerOptions.TrackerSpeechEnableBounce; options.TrackerRecognitionThreshold = TrackerOptions.TrackerRecognitionThreshold / 100; diff --git a/src/TrackerCouncil.Smz3.Tracking/Services/IUIService.cs b/src/TrackerCouncil.Smz3.Tracking/Services/IUIService.cs index 084afe340..c068cba56 100644 --- a/src/TrackerCouncil.Smz3.Tracking/Services/IUIService.cs +++ b/src/TrackerCouncil.Smz3.Tracking/Services/IUIService.cs @@ -81,12 +81,4 @@ public interface IUIService /// The base path of the desired sprite /// The full path of the sprite or null if it's not found public string? GetSpritePath(string category, string imageFileName, out string? profilePath, string? basePath = null); - - /// - /// Gets the images for tracker talking - /// - /// The selected profile - /// The base path of the folder used - /// A dictionary of all of the available tracker speech images - public Dictionary GetTrackerSpeechSprites(out string? profilePath, string? basePath = null); } diff --git a/src/TrackerCouncil.Smz3.Tracking/Services/UIService.cs b/src/TrackerCouncil.Smz3.Tracking/Services/UIService.cs index 42ed60e41..94152cf72 100644 --- a/src/TrackerCouncil.Smz3.Tracking/Services/UIService.cs +++ b/src/TrackerCouncil.Smz3.Tracking/Services/UIService.cs @@ -174,37 +174,6 @@ UIConfig uiConfig return null; } - /// - /// Gets the images for tracker talking - /// - /// The selected profile - /// The base path of the folder used - /// A dictionary of all of the available tracker speech images - public Dictionary GetTrackerSpeechSprites(out string? profilePath, string? basePath = null) - { - var toReturn = new Dictionary(); - - foreach (var idleSprite in GetCategorySprites("Tracker", out profilePath, basePath).Where(x => x.EndsWith("_idle.png"))) - { - var file = new FileInfo(idleSprite); - - var reaction = file.Name.Replace("_idle.png", "").ToLower(); - var talkSprite = idleSprite.Replace("_idle.png", "_talk.png"); - - if (File.Exists(talkSprite)) - { - toReturn.Add(reaction, new TrackerSpeechImages() - { - ReactionName = reaction, - IdleImage = idleSprite, - TalkingImage = talkSprite, - }); - } - } - - return toReturn; - } - /// /// Returns all of the sprites for a category /// diff --git a/src/TrackerCouncil.Smz3.Tracking/TrackerSpeechImages.cs b/src/TrackerCouncil.Smz3.Tracking/TrackerSpeechImages.cs deleted file mode 100644 index ed9eba3b2..000000000 --- a/src/TrackerCouncil.Smz3.Tracking/TrackerSpeechImages.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace TrackerCouncil.Smz3.Tracking; - -public class TrackerSpeechImages -{ - public required string ReactionName { get; set; } - public required string IdleImage { get; set; } - public required string TalkingImage { get; set; } -} diff --git a/src/TrackerCouncil.Smz3.UI/ServiceCollectionExtensions.cs b/src/TrackerCouncil.Smz3.UI/ServiceCollectionExtensions.cs index 7f97bfe98..9e423a3be 100644 --- a/src/TrackerCouncil.Smz3.UI/ServiceCollectionExtensions.cs +++ b/src/TrackerCouncil.Smz3.UI/ServiceCollectionExtensions.cs @@ -38,6 +38,7 @@ public static IServiceCollection ConfigureServices(this IServiceCollection servi services.AddSingleton(); services.AddMultiplayerServices(); services.AddSingleton(); + services.AddSingleton(); services.AddTransient(); // Chat @@ -53,7 +54,7 @@ public static IServiceCollection ConfigureServices(this IServiceCollection servi services.AddSingleton(); services.AddTransient(); services.AddTransient(); - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddAvaloniaControlServices(); diff --git a/src/TrackerCouncil.Smz3.UI/Services/MainWindowService.cs b/src/TrackerCouncil.Smz3.UI/Services/MainWindowService.cs index 442a5aed2..a3e868b87 100644 --- a/src/TrackerCouncil.Smz3.UI/Services/MainWindowService.cs +++ b/src/TrackerCouncil.Smz3.UI/Services/MainWindowService.cs @@ -12,6 +12,7 @@ using GitHubReleaseChecker; using Microsoft.Extensions.Logging; using TrackerCouncil.Smz3.Chat.Integration; +using TrackerCouncil.Smz3.Data; using TrackerCouncil.Smz3.Data.Options; using TrackerCouncil.Smz3.Data.Services; using TrackerCouncil.Smz3.UI.ViewModels; @@ -25,8 +26,9 @@ public class MainWindowService( ILogger logger, IChatAuthenticationService chatAuthenticationService, IGitHubConfigDownloaderService gitHubConfigDownloaderService, - IGitHubSpriteDownloaderService gitHubSpriteDownloaderService, - SpriteService spriteService) : ControlService + IGitHubFileSynchronizerService gitHubFileSynchronizerService, + SpriteService spriteService, + TrackerSpriteService trackerSpriteService) : ControlService { private MainWindowViewModel _model = new(); private MainWindow _window = null!; @@ -110,30 +112,60 @@ public async Task DownloadSpritesAsync() if (string.IsNullOrEmpty(_options.GeneralOptions.Z3RomPath) || !_options.GeneralOptions.DownloadSpritesOnStartup) { + await spriteService.LoadSpritesAsync(); + trackerSpriteService.LoadSprites(); return; } - var toDownload = await gitHubSpriteDownloaderService.GetSpritesToDownloadAsync("TheTrackerCouncil", "SMZ3CasSprites"); + var spriteDownloadRequest = new GitHubFileDownloaderRequest + { + RepoOwner = "TheTrackerCouncil", + RepoName = "SMZ3CasSprites", + DestinationFolder = RandomizerDirectories.SpritePath, + HashPath = RandomizerDirectories.SpriteHashYamlFilePath, + InitialJsonPath = RandomizerDirectories.SpriteInitialJsonFilePath, + ValidPathCheck = p => Sprite.ValidDownloadExtensions.Contains(Path.GetExtension(p).ToLowerInvariant()), + ConvertGitHubPathToLocalPath = p => p.Replace("Sprites/", ""), + }; + + var toDownload = await gitHubFileSynchronizerService.GetGitHubFileDetailsAsync(spriteDownloadRequest); + + spriteDownloadRequest = new GitHubFileDownloaderRequest + { + RepoOwner = "TheTrackerCouncil", + RepoName = "TrackerSprites", + DestinationFolder = RandomizerDirectories.TrackerSpritePath, + HashPath = RandomizerDirectories.TrackerSpriteHashYamlFilePath, + InitialJsonPath = RandomizerDirectories.TrackerSpriteInitialJsonFilePath, + ValidPathCheck = p => p.EndsWith(".png", StringComparison.OrdinalIgnoreCase) || p.EndsWith(".gif", StringComparison.OrdinalIgnoreCase), + }; + + toDownload.AddRange(await gitHubFileSynchronizerService.GetGitHubFileDetailsAsync(spriteDownloadRequest)); if (toDownload is not { Count: > 4 }) { - await gitHubSpriteDownloaderService.DownloadSpritesAsync("TheTrackerCouncil", "SMZ3CasSprites", toDownload); - return; + await gitHubFileSynchronizerService.SyncGitHubFilesAsync(toDownload); } else { await Dispatcher.UIThread.InvokeAsync(async () => { SpriteDownloadStart?.Invoke(this, EventArgs.Empty); - await gitHubSpriteDownloaderService.DownloadSpritesAsync("TheTrackerCouncil", "SMZ3CasSprites", toDownload); + await gitHubFileSynchronizerService.SyncGitHubFilesAsync(toDownload); SpriteDownloadEnd?.Invoke(this, EventArgs.Empty); }); } + + await spriteService.LoadSpritesAsync(); + trackerSpriteService.LoadSprites(); } private async Task CheckForUpdates() { - if (!_options.GeneralOptions.CheckForUpdatesOnStartup) return; + if (!_options.GeneralOptions.CheckForUpdatesOnStartup) + { + return; + } var version = FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion; @@ -152,8 +184,6 @@ private async Task CheckForUpdates() { logger.LogError(ex, "Error getting GitHub release"); } - - await spriteService.LoadSpritesAsync(); } } diff --git a/src/TrackerCouncil.Smz3.UI/Services/SpriteDownloadWindowService.cs b/src/TrackerCouncil.Smz3.UI/Services/SpriteDownloadWindowService.cs index 059a4e889..7fd838628 100644 --- a/src/TrackerCouncil.Smz3.UI/Services/SpriteDownloadWindowService.cs +++ b/src/TrackerCouncil.Smz3.UI/Services/SpriteDownloadWindowService.cs @@ -4,13 +4,13 @@ namespace TrackerCouncil.Smz3.UI.Services; -public class SpriteDownloadWindowService(IGitHubSpriteDownloaderService gitHubSpriteDownloaderService) : ControlService +public class SpriteDownloadWindowService(IGitHubFileSynchronizerService gitHubFileSynchronizerService) : ControlService { private SpriteDownloadWindowViewModel _model = new(); public SpriteDownloadWindowViewModel InitializeModel() { - gitHubSpriteDownloaderService.SpriteDownloadUpdate += (sender, args) => + gitHubFileSynchronizerService.SynchronizeUpdate += (sender, args) => { _model.NumTotal = args.Total; _model.NumCompleted = args.Completed; @@ -21,6 +21,6 @@ public SpriteDownloadWindowViewModel InitializeModel() public void CancelDownload() { - gitHubSpriteDownloaderService.CancelDownload(); + gitHubFileSynchronizerService.CancelDownload(); } } diff --git a/src/TrackerCouncil.Smz3.UI/Services/TrackerSpeechWindowService.cs b/src/TrackerCouncil.Smz3.UI/Services/TrackerSpeechWindowService.cs index 35892c89c..6b39499ec 100644 --- a/src/TrackerCouncil.Smz3.UI/Services/TrackerSpeechWindowService.cs +++ b/src/TrackerCouncil.Smz3.UI/Services/TrackerSpeechWindowService.cs @@ -6,13 +6,14 @@ using Avalonia.Threading; using AvaloniaControls.ControlServices; using TrackerCouncil.Smz3.Data.Options; +using TrackerCouncil.Smz3.Data.Services; using TrackerCouncil.Smz3.Tracking; using TrackerCouncil.Smz3.Tracking.Services; using TrackerCouncil.Smz3.UI.ViewModels; namespace TrackerCouncil.Smz3.UI.Services; -public class TrackerSpeechWindowService(ICommunicator communicator, IUIService uiService, OptionsFactory optionsFactory) : ControlService +public class TrackerSpeechWindowService(ICommunicator communicator, IUIService uiService, OptionsFactory optionsFactory, TrackerSpriteService trackerSpriteService) : ControlService { TrackerSpeechWindowViewModel _model = new(); @@ -21,18 +22,17 @@ public class TrackerSpeechWindowService(ICommunicator communicator, IUIService u Interval = TimeSpan.FromSeconds(1.0 / 60), }; - private TrackerSpeechImages? _currentSpeechImages; - private Dictionary _availableSpeechImages = []; + private TrackerSpeechImagePack? _trackerSpeechImagePack; + private TrackerSpeechReactionImages? _currentSpeechImages; private int _tickCount; private readonly int _maxTicks = 12; private readonly double _bounceHeight = 6; private int _prevViseme; private bool _enableBounce; - private string? _currentReactionType; public TrackerSpeechWindowViewModel GetViewModel() { - _availableSpeechImages = uiService.GetTrackerSpeechSprites(out _); + _trackerSpeechImagePack = trackerSpriteService.GetPack(); SetReactionType(); var options = optionsFactory.Create(); @@ -127,29 +127,12 @@ private void Communicator_SpeakCompleted(object? sender, SpeakCompletedEventArgs private void SetReactionType(string reaction = "default") { - var newReactionType = reaction.ToLower(); - - if (_currentReactionType == newReactionType) + if (_trackerSpeechImagePack == null) { return; } - if (_availableSpeechImages.TryGetValue(newReactionType, out var requestedSpeechImage)) - { - _currentReactionType = newReactionType; - _currentSpeechImages = requestedSpeechImage; - } - else if (_availableSpeechImages.TryGetValue("default", out var defaultSpeechImage)) - { - _currentReactionType = "default"; - _currentSpeechImages = defaultSpeechImage; - } - else - { - var newPair = _availableSpeechImages.FirstOrDefault(); - _currentReactionType = newPair.Key.ToLower(); - _currentSpeechImages = newPair.Value; - } + _currentSpeechImages = _trackerSpeechImagePack.GetReactionImages(reaction); } public string GetBackgroundHex() From 0d0d42ae38011b9db2188f79cd2d35847c230716 Mon Sep 17 00:00:00 2001 From: MattEqualsCoder Date: Tue, 17 Dec 2024 08:46:44 -0500 Subject: [PATCH 2/2] Fix unit tests (hopefully) --- .../Configuration/ConfigProvider.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/TrackerCouncil.Smz3.Data/Configuration/ConfigProvider.cs b/src/TrackerCouncil.Smz3.Data/Configuration/ConfigProvider.cs index 2d0956a56..7ffd1f7ae 100644 --- a/src/TrackerCouncil.Smz3.Data/Configuration/ConfigProvider.cs +++ b/src/TrackerCouncil.Smz3.Data/Configuration/ConfigProvider.cs @@ -43,11 +43,14 @@ public ConfigProvider(ILogger? logger) _basePath = RandomizerDirectories.ConfigPath; _logger = logger; - var toDelete = Directory.EnumerateDirectories(_basePath) - .Where(directory => DeprecatedConfigProfiles.Contains(Path.GetFileName(directory))).ToList(); - foreach (var directory in toDelete) + if (Directory.Exists(_basePath)) { - Directory.Delete(directory, true); + var toDelete = Directory.EnumerateDirectories(_basePath) + .Where(directory => DeprecatedConfigProfiles.Contains(Path.GetFileName(directory))).ToList(); + foreach (var directory in toDelete) + { + Directory.Delete(directory, true); + } } }