From cc04bc6a1fefc72cfcf08d9741c26dc3748b6fb2 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 11 Mar 2026 12:25:27 +0000 Subject: [PATCH 01/28] Initial rewrite of import process --- Downloads/VersionSelector.xaml | 4 +- Downloads/VersionSelector.xaml.cs | 6 +- Games/RomMGameInfo.cs | 4 +- Games/RomMImport.cs | 473 ++++++++++++++++++++++++++++++ Games/RomMInstallController.cs | 92 ++---- IRomm.cs | 3 +- Models/RomM/Rom/RomMRom.cs | 33 ++- Models/RomM/Rom/RomMRomLocal.cs | 39 +++ Models/RomM/Rom/RomMRomUser.cs | 3 + RomM.cs | 385 ++++-------------------- 10 files changed, 621 insertions(+), 421 deletions(-) create mode 100644 Games/RomMImport.cs create mode 100644 Models/RomM/Rom/RomMRomLocal.cs diff --git a/Downloads/VersionSelector.xaml b/Downloads/VersionSelector.xaml index 7b1651f..1a1f557 100644 --- a/Downloads/VersionSelector.xaml +++ b/Downloads/VersionSelector.xaml @@ -11,10 +11,10 @@ - + - + diff --git a/Downloads/VersionSelector.xaml.cs b/Downloads/VersionSelector.xaml.cs index 955f4fc..7836da8 100644 --- a/Downloads/VersionSelector.xaml.cs +++ b/Downloads/VersionSelector.xaml.cs @@ -10,13 +10,13 @@ namespace RomM.VersionSelector public partial class RomMVersionSelector : PluginUserControl { - public ObservableCollection Siblings { get; set; } + public ObservableCollection RomVersions { get; set; } public bool Cancelled { get; set; } = true; - public RomMVersionSelector(List siblings) + public RomMVersionSelector(List romVersions) { - Siblings = new ObservableCollection(siblings); + RomVersions = new ObservableCollection(romVersions); InitializeComponent(); } diff --git a/Games/RomMGameInfo.cs b/Games/RomMGameInfo.cs index cf82e2f..485e8d9 100644 --- a/Games/RomMGameInfo.cs +++ b/Games/RomMGameInfo.cs @@ -7,7 +7,7 @@ using System.IO; using System.Linq; using ProtoBuf; -using Playnite.SDK; +using RomM.Models.RomM.Rom; namespace RomM.Games { @@ -68,7 +68,7 @@ private static T FromGameIdString(string gameId) where T : RomMGameInfo } } - public InstallController GetInstallController(Game game, RomM romm, bool HasSiblings, int SelectedSibling) => new RomMInstallController(game, romm, HasSiblings, SelectedSibling); + public InstallController GetInstallController(Game game, RomM romm, RomMSavedSibing GameData) => new RomMInstallController(game, romm, GameData); public UninstallController GetUninstallController(Game game, RomM romm) => new RomMUninstallController(game, romm); diff --git a/Games/RomMImport.cs b/Games/RomMImport.cs new file mode 100644 index 0000000..646a8f3 --- /dev/null +++ b/Games/RomMImport.cs @@ -0,0 +1,473 @@ +using Newtonsoft.Json; +using Playnite.SDK; +using Playnite.SDK.Models; +using Playnite.SDK.Plugins; +using RomM.Models.RomM.Rom; +using RomM.Settings; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace RomM.Games +{ + internal class RomMImport + { + private readonly RomM _plugin; + LibraryImportGamesArgs _args; + EmulatorMapping _mapping; + List _ROMs; + Dictionary _completionStatusMap; + List _favourites; + + List _metadataPlugins = new List(); + + public RomMImport(RomM plugin, LibraryImportGamesArgs args, EmulatorMapping mapping, List roms, List favourites) + { + _plugin = plugin; + _args = args; + _mapping = mapping; + _ROMs = roms; + _completionStatusMap = plugin.Playnite.Database.CompletionStatuses.ToDictionary(cs => cs.Name, cs => cs.Id); + _favourites = favourites; + + foreach (var addons in plugin.Playnite.Addons.Plugins) + { + try + { + var metadataPlugin = (MetadataPlugin)addons; + _metadataPlugins.Add(metadataPlugin); + } + catch (Exception) + { + } + } + } + + // Helper functions + private string CombineUrl(string baseUrl, string relativePath) + { + return $"{baseUrl?.TrimEnd('/')}/{relativePath?.TrimStart('/') ?? ""}"; + } + + // Main library import functions + + public List ProcessData() + { + var games = new List(); + List ImportedGamesIDs = new List(); + + foreach (var ROM in _ROMs) + { + if (_args.CancelToken.IsCancellationRequested) + break; + + // Skip game import if the ROM is apart of the exclusion list + if(_plugin.Playnite.Database.ImportExclusions[Playnite.ImportExclusionItem.GetId($"{ROM.Id}:{ROM.SHA1}", _plugin.Id)] != null) + { + _plugin.Logger.Warn($"Excluding {ROM.Name} from import."); + continue; + } + + // Skip if ROM has no filename + if (string.IsNullOrEmpty(ROM.FileName)) + { + _plugin.Playnite.Notifications.Add(new NotificationMessage(_plugin.Id.ToString(), $"Filename for ROM ID: {ROM.Id} doesn't exist!\nDoes ROM exist on the servers filesystem?", NotificationType.Error)); + continue; + } + + // Fail-safe incase none of these are set to true + if (!ROM.HasSimpleSingleFile & !ROM.HasNestedSingleFile & !ROM.HasMultipleFiles) + ROM.HasMultipleFiles = true; + + // Merging revisions + if (_plugin.Settings.MergeRevisions && ROM.Siblings?.Count > 0) + { + if (CheckForMainSibling(ROM) == MainSibling.Other) + continue; + + if (ROM.Processed) + continue; + } + + // Save Game ROM data to file + SaveGameData(ROM); + + // Skip full import if ROM has already been imported + string gameID = $"{ROM.Id}:{ROM.SHA1}"; + Guid statusID = new Guid(); + var game = _plugin.Playnite.Database.Games.FirstOrDefault(g => g.GameId == gameID); + if (game != null) + { + // Sync user data + if(_plugin.Settings.KeepRomMSynced) + { + statusID = DetermineCompletionStatus(ROM); + + game.Favorite = _favourites.Exists(f => f == ROM.Id); + + if (statusID != Guid.Empty) + { + game.CompletionStatusId = statusID; + } + _plugin.Playnite.Database.Games.Update(game); + } + + ImportedGamesIDs.Add(gameID); + continue; + } + + var importedGame = ImportGame(ROM, statusID); + + if (importedGame != null) + { + games.Add(importedGame); + ImportedGamesIDs.Add(gameID); + } + else + { + _plugin.Logger.Error($"Failed to import RomM GameID: {ROM.Id}"); + continue; + } + } + _plugin.Logger.Debug($"Finished adding new games for {_mapping.Platform.Name}"); + + + RemoveMissingGames(ImportedGamesIDs); + + + return games; + } + private Game ImportGame(RomMRom ROM, Guid StatusID) + { + var rootInstallDir = _plugin.Playnite.Paths.IsPortable + ? _mapping.DestinationPathResolved.Replace(_plugin.Playnite.Paths.ApplicationPath, ExpandableVariables.PlayniteDirectory) + : _mapping.DestinationPathResolved; + var gameInstallDir = Path.Combine(rootInstallDir, Path.GetFileNameWithoutExtension(ROM.Name)); + var pathToGame = Path.Combine(gameInstallDir, ROM.Name); + + var gameNameWithTags = + $"{ROM.Name}" + + $"{(ROM.Regions.Count > 0 ? $" ({string.Join(", ", ROM.Regions)})" : "")}" + + $"{(!string.IsNullOrEmpty(ROM.Revision) ? $" (Rev {ROM.Revision})" : "")}" + + $"{(ROM.Tags.Count > 0 ? $" ({string.Join(", ", ROM.Tags)})" : "")}"; + + var preferedRatingsBoard = _plugin.Playnite.ApplicationSettings.AgeRatingOrgPriority; + var agerating = ROM.Metadatum.Age_Ratings.Count > 0 ? new HashSet(ROM.Metadatum.Age_Ratings.Where(r => r.Split(':')[0] == preferedRatingsBoard.ToString()).Select(r => new MetadataNameProperty(r.ToString()))) : null; + + var status = _plugin.Playnite.Database.CompletionStatuses.Get(StatusID); + var completionStatusProperty = status != null ? new MetadataNameProperty(status.Name) : null; + + List gameLinks = new List(); + if(ROM.SSId != null) + gameLinks.Add(new Link("Screenscraper", $"https://www.screenscraper.fr/gameinfos.php?gameid={ROM.SSId}")); + if (ROM.HasheousId != null) + gameLinks.Add(new Link("Hasheous", $"https://hasheous.org/index.html?page=dataobjectdetail&type=game&id={ROM.HasheousId}")); + if (ROM.RAId != null) + gameLinks.Add(new Link("RetroAchievements", $"https://retroachievements.org/game/{ROM.RAId}")); + if (ROM.HLTBId != null) + gameLinks.Add(new Link("HowLongToBeat", $"https://howlongtobeat.com/game/{ROM.HLTBId}")); + + var metadata = new GameMetadata + { + Source = _plugin.SourceName, + GameId = $"{ROM.Id}:{ROM.SHA1}", + + Name = ROM.Name, + Description = ROM.Summary, + + Platforms = new HashSet { new MetadataNameProperty(_mapping.Platform.Name ?? "") }, + Regions = new HashSet(ROM.Regions.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), + Genres = new HashSet(ROM.Metadatum.Genres.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), + AgeRatings = agerating, + Series = new HashSet(ROM.Metadatum.Franchises.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), + Features = new HashSet(ROM.Metadatum.Gamemodes.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), + Categories = new HashSet(ROM.Metadatum.Collections.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), + + ReleaseDate = ROM.Metadatum.Release_Date.HasValue ? new ReleaseDate(new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(ROM.Metadatum.Release_Date.Value).ToLocalTime()) : new ReleaseDate(), + CommunityScore = (int?)ROM.Metadatum.Average_Rating, + + CoverImage = !string.IsNullOrEmpty(ROM.PathCoverL) ? new MetadataFile($"{_plugin.Settings.RomMHost}{ROM.PathCoverL}") : null, + + Favorite = _favourites.Exists(f => f == ROM.Id), + LastActivity = ROM.RomUser.LastPlayed, + UserScore = ROM.RomUser.Rating * 10, //RomM-Rating is 1-10, Playnite 1-100, so it can unfortunately only by synced one direction without loosing decimals + CompletionStatus = completionStatusProperty, + Links = gameLinks, + Roms = new List { new GameRom(gameNameWithTags, pathToGame) }, + InstallDirectory = gameInstallDir, + IsInstalled = File.Exists(pathToGame), + InstallSize = ROM.FileSizeBytes, + GameActions = new List + { + new GameAction + { + Name = $"Play in {_mapping.Emulator.Name}", + Type = GameActionType.Emulator, + EmulatorId = _mapping.EmulatorId, + EmulatorProfileId = _mapping.EmulatorProfileId, + IsPlayAction = true, + }, + new GameAction + { + Type = GameActionType.URL, + Name = "View in RomM", + Path = CombineUrl(_plugin.Settings.RomMHost, $"rom/{ROM.Id}"), + IsPlayAction = false + } + } + }; + + // Import new game + Game game = _plugin.Playnite.Database.ImportGame(metadata, _plugin); + + if (ROM.HasManual) + { + game.Manual = $"{_plugin.Settings.RomMHost}/assets/romm/resources/{ROM.ManualPath}"; + } + + // NOTE: Due to the switch from GetGames to ImportGames metadata plugins don't run. + // This is currently commented out as the user should be given the option to run metadata scans with library import + // and a more sophisticated system need implementing to import that data + //Pull metadata from plugins + //game = PluginMetaData(game); + //_plugin.Playnite.Database.Games.Update(game); + + return game; + } + private void RemoveMissingGames(List ImportedGames) + { + var gamesInDatabase = _plugin.Playnite.Database.Games.Where(g => + g.Source != null && g.Source.Name == _plugin.SourceName.ToString() && + g.Platforms != null && g.Platforms.Any(p => p.Name == _mapping.Platform.Name) + ); + + _plugin.Logger.Debug($"Starting to remove not found games for {_mapping.Platform.Name}."); + + foreach (var game in gamesInDatabase) + { + if (_args.CancelToken.IsCancellationRequested) + break; + + if (ImportedGames.Contains(game.GameId)) + { + continue; + } + + _plugin.Playnite.Database.Games.Remove(game.Id); + } + + _plugin.Logger.Debug($"Finished removing not found games for {_mapping.Platform.Name}"); + } + + private MainSibling CheckForMainSibling(RomMRom ROM) + { + //Check to see if ROM is the main sibling + if (ROM.RomUser.IsMainSibling) + return MainSibling.Current; + + //Find if there is a main sibling + foreach (var sibling in ROM.Siblings) + { + var siblingROM = _ROMs.Find(x => x.Id == sibling.Id); + + if (siblingROM.RomUser.IsMainSibling) + { + return MainSibling.Other; + } + } + + return MainSibling.None; + } + private void SaveGameData(RomMRom ROM) + { + RomMRomLocal toSave = new RomMRomLocal(); + + if (_plugin.Settings.MergeRevisions && ROM.Siblings?.Count > 0) + { + // Check to see if game is already installed + string GameID = $"{ROM.Id}:{ROM.SHA1}"; + var game = _plugin.Playnite.Database.Games.FirstOrDefault(x => x.GameId == GameID); + + if(game != null && game.IsInstalled) + { + try + { + // Load Game data from file + string olddata = File.ReadAllText($"{_plugin.ROMDataPath}{ROM.SHA1}.json"); + toSave = JsonConvert.DeserializeObject(olddata); + + List toSaveSiblings = toSave.Siblings; + + // Remove all sibling data so any ROMs still linked to Game will be added back + toSave.Siblings.Clear(); + + // Check to see if sibling still exists + foreach (var sibling in ROM.Siblings) + { + var siblingItem = _ROMs.Find(x => x.Id == sibling.Id); + + if (toSaveSiblings.Exists(x => x.Id == sibling.Id)) + { + // Add sibling back to file + toSave.Siblings.Add(toSaveSiblings.Find(x => x.Id == sibling.Id)); + } + else + { + // Add new sibling + var newSibling = new RomMSavedSibing(); + + if (string.IsNullOrEmpty(siblingItem.FileName)) + { + _plugin.Playnite.Notifications.Add(new NotificationMessage(_plugin.Id.ToString(), $"Filename for ROM ID: {siblingItem.Id} doesn't exist!\nDoes ROM exist on the servers filesystem?", NotificationType.Error)); + continue; + } + + newSibling.Id = sibling.Id; + newSibling.FileName = ROM.HasMultipleFiles ? Path.GetFileName(siblingItem.FileName) : Path.GetFileName(siblingItem.Files.First().FileName); + newSibling.DownloadURL = CombineUrl(_plugin.Settings.RomMHost, $"api/roms/{newSibling.Id}/content/{newSibling.FileName}"); + newSibling.HasMultipleFiles = ROM.HasMultipleFiles; + newSibling.IsSelected = false; + newSibling.Mapping = _mapping; + + _ROMs.Find(x => x.Id == siblingItem.Id).Processed = true; + toSave.Siblings.Add(newSibling); + } + } + + // Write old and new data back to file + olddata = JsonConvert.SerializeObject(toSave); + File.WriteAllText($"{_plugin.Playnite.Paths.ExtensionsDataPath}\\{_plugin.Id}\\Games\\{ROM.SHA1}.json", olddata); + return; + } + catch { } + } + else // Game hasn't been import or isnt installed + { + // Save base ROM data + toSave.Id = ROM.Id; + toSave.Name = ROM.Name; + toSave.SHA1 = ROM.SHA1; + toSave.FileName = ROM.HasMultipleFiles ? Path.GetFileName(ROM.FileName) : Path.GetFileName(ROM.Files.First().FileName); + toSave.DownloadURL = CombineUrl(_plugin.Settings.RomMHost, $"api/roms/{toSave.Id}/content/{toSave.FileName}"); + toSave.HasMultipleFiles = ROM.HasMultipleFiles; + toSave.IsSelected = false; + toSave.Mapping = _mapping; + + toSave.Siblings = new List(); + + // Save sibling data + foreach (var sibling in ROM.Siblings) + { + var siblingItem = _ROMs.Find(x => x.Id == sibling.Id); + + if (string.IsNullOrEmpty(siblingItem.FileName)) + { + _plugin.Playnite.Notifications.Add(new NotificationMessage(_plugin.Id.ToString(), $"Filename for ROM ID: {siblingItem.Id} doesn't exist!\nDoes ROM exist on the servers filesystem?", NotificationType.Error)); + continue; + } + + RomMSavedSibing saveSibling = new RomMSavedSibing(); + saveSibling.Id = siblingItem.Id; + saveSibling.FileName = ROM.HasMultipleFiles ? Path.GetFileName(ROM.FileName) : Path.GetFileName(ROM.Files.First().FileName); + saveSibling.DownloadURL = CombineUrl(_plugin.Settings.RomMHost, $"api/roms/{saveSibling.Id}/content/{saveSibling.FileName}"); + saveSibling.HasMultipleFiles = ROM.HasMultipleFiles; + saveSibling.IsSelected = false; + saveSibling.Mapping = _mapping; + + _ROMs.Find(x => x.Id == siblingItem.Id).Processed = true; + + toSave.Siblings.Add(saveSibling); + } + } + + } + else //Merge Revisons not enabled or no siblings present + { + // Save base ROM data + toSave.Id = ROM.Id; + toSave.Name = ROM.Name; + toSave.SHA1 = ROM.SHA1; + toSave.FileName = ROM.HasMultipleFiles ? Path.GetFileName(ROM.FileName) : Path.GetFileName(ROM.Files.First().FileName); + toSave.DownloadURL = CombineUrl(_plugin.Settings.RomMHost, $"api/roms/{toSave.Id}/content/{toSave.FileName}"); + toSave.HasMultipleFiles = ROM.HasMultipleFiles; + toSave.IsSelected = false; + toSave.Mapping = _mapping; + + toSave.Siblings = new List(); + } + + // Write data to file + string json = JsonConvert.SerializeObject(toSave); + File.WriteAllText($"{_plugin.ROMDataPath}{ROM.SHA1}.json", json); + + } + + private Guid DetermineCompletionStatus(RomMRom ROM) + { + string completionStatus; + // Determine status in Playnite. Backlogged and "now playing" take precedent over the status options + if (ROM.RomUser.Backlogged || ROM.RomUser.NowPlaying) + { + completionStatus = ROM.RomUser.NowPlaying ? RomMRomUser.CompletionStatusMap["now_playing"] : RomMRomUser.CompletionStatusMap["backlogged"]; + } + else + { + completionStatus = RomMRomUser.CompletionStatusMap[ROM.RomUser.Status ?? "not_played"]; + } + + _completionStatusMap.TryGetValue(completionStatus, out var statusId); + + var status = _plugin.Playnite.Database.CompletionStatuses.Get(statusId); + var completionStatusProperty = status != null ? new MetadataNameProperty(status.Name) : null; + + return statusId; + } + + + + private Game PluginMetaData(Game Game) + { + foreach (var plugin in _metadataPlugins) + { + try + { + var pluginProvider = plugin.GetMetadataProvider(new MetadataRequestOptions(Game, true)); + + try + { + if (pluginProvider.AvailableFields.Any(x => x == MetadataField.Icon)) + { + Game.Icon = string.IsNullOrEmpty(Game.Icon) ? pluginProvider.GetIcon(new GetMetadataFieldArgs()).Path : Game.Icon; + } + } + catch{} + try + { + if (pluginProvider.AvailableFields.Any(x => x == MetadataField.BackgroundImage)) + { + Game.BackgroundImage = string.IsNullOrEmpty(Game.BackgroundImage) ? pluginProvider.GetBackgroundImage(new GetMetadataFieldArgs()).Path : Game.BackgroundImage; + } + } + catch{} + try + { + if (pluginProvider.AvailableFields.Any(x => x == MetadataField.CoverImage)) + { + Game.CoverImage = string.IsNullOrEmpty(Game.CoverImage) ? pluginProvider.GetCoverImage(new GetMetadataFieldArgs()).Path : Game.CoverImage; + } + } + catch{} + } + catch + { + + } + + + } + + return Game; + } + } +} diff --git a/Games/RomMInstallController.cs b/Games/RomMInstallController.cs index 58c89eb..6d3ce69 100644 --- a/Games/RomMInstallController.cs +++ b/Games/RomMInstallController.cs @@ -12,107 +12,57 @@ namespace RomM.Games { + enum InstallStatus + { + Cancelled = -1 + } + internal class RomMInstallController : InstallController { protected readonly IRomM _romM; public ILogger Logger => LogManager.GetLogger(); - public bool HasSiblings = false; - public int SelectedSibling = -1; + public RomMSavedSibing _gameData; - internal RomMInstallController(Game game, IRomM romM, bool hasSiblings, int selectedSibling) : base(game) + internal RomMInstallController(Game game, IRomM romM, RomMSavedSibing GameData) : base(game) { Name = "Download"; _romM = romM; - HasSiblings = hasSiblings; - SelectedSibling = selectedSibling; + _gameData = GameData; } public override void Install(InstallActionArgs args) { - var info = Game.GetRomMGameInfo(); - - if (SelectedSibling == -2) + if (_gameData.Id == (int)InstallStatus.Cancelled) { CancelInstall(); return; - } - - // Replace info if a different version of the game is selected - if (HasSiblings && SelectedSibling != -1) - { - List siblingInfos = new List(); - - var version = Game.Version; - if (version == null || !version.StartsWith("RomM:")) - { - _romM.Playnite.Notifications.Add( - Game.GameId, - $"Failed to download {Game.Name}.{Environment.NewLine}{Environment.NewLine}{$"Couldn't find RomMId for {Game.Name}."}", - NotificationType.Error); - - CancelInstall(); - return; - } - - int romMId; - if (!int.TryParse(version.Split(':')[1], out romMId)) - { - _romM.Playnite.Notifications.Add( - Game.GameId, - $"Failed to download {Game.Name}.{Environment.NewLine}{Environment.NewLine}{$"Malformed version string? {version} > {romMId}"}", - NotificationType.Error); - - CancelInstall(); - return; - } - - siblingInfos = JsonConvert.DeserializeObject>(File.ReadAllText($"{_romM.ROMsWithSiblingsPath}{romMId}.json")); - - var selectedSibling = siblingInfos.Find(x => x.Id == SelectedSibling); - if (selectedSibling != null) - { - info.FileName = selectedSibling.FileName; - info.DownloadUrl = selectedSibling.DownloadURL; - // This has to be changed as systems can have single ROM and Multi ROM files. E.g. .chd vs .bin/.cue - info.HasMultipleFiles = selectedSibling.HasMultipleFiles; - } - else - { - _romM.Playnite.Notifications.Add( - Game.GameId, - $"Failed to find selected version of {Game.Name}.{Environment.NewLine}Selected sibling ID {SelectedSibling} was not found. Reimport libary!", - NotificationType.Error); - CancelInstall(); - return; - } - - } + } - var dstPath = info.Mapping?.DestinationPathResolved + var dstPath = _gameData.Mapping?.DestinationPathResolved ?? throw new Exception("Mapped emulator data cannot be found, try removing and re-adding."); // Paths (same as before) - var installDir = Path.Combine(dstPath, Path.GetFileNameWithoutExtension(info.FileName)); + var installDir = Path.Combine(dstPath, Path.GetFileNameWithoutExtension(_gameData.FileName)); // If RomM indicates multiple files, we download as an archive name (zip) into the install folder. // Otherwise we download the single ROM file. - var downloadFilePath = info.HasMultipleFiles - ? Path.Combine(installDir, info.FileName + ".zip") - : Path.Combine(installDir, info.FileName); + var downloadFilePath = _gameData.HasMultipleFiles + ? Path.Combine(installDir, _gameData.FileName + ".zip") + : Path.Combine(installDir, _gameData.FileName); var req = new DownloadRequest { GameId = Game.Id, GameName = Game.Name, - DownloadUrl = info.DownloadUrl, + DownloadUrl = _gameData.DownloadURL, InstallDir = installDir, GamePath = downloadFilePath, Use7z = _romM.Settings.Use7z, PathTo7Z = _romM.Settings.PathTo7z, - HasMultipleFiles = info.HasMultipleFiles, - AutoExtract = info.Mapping != null && info.Mapping.AutoExtract, + HasMultipleFiles = _gameData.HasMultipleFiles, + AutoExtract = _gameData.Mapping != null && _gameData.Mapping.AutoExtract, // Called by queue AFTER download/extract is done BuildRoms = () => @@ -127,11 +77,11 @@ public override void Install(InstallActionArgs args) } // Otherwise, we assume extracted files are in installDir - var supported = GetEmulatorSupportedFileTypes(info); + var supported = GetEmulatorSupportedFileTypes(_gameData); var actualRomFiles = GetRomFiles(installDir, supported); // Prefer .m3u if requested - var useM3u = info.Mapping != null && info.Mapping.UseM3u; + var useM3u = _gameData.Mapping != null && _gameData.Mapping.UseM3u; if (useM3u) { var m3uFile = actualRomFiles.FirstOrDefault(m => @@ -223,7 +173,7 @@ private static string[] GetRomFiles(string installDir, List supportedFil }).ToArray(); } - private static List GetEmulatorSupportedFileTypes(RomMGameInfo info) + private static List GetEmulatorSupportedFileTypes(RomMSavedSibing info) { if (info.Mapping.EmulatorProfile is CustomEmulatorProfile) { diff --git a/IRomm.cs b/IRomm.cs index 2017abb..b480471 100644 --- a/IRomm.cs +++ b/IRomm.cs @@ -4,10 +4,9 @@ namespace RomM { internal interface IRomM { - ILogger Logger { get; } + ILogger Logger { get; } IPlayniteAPI Playnite { get; } Settings.SettingsViewModel Settings { get; } - string ROMsWithSiblingsPath { get; } Downloads.DownloadQueueController DownloadQueueController { get; } string GetPluginUserDataPath(); } diff --git a/Models/RomM/Rom/RomMRom.cs b/Models/RomM/Rom/RomMRom.cs index 1800401..d9b2334 100644 --- a/Models/RomM/Rom/RomMRom.cs +++ b/Models/RomM/Rom/RomMRom.cs @@ -47,7 +47,7 @@ public class RomMFile public string FullPath { get; set; } } - public class RomMSibling : ObservableObject + public class RomMSibling { [JsonProperty("id")] public int Id { get; set; } @@ -60,12 +60,6 @@ public class RomMSibling : ObservableObject [JsonProperty("fs_name_no_ext")] public string FileNameNoExt { get; set; } - - // Don't add JsonProperty data not from server - public string FileName { get; set; } - public string DownloadURL { get; set; } - public bool HasMultipleFiles { get; set; } - public bool isSelected { get; set; } = false; } public class RomMRom @@ -82,6 +76,18 @@ public class RomMRom [JsonProperty("moby_id")] public object MobyId { get; set; } + [JsonProperty("ss_id")] + public int? SSId { get; set; } + + [JsonProperty("ra_id")] + public int? RAId { get; set; } + + [JsonProperty("hasheous_id")] + public int? HasheousId { get; set; } + + [JsonProperty("hltb_id")] + public int? HLTBId { get; set; } + [JsonProperty("platform_id")] public int PlatformId { get; set; } @@ -184,6 +190,15 @@ public class RomMRom [JsonProperty("siblings")] public List Siblings { get; set; } + [JsonProperty("sha1_hash")] + public string SHA1 { get; set; } + + [JsonProperty("has_manual")] + public bool HasManual { get; set; } + + [JsonProperty("path_manual")] + public string ManualPath { get; set; } + [JsonProperty("full_path")] public string FullPath { get; set; } @@ -198,5 +213,7 @@ public class RomMRom [JsonProperty("sort_comparator")] public string SortComparator { get; set; } - } + + public bool Processed { get; set; } = false; +} } diff --git a/Models/RomM/Rom/RomMRomLocal.cs b/Models/RomM/Rom/RomMRomLocal.cs new file mode 100644 index 0000000..cead855 --- /dev/null +++ b/Models/RomM/Rom/RomMRomLocal.cs @@ -0,0 +1,39 @@ +using RomM.Settings; +using System.Collections.Generic; + +namespace RomM.Models.RomM.Rom +{ + + + enum MainSibling + { + None = -1, + Current = 0, + Other = 1 + } + + public struct RomMSavedSibing + { + public int Id { get; set; } + public string FileName { get; set; } + public bool HasMultipleFiles { get; set; } + public string DownloadURL { get; set; } + public bool IsSelected { get; set; } + public EmulatorMapping Mapping { get; set; } + } + + public class RomMRomLocal + { + public int Id { get; set; } + public string Name { get; set; } + public string SHA1 { get; set; } + public string FileName { get; set; } + public bool HasMultipleFiles { get; set; } + public string DownloadURL { get; set; } + public bool IsSelected { get; set; } + public EmulatorMapping Mapping { get; set; } + + public List Siblings { get; set; } + + } +} diff --git a/Models/RomM/Rom/RomMRomUser.cs b/Models/RomM/Rom/RomMRomUser.cs index d7786cf..19254a6 100644 --- a/Models/RomM/Rom/RomMRomUser.cs +++ b/Models/RomM/Rom/RomMRomUser.cs @@ -12,6 +12,9 @@ public class RomMRomUser [JsonProperty("user_id")] public int UserId { get; set; } + [JsonProperty("is_main_sibling")] + public bool IsMainSibling { get; set; } + [JsonProperty("last_played")] public DateTime? LastPlayed { get; set; } diff --git a/RomM.cs b/RomM.cs index db8519b..8d91e2c 100644 --- a/RomM.cs +++ b/RomM.cs @@ -68,8 +68,7 @@ public class RomM : LibraryPlugin, IRomM public ILogger Logger => LogManager.GetLogger(); public IPlayniteAPI Playnite { get; private set; } public SettingsViewModel Settings { get; private set; } - - public string ROMsWithSiblingsPath { get; private set; } + public string ROMDataPath { get; private set; } public DownloadQueueController DownloadQueueController { get; private set; } @@ -84,9 +83,10 @@ public RomM(IPlayniteAPI api) : base(api) Playnite = api; Properties = new LibraryPluginProperties { - HasSettings = true + HasSettings = true, + HasCustomizedGameImport = true, }; - ROMsWithSiblingsPath = $"{Playnite.Paths.ExtensionsDataPath}\\{Id}\\ROMsWithSiblings\\"; + ROMDataPath = $"{Playnite.Paths.ExtensionsDataPath}\\{Id}\\Games\\"; // Initialise the download queue downloadsVm = new DownloadQueueViewModel(); @@ -258,6 +258,9 @@ public override void OnApplicationStarted(OnApplicationStartedEventArgs args) { base.OnApplicationStarted(args); + if (!Directory.Exists($"{ROMDataPath}")) + Directory.CreateDirectory($"{ROMDataPath}"); + Settings = new SettingsViewModel(this, this); HttpClientSingleton.ConfigureBasicAuth(Settings.RomMUsername, Settings.RomMPassword); Playnite.UriHandler.RegisterSource("romm", HandleRommUri); @@ -301,23 +304,9 @@ public override void OnApplicationStarted(OnApplicationStartedEventArgs args) { if (item.PluginId == PluginId) { - var version = item.Version; - if (version == null || !version.StartsWith("RomM:")) - { - Logger.Warn($"Couldn't find RomMId for {item.Name}."); - continue; - } - - int romMId; - if (!int.TryParse(version.Split(':')[1], out romMId)) - { - Logger.Error($"Malformed version string? {version} > {romMId}"); - continue; - } - - if (File.Exists($"{ROMsWithSiblingsPath}{romMId}.json")) + if (File.Exists($"{ROMDataPath}{item.GameId.Split(':')[1]}.json")) { - File.Delete($"{ROMsWithSiblingsPath}{romMId}.json"); + File.Delete($"{ROMDataPath}{item.GameId.Split(':')[1]}.json"); } } @@ -397,28 +386,28 @@ public static async Task GetAsyncWithParams(string baseUrl, return await HttpClientSingleton.Instance.GetAsync(uriBuilder.Uri); } - public override IEnumerable GetGames(LibraryGetGamesArgs args) + public override IEnumerable ImportGames(LibraryImportGamesArgs args) { if (Playnite.ApplicationInfo.Mode == ApplicationMode.Fullscreen && !Settings.ScanGamesInFullScreen) { - return new List(); + return new List(); } if (string.IsNullOrEmpty(Settings.RomMHost) || string.IsNullOrEmpty(Settings.RomMUsername) || string.IsNullOrEmpty(Settings.RomMPassword)) { - Logger.Warn("RomM host, username or password is not set."); - return new List(); + Playnite.Notifications.Add(Id.ToString(), "RomM host, username or password is not set.", NotificationType.Error); + return new List(); } IList apiPlatforms = FetchPlatforms(); - List games = new List(); + List games = new List(); IEnumerable enabledMappings = SettingsViewModel.Instance.Mappings?.Where(m => m.Enabled); if (enabledMappings == null || !enabledMappings.Any()) { - Logger.Warn("No emulators are configured or enabled in RomM settings. No games will be fetched."); + Playnite.Notifications.Add(Id.ToString(), "No emulators are configured or enabled in RomM settings. No games will be fetched.", NotificationType.Error); return games; } @@ -430,18 +419,17 @@ public override IEnumerable GetGames(LibraryGetGamesArgs args) if (args.CancelToken.IsCancellationRequested) break; + // Check mapping has an Emulator, Profile & Platform assigned to it if (mapping.Emulator == null) { Logger.Warn($"Emulator {mapping.EmulatorId} not found, skipping."); continue; } - if (mapping.EmulatorProfile == null) { Logger.Warn($"Emulator profile {mapping.EmulatorProfileId} for emulator {mapping.EmulatorId} not found, skipping."); continue; } - if (mapping.Platform == null) { Logger.Warn($"Platform {mapping.PlatformId} not found, skipping."); @@ -453,7 +441,7 @@ public override IEnumerable GetGames(LibraryGetGamesArgs args) if (apiPlatform == null) { - Logger.Warn($"Platform {mapping.Platform.Name} with IGDB ID {mapping.Platform.IgdbId} not found in RomM, skipping."); + Playnite.Notifications.Add(Id.ToString(), $"Platform {mapping.Platform.Name} with IGDB ID {mapping.Platform.IgdbId} not found in RomM, skipping.", NotificationType.Error); continue; } @@ -463,7 +451,6 @@ public override IEnumerable GetGames(LibraryGetGamesArgs args) int offset = 0; bool hasMoreData = true; var allRoms = new List(); - var responseGameIDs = new HashSet(); while (hasMoreData) { @@ -517,252 +504,13 @@ public override IEnumerable GetGames(LibraryGetGamesArgs args) { Logger.Debug($"Finished parsing response for {apiPlatform.Name}."); - var rootInstallDir = PlayniteApi.Paths.IsPortable - ? mapping.DestinationPathResolved.Replace(PlayniteApi.Paths.ApplicationPath, ExpandableVariables.PlayniteDirectory) - : mapping.DestinationPathResolved; - - var completionStatusMap = PlayniteApi.Database.CompletionStatuses.ToDictionary(cs => cs.Name, cs => cs.Id); - - if (!Directory.Exists(ROMsWithSiblingsPath)) - Directory.CreateDirectory(ROMsWithSiblingsPath); - - List ImportedROMsWithSiblings = new List(); - if (Settings.MergeRevisions) - { - foreach (string filename in Directory.EnumerateFiles(ROMsWithSiblingsPath, "*.json", SearchOption.TopDirectoryOnly)) - { - int FileId; - if (!int.TryParse(Path.GetFileNameWithoutExtension(filename), out FileId)) - { - continue; - } - - ImportedROMsWithSiblings.Add(FileId); - } - } - - foreach (var item in allRoms) - { - if (args.CancelToken.IsCancellationRequested) - break; - - // Check for siblings and if one has already been imported skip - if (Settings.MergeRevisions && item.Siblings.Count > 0) - { - bool foundSibling = false; - - foreach (var sibling in item.Siblings) - { - if (ImportedROMsWithSiblings.Contains(sibling.Id)) - { - foundSibling = true; - break; - } - } - - if (foundSibling) - continue; - } - - var gameName = item.Name; - // Not sure if this a server bug or if my RomM server is borked but some games like Wii U dont have any of these enabled - if (!item.HasSimpleSingleFile & !item.HasNestedSingleFile & !item.HasMultipleFiles) - item.HasMultipleFiles = true; - - // Defensive: never allow path segments from server-provided filename & make sure single ROM files have an extention - var fileName = item.HasMultipleFiles ? Path.GetFileName(item.FileName) : Path.GetFileName(item.Files.Where(f => f.FullPath.Count(c => c == '/') <= 3).FirstOrDefault().FileName); - if (string.IsNullOrWhiteSpace(fileName)) - { - Logger.Warn($"Rom {item.Id} returned empty/invalid filename, skipping."); - continue; - } - - var urlCover = item.UrlCover; - var gameInstallDir = Path.Combine(rootInstallDir, Path.GetFileNameWithoutExtension(fileName)); - var pathToGame = Path.Combine(gameInstallDir, fileName); + - var info = new RomMGameInfo - { - MappingId = mapping.MappingId, - FileName = fileName, - DownloadUrl = CombineUrl(Settings.RomMHost, $"api/roms/{item.Id}/content/{fileName}"), - HasMultipleFiles = item.HasMultipleFiles - }; - - var gameId = info.AsGameId(); - responseGameIDs.Add(gameId); - - // Save sibling data so user can select the version they want installed - if (Settings.MergeRevisions && item.Siblings.Count > 0) - { - List gameInfos = new List(); - - var baseSibling = new RomMSibling - { - Id = item.Id, - Name = item.Name, - FileNameNoTags = item.FileNameNoTags, - FileNameNoExt = item.FileNameNoExt, - FileName = fileName, - HasMultipleFiles = item.HasMultipleFiles, - DownloadURL = CombineUrl(Settings.RomMHost, $"api/roms/{item.Id}/content/{fileName}"), - isSelected = true - }; - gameInfos.Add(baseSibling); - - foreach (var sibling in item.Siblings) - { - var siblingItem = allRoms.Find(x => x.Id == sibling.Id); + // Import games for current mapping + //TODO: Setup ProcessData to run async + RomMImport newImport = new RomMImport(this, args, mapping, allRoms, favorites); + newImport.ProcessData(); - if (siblingItem == null) - { - Logger.Error($"Unable to find sibling data for id:{sibling.Id}"); - continue; - } - - var siblingfileName = ""; - try - { - siblingfileName = siblingItem.HasMultipleFiles ? Path.GetFileName(siblingItem.FileName) : Path.GetFileName(siblingItem.Files.Where(f => f.FullPath.Count(c => c == '/') <= 3).FirstOrDefault().FileName); - } - catch (Exception ex) - { - Logger.Error($"ROM: {item.Id} Error:{ex.ToString()}! Skipping ROM!"); - continue; - } - - if (string.IsNullOrWhiteSpace(siblingfileName)) - { - Logger.Warn($"Rom {siblingItem.Id} returned empty/invalid filename, skipping."); - continue; - } - - sibling.FileName = siblingfileName; - sibling.DownloadURL = CombineUrl(Settings.RomMHost, $"api/roms/{sibling.Id}/content/{siblingfileName}"); - sibling.HasMultipleFiles = siblingItem.HasMultipleFiles; - - gameInfos.Add(sibling); - } - - File.WriteAllText($"{ROMsWithSiblingsPath}{item.Id}.json", JsonConvert.SerializeObject(gameInfos)); - ImportedROMsWithSiblings.Add(item.Id); - } - - string completionStatus; - // Determine status in Playnite. Backlogged and "now playing" take precedent over the status options - if (item.RomUser.Backlogged || item.RomUser.NowPlaying) - { - completionStatus = item.RomUser.NowPlaying ? RomMRomUser.CompletionStatusMap["now_playing"] : RomMRomUser.CompletionStatusMap["backlogged"]; - } - else - { - completionStatus = RomMRomUser.CompletionStatusMap[item.RomUser.Status ?? "not_played"]; - } - - completionStatusMap.TryGetValue(completionStatus, out var statusId); - - var status = PlayniteApi.Database.CompletionStatuses.Get(statusId); - var completionStatusProperty = status != null ? new MetadataNameProperty(status.Name) : null; - - // Check if the game is already installed - var game = Playnite.Database.Games.FirstOrDefault(g => g.GameId == gameId); - if (game != null) - { - // If it is already installed, we sync over metadata like favorite and status - if (Settings.KeepRomMSynced == true) - { - game.Favorite = favorites.Exists(f => f == item.Id); - - if (statusId != Guid.Empty) - { - game.CompletionStatusId = statusId; - } - - // Using the Version-Field for storing the ID instead of "RomMGameInfo" - // Could be useful in the future: https://github.com/JosefNemec/Playnite/issues/801 - game.Version = $"RomM:{item.Id}"; - - ignoredGameIds.TryAdd(game.Id, 0); - Playnite.Database.Games.Update(game); - } - continue; - } - - var gameNameWithTags = - $"{gameName}" + - $"{(item.Regions.Count > 0 ? $" ({string.Join(", ", item.Regions)})" : "")}" + - $"{(!string.IsNullOrEmpty(item.Revision) ? $" (Rev {item.Revision})" : "")}" + - $"{(item.Tags.Count > 0 ? $" ({string.Join(", ", item.Tags)})" : "")}"; - - // Add newly found game - games.Add(new GameMetadata - { - Source = SourceName, - Name = gameName, - Roms = new List { new GameRom(gameNameWithTags, pathToGame) }, - InstallDirectory = gameInstallDir, - IsInstalled = File.Exists(pathToGame), - GameId = gameId, - Platforms = new HashSet { new MetadataNameProperty(mapping.Platform.Name ?? "") }, - Regions = new HashSet(item.Regions.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), - Genres = new HashSet(item.Metadatum.Genres.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), - ReleaseDate = item.Metadatum.Release_Date.HasValue ? new ReleaseDate(new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(item.Metadatum.Release_Date.Value).ToLocalTime()) : new ReleaseDate(), - Series = new HashSet(item.Metadatum.Franchises.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), - CommunityScore = (int?)item.Metadatum.Average_Rating, - Features = new HashSet(item.Metadatum.Gamemodes.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), - Categories = new HashSet(item.Metadatum.Collections.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), - InstallSize = item.FileSizeBytes, - Description = item.Summary, - CoverImage = !string.IsNullOrEmpty(urlCover) ? new MetadataFile(urlCover) : null, - Favorite = favorites.Exists(f => f == item.Id), - LastActivity = item.RomUser.LastPlayed, - UserScore = item.RomUser.Rating * 10, //RomM-Rating is 1-10, Playnite 1-100, so it can unfortunately only by synced one direction without loosing decimals - CompletionStatus = completionStatusProperty, - GameActions = new List - { - new GameAction - { - Name = $"Play in {mapping.Emulator.Name}", - Type = GameActionType.Emulator, - EmulatorId = mapping.EmulatorId, - EmulatorProfileId = mapping.EmulatorProfileId, - IsPlayAction = true, - }, - new GameAction - { - Type = GameActionType.URL, - Name = "View in RomM", - Path = CombineUrl(Settings.RomMHost, $"rom/{item.Id}"), - IsPlayAction = false - } - }, - Version = $"RomM:{item.Id}" - }); - } - - Logger.Debug($"Finished adding new games for {apiPlatform.Name}"); - - var gamesInDatabase = Playnite.Database.Games.Where(g => - g.Source != null && g.Source.Name == SourceName.ToString() && - g.Platforms != null && g.Platforms.Any(p => p.Name == mapping.Platform.Name) - ); - - Logger.Debug($"Starting to remove not found games for {apiPlatform.Name}."); - - foreach (var game in gamesInDatabase) - { - if (args.CancelToken.IsCancellationRequested) - break; - - if (responseGameIDs.Contains(game.GameId)) - { - continue; - } - - Playnite.Database.Games.Remove(game.Id); - } - - Logger.Debug($"Finished removing not found games for {apiPlatform.Name}"); } catch (HttpRequestException e) { @@ -798,21 +546,8 @@ public override IEnumerable GetGameMenuItems(GetGameMenuItemsArgs if (args.Games.First().PluginId == PluginId) { - var version = args.Games.First().Version; - if (version == null || !version.StartsWith("RomM:")) - { - Logger.Warn($"Couldn't find RomMId for {args.Games.First().Name}."); - return gameMenuItems; - } - - int romMId; - if (!int.TryParse(version.Split(':')[1], out romMId)) - { - Logger.Error($"Malformed version string? {version} > {romMId}"); - return gameMenuItems; - } - if (Settings.MergeRevisions && File.Exists($"{ROMsWithSiblingsPath}{romMId}.json") && args.Games.First().IsInstalled) + if (Settings.MergeRevisions && File.Exists($"{ROMDataPath}{args.Games.First().GameId.Split(':')[1]}.json") && args.Games.First().IsInstalled) { gameMenuItems.Add(new GameMenuItem { @@ -832,36 +567,33 @@ public override IEnumerable GetInstallActions(GetInstallActio { if (args.Game.PluginId == Id) { - bool hasSiblings = false; - int siblingID = -1; + string gameID = args.Game.GameId; + int romMId = int.Parse(gameID.Split(':')[0]); + string romMSHA1 = gameID.Split(':')[1]; - var version = args.Game.Version; - if (version == null || !version.StartsWith("RomM:")) - { - Logger.Warn($"Couldn't find RomMId for {args.Game.Name}."); - //Set SiblingId to -2 to cancel request - siblingID = -2; - } + string json = File.ReadAllText($"{ROMDataPath}{romMSHA1}.json"); + var gameData = JsonConvert.DeserializeObject(json); - int romMId = -1; - if (siblingID != -2) + RomMSavedSibing romData = new RomMSavedSibing { - if (!int.TryParse(version.Split(':')[1], out romMId)) - { - Logger.Error($"Malformed version string? {version} > {romMId}"); - siblingID = -2; - } - } + Id = gameData.Id, + FileName = gameData.FileName, + HasMultipleFiles = gameData.HasMultipleFiles, + DownloadURL = gameData.DownloadURL, + IsSelected = gameData.IsSelected, + Mapping = gameData.Mapping + }; // If Siblings are avaiable prompt user with version selection - if (Settings.MergeRevisions && File.Exists($"{ROMsWithSiblingsPath}{romMId}.json") && siblingID != -2) + if (Settings.MergeRevisions && gameData.Siblings.Count > 0) { - List siblingInfos = new List(); - string json = File.ReadAllText($"{ROMsWithSiblingsPath}{romMId}.json"); - siblingInfos = JsonConvert.DeserializeObject>(json); + List gameVersions = new List(); - RomMVersionSelector VersionSelectorControl = new RomMVersionSelector(siblingInfos); + // Add roms to list to be selected + gameVersions.Add(romData); + gameVersions.AddRange(gameData.Siblings); + RomMVersionSelector VersionSelectorControl = new RomMVersionSelector(gameVersions); var window = Playnite.Dialogs.CreateWindow(new WindowCreationOptions { ShowMinimizeButton = false, @@ -883,11 +615,11 @@ public override IEnumerable GetInstallActions(GetInstallActio if (VersionSelectorControl.Cancelled) { - siblingID = -2; + romData.Id = (int)InstallStatus.Cancelled; } else { - //Uninstall old ROM before installing new one + // Uninstall old ROM before installing new one if (args.Game.IsInstalled) { Playnite.UninstallGame(args.Game.Id); @@ -896,16 +628,15 @@ public override IEnumerable GetInstallActions(GetInstallActio Playnite.Database.Games.Update(args.Game); } - hasSiblings = true; - siblingID = VersionSelectorControl.Siblings.Where(x => x.isSelected).First().Id; - - //Write result back to json file - siblingInfos = VersionSelectorControl.Siblings.ToList(); - File.WriteAllText($"{ROMsWithSiblingsPath}{romMId}.json", JsonConvert.SerializeObject(siblingInfos)); + // Write result back to json file + gameData.IsSelected = VersionSelectorControl.RomVersions[0].IsSelected; + VersionSelectorControl.RomVersions.RemoveAt(0); + gameData.Siblings = VersionSelectorControl.RomVersions.ToList(); + File.WriteAllText($"{ROMDataPath}{romMSHA1}.json", JsonConvert.SerializeObject(gameData)); } } - yield return args.Game.GetRomMGameInfo().GetInstallController(args.Game, this, hasSiblings, siblingID); + yield return new RomMInstallController(args.Game, this, romData); } } @@ -956,21 +687,9 @@ private void OnItemUpdated(object sender, ItemUpdatedEventArgs e) // This GameId is marked as an internal update, should be ignored this time ignoredGameIds.TryRemove(newGame.Id, out _); continue; - } - - var version = newGame.Version; - if (version == null || !version.StartsWith("RomM:")) - { - Logger.Warn($"Couldn't find RomMId for {update.NewData.Name}."); - continue; - } + } - int romMId; - if (!int.TryParse(version.Split(':')[1], out romMId)) - { - Logger.Error($"Malformed version string? {version} > {romMId}"); - continue; - } + int romMId = int.Parse(newGame.GameId.Split(':')[0]); if (oldGame.Favorite != newGame.Favorite) { From 1b9385b57d344e44389d3003e387029660051878 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 12 Mar 2026 04:58:49 +0000 Subject: [PATCH 02/28] Add metadata provider - Added metadata provider class so users can download metadata in Playnite using the Official Store option --- Games/RomMImport.cs | 6 +-- Games/RomMMetadataProvider.cs | 68 ++++++++++++++++++++++++++++++ IRomm.cs | 9 +++- RomM.cs | 78 +++++++++++++++++++++-------------- 4 files changed, 125 insertions(+), 36 deletions(-) create mode 100644 Games/RomMMetadataProvider.cs diff --git a/Games/RomMImport.cs b/Games/RomMImport.cs index 646a8f3..a1e684c 100644 --- a/Games/RomMImport.cs +++ b/Games/RomMImport.cs @@ -51,7 +51,6 @@ private string CombineUrl(string baseUrl, string relativePath) } // Main library import functions - public List ProcessData() { var games = new List(); @@ -170,7 +169,7 @@ private Game ImportGame(RomMRom ROM, Guid StatusID) var metadata = new GameMetadata { - Source = _plugin.SourceName, + Source = _plugin.Source, GameId = $"{ROM.Id}:{ROM.SHA1}", Name = ROM.Name, @@ -238,7 +237,7 @@ private Game ImportGame(RomMRom ROM, Guid StatusID) private void RemoveMissingGames(List ImportedGames) { var gamesInDatabase = _plugin.Playnite.Database.Games.Where(g => - g.Source != null && g.Source.Name == _plugin.SourceName.ToString() && + g.Source != null && g.Source.Name == _plugin.Source.ToString() && g.Platforms != null && g.Platforms.Any(p => p.Name == _mapping.Platform.Name) ); @@ -424,7 +423,6 @@ private Guid DetermineCompletionStatus(RomMRom ROM) return statusId; } - private Game PluginMetaData(Game Game) { diff --git a/Games/RomMMetadataProvider.cs b/Games/RomMMetadataProvider.cs new file mode 100644 index 0000000..755f9e2 --- /dev/null +++ b/Games/RomMMetadataProvider.cs @@ -0,0 +1,68 @@ +using Playnite.SDK; +using Playnite.SDK.Models; +using RomM.Models.RomM.Rom; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace RomM.Games +{ + public class RomMMetadataProvider : LibraryMetadataProvider + { + private readonly IRomM _romM; + public RomMMetadataProvider(RomM romM) + { + _romM = romM; + } + + public override GameMetadata GetMetadata(Game game) + { + int romMId; + if (!int.TryParse(game.GameId.Split(':')[0], out romMId)) + { + _romM.Logger.Error($"{game.Name} GameID is malformed!"); + return null; + } + + RomMRom romMGame = _romM.FetchRom(romMId.ToString()); + + var preferedRatingsBoard = _romM.Playnite.ApplicationSettings.AgeRatingOrgPriority; + var agerating = romMGame.Metadatum.Age_Ratings.Count > 0 ? new HashSet(romMGame.Metadatum.Age_Ratings.Where(r => r.Split(':')[0] == preferedRatingsBoard.ToString()).Select(r => new MetadataNameProperty(r.ToString()))) : null; + + List gameLinks = new List(); + if (romMGame.SSId != null) + gameLinks.Add(new Link("Screenscraper", $"https://www.screenscraper.fr/gameinfos.php?gameid={romMGame.SSId}")); + if (romMGame.HasheousId != null) + gameLinks.Add(new Link("Hasheous", $"https://hasheous.org/index.html?page=dataobjectdetail&type=game&id={romMGame.HasheousId}")); + if (romMGame.RAId != null) + gameLinks.Add(new Link("RetroAchievements", $"https://retroachievements.org/game/{romMGame.RAId}")); + if (romMGame.HLTBId != null) + gameLinks.Add(new Link("HowLongToBeat", $"https://howlongtobeat.com/game/{romMGame.HLTBId}")); + + var metadata = new GameMetadata + { + Name = romMGame.Name, + Description = romMGame.Summary, + + Regions = new HashSet(romMGame.Regions.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), + Genres = new HashSet(romMGame.Metadatum.Genres.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), + AgeRatings = agerating, + Series = new HashSet(romMGame.Metadatum.Franchises.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), + Features = new HashSet(romMGame.Metadatum.Gamemodes.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), + Categories = new HashSet(romMGame.Metadatum.Collections.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), + + ReleaseDate = romMGame.Metadatum.Release_Date.HasValue ? new ReleaseDate(new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(romMGame.Metadatum.Release_Date.Value).ToLocalTime()) : new ReleaseDate(), + CommunityScore = (int?)romMGame.Metadatum.Average_Rating, + + CoverImage = !string.IsNullOrEmpty(romMGame.PathCoverL) ? new MetadataFile($"{_romM.Settings.RomMHost}{romMGame.PathCoverL}") : null, + + LastActivity = romMGame.RomUser.LastPlayed, + UserScore = romMGame.RomUser.Rating * 10, //RomM-Rating is 1-10, Playnite 1-100, so it can unfortunately only by synced one direction without loosing decimals + Links = gameLinks, + + }; + + return metadata; + } + } +} diff --git a/IRomm.cs b/IRomm.cs index b480471..4f618ae 100644 --- a/IRomm.cs +++ b/IRomm.cs @@ -1,4 +1,6 @@ using Playnite.SDK; +using Playnite.SDK.Models; +using RomM.Models.RomM.Rom; namespace RomM { @@ -6,8 +8,11 @@ internal interface IRomM { ILogger Logger { get; } IPlayniteAPI Playnite { get; } - Settings.SettingsViewModel Settings { get; } + Settings.SettingsViewModel Settings { get; } + MetadataProperty Source { get; } Downloads.DownloadQueueController DownloadQueueController { get; } string GetPluginUserDataPath(); - } + RomMRom FetchRom(string romId); + + } } \ No newline at end of file diff --git a/RomM.cs b/RomM.cs index 8d91e2c..88d4c22 100644 --- a/RomM.cs +++ b/RomM.cs @@ -12,7 +12,6 @@ using RomM.Models.RomM.Rom; using RomM.Settings; using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Specialized; using System.IO; @@ -68,10 +67,11 @@ public class RomM : LibraryPlugin, IRomM public ILogger Logger => LogManager.GetLogger(); public IPlayniteAPI Playnite { get; private set; } public SettingsViewModel Settings { get; private set; } + public string ROMDataPath { get; private set; } + public MetadataProperty Source { get; private set; } public DownloadQueueController DownloadQueueController { get; private set; } - internal RomMDownloadsSidebarItem DownloadsSidebar { get; private set; } private readonly DownloadQueueViewModel downloadsVm; @@ -123,7 +123,6 @@ internal IList FetchPlatforms() return new List(); } } - internal IList FetchFavorites() { string apiFavoriteUrl = CombineUrl(Settings.RomMHost, "api/collections"); @@ -164,7 +163,6 @@ internal RomMCollection CreateFavorites() return null; } } - internal void UpdateFavorites(RomMCollection favoriteCollection, List romIds) { if (favoriteCollection == null) @@ -187,7 +185,7 @@ internal void UpdateFavorites(RomMCollection favoriteCollection, List romId } } - internal RomMRom FetchRom(string romId) + public RomMRom FetchRom(string romId) { string romUrl = CombineUrl(Settings.RomMHost, $"api/roms/{romId}"); try @@ -264,6 +262,7 @@ public override void OnApplicationStarted(OnApplicationStartedEventArgs args) Settings = new SettingsViewModel(this, this); HttpClientSingleton.ConfigureBasicAuth(Settings.RomMUsername, Settings.RomMPassword); Playnite.UriHandler.RegisterSource("romm", HandleRommUri); + Source = SourceName; // Portable path fix: expand "{PlayniteDir}" to absolute paths in DB on startup if (Playnite.Paths.IsPortable) @@ -522,24 +521,22 @@ public override IEnumerable ImportGames(LibraryImportGamesArgs args) return games; } - public override IEnumerable GetSidebarItems() - { - if (DownloadsSidebar != null) - { - yield return DownloadsSidebar; - } - } - public override ISettings GetSettings(bool firstRunSettings) { return Settings; } - public override UserControl GetSettingsView(bool firstRunSettings) { return new SettingsView(); } + public override IEnumerable GetSidebarItems() + { + if (DownloadsSidebar != null) + { + yield return DownloadsSidebar; + } + } public override IEnumerable GetGameMenuItems(GetGameMenuItemsArgs args) { List gameMenuItems = new List(); @@ -568,13 +565,35 @@ public override IEnumerable GetInstallActions(GetInstallActio if (args.Game.PluginId == Id) { string gameID = args.Game.GameId; - int romMId = int.Parse(gameID.Split(':')[0]); + RomMSavedSibing romData = new RomMSavedSibing(); + RomMRomLocal gameData = new RomMRomLocal(); + + // Pull game file from RomM data directory + int romMId; string romMSHA1 = gameID.Split(':')[1]; + if (!int.TryParse(args.Game.GameId.Split(':')[0], out romMId) || !File.Exists($"{ROMDataPath}{romMSHA1}.json")) + { + Logger.Error($"{args.Game.Name} GameID is malformed!"); + romData.Id = (int)InstallStatus.Cancelled; + yield return new RomMInstallController(args.Game, this, romData); + } - string json = File.ReadAllText($"{ROMDataPath}{romMSHA1}.json"); - var gameData = JsonConvert.DeserializeObject(json); + try + { + string json = File.ReadAllText($"{ROMDataPath}{romMSHA1}.json"); + gameData = JsonConvert.DeserializeObject(json); + } + catch (Exception) + { + Logger.Error($"{args.Game.Name} GameID is malformed or {romMSHA1} json file is corrupted!"); + romData.Id = (int)InstallStatus.Cancelled; + } - RomMSavedSibing romData = new RomMSavedSibing + if(romData.Id == (int)InstallStatus.Cancelled) + yield return new RomMInstallController(args.Game, this, romData); + + // Set ROM data to base ROM + romData = new RomMSavedSibing { Id = gameData.Id, FileName = gameData.FileName, @@ -639,15 +658,13 @@ public override IEnumerable GetInstallActions(GetInstallActio yield return new RomMInstallController(args.Game, this, romData); } } - public override IEnumerable GetUninstallActions(GetUninstallActionsArgs args) { if (args.Game.PluginId == Id) { - yield return args.Game.GetRomMGameInfo().GetUninstallController(args.Game, this); + yield return new RomMUninstallController(args.Game, this); } } - public override void OnGameInstalled(OnGameInstalledEventArgs args) { base.OnGameInstalled(args); @@ -658,7 +675,11 @@ public override void OnGameInstalled(OnGameInstalledEventArgs args) } } - private readonly ConcurrentDictionary ignoredGameIds = new ConcurrentDictionary(); + public override LibraryMetadataProvider GetMetadataDownloader() + { + return new RomMMetadataProvider(this); + } + private void OnItemUpdated(object sender, ItemUpdatedEventArgs e) { Task.Run(async () => @@ -682,15 +703,12 @@ private void OnItemUpdated(object sender, ItemUpdatedEventArgs e) if (Settings.KeepRomMSynced == true) { - if (ignoredGameIds.ContainsKey(newGame.Id)) + int romMId; + if(!int.TryParse(newGame.GameId.Split(':')[0], out romMId)) { - // This GameId is marked as an internal update, should be ignored this time - ignoredGameIds.TryRemove(newGame.Id, out _); - continue; - } + Logger.Error($"{newGame.Name} GameID is malformed!"); + } - int romMId = int.Parse(newGame.GameId.Split(':')[0]); - if (oldGame.Favorite != newGame.Favorite) { Logger.Info($"Favorites changed for {romMId}."); @@ -750,4 +768,4 @@ private void OnItemUpdated(object sender, ItemUpdatedEventArgs e) }); } } -} +} \ No newline at end of file From 961147951ab3d53c8c463453b4287e2c5c6e089d Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 12 Mar 2026 14:02:47 +0000 Subject: [PATCH 03/28] Various additions & fixes (Read Desc.) - Added: migration from old gameId to new gameId - Added: option for user to keep game deleted from RomM server, if the same ROM is added back to the server it will update the Id - Added :option to skip ROMs that are missing from RomM filesystem - Added: option to filter out genres globally - Bugfix: added GameInstallInfo so mapping stops being save multiple times per file - Bugfix: fixed sibling filename being saved with base filename - Bugfix: fixed Installer never checking to see if m3u is supported by the emulator - Bugfix: 7z path not being saved correctly --- Downloads/VersionSelector.xaml.cs | 6 +-- Games/RomMGameInfo.cs | 2 +- Games/RomMImport.cs | 75 ++++++++++++++++++++++++---- Games/RomMInstallController.cs | 8 +-- Models/RomM/Rom/RomMRomLocal.cs | 11 +++- RomM.cs | 83 +++++++++++++++++++++++++------ Settings/Settings.cs | 17 +++++-- Settings/SettingsView.xaml | 30 ++++++++++- Settings/SettingsView.xaml.cs | 1 + 9 files changed, 191 insertions(+), 42 deletions(-) diff --git a/Downloads/VersionSelector.xaml.cs b/Downloads/VersionSelector.xaml.cs index 7836da8..b46b47c 100644 --- a/Downloads/VersionSelector.xaml.cs +++ b/Downloads/VersionSelector.xaml.cs @@ -10,13 +10,13 @@ namespace RomM.VersionSelector public partial class RomMVersionSelector : PluginUserControl { - public ObservableCollection RomVersions { get; set; } + public ObservableCollection RomVersions { get; set; } public bool Cancelled { get; set; } = true; - public RomMVersionSelector(List romVersions) + public RomMVersionSelector(List romVersions) { - RomVersions = new ObservableCollection(romVersions); + RomVersions = new ObservableCollection(romVersions); InitializeComponent(); } diff --git a/Games/RomMGameInfo.cs b/Games/RomMGameInfo.cs index 485e8d9..9de9beb 100644 --- a/Games/RomMGameInfo.cs +++ b/Games/RomMGameInfo.cs @@ -68,7 +68,7 @@ private static T FromGameIdString(string gameId) where T : RomMGameInfo } } - public InstallController GetInstallController(Game game, RomM romm, RomMSavedSibing GameData) => new RomMInstallController(game, romm, GameData); + public InstallController GetInstallController(Game game, RomM romm, GameInstallInfo GameData) => new RomMInstallController(game, romm, GameData); public UninstallController GetUninstallController(Game game, RomM romm) => new RomMUninstallController(game, romm); diff --git a/Games/RomMImport.cs b/Games/RomMImport.cs index a1e684c..09e7094 100644 --- a/Games/RomMImport.cs +++ b/Games/RomMImport.cs @@ -116,8 +116,24 @@ public List ProcessData() continue; } - var importedGame = ImportGame(ROM, statusID); + // Migrate old RomMGameInfo id to new romMId:SHA1 id + if (UpdatedOldGameID(ROM)) + { + ImportedGamesIDs.Add(gameID); + continue; + } + + // If keep deleted games is enabled and a deleted game gets re-added back to the server under a new romMId, Update playnite entry + if(_plugin.Settings.KeepDeletedGames) + { + if(UpdatedDeletedGame(ROM)) + { + ImportedGamesIDs.Add(gameID); + continue; + } + } + var importedGame = ImportGame(ROM, statusID); if (importedGame != null) { games.Add(importedGame); @@ -131,9 +147,10 @@ public List ProcessData() } _plugin.Logger.Debug($"Finished adding new games for {_mapping.Platform.Name}"); - - RemoveMissingGames(ImportedGamesIDs); - + if (!_plugin.Settings.KeepDeletedGames) + { + RemoveMissingGames(ImportedGamesIDs); + } return games; } @@ -258,6 +275,45 @@ private void RemoveMissingGames(List ImportedGames) _plugin.Logger.Debug($"Finished removing not found games for {_mapping.Platform.Name}"); } + private bool UpdatedOldGameID(RomMRom ROM) + { + var filename = ROM.HasMultipleFiles ? Path.GetFileName(ROM.FileName) : Path.GetFileName(ROM.Files.First().FileName); + var info = new RomMGameInfo + { + MappingId = _mapping.MappingId, + FileName = filename, + DownloadUrl = CombineUrl(_plugin.Settings.RomMHost, $"api/roms/{ROM.Id}/content/{filename}"), + HasMultipleFiles = ROM.HasMultipleFiles + }; + + // Check to see if a game already exists with + var oldgame = _plugin.Playnite.Database.Games.FirstOrDefault(g => g.GameId == info.AsGameId()); + if (oldgame != null) + { + oldgame.GameId = $"{ROM.Id}:{ROM.SHA1}"; + _plugin.Playnite.Database.Games.Update(oldgame); + return true; + } + else + { + return false; + } + } + private bool UpdatedDeletedGame(RomMRom ROM) + { + // Check to see if a game already exists with an old romMId + var oldgame = _plugin.Playnite.Database.Games.FirstOrDefault(g => g.GameId.Split(':')[1] == ROM.SHA1); + if (oldgame != null) + { + oldgame.GameId = $"{ROM.Id}:{ROM.SHA1}"; + _plugin.Playnite.Database.Games.Update(oldgame); + return true; + } + else + { + return false; + } + } private MainSibling CheckForMainSibling(RomMRom ROM) { @@ -323,11 +379,10 @@ private void SaveGameData(RomMRom ROM) } newSibling.Id = sibling.Id; - newSibling.FileName = ROM.HasMultipleFiles ? Path.GetFileName(siblingItem.FileName) : Path.GetFileName(siblingItem.Files.First().FileName); + newSibling.FileName = siblingItem.HasMultipleFiles ? Path.GetFileName(siblingItem.FileName) : Path.GetFileName(siblingItem.Files.First().FileName); newSibling.DownloadURL = CombineUrl(_plugin.Settings.RomMHost, $"api/roms/{newSibling.Id}/content/{newSibling.FileName}"); - newSibling.HasMultipleFiles = ROM.HasMultipleFiles; + newSibling.HasMultipleFiles = siblingItem.HasMultipleFiles; newSibling.IsSelected = false; - newSibling.Mapping = _mapping; _ROMs.Find(x => x.Id == siblingItem.Id).Processed = true; toSave.Siblings.Add(newSibling); @@ -368,11 +423,10 @@ private void SaveGameData(RomMRom ROM) RomMSavedSibing saveSibling = new RomMSavedSibing(); saveSibling.Id = siblingItem.Id; - saveSibling.FileName = ROM.HasMultipleFiles ? Path.GetFileName(ROM.FileName) : Path.GetFileName(ROM.Files.First().FileName); + saveSibling.FileName = siblingItem.HasMultipleFiles ? Path.GetFileName(siblingItem.FileName) : Path.GetFileName(siblingItem.Files.First().FileName); saveSibling.DownloadURL = CombineUrl(_plugin.Settings.RomMHost, $"api/roms/{saveSibling.Id}/content/{saveSibling.FileName}"); - saveSibling.HasMultipleFiles = ROM.HasMultipleFiles; + saveSibling.HasMultipleFiles = siblingItem.HasMultipleFiles; saveSibling.IsSelected = false; - saveSibling.Mapping = _mapping; _ROMs.Find(x => x.Id == siblingItem.Id).Processed = true; @@ -423,7 +477,6 @@ private Guid DetermineCompletionStatus(RomMRom ROM) return statusId; } - private Game PluginMetaData(Game Game) { foreach (var plugin in _metadataPlugins) diff --git a/Games/RomMInstallController.cs b/Games/RomMInstallController.cs index 6d3ce69..4dc478f 100644 --- a/Games/RomMInstallController.cs +++ b/Games/RomMInstallController.cs @@ -21,9 +21,9 @@ internal class RomMInstallController : InstallController { protected readonly IRomM _romM; public ILogger Logger => LogManager.GetLogger(); - public RomMSavedSibing _gameData; + public GameInstallInfo _gameData; - internal RomMInstallController(Game game, IRomM romM, RomMSavedSibing GameData) : base(game) + internal RomMInstallController(Game game, IRomM romM, GameInstallInfo GameData) : base(game) { Name = "Download"; _romM = romM; @@ -81,7 +81,7 @@ public override void Install(InstallActionArgs args) var actualRomFiles = GetRomFiles(installDir, supported); // Prefer .m3u if requested - var useM3u = _gameData.Mapping != null && _gameData.Mapping.UseM3u; + var useM3u = _gameData.Mapping != null && _gameData.Mapping.UseM3u && supported.Any(x => x.ToLower() == "m3u"); if (useM3u) { var m3uFile = actualRomFiles.FirstOrDefault(m => @@ -173,7 +173,7 @@ private static string[] GetRomFiles(string installDir, List supportedFil }).ToArray(); } - private static List GetEmulatorSupportedFileTypes(RomMSavedSibing info) + private static List GetEmulatorSupportedFileTypes(GameInstallInfo info) { if (info.Mapping.EmulatorProfile is CustomEmulatorProfile) { diff --git a/Models/RomM/Rom/RomMRomLocal.cs b/Models/RomM/Rom/RomMRomLocal.cs index cead855..b7b290b 100644 --- a/Models/RomM/Rom/RomMRomLocal.cs +++ b/Models/RomM/Rom/RomMRomLocal.cs @@ -12,7 +12,7 @@ enum MainSibling Other = 1 } - public struct RomMSavedSibing + public struct GameInstallInfo { public int Id { get; set; } public string FileName { get; set; } @@ -22,6 +22,15 @@ public struct RomMSavedSibing public EmulatorMapping Mapping { get; set; } } + public struct RomMSavedSibing + { + public int Id { get; set; } + public string FileName { get; set; } + public bool HasMultipleFiles { get; set; } + public string DownloadURL { get; set; } + public bool IsSelected { get; set; } + } + public class RomMRomLocal { public int Id { get; set; } diff --git a/RomM.cs b/RomM.cs index 88d4c22..93934c9 100644 --- a/RomM.cs +++ b/RomM.cs @@ -353,14 +353,6 @@ public override void OnApplicationStopped(OnApplicationStoppedEventArgs args) } } - // Old-style overload (keeps older call sites working) - public static Task GetAsync( - string url, - HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) - { - return HttpClientSingleton.Instance.GetAsync(url, completionOption); - } - // New-style overload (used by DownloadQueueController) public static Task GetAsync( string url, @@ -436,6 +428,38 @@ public override IEnumerable ImportGames(LibraryImportGamesArgs args) } string url = CombineUrl(Settings.RomMHost, "api/roms"); + + if (Settings.SkipMissingFiles) + { + url += "?missing=false"; + } + + // Exclude genres from import + List excludeGenres = Settings.ExcludeGenres.Split(';').ToList(); + if(!string.IsNullOrEmpty(Settings.ExcludeGenres)) + { + // Add ? if it hasn't been added already + if (!Settings.SkipMissingFiles) + { + url += "?"; + } + + if (excludeGenres.Count > 0) + { + + foreach (var genre in excludeGenres) + { + url += $"genres={HttpUtility.UrlEncode(genre)}&"; + } + } + else + { + url += $"genres={HttpUtility.UrlEncode(Settings.ExcludeGenres)}"; + } + } + + + RomMPlatform apiPlatform = apiPlatforms.FirstOrDefault(p => p.IgdbId == mapping.Platform.IgdbId); if (apiPlatform == null) @@ -458,11 +482,12 @@ public override IEnumerable ImportGames(LibraryImportGamesArgs args) NameValueCollection queryParams = new NameValueCollection { - { "limit", pageSize.ToString() }, - { "offset", offset.ToString() }, { "platform_ids", apiPlatform.Id.ToString() }, + { "genres_logic", "none" }, { "order_by", "name" }, { "order_dir", "asc" }, + { "limit", pageSize.ToString() }, + { "offset", offset.ToString() }, }; try @@ -565,7 +590,7 @@ public override IEnumerable GetInstallActions(GetInstallActio if (args.Game.PluginId == Id) { string gameID = args.Game.GameId; - RomMSavedSibing romData = new RomMSavedSibing(); + GameInstallInfo romData = new GameInstallInfo(); RomMRomLocal gameData = new RomMRomLocal(); // Pull game file from RomM data directory @@ -593,7 +618,7 @@ public override IEnumerable GetInstallActions(GetInstallActio yield return new RomMInstallController(args.Game, this, romData); // Set ROM data to base ROM - romData = new RomMSavedSibing + romData = new GameInstallInfo { Id = gameData.Id, FileName = gameData.FileName, @@ -606,11 +631,25 @@ public override IEnumerable GetInstallActions(GetInstallActio // If Siblings are avaiable prompt user with version selection if (Settings.MergeRevisions && gameData.Siblings.Count > 0) { - List gameVersions = new List(); + List gameVersions = new List(); // Add roms to list to be selected gameVersions.Add(romData); - gameVersions.AddRange(gameData.Siblings); + + foreach (var sibling in gameData.Siblings) + { + var siblingROMData = new GameInstallInfo + { + Id = sibling.Id, + FileName = sibling.FileName, + HasMultipleFiles = sibling.HasMultipleFiles, + DownloadURL = sibling.DownloadURL, + IsSelected = sibling.IsSelected, + Mapping = gameData.Mapping + }; + + gameVersions.Add(siblingROMData); + } RomMVersionSelector VersionSelectorControl = new RomMVersionSelector(gameVersions); var window = Playnite.Dialogs.CreateWindow(new WindowCreationOptions @@ -650,7 +689,21 @@ public override IEnumerable GetInstallActions(GetInstallActio // Write result back to json file gameData.IsSelected = VersionSelectorControl.RomVersions[0].IsSelected; VersionSelectorControl.RomVersions.RemoveAt(0); - gameData.Siblings = VersionSelectorControl.RomVersions.ToList(); + + // There is probalby a LINQ you can do for this instead of clear and replace all siblings as + // only the isSelected option needs altering + gameData.Siblings.Clear(); + foreach (var versions in VersionSelectorControl.RomVersions) + { + gameData.Siblings.Add(new RomMSavedSibing + { + Id = versions.Id, + FileName = versions.FileName, + HasMultipleFiles = versions.HasMultipleFiles, + DownloadURL = versions.DownloadURL, + IsSelected = versions.IsSelected, + }); + } File.WriteAllText($"{ROMDataPath}{romMSHA1}.json", JsonConvert.SerializeObject(gameData)); } } diff --git a/Settings/Settings.cs b/Settings/Settings.cs index 4c8773c..2b1d946 100644 --- a/Settings/Settings.cs +++ b/Settings/Settings.cs @@ -6,6 +6,7 @@ using System.Collections.ObjectModel; using System.IO; using System.Linq; +using System.Web; namespace RomM.Settings { @@ -34,10 +35,11 @@ public class SettingsViewModel : ObservableObject, ISettings public bool Use7z { get; set; } = false; public string PathTo7z { get; set; } = ""; public bool MergeRevisions { get; set; } = false; + public bool KeepDeletedGames { get; set; } = false; + public string ExcludeGenres { get; set; } = ""; + public bool SkipMissingFiles { get; set; } = false; - public SettingsViewModel() - { - } + public SettingsViewModel(){} internal SettingsViewModel(Plugin plugin, IRomM romM) { @@ -49,9 +51,12 @@ internal SettingsViewModel(Plugin plugin, IRomM romM) bool forceSave = false; var savedSettings = plugin.LoadPluginSettings(); - if (savedSettings == null) { + if (savedSettings == null) + { forceSave = true; - } else { + } + else + { ScanGamesInFullScreen = savedSettings.ScanGamesInFullScreen; NotifyOnInstallComplete = savedSettings.NotifyOnInstallComplete; RomMHost = savedSettings.RomMHost; @@ -62,6 +67,8 @@ internal SettingsViewModel(Plugin plugin, IRomM romM) Use7z = savedSettings.Use7z; PathTo7z = savedSettings.PathTo7z; MergeRevisions = savedSettings.MergeRevisions; + KeepDeletedGames = savedSettings.KeepDeletedGames; + ExcludeGenres = savedSettings.ExcludeGenres; } if (Mappings == null) diff --git a/Settings/SettingsView.xaml b/Settings/SettingsView.xaml index 2fe3901..f61b504 100644 --- a/Settings/SettingsView.xaml +++ b/Settings/SettingsView.xaml @@ -121,11 +121,31 @@ From 92de6b905b8686df666dfd74541270183ccd706c Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 27 Apr 2026 22:31:32 +0100 Subject: [PATCH 18/28] Replace failed text with a notification bar --- Settings/Settings.cs | 153 +++++++---- Settings/SettingsView.xaml | 468 +++++++++++++++++++--------------- Settings/SettingsView.xaml.cs | 7 +- 3 files changed, 359 insertions(+), 269 deletions(-) diff --git a/Settings/Settings.cs b/Settings/Settings.cs index 6cbe334..3da94ad 100644 --- a/Settings/Settings.cs +++ b/Settings/Settings.cs @@ -6,6 +6,7 @@ using RomM.Models.RomM; using RomM.Models.RomM.Platform; + using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -15,6 +16,7 @@ using System.Reflection; using System.Text.RegularExpressions; using System.Windows.Data; +using System.Windows.Media; using System.Windows.Media.Imaging; namespace RomM.Settings @@ -23,80 +25,109 @@ public class SettingsViewModel : ObservableObject, ISettings { private readonly Plugin _plugin; private SettingsViewModel editingClone { get; set; } - [JsonIgnore] - internal readonly IPlayniteAPI PlayniteAPI; - [JsonIgnore] - internal readonly IRomM RomM; + [JsonIgnore] internal readonly IPlayniteAPI PlayniteAPI; + [JsonIgnore] internal readonly IRomM RomM; public static SettingsViewModel Instance { get; private set; } #region Backing Variables - [JsonIgnore] - private string _romMHost = ""; - [JsonIgnore] - private string _romMServerVersion = "---"; - [JsonIgnore] - private string _romMClientToken = ""; - [JsonIgnore] - private bool _useBasicAuth = true; - [JsonIgnore] - private string _romMUsername = ""; - [JsonIgnore] - private string _romMPassword = ""; - [JsonIgnore] - private string _defaultprofilepath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), @"profile.png"); - [JsonIgnore] - private string _profilepath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), @"profile.png"); - [JsonIgnore] - private string _romMUser = "----"; - [JsonIgnore] - private string _profileType = "----"; - [JsonIgnore] - private bool _connectionFailed = false; - [JsonIgnore] - private bool _platformSynced = false; - [JsonIgnore] - private bool _platformSyncFailed = false; + [JsonIgnore] private string _romMHost = ""; + [JsonIgnore] private string _romMServerVersion = "---"; + [JsonIgnore] private string _romMClientToken = ""; + [JsonIgnore] private bool _useBasicAuth = true; + [JsonIgnore] private string _romMUsername = ""; + [JsonIgnore] private string _romMPassword = ""; - [JsonIgnore] - private string _excludeGenres = ""; - [JsonIgnore] - private string _7zPath = ""; + [JsonIgnore] private string _defaultprofilepath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), @"profile.png"); + [JsonIgnore] private string _profilepath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), @"profile.png"); + [JsonIgnore] private string _romMUser = "----"; + [JsonIgnore] private string _profileType = "----"; + + [JsonIgnore] private string _excludeGenres = ""; + [JsonIgnore] private string _7zPath = ""; + + [JsonIgnore] private List _romMPlatforms = new List(); + + [JsonIgnore] private bool _notify = false; + [JsonIgnore] private string _notifyText = ""; + [JsonIgnore] private string _notifyIcon = ""; + [JsonIgnore] private Color _notfiyColour; + [JsonIgnore] private Brush _notfiyTextColour; - [JsonIgnore] - private List _romMPlatforms = new List(); #endregion + #region Notifcation Bar [JsonIgnore] - public bool ConnectionFailed + public bool Notify { - get => _connectionFailed; + get => _notify; set { - _connectionFailed = value; + _notify = value; OnPropertyChanged(); } } [JsonIgnore] - public bool PlatformSynced + public string NotifyText { - get => _platformSynced; + get => _notifyText; set { - _platformSynced = value; + _notifyText = value; OnPropertyChanged(); } } [JsonIgnore] - public bool PlatformSyncFailed + public string NotifyIcon { - get => _platformSyncFailed; + get => _notifyIcon; set { - _platformSyncFailed = value; + _notifyIcon = value; OnPropertyChanged(); } } + [JsonIgnore] + public Color NotfiyColour + { + get => _notfiyColour; + set + { + _notfiyColour = value; + OnPropertyChanged(); + } + } + [JsonIgnore] + public Brush NotfiyTextColour + { + get => _notfiyTextColour; + set + { + _notfiyTextColour = value; + OnPropertyChanged(); + } + } + public void UpdateNotifcationBar(string Message, bool IsError = false) + { + if (IsError) + { + NotfiyColour = (Color)ColorConverter.ConvertFromString("#730000"); + NotfiyTextColour = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#ff6b6b")); + NotifyIcon = $" \uE730"; + NotifyText = $" {Message}"; + Notify = true; + } + else + { + NotfiyColour = (Color)ColorConverter.ConvertFromString("#035900"); + NotfiyTextColour = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#91ff8e")); + NotifyIcon = $" \uE73E"; + NotifyText = $" {Message}"; + Notify = true; + } + } + + #endregion public string RomMHost { @@ -123,6 +154,8 @@ public string RomMClientToken OnPropertyChanged(); } } + public static readonly Regex ApiTokenPattern = new Regex(@"^rmm_[0-9a-f]{64}$", RegexOptions.Compiled); + public bool UseBasicAuth { get => _useBasicAuth; @@ -159,8 +192,8 @@ public string RomMUser OnPropertyChanged(); } } - [JsonIgnore] - public string ClientTokenURL + + [JsonIgnore] public string ClientTokenURL { get => $"{RomMHost}/client-api-tokens"; set { } @@ -240,8 +273,6 @@ public List RomMPlatforms } } - public static readonly Regex ApiTokenPattern = new Regex(@"^rmm_[0-9a-f]{64}$", RegexOptions.Compiled); - public SettingsViewModel(){} internal SettingsViewModel(Plugin plugin, IRomM romM) @@ -306,18 +337,24 @@ internal SettingsViewModel(Plugin plugin, IRomM romM) public bool TestConnection() { + Notify = false; + try { if(string.IsNullOrEmpty(RomMHost)) { - throw new ArgumentException("host not set!"); + throw new ArgumentException("Host not set!"); + } + if(!Uri.IsWellFormedUriString(RomMHost, UriKind.RelativeOrAbsolute)) + { + throw new ArgumentException("Host is not a valid URL!"); } if(UseBasicAuth) { if(string.IsNullOrEmpty(RomMUsername) || string.IsNullOrEmpty(RomMPassword)) { - throw new ArgumentException("username or password not set!"); + throw new ArgumentException("Username/Password not set!"); } HttpClientSingleton.ConfigureBasicAuth(RomMUsername, RomMPassword); @@ -326,12 +363,12 @@ public bool TestConnection() { if (string.IsNullOrEmpty(RomMClientToken)) { - throw new ArgumentException("client token not set!"); + throw new ArgumentException("Client token not set!"); } if(!ApiTokenPattern.IsMatch(RomMClientToken)) { - throw new ArgumentException("client token format invaild!"); + throw new ArgumentException("Client token format invaild!"); } HttpClientSingleton.ConfigureAPIAuth(RomMClientToken); @@ -379,17 +416,18 @@ public bool TestConnection() RomMProfileType = userinfo.Role; RomMUser = userinfo.Username; - ConnectionFailed = false; + UpdateNotifcationBar("Authenticated!"); } catch (Exception ex) { - ConnectionFailed = true; + Notify = true; ProfilePath = _defaultprofilepath; RomMUser = "----"; RomMProfileType = "----"; ServerVersion = "---"; LogManager.GetLogger().Error($"Failed to read response! {ex}"); - PlayniteAPI.Notifications.Add(new NotificationMessage(RomM.Id.ToString(), $"RomM - Failed to poll server: {ex.Message}", NotificationType.Error)); + UpdateNotifcationBar($"Authentication failed: {ex.Message}", true); + PlayniteAPI.Notifications.Add(new NotificationMessage($"RomMPlugin.Authentication.Failed.{ex.Message}", $"RomM - Authentication failed: {ex.Message}", NotificationType.Error)); return false; } @@ -447,10 +485,12 @@ public bool VerifySettings(out List errors) if (string.IsNullOrEmpty(m.DestinationPathResolved)) { mappingErrors.Add($"{m.MappingId}: No destination path specified."); + UpdateNotifcationBar($"{m.MappingId}: No destination path specified.", true); } else if (!Directory.Exists(m.DestinationPathResolved)) { mappingErrors.Add($"{m.MappingId}: Destination path doesn't exist ({m.DestinationPathResolved})."); + UpdateNotifcationBar($"{m.MappingId}: Destination path doesn't exist ({m.DestinationPathResolved}).", true); } }); @@ -459,6 +499,7 @@ public bool VerifySettings(out List errors) } } + // Used to load profile image into cache so it can be changed while the application is running public class ImageCacheConverter : IValueConverter { diff --git a/Settings/SettingsView.xaml b/Settings/SettingsView.xaml index 0352d08..56200fa 100644 --- a/Settings/SettingsView.xaml +++ b/Settings/SettingsView.xaml @@ -1,8 +1,10 @@ - @@ -11,220 +13,268 @@ - - - + + + diff --git a/Settings/SettingsView.xaml.cs b/Settings/SettingsView.xaml.cs index fe306a6..212b331 100644 --- a/Settings/SettingsView.xaml.cs +++ b/Settings/SettingsView.xaml.cs @@ -52,8 +52,7 @@ private void Hyperlink_RequestNavigate(object sender, System.Windows.Navigation. private async void Click_PullPlatforms(object sender, RoutedEventArgs e) { - SettingsViewModel.Instance.PlatformSynced = false; - SettingsViewModel.Instance.PlatformSyncFailed = false; + SettingsViewModel.Instance.Notify = false; try { @@ -62,12 +61,12 @@ private async void Click_PullPlatforms(object sender, RoutedEventArgs e) string body = await response.Content.ReadAsStringAsync(); SettingsViewModel.Instance.RomMPlatforms = JsonConvert.DeserializeObject>(body); - SettingsViewModel.Instance.PlatformSynced = true; + SettingsViewModel.Instance.UpdateNotifcationBar("Platforms successfully retrieved!"); } catch (Exception ex) { LogManager.GetLogger().Error($"RomM - failed to get platforms: {ex}"); - SettingsViewModel.Instance.PlatformSyncFailed = true; + SettingsViewModel.Instance.UpdateNotifcationBar($"Failed to get platforms: {ex.Message}!", true); } } From 7bea6e472a4afb2429f0b004a4f3d5bf73db8221 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 28 Apr 2026 12:15:31 +0100 Subject: [PATCH 19/28] Hopefully mitigate random DependencySource errors --- Settings/Settings.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Settings/Settings.cs b/Settings/Settings.cs index 3da94ad..96b5350 100644 --- a/Settings/Settings.cs +++ b/Settings/Settings.cs @@ -51,8 +51,8 @@ public class SettingsViewModel : ObservableObject, ISettings [JsonIgnore] private bool _notify = false; [JsonIgnore] private string _notifyText = ""; [JsonIgnore] private string _notifyIcon = ""; - [JsonIgnore] private Color _notfiyColour; - [JsonIgnore] private Brush _notfiyTextColour; + [JsonIgnore] private Color _notfiyColour = Colors.DarkSlateGray; + [JsonIgnore] private Brush _notfiyTextColour = new SolidColorBrush(Colors.LightGray); #endregion @@ -513,6 +513,7 @@ public object Convert(object value, Type targetType, image.CacheOption = BitmapCacheOption.OnLoad; image.UriSource = new Uri(path); image.EndInit(); + image.Freeze(); return image; From abdc0782778967cbef60a1a421036e833898744f Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 28 Apr 2026 15:57:22 +0100 Subject: [PATCH 20/28] Only update notification bar from settings --- Settings/Settings.cs | 13 ++++++++----- Settings/SettingsView.xaml.cs | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Settings/Settings.cs b/Settings/Settings.cs index 96b5350..da93ea4 100644 --- a/Settings/Settings.cs +++ b/Settings/Settings.cs @@ -335,7 +335,7 @@ internal SettingsViewModel(Plugin plugin, IRomM romM) } } - public bool TestConnection() + public bool TestConnection(bool UpdateNotificationBar = false) { Notify = false; @@ -416,7 +416,8 @@ public bool TestConnection() RomMProfileType = userinfo.Role; RomMUser = userinfo.Username; - UpdateNotifcationBar("Authenticated!"); + if(UpdateNotificationBar) + UpdateNotifcationBar("Authenticated!"); } catch (Exception ex) { @@ -426,7 +427,10 @@ public bool TestConnection() RomMProfileType = "----"; ServerVersion = "---"; LogManager.GetLogger().Error($"Failed to read response! {ex}"); - UpdateNotifcationBar($"Authentication failed: {ex.Message}", true); + + if (UpdateNotificationBar) + UpdateNotifcationBar($"Authentication failed: {ex.Message}", true); + PlayniteAPI.Notifications.Add(new NotificationMessage($"RomMPlugin.Authentication.Failed.{ex.Message}", $"RomM - Authentication failed: {ex.Message}", NotificationType.Error)); return false; } @@ -458,7 +462,7 @@ public void EndEdit() } else { - HttpClientSingleton.Instance.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", RomMClientToken); + HttpClientSingleton.ConfigureAPIAuth(RomMClientToken); } } @@ -513,7 +517,6 @@ public object Convert(object value, Type targetType, image.CacheOption = BitmapCacheOption.OnLoad; image.UriSource = new Uri(path); image.EndInit(); - image.Freeze(); return image; diff --git a/Settings/SettingsView.xaml.cs b/Settings/SettingsView.xaml.cs index 212b331..2f6d364 100644 --- a/Settings/SettingsView.xaml.cs +++ b/Settings/SettingsView.xaml.cs @@ -25,7 +25,7 @@ public SettingsView() private void Click_TestConnection(object sender, RoutedEventArgs e) { - SettingsViewModel.Instance.TestConnection(); + SettingsViewModel.Instance.TestConnection(true); e.Handled = true; } From b42af109e98d28761e4c5a8dc63d0648fcd889bf Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 1 May 2026 13:49:00 +0100 Subject: [PATCH 21/28] Replace RomMSavedSibling to RomMRevision --- Downloads/VersionSelector.xaml.cs | 6 ++-- Games/RomMImport.cs | 35 +++++++++--------- Models/RomM/Rom/RomMRomLocal.cs | 12 ++----- RomM.cs | 59 ++++++++----------------------- 4 files changed, 39 insertions(+), 73 deletions(-) diff --git a/Downloads/VersionSelector.xaml.cs b/Downloads/VersionSelector.xaml.cs index b46b47c..d99d290 100644 --- a/Downloads/VersionSelector.xaml.cs +++ b/Downloads/VersionSelector.xaml.cs @@ -10,13 +10,13 @@ namespace RomM.VersionSelector public partial class RomMVersionSelector : PluginUserControl { - public ObservableCollection RomVersions { get; set; } + public ObservableCollection RomVersions { get; set; } public bool Cancelled { get; set; } = true; - public RomMVersionSelector(List romVersions) + public RomMVersionSelector(List romVersions) { - RomVersions = new ObservableCollection(romVersions); + RomVersions = new ObservableCollection(romVersions); InitializeComponent(); } diff --git a/Games/RomMImport.cs b/Games/RomMImport.cs index 8ecbbfb..7670172 100644 --- a/Games/RomMImport.cs +++ b/Games/RomMImport.cs @@ -372,13 +372,17 @@ private void SaveGameData(RomMRom ROM) string[] versionBreakdown = _plugin.Settings.ServerVersion.Split('.'); float versionParsed = float.Parse(versionBreakdown[0]) + (float.Parse(versionBreakdown[1]) / 100); - RomMRomLocal toSave = new RomMRomLocal(); - - // Save base ROM data - toSave.Id = ROM.Id; + RomMRomLocal toSave = new RomMRomLocal(); toSave.Name = ROM.Name; toSave.SHA1 = ROM.SHA1; - toSave.HasMultipleFiles = ROM.HasMultipleFiles; + toSave.MappingID = _mapping.MappingId; + toSave.ROMVersions = new List(); + + RomMRevision baseROM = new RomMRevision(); + + // Save base ROM data + baseROM.Id = ROM.Id; + baseROM.HasMultipleFiles = ROM.HasMultipleFiles; if(!ROM.HasMultipleFiles) { var romfile = DetermineFile(ROM); @@ -388,30 +392,29 @@ private void SaveGameData(RomMRom ROM) return; } - toSave.FileName = romfile.FileName; - toSave.DownloadURL = versionParsed <= 4.7 ? + baseROM.FileName = romfile.FileName; + baseROM.DownloadURL = versionParsed <= 4.7 ? _plugin.CombineUrl(_plugin.Settings.RomMHost, $"api/romsfiles/{romfile.Id}/content/{romfile.FileName}") : _plugin.CombineUrl(_plugin.Settings.RomMHost, $"api/roms/{romfile.Id}/files/content/{romfile.FileName}"); } else { - toSave.FileName = ROM.FileName; - toSave.DownloadURL = _plugin.CombineUrl(_plugin.Settings.RomMHost, $"api/roms/{ROM.Id}/content/{ROM.FileName}"); - } - toSave.IsSelected = false; - toSave.MappingID = _mapping.MappingId; + baseROM.FileName = ROM.FileName; + baseROM.DownloadURL = _plugin.CombineUrl(_plugin.Settings.RomMHost, $"api/roms/{ROM.Id}/content/{ROM.FileName}"); + } + baseROM.IsSelected = false; + toSave.ROMVersions.Add(baseROM); // Save sibling data if (_plugin.Settings.MergeRevisions && ROM.Siblings?.Count > 0) { - toSave.Siblings = new List(); foreach (var sibling in ROM.Siblings) { var siblingROM = _ROMs.Find(x => x.Id == sibling.Id); if(siblingROM != null) { - RomMSavedSibing saveSibling = new RomMSavedSibing(); + RomMRevision saveSibling = new RomMRevision(); saveSibling.Id = siblingROM.Id; saveSibling.HasMultipleFiles = siblingROM.HasMultipleFiles; @@ -425,7 +428,7 @@ private void SaveGameData(RomMRom ROM) } saveSibling.FileName = romfile.FileName; - toSave.DownloadURL = versionParsed <= 4.7 ? + saveSibling.DownloadURL = versionParsed <= 4.7 ? _plugin.CombineUrl(_plugin.Settings.RomMHost, $"api/romsfiles/{romfile.Id}/content/{romfile.FileName}") : _plugin.CombineUrl(_plugin.Settings.RomMHost, $"api/roms/{romfile.Id}/files/content/{romfile.FileName}"); } @@ -437,7 +440,7 @@ private void SaveGameData(RomMRom ROM) saveSibling.IsSelected = false; _ROMs.First(x => x.Id == sibling.Id).Processed = true; - toSave.Siblings.Add(saveSibling); + toSave.ROMVersions.Add(saveSibling); } } } diff --git a/Models/RomM/Rom/RomMRomLocal.cs b/Models/RomM/Rom/RomMRomLocal.cs index bf6c2f9..12ceb07 100644 --- a/Models/RomM/Rom/RomMRomLocal.cs +++ b/Models/RomM/Rom/RomMRomLocal.cs @@ -4,8 +4,6 @@ namespace RomM.Models.RomM.Rom { - - enum MainSibling { None = -1, @@ -19,11 +17,10 @@ public struct GameInstallInfo public string FileName { get; set; } public bool HasMultipleFiles { get; set; } public string DownloadURL { get; set; } - public bool IsSelected { get; set; } public EmulatorMapping Mapping { get; set; } } - public struct RomMSavedSibing + public struct RomMRevision { public int Id { get; set; } public string FileName { get; set; } @@ -34,16 +31,11 @@ public struct RomMSavedSibing public class RomMRomLocal { - public int Id { get; set; } public string Name { get; set; } public string SHA1 { get; set; } - public string FileName { get; set; } - public bool HasMultipleFiles { get; set; } - public string DownloadURL { get; set; } - public bool IsSelected { get; set; } public Guid MappingID { get; set; } - public List Siblings { get; set; } + public List ROMVersions { get; set; } } } diff --git a/RomM.cs b/RomM.cs index abfa4b4..aa6e3f2 100644 --- a/RomM.cs +++ b/RomM.cs @@ -341,7 +341,7 @@ public override IEnumerable GetGameMenuItems(GetGameMenuItemsArgs { string json = File.ReadAllText($"{ROMDataPath}{args.Games.First().GameId.Split(':')[1]}.json"); var gameData = JsonConvert.DeserializeObject(json); - if(gameData.Siblings.Count > 0) + if(gameData.ROMVersions.Count > 1) { gameMenuItems.Add(new GameMenuItem { @@ -405,38 +405,18 @@ public override IEnumerable GetInstallActions(GetInstallActio // Set ROM data to base ROM romData = new GameInstallInfo { - Id = gameData.Id, - FileName = gameData.FileName, - HasMultipleFiles = gameData.HasMultipleFiles, - DownloadURL = gameData.DownloadURL, - IsSelected = gameData.IsSelected, + Id = gameData.ROMVersions[0].Id, + FileName = gameData.ROMVersions[0].FileName, + HasMultipleFiles = gameData.ROMVersions[0].HasMultipleFiles, + DownloadURL = gameData.ROMVersions[0].DownloadURL, Mapping = Settings.Mappings.FirstOrDefault(x => x.MappingId == gameData.MappingID) }; // If Siblings are avaiable prompt user with version selection - if (Settings.MergeRevisions && gameData.Siblings?.Count > 0) + if (Settings.MergeRevisions && gameData.ROMVersions?.Count > 1) { - List gameVersions = new List(); - // Add roms to list to be selected - gameVersions.Add(romData); - - foreach (var sibling in gameData.Siblings) - { - var siblingROMData = new GameInstallInfo - { - Id = sibling.Id, - FileName = sibling.FileName, - HasMultipleFiles = sibling.HasMultipleFiles, - DownloadURL = sibling.DownloadURL, - IsSelected = sibling.IsSelected, - Mapping = Settings.Mappings.FirstOrDefault(x => x.MappingId == gameData.MappingID) - }; - - gameVersions.Add(siblingROMData); - } - - RomMVersionSelector VersionSelectorControl = new RomMVersionSelector(gameVersions); + RomMVersionSelector VersionSelectorControl = new RomMVersionSelector(gameData.ROMVersions); var window = Playnite.Dialogs.CreateWindow(new WindowCreationOptions { ShowMinimizeButton = false, @@ -471,24 +451,15 @@ public override IEnumerable GetInstallActions(GetInstallActio Playnite.Database.Games.Update(args.Game); } - // Write result back to json file - gameData.IsSelected = VersionSelectorControl.RomVersions[0].IsSelected; - VersionSelectorControl.RomVersions.RemoveAt(0); - // There is probalby a LINQ you can do for this instead of clear and replace all siblings as - // only the isSelected option needs altering - gameData.Siblings.Clear(); - foreach (var versions in VersionSelectorControl.RomVersions) - { - gameData.Siblings.Add(new RomMSavedSibing - { - Id = versions.Id, - FileName = versions.FileName, - HasMultipleFiles = versions.HasMultipleFiles, - DownloadURL = versions.DownloadURL, - IsSelected = versions.IsSelected, - }); - } + var selectedrevision = VersionSelectorControl.RomVersions.First(x => x.IsSelected); + romData.Id = selectedrevision.Id; + romData.FileName = selectedrevision.FileName; + romData.HasMultipleFiles = selectedrevision.HasMultipleFiles; + romData.DownloadURL = selectedrevision.DownloadURL; + + gameData.ROMVersions = VersionSelectorControl.RomVersions.ToList(); + File.WriteAllText($"{ROMDataPath}{romMSHA1}.json", JsonConvert.SerializeObject(gameData)); } } From 4b1a7e4cb5ab63e19b5510ce2d21e60faef104dd Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 3 May 2026 02:49:31 +0100 Subject: [PATCH 22/28] Suggestions (Round 1) --- Games/RomMImport.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/Games/RomMImport.cs b/Games/RomMImport.cs index 7670172..be4a7e0 100644 --- a/Games/RomMImport.cs +++ b/Games/RomMImport.cs @@ -45,6 +45,7 @@ private RomMFile DetermineFile(RomMRom ROM) fullpaths.Add(file.FullPath); } + // Sort files by how many folders deep the file is and return the file that is at the highest point fullpaths = fullpaths.OrderBy(x => x.Count(c => c == '/')).ToList(); return ROM.Files.Where(x => x.FullPath == fullpaths[0]).FirstOrDefault(); } @@ -67,7 +68,7 @@ public List ProcessData() // Some newer platforms don't get a hash value so we will compromise with this if (string.IsNullOrEmpty(ROM.SHA1)) { - var tohash = $"{ROM.Name}{ROM.FileSizeBytes}"; + var tohash = $"{ROM.Id}{ROM.FileNameNoExt}"; using (SHA1Managed sha1 = new SHA1Managed()) { @@ -83,7 +84,7 @@ public List ProcessData() } } - // Skip game import if the ROM is apart of the exclusion list + // Skip game import if the ROM is part of the exclusion list if (_plugin.Playnite.Database.ImportExclusions[Playnite.ImportExclusionItem.GetId($"{ROM.Id}:{ROM.SHA1}", _plugin.Id)] != null) { _plugin.Logger.Warn($"[Importer] Excluding {ROM.Name} from import."); @@ -195,11 +196,7 @@ private Game ImportGame(RomMRom ROM, Guid StatusID) var gameInstallDir = Path.Combine(rootInstallDir, Path.GetFileNameWithoutExtension(ROM.Name)); var pathToGame = Path.Combine(gameInstallDir, ROM.Name); - var gameNameWithTags = - $"{ROM.Name}" + - $"{(ROM.Regions.Count > 0 ? $" ({string.Join(", ", ROM.Regions)})" : "")}" + - $"{(!string.IsNullOrEmpty(ROM.Revision) ? $" (Rev {ROM.Revision})" : "")}" + - $"{(ROM.Tags.Count > 0 ? $" ({string.Join(", ", ROM.Tags)})" : "")}"; + var gameNameWithTags = ROM.FileNameNoExt; var preferedRatingsBoard = _plugin.Playnite.ApplicationSettings.AgeRatingOrgPriority; var agerating = ROM.Metadatum.Age_Ratings.Count > 0 ? new HashSet(ROM.Metadatum.Age_Ratings.Where(r => r.Split(':')[0] == preferedRatingsBoard.ToString()).Select(r => new MetadataNameProperty(r.ToString()))) : null; @@ -240,7 +237,7 @@ private Game ImportGame(RomMRom ROM, Guid StatusID) Favorite = _favourites.Exists(f => f == ROM.Id), LastActivity = ROM.RomUser.LastPlayed, - UserScore = ROM.RomUser.Rating * 10, //RomM-Rating is 1-10, Playnite 1-100, so it can unfortunately only by synced one direction without loosing decimals + UserScore = ROM.RomUser.Rating * 10, //RomM-Rating is 1-10, Playnite 1-100, so it can unfortunately only by synced one direction without losing decimals CompletionStatus = completionStatusProperty, Links = gameLinks, Roms = new List { new GameRom(gameNameWithTags, pathToGame) }, From f04d957f2b9696c055236baadbdc3defc0b0ab4f Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 4 May 2026 23:41:34 +0100 Subject: [PATCH 23/28] Use built-in version type for checking server version --- Games/RomMImport.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Games/RomMImport.cs b/Games/RomMImport.cs index be4a7e0..f3e242e 100644 --- a/Games/RomMImport.cs +++ b/Games/RomMImport.cs @@ -366,8 +366,7 @@ private MainSibling CheckForMainSibling(RomMRom ROM) } private void SaveGameData(RomMRom ROM) { - string[] versionBreakdown = _plugin.Settings.ServerVersion.Split('.'); - float versionParsed = float.Parse(versionBreakdown[0]) + (float.Parse(versionBreakdown[1]) / 100); + Version versionParsed = new Version(_plugin.Settings.ServerVersion); RomMRomLocal toSave = new RomMRomLocal(); toSave.Name = ROM.Name; @@ -390,7 +389,7 @@ private void SaveGameData(RomMRom ROM) } baseROM.FileName = romfile.FileName; - baseROM.DownloadURL = versionParsed <= 4.7 ? + baseROM.DownloadURL = versionParsed.CompareTo(new Version(4,8)) < 0 ? _plugin.CombineUrl(_plugin.Settings.RomMHost, $"api/romsfiles/{romfile.Id}/content/{romfile.FileName}") : _plugin.CombineUrl(_plugin.Settings.RomMHost, $"api/roms/{romfile.Id}/files/content/{romfile.FileName}"); } @@ -425,7 +424,7 @@ private void SaveGameData(RomMRom ROM) } saveSibling.FileName = romfile.FileName; - saveSibling.DownloadURL = versionParsed <= 4.7 ? + saveSibling.DownloadURL = versionParsed.CompareTo(new Version(4, 8)) < 0 ? _plugin.CombineUrl(_plugin.Settings.RomMHost, $"api/romsfiles/{romfile.Id}/content/{romfile.FileName}") : _plugin.CombineUrl(_plugin.Settings.RomMHost, $"api/roms/{romfile.Id}/files/content/{romfile.FileName}"); } From 1abef86fc1ee2024e772f75ae7c9ce1e65bf85a4 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 5 May 2026 00:08:52 +0100 Subject: [PATCH 24/28] Properly save and restore isSelected --- Games/RomMImport.cs | 21 +++++++++++++++++++++ Models/RomM/Rom/RomMRomLocal.cs | 2 +- RomM.cs | 7 ++++++- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/Games/RomMImport.cs b/Games/RomMImport.cs index f3e242e..cd567ce 100644 --- a/Games/RomMImport.cs +++ b/Games/RomMImport.cs @@ -441,6 +441,27 @@ private void SaveGameData(RomMRom ROM) } } + // Apply isSelected to data that is about to be saved + if (File.Exists($"{_plugin.ROMDataPath}{ROM.SHA1}.json")) + { + try + { + string localROMjson = File.ReadAllText($"{_plugin.ROMDataPath}{ROM.SHA1}.json"); + var localROM = JsonConvert.DeserializeObject(localROMjson); + foreach (var revision in localROM.ROMVersions) + { + var matchedRevision = toSave.ROMVersions.FirstOrDefault(x => x.Id == revision.Id); + + if (matchedRevision != null) + matchedRevision.IsSelected = revision.IsSelected; + } + } + catch (Exception) + { + _plugin.Logger.Error($"{ROM.Name} GameID is malformed or {ROM.SHA1} json file is corrupted!"); + } + } + // Write data to file string json = JsonConvert.SerializeObject(toSave); File.WriteAllText($"{_plugin.ROMDataPath}{ROM.SHA1}.json", json); diff --git a/Models/RomM/Rom/RomMRomLocal.cs b/Models/RomM/Rom/RomMRomLocal.cs index 12ceb07..1e1a50f 100644 --- a/Models/RomM/Rom/RomMRomLocal.cs +++ b/Models/RomM/Rom/RomMRomLocal.cs @@ -20,7 +20,7 @@ public struct GameInstallInfo public EmulatorMapping Mapping { get; set; } } - public struct RomMRevision + public class RomMRevision { public int Id { get; set; } public string FileName { get; set; } diff --git a/RomM.cs b/RomM.cs index aa6e3f2..2cfe17b 100644 --- a/RomM.cs +++ b/RomM.cs @@ -460,9 +460,14 @@ public override IEnumerable GetInstallActions(GetInstallActio gameData.ROMVersions = VersionSelectorControl.RomVersions.ToList(); - File.WriteAllText($"{ROMDataPath}{romMSHA1}.json", JsonConvert.SerializeObject(gameData)); } } + else + { + gameData.ROMVersions[0].IsSelected = true; + } + + File.WriteAllText($"{ROMDataPath}{romMSHA1}.json", JsonConvert.SerializeObject(gameData)); } yield return new RomMInstallController(args.Game, this, romData); From 2b53d099a0c0f465b94c25bd3f8ba21554301875 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 7 May 2026 19:02:23 +0100 Subject: [PATCH 25/28] Add single save backup system --- Games/RomMImport.cs | 64 ++- Games/RomMImportController.cs | 1 + Models/RomM/Rom/RomMRomLocal.cs | 2 + Models/RomM/Rom/RomMSave.cs | 160 ++++++ RomM.cs | 26 +- RomM.csproj | 8 + Save/DeleteSave.xaml | 39 ++ Save/DeleteSave.xaml.cs | 44 ++ Save/SaveController.cs | 827 ++++++++++++++++++++++++++++++++ Save/SaveSelector.xaml | 45 ++ Save/SaveSelector.xaml.cs | 40 ++ Settings/EmulatorMapping.cs | 92 ++-- Settings/Settings.cs | 20 +- Settings/SettingsView.xaml | 794 ++++++++++++++++++++++-------- Settings/SettingsView.xaml.cs | 115 ++++- 15 files changed, 2000 insertions(+), 277 deletions(-) create mode 100644 Models/RomM/Rom/RomMSave.cs create mode 100644 Save/DeleteSave.xaml create mode 100644 Save/DeleteSave.xaml.cs create mode 100644 Save/SaveController.cs create mode 100644 Save/SaveSelector.xaml create mode 100644 Save/SaveSelector.xaml.cs diff --git a/Games/RomMImport.cs b/Games/RomMImport.cs index cd567ce..6a9ac5c 100644 --- a/Games/RomMImport.cs +++ b/Games/RomMImport.cs @@ -368,7 +368,7 @@ private void SaveGameData(RomMRom ROM) { Version versionParsed = new Version(_plugin.Settings.ServerVersion); - RomMRomLocal toSave = new RomMRomLocal(); + RomMRomLocal toSave = new RomMRomLocal(); toSave.Name = ROM.Name; toSave.SHA1 = ROM.SHA1; toSave.MappingID = _mapping.MappingId; @@ -377,9 +377,9 @@ private void SaveGameData(RomMRom ROM) RomMRevision baseROM = new RomMRevision(); // Save base ROM data - baseROM.Id = ROM.Id; + baseROM.Id = ROM.Id; baseROM.HasMultipleFiles = ROM.HasMultipleFiles; - if(!ROM.HasMultipleFiles) + if (!ROM.HasMultipleFiles) { var romfile = DetermineFile(ROM); if (romfile == null) @@ -399,7 +399,7 @@ private void SaveGameData(RomMRom ROM) baseROM.DownloadURL = _plugin.CombineUrl(_plugin.Settings.RomMHost, $"api/roms/{ROM.Id}/content/{ROM.FileName}"); } baseROM.IsSelected = false; - toSave.ROMVersions.Add(baseROM); + toSave.ROMVersions.Add(baseROM); // Save sibling data if (_plugin.Settings.MergeRevisions && ROM.Siblings?.Count > 0) @@ -408,7 +408,7 @@ private void SaveGameData(RomMRom ROM) foreach (var sibling in ROM.Siblings) { var siblingROM = _ROMs.Find(x => x.Id == sibling.Id); - if(siblingROM != null) + if (siblingROM != null) { RomMRevision saveSibling = new RomMRevision(); @@ -432,7 +432,7 @@ private void SaveGameData(RomMRom ROM) { saveSibling.FileName = siblingROM.FileName; saveSibling.DownloadURL = _plugin.CombineUrl(_plugin.Settings.RomMHost, $"api/roms/{siblingROM.Id}/content/{siblingROM.FileName}"); - } + } saveSibling.IsSelected = false; _ROMs.First(x => x.Id == sibling.Id).Processed = true; @@ -448,13 +448,57 @@ private void SaveGameData(RomMRom ROM) { string localROMjson = File.ReadAllText($"{_plugin.ROMDataPath}{ROM.SHA1}.json"); var localROM = JsonConvert.DeserializeObject(localROMjson); - foreach (var revision in localROM.ROMVersions) + + // Check to see if game is installed but no revision is selected! + var game = _plugin.PlayniteApi.Database.Games.FirstOrDefault(x => x.GameId == $"{ROM.Id}:{ROM.SHA1}"); + if(localROM.ROMVersions.All(x => !x.IsSelected) && game != null && game.IsInstalled) { - var matchedRevision = toSave.ROMVersions.FirstOrDefault(x => x.Id == revision.Id); + if(localROM.ROMVersions.Count <= 1) + { + var matchedRevision = toSave.ROMVersions.FirstOrDefault(x => x.Id == localROM.ROMVersions[0].Id); + + if (matchedRevision != null) + { + matchedRevision.IsSelected = true; + matchedRevision.Save = localROM.ROMVersions[0].Save; + } + } + else + { + foreach (var revision in localROM.ROMVersions) + { + var dstPath = _mapping.DestinationPathResolved; + var installDir = Path.Combine(dstPath, Path.GetFileNameWithoutExtension(revision.FileName)); + + if (Directory.Exists(installDir)) + { + var matchedRevision = toSave.ROMVersions.FirstOrDefault(x => x.Id == revision.Id); + + if (matchedRevision != null) + { + matchedRevision.IsSelected = true; + matchedRevision.Save = revision.Save; + break; + } + } - if (matchedRevision != null) - matchedRevision.IsSelected = revision.IsSelected; + } + } } + else // Apply isSelected and Save data to revisions with matching ID + { + foreach (var revision in localROM.ROMVersions) + { + var matchedRevision = toSave.ROMVersions.FirstOrDefault(x => x.Id == revision.Id); + + if (matchedRevision != null) + { + matchedRevision.IsSelected = revision.IsSelected; + matchedRevision.Save = revision.Save; + } + + } + } } catch (Exception) { diff --git a/Games/RomMImportController.cs b/Games/RomMImportController.cs index d40bcfe..ec2fea8 100644 --- a/Games/RomMImportController.cs +++ b/Games/RomMImportController.cs @@ -92,6 +92,7 @@ public List Import(LibraryImportGamesArgs args) games.AddRange(task.Result); } + _plugin.Settings.SaveController.ReloadROMs(); return games; } diff --git a/Models/RomM/Rom/RomMRomLocal.cs b/Models/RomM/Rom/RomMRomLocal.cs index 1e1a50f..e4d34c1 100644 --- a/Models/RomM/Rom/RomMRomLocal.cs +++ b/Models/RomM/Rom/RomMRomLocal.cs @@ -27,6 +27,8 @@ public class RomMRevision public bool HasMultipleFiles { get; set; } public string DownloadURL { get; set; } public bool IsSelected { get; set; } + public bool NeverSave { get; set; } = false; + public RomMSave Save { get; set; } } public class RomMRomLocal diff --git a/Models/RomM/Rom/RomMSave.cs b/Models/RomM/Rom/RomMSave.cs new file mode 100644 index 0000000..e1b4a89 --- /dev/null +++ b/Models/RomM/Rom/RomMSave.cs @@ -0,0 +1,160 @@ +using Newtonsoft.Json; +using Playnite.SDK.Models; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web; + +namespace RomM.Models.RomM.Rom +{ + public enum SaveSyncStatus + { + NotEnabled, + RemoteNewer, + LocalNewer, + InSync, + NotUploaded + } + + public class RomMSave : ObservableObject + { + [JsonIgnore] private int _id; + [JsonIgnore] private int _romID; + [JsonIgnore] private string _fileName; + [JsonIgnore] private long _fileSize; + [JsonIgnore] private bool _missingFromFS; + [JsonIgnore] private DateTime _lastUpdated; + [JsonIgnore] private string _saveFolder; + [JsonIgnore] private bool _syncEnabled = false; + [JsonIgnore] private SaveSyncStatus _isInSync = SaveSyncStatus.NotEnabled; + [JsonIgnore] private string _gameName; + + + [JsonProperty("id")] + public int ID + { + get => _id; + set + { + _id = value; + OnPropertyChanged(); + } + } + + [JsonProperty("rom_id")] + public int ROMID + { + get => _romID; + set + { + _romID = value; + OnPropertyChanged(); + } + } + + [JsonProperty("file_name")] + public string FileName + { + get => _fileName; + set + { + _fileName = value; + OnPropertyChanged(); + } + } + + [JsonProperty("file_size_bytes")] + public long FileSize + { + get => _fileSize; + set + { + _fileSize = value; + OnPropertyChanged(); + } + } + + [JsonProperty("missing_from_fs")] + public bool MissingFromFS + { + get => _missingFromFS; + set + { + _missingFromFS = value; + OnPropertyChanged(); + } + } + + [JsonProperty("updated_at")] + public DateTime LastUpdated + { + get => _lastUpdated; + set + { + // Round date time to the second + _lastUpdated = value.AddTicks(-(value.Ticks % TimeSpan.TicksPerSecond)); + OnPropertyChanged(); + } + } + + public string SaveFolder + { + get => _saveFolder; + set + { + if(value == null) + _saveFolder = ""; + else + _saveFolder = value.TrimEnd('/'); + + OnPropertyChanged(); + } + } + public bool SyncEnabled + { + get => _syncEnabled; + set + { + _syncEnabled = value; + OnPropertyChanged(); + } + } + public SaveSyncStatus IsInSync + { + get => _isInSync; + set + { + _isInSync = value; + OnPropertyChanged(); + } + } + + [JsonIgnore] public string GameName + { + get => _gameName; + set + { + _gameName = value; + OnPropertyChanged(); + } + } + [JsonIgnore] public string LastUpdatedString + { + get => $"(Last Updated: {LastUpdated})"; + set + { + OnPropertyChanged(); + } + } + } + public class PossibleSave + { + public FileInfo File { get; set; } + public RomMRevision Game { get; set; } + public bool IsSelected { get; set; } + + } +} diff --git a/RomM.cs b/RomM.cs index 2cfe17b..c025ab9 100644 --- a/RomM.cs +++ b/RomM.cs @@ -1,14 +1,17 @@ using Newtonsoft.Json; + using Playnite.SDK; using Playnite.SDK.Events; using Playnite.SDK.Models; using Playnite.SDK.Plugins; -using RomM.Games; + using RomM.Downloads; -using RomM.VersionSelector; +using RomM.Games; using RomM.Models.RomM.Collection; using RomM.Models.RomM.Rom; using RomM.Settings; +using RomM.VersionSelector; + using System; using System.Collections.Generic; using System.IO; @@ -73,7 +76,7 @@ public class RomM : LibraryPlugin, IRomM public DownloadQueueController DownloadQueueController { get; private set; } internal RomMDownloadsSidebarItem DownloadsSidebar { get; private set; } private readonly DownloadQueueViewModel downloadsVm; - + // Implementing Client adds ability to open it via special menu in playnite public override LibraryClient Client { get; } = new RomMClient(); @@ -494,6 +497,23 @@ public override LibraryMetadataProvider GetMetadataDownloader() { return new RomMMetadataProvider(this); } + + public override void OnGameStarting(OnGameStartingEventArgs args) + { + if(args.Game.PluginId == PluginId) + { + Settings.SaveController.GameLaunched(args.Game); + } + } + + public override void OnGameStopped(OnGameStoppedEventArgs args) + { + if (args.Game.PluginId == PluginId) + { + Settings.SaveController.GameStopped(); + } + } + #endregion #region RomM Status Syncing diff --git a/RomM.csproj b/RomM.csproj index 160dd3b..ec4f28e 100644 --- a/RomM.csproj +++ b/RomM.csproj @@ -72,6 +72,14 @@ MSBuild:Compile Designer + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + MSBuild:Compile Designer diff --git a/Save/DeleteSave.xaml b/Save/DeleteSave.xaml new file mode 100644 index 0000000..7fe3265 --- /dev/null +++ b/Save/DeleteSave.xaml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Save/DeleteSave.xaml.cs b/Save/DeleteSave.xaml.cs new file mode 100644 index 0000000..5f61ad6 --- /dev/null +++ b/Save/DeleteSave.xaml.cs @@ -0,0 +1,44 @@ +using Playnite.SDK.Controls; + +using RomM.Models.RomM.Rom; + +using System.Collections.ObjectModel; +using System.Windows; + +namespace RomM.Save +{ + public partial class RomMDeleteSaveView : PluginUserControl + { + public bool Local = false; + public bool Remote = false; + + public RomMDeleteSaveView() + { + InitializeComponent(); + } + + private void Click_Cancel(object sender, RoutedEventArgs e) + { + ((Window)Parent).Close(); + } + + private void Click_LocalOnly(object sender, RoutedEventArgs e) + { + Local = true; + ((Window)Parent).Close(); + } + + private void Click_RemoteOnly(object sender, RoutedEventArgs e) + { + Remote = true; + ((Window)Parent).Close(); + } + + private void Click_Delete(object sender, RoutedEventArgs e) + { + Local = true; + Remote = true; + ((Window)Parent).Close(); + } + } +} \ No newline at end of file diff --git a/Save/SaveController.cs b/Save/SaveController.cs new file mode 100644 index 0000000..0c4f027 --- /dev/null +++ b/Save/SaveController.cs @@ -0,0 +1,827 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Playnite.SDK; +using Playnite.SDK.Models; +using Playnite.SDK.Plugins; +using RomM.Games; +using RomM.Models.RomM.Rom; +using RomM.Settings; + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.Eventing.Reader; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Windows; + +namespace RomM.Save +{ + + public class SaveController : ObservableObject + { + private readonly RomM Plugin; + private SettingsViewModel Settings; + private RomMRomLocal LaunchedGame; + private List PossibleSaveFiles; + + private EmulatorMapping _currentMapping; + private ObservableCollection _remoteSaves = new ObservableCollection(); + private ObservableCollection _localSaves = new ObservableCollection(); + private ObservableCollection _possibleSaves = new ObservableCollection(); + + public List ROMs { get; set; } + + public ObservableCollection Mappings + { + get => Settings.Mappings; + set + { + OnPropertyChanged(); + } + } + public EmulatorMapping CurrentMapping + { + get => _currentMapping; + set + { + // Save save data to file + if(_currentMapping != null) + SaveROMRevisions(_currentMapping.MappingId); + + _currentMapping = value; + OnPropertyChanged(nameof(FilteredROMs)); + OnPropertyChanged(); + + // Pull mapping saves + if (_currentMapping != null) + { + SyncLocalSaves(); + SyncPotentialSaves(); + SyncRemoteSaves(); + Settings.UpdateNotifcationBar($"Synced saves for {CurrentMapping.RomMPlatform.Name} with {RemoteSaves.Count + LocalSaves.Count} saves found!"); + } + + } + } + + public ObservableCollection RemoteSaves + { + get => _remoteSaves; + set + { + _remoteSaves = value; + OnPropertyChanged(); + } + } + public ObservableCollection LocalSaves + { + get => _localSaves; + set + { + _localSaves = value; + OnPropertyChanged(); + } + } + public ObservableCollection PossibleSaves + { + get => _possibleSaves; + set + { + _possibleSaves = value; + OnPropertyChanged(); + } + } + + public ObservableCollection FilteredROMs + { + get + { + if(ROMs != null && CurrentMapping != null) + { + return ROMs.Where(x => x.MappingID == CurrentMapping.MappingId).SelectMany(y => y.ROMVersions).OrderBy(z => z.FileName).ToObservable(); + } + return new ObservableCollection(); + } + set + { + OnPropertyChanged(); + } + } + + public SaveController(RomM plugin) + { + Plugin = plugin; + Settings = SettingsViewModel.Instance; + Mappings = Settings.Mappings; + ROMs = new List(); + + foreach (var game in Directory.EnumerateFiles(Plugin.ROMDataPath, "*.json", SearchOption.TopDirectoryOnly)) + { + try + { + string json = File.ReadAllText(game); + var gamedata = JsonConvert.DeserializeObject(json); + ROMs.Add(gamedata); + } + catch (Exception ex) + { + Plugin.Logger.Error($"[Save Controller] Failed to read json file! - {game}\n\t{ex}"); + } + } + + } + + public void ReloadROMs() + { + ROMs = new List(); + + foreach (var game in Directory.EnumerateFiles(Plugin.ROMDataPath, "*.json", SearchOption.TopDirectoryOnly)) + { + try + { + string json = File.ReadAllText(game); + ROMs.Add(JsonConvert.DeserializeObject(json)); + } + catch (Exception ex) + { + Plugin.Logger.Error($"[Save Controller] Failed to read json file! - {game}\n\t{ex}"); + } + } + } + + public void SyncRemoteSaves(bool manual = false) + { + try + { + if (!Uri.IsWellFormedUriString(Settings.RomMHost, UriKind.RelativeOrAbsolute)) + throw new ArgumentException("Host is not a valid URL!"); + + if(CurrentMapping == null) + throw new ArgumentException("No mapping selected cannot sync save for mapping!"); + + var response = HttpClientSingleton.Instance.GetAsync($"{Settings.RomMHost}/api/saves?platform_id={CurrentMapping.RomMPlatformId}").GetAwaiter().GetResult(); + response.EnsureSuccessStatusCode(); + + Stream body = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult(); + using (StreamReader reader = new StreamReader(body)) + { + var data = reader.ReadToEnd(); + if (data == "[]") + { + RemoteSaves = new ObservableCollection(); + return; + } + + var jsonResponse = JArray.Parse(data); + RemoteSaves = jsonResponse.ToObject>(); + } + + var toremove = new List(); + + foreach (var rs in RemoteSaves) + { + // Remove remote save if stored locally + if (LocalSaves.Any(x => x.ID == rs.ID)) + { + toremove.Add(rs); + continue; + } + + RomMRevision rom = ROMs.Where(x => x.MappingID == CurrentMapping.MappingId).SelectMany(y => y.ROMVersions).First(z => z.Id == rs.ROMID); + if(rom != null) + { + if(rom.Save != null && rs.FileName == rom.Save.FileName) + { + rs.SyncEnabled = rom.Save.SyncEnabled; + if (rs.SyncEnabled) + { + rs.IsInSync = rs.LastUpdated > rom.Save.LastUpdated ? SaveSyncStatus.RemoteNewer : + (rs.LastUpdated < rom.Save.LastUpdated ? SaveSyncStatus.LocalNewer : + (rs.LastUpdated == rom.Save.LastUpdated ? SaveSyncStatus.InSync : SaveSyncStatus.NotEnabled)); + } + + rs.SaveFolder = rom.Save.SaveFolder; + } + + rs.GameName = Path.GetFileNameWithoutExtension(rom.FileName); + + } + } + + foreach (var remove in toremove) + { + RemoteSaves.Remove(remove); + } + + if(manual) + Settings.UpdateNotifcationBar($"Synced saves for {CurrentMapping.RomMPlatform.Name} with {RemoteSaves.Count} saves found!"); + + } + catch (Exception ex) + { + Plugin.Logger.Error($"[SaveController] {ex}"); + Settings.UpdateNotifcationBar(ex.Message, true); + } + } + public void RemoteSaveEnabled(RomMSave save) + { + if(save.SyncEnabled) + { + RomMRevision rom = ROMs.Where(x => x.MappingID == CurrentMapping.MappingId).SelectMany(y => y.ROMVersions).First(z => z.Id == save.ROMID); + if (rom != null) + { + if (rom.Save != null) + { + RemoteSaves.First(x => x.ID == save.ID).IsInSync = + save.LastUpdated > rom.Save.LastUpdated ? SaveSyncStatus.RemoteNewer : + (save.LastUpdated < rom.Save.LastUpdated ? SaveSyncStatus.LocalNewer : + SaveSyncStatus.InSync); + } + else + { + + RemoteSaves.First(x => x.ID == save.ID).IsInSync = SaveSyncStatus.RemoteNewer; + } + + foreach (var saves in RemoteSaves.Where(x => x.ROMID == save.ROMID && x.ID != save.ID)) + { + saves.SyncEnabled = false; + } + } + + } + else + { + RemoteSaves.First(x => x.ID == save.ID).IsInSync = SaveSyncStatus.NotEnabled; + } + } + + public void SyncLocalSaves() + { + ObservableCollection newlocalSaves = new ObservableCollection(); + foreach (var rom in FilteredROMs) + { + if (rom.Save != null) + { + if (rom.Save.ID != -1) + { + var save = FetchSaveInfo(rom.Save.ID); + + if (save != null) + { + rom.Save.IsInSync = save.LastUpdated > rom.Save.LastUpdated ? SaveSyncStatus.RemoteNewer : + (save.LastUpdated < rom.Save.LastUpdated ? SaveSyncStatus.LocalNewer : + SaveSyncStatus.InSync); + } + else + { + rom.Save.IsInSync = SaveSyncStatus.NotUploaded; + rom.Save.ID = -1; + } + } + + rom.Save.GameName = Path.GetFileNameWithoutExtension(rom.FileName); + + newlocalSaves.Add(rom.Save); + } + } + + LocalSaves = newlocalSaves; + } + public void LocalSaveEnabled(RomMSave save) + { + if (save.SyncEnabled) + { + RomMRevision rom = ROMs.Where(x => x.MappingID == CurrentMapping.MappingId).SelectMany(y => y.ROMVersions).First(z => z.Id == save.ROMID); + if (rom != null) + { + + var remotesave = FetchSaveInfo(rom.Save.ID); + + if(remotesave != null) + { + rom.Save.IsInSync = remotesave.LastUpdated > rom.Save.LastUpdated ? SaveSyncStatus.RemoteNewer : + (remotesave.LastUpdated < rom.Save.LastUpdated ? SaveSyncStatus.LocalNewer : + SaveSyncStatus.InSync); + } + else + { + rom.Save.IsInSync = SaveSyncStatus.NotUploaded; + } + } + + foreach (var saves in LocalSaves.Where(x => x.ROMID == save.ROMID && x.ID != save.ID)) + { + saves.SyncEnabled = false; + } + + } + else + { + LocalSaves.First(x => x.ID == save.ID).IsInSync = SaveSyncStatus.NotEnabled; + } + } + + public void SyncPotentialSaves() + { + if (CurrentMapping != null && !string.IsNullOrEmpty(CurrentMapping.GeneralSavePath) && Directory.Exists(CurrentMapping.GeneralSavePath)) + { + var newpossiblesaves = new ObservableCollection(); + List fileExtentions = new List(); + + if (!string.IsNullOrEmpty(CurrentMapping.SaveFileExtentions)) + { + if(CurrentMapping.SaveFileExtentions.TrimEnd(';').Contains(';')) + { + fileExtentions = CurrentMapping.SaveFileExtentions.TrimEnd(';').Split(';').ToList(); + } + else + { + fileExtentions.Add(CurrentMapping.SaveFileExtentions.TrimEnd(';')); + } + } + + foreach (var file in new DirectoryInfo(CurrentMapping.GeneralSavePath).GetFiles("*.*", SearchOption.AllDirectories)) + { + var possiblesave = new PossibleSave(); + + if (fileExtentions.Count != 0) + { + if(fileExtentions.Any(x => Path.GetExtension(file.FullName) == x)) + { + possiblesave.File = file; + } + else + { + continue; + } + } + else + { + possiblesave.File = file; + } + + if(!FilteredROMs.Any(x => x.Save != null && x.Save.FileName == file.Name && x.Save.SaveFolder == file.DirectoryName)) + { + newpossiblesaves.Add(possiblesave); + } + } + + PossibleSaves = newpossiblesaves; + + } + else + { + Settings.UpdateNotifcationBar("Current mapping has no general save path cannot find possible savefiles", true); + PossibleSaves = new ObservableCollection(); + } + } + public void UploadNewSave(PossibleSave possiblesave) + { + if (possiblesave.Game != null) + { + var save = new RomMSave(); + save.ROMID = possiblesave.Game.Id; + save.SaveFolder = possiblesave.File.DirectoryName; + save.FileName = possiblesave.File.Name; + save.LastUpdated = possiblesave.File.LastWriteTime; + save.IsInSync = SaveSyncStatus.LocalNewer; + save.SyncEnabled = true; + + UploadSave(save, CurrentMapping.MappingId, true); + PossibleSaves.Remove(possiblesave); + SyncLocalSaves(); + } + else + { + Settings.UpdateNotifcationBar("No game selected cannot upload!", true); + } + } + + public void SaveROMRevisions(Guid mappingId) + { + foreach (var rom in ROMs.Where(x => x.MappingID == mappingId)) + { + foreach (var revision in rom.ROMVersions) + { + if (revision.Save == null) + { + RomMSave save = RemoteSaves.FirstOrDefault(x => x.SyncEnabled && x.ROMID == revision.Id); + if (save == null) + continue; + + revision.Save = save; + } + } + + File.WriteAllText($"{Plugin.ROMDataPath}{rom.SHA1}.json", JsonConvert.SerializeObject(rom)); + } + } + + public void GameLaunched(Game game) + { + string romMSHA1 = game.GameId.Split(':')[1]; + if (!File.Exists($"{Plugin.ROMDataPath}{romMSHA1}.json")) + { + Plugin.Logger.Error($"{game.Name} GameID is malformed!"); + } + + try + { + string json = File.ReadAllText($"{Plugin.ROMDataPath}{romMSHA1}.json"); + LaunchedGame = JsonConvert.DeserializeObject(json); + } + catch (Exception) + { + Plugin.Logger.Error($"{game.Name} GameID is malformed or {romMSHA1} json file is corrupted!"); + } + + // Check to see if save needs syncing + var mapping = Settings.Mappings.FirstOrDefault(x => x.MappingId == LaunchedGame.MappingID); + if (mapping != null && mapping.DownloadSaveBeforeGame) + { + var rom = LaunchedGame.ROMVersions.FirstOrDefault(x => x.IsSelected); + if(rom != null && rom.Save != null) + { + var save = FetchSaveInfo(rom.Save.ID); + rom.Save.IsInSync = save.LastUpdated > rom.Save.LastUpdated ? SaveSyncStatus.RemoteNewer : + (save.LastUpdated < rom.Save.LastUpdated ? SaveSyncStatus.LocalNewer : + SaveSyncStatus.InSync); + + + // Check to see if save has been deleted or a newer save is on the server + if (rom.Save.IsInSync == SaveSyncStatus.RemoteNewer || !File.Exists($"{rom.Save.SaveFolder}/{rom.Save.FileName}")) + { + rom.Save.IsInSync = SaveSyncStatus.RemoteNewer; + SyncSave(rom.Save, LaunchedGame.MappingID); + } + } + + PossibleSaveFiles = new DirectoryInfo(mapping.GeneralSavePath).GetFiles("*.*", SearchOption.AllDirectories).ToList(); + } + } + public void GameStopped() + { + var mapping = Settings.Mappings.FirstOrDefault(x => x.MappingId == LaunchedGame.MappingID); + if (mapping != null && mapping.UploadSaveAfterGame) + { + var rom = LaunchedGame.ROMVersions.FirstOrDefault(x => x.IsSelected); + + if (rom != null && !rom.NeverSave) + { + if (rom.Save != null) + { + rom.Save.IsInSync = SaveSyncStatus.LocalNewer; + Settings.SaveController.SyncSave(rom.Save, LaunchedGame.MappingID); + } + else + { + + if(PossibleSaveFiles.Count > 0) + { + var files = new DirectoryInfo(mapping.GeneralSavePath).GetFiles("*.*", SearchOption.AllDirectories); + var saveFiles = new ObservableCollection(); + string[] fileExtentions = new string[0]; + + if (!string.IsNullOrEmpty(CurrentMapping.SaveFileExtentions)) + { + if (CurrentMapping.SaveFileExtentions.TrimEnd(';').Contains(';')) + { + fileExtentions = CurrentMapping.SaveFileExtentions.Split(';'); + } + else + { + fileExtentions = new string[1]; + fileExtentions[0] = CurrentMapping.SaveFileExtentions; + } + } + + foreach (var file in files) + { + // Check for new or updated possible save files + if(!PossibleSaveFiles.Contains(file) || PossibleSaveFiles.Find(x => x.FullName == file.FullName).LastWriteTime != file.LastWriteTime) + { + // If mapping has set file extentions filter out files that don't have that extention + if(fileExtentions.Length != 0) + { + if (fileExtentions.Any(x => Path.GetExtension(file.FullName) == x)) + { + var save = new PossibleSave(); + save.File = file; + saveFiles.Add(save); + } + } + else + { + var save = new PossibleSave(); + save.File = file; + saveFiles.Add(save); + } + + + } + } + + if (saveFiles.Count > 0) + { + RomMSaveSelector saveSelectorControl = new RomMSaveSelector(saveFiles); + var window = Plugin.Playnite.Dialogs.CreateWindow(new WindowCreationOptions + { + ShowMinimizeButton = false, + ShowMaximizeButton = false, + ShowCloseButton = false, + }); + + window.Height = 215; + window.Width = 600; + + window.Title = "Select save!"; + window.ShowInTaskbar = false; + window.ResizeMode = ResizeMode.NoResize; + window.Owner = API.Instance.Dialogs.GetCurrentAppWindow(); + window.WindowStartupLocation = WindowStartupLocation.CenterOwner; + window.Content = saveSelectorControl; + + window.ShowDialog(); + + if (saveSelectorControl.Cancelled) + { + return; + } + else if (saveSelectorControl.NeverSave) + { + rom.NeverSave = true; + File.WriteAllText($"{Plugin.ROMDataPath}{LaunchedGame.SHA1}.json", JsonConvert.SerializeObject(LaunchedGame)); + } + else + { + var selectedfile = saveSelectorControl.Saves.FirstOrDefault(x => x.IsSelected); + + var save = new RomMSave(); + save.FileName = selectedfile.File.Name; + save.SaveFolder = selectedfile.File.DirectoryName; + save.ROMID = rom.Id; + save.IsInSync = SaveSyncStatus.LocalNewer; + save.SyncEnabled = true; + + UploadSave(save, mapping.MappingId, true); + } + } + + } + + + + + } + } + } + } + + public void SyncSave(RomMSave save, Guid mappingID) + { + switch (save.IsInSync) + { + case SaveSyncStatus.RemoteNewer: + DownloadSave(save, mappingID); + break; + + case SaveSyncStatus.LocalNewer: + UploadSave(save, mappingID); + break; + + case SaveSyncStatus.NotUploaded: + UploadSave(save, mappingID, true); + break; + + default: + break; + } + } + public void RemoveSaveEntry(RomMSave save) + { + var rom = ROMs.Where(x => x.MappingID == CurrentMapping.MappingId).SelectMany(y => y.ROMVersions).First(z => z.Id == save.ROMID); + if (rom != null) + { + rom.Save = null; + } + + SyncLocalSaves(); + SyncPotentialSaves(); + SyncRemoteSaves(); + SaveROMRevisions(CurrentMapping.MappingId); + } + public void DeleteSave(RomMSave save) + { + RomMDeleteSaveView savedeleteControl = new RomMDeleteSaveView(); + var window = Plugin.Playnite.Dialogs.CreateWindow(new WindowCreationOptions + { + ShowMinimizeButton = false, + ShowMaximizeButton = false, + ShowCloseButton = false, + }); + + window.Height = 100; + window.Width = 500; + + window.Title = "Delete save!"; + window.ShowInTaskbar = false; + window.ResizeMode = ResizeMode.NoResize; + window.Owner = API.Instance.Dialogs.GetCurrentAppWindow(); + window.WindowStartupLocation = WindowStartupLocation.CenterOwner; + window.Content = savedeleteControl; + + window.ShowDialog(); + + var rom = ROMs.Where(x => x.MappingID == CurrentMapping.MappingId).SelectMany(y => y.ROMVersions).First(z => z.Id == save.ROMID); + + if(savedeleteControl.Remote || savedeleteControl.Local) + { + if (savedeleteControl.Remote) + { + try + { + if (!Uri.IsWellFormedUriString(Settings.RomMHost, UriKind.RelativeOrAbsolute)) + throw new ArgumentException("Host is not a valid URL!"); + + var saves = new List(); + saves.Add(rom.Save.ID); + var jsonsaves = JsonConvert.SerializeObject(saves); + + var response = HttpClientSingleton.Instance.PostAsync($"{Settings.RomMHost}/api/saves/delete", new StringContent(jsonsaves)).GetAwaiter().GetResult(); + response.EnsureSuccessStatusCode(); + } + catch (Exception ex) + { + Settings.UpdateNotifcationBar(ex.Message, true); + Plugin.Logger.Error(ex.ToString()); + } + } + else + { + rom.Save.IsInSync = SaveSyncStatus.NotUploaded; + } + + if (savedeleteControl.Local) + { + File.Delete($"{rom.Save.SaveFolder}/{rom.Save.FileName}"); + rom.Save = null; + } + } + + SaveROMRevisions(CurrentMapping.MappingId); + } + private RomMSave FetchSaveInfo(int id) + { + try + { + if (!Uri.IsWellFormedUriString(Settings.RomMHost, UriKind.RelativeOrAbsolute)) + throw new ArgumentException("Host is not a valid URL!"); + + var response = HttpClientSingleton.Instance.GetAsync($"{Settings.RomMHost}/api/saves/{id}").GetAwaiter().GetResult(); + response.EnsureSuccessStatusCode(); + + Stream body = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult(); + using (StreamReader reader = new StreamReader(body)) + { + var data = reader.ReadToEnd(); + var jsonResponse = JObject.Parse(data); + return jsonResponse.ToObject(); + } + } + catch (Exception ex) + { + Plugin.Logger.Error($"[SaveController] {ex}"); + Settings.UpdateNotifcationBar(ex.Message, true); + return null; + } + } + private void DownloadSave(RomMSave save, Guid mappingID) + { + try + { + if (!Uri.IsWellFormedUriString(Settings.RomMHost, UriKind.RelativeOrAbsolute)) + throw new ArgumentException("Host is not a valid URL!"); + + if (string.IsNullOrEmpty(Mappings.First(x => x.MappingId == mappingID).GeneralSavePath)) + throw new ArgumentException($"{Mappings.First(x => x.MappingId == mappingID).MappingName} has no general save location set, skipping save download!"); + + if (string.IsNullOrEmpty(save.SaveFolder)) + save.SaveFolder = Mappings.First(x => x.MappingId == mappingID).GeneralSavePath; + + var response = HttpClientSingleton.Instance.GetAsync($"{Settings.RomMHost}/api/saves/{save.ID}/content").GetAwaiter().GetResult(); + response.EnsureSuccessStatusCode(); + + var body = response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult(); + File.WriteAllBytes($"{save.SaveFolder}/{save.FileName}", body); + save.IsInSync = SaveSyncStatus.InSync; + + var rom = ROMs.Where(x => x.MappingID == mappingID).SelectMany(y => y.ROMVersions).First(z => z.Id == save.ROMID); + rom.Save = save; + + string filesizestring; + + if (rom.Save.FileSize > 1000000) + filesizestring = $"({((float)rom.Save.FileSize) / 1000 / 1000}MB)"; + else if (rom.Save.FileSize > 1000) + filesizestring = $"({((float)rom.Save.FileSize) / 1000}KB)"; + else + filesizestring = $"({rom.Save.FileSize}B)"; + + Plugin.PlayniteApi.Notifications.Add(new Playnite.SDK.NotificationMessage($"RomM.Save.Downloaded.{rom.Id}", $"Downloaded {save.FileName} from RomM Server! {filesizestring}", Playnite.SDK.NotificationType.Info)); + Settings.UpdateNotifcationBar($"Downloaded {save.FileName} from RomM Server! {filesizestring}"); + + SyncLocalSaves(); + SyncRemoteSaves(); + SaveROMRevisions(mappingID); + } + catch (Exception ex) + { + Settings.UpdateNotifcationBar(ex.Message, true); + } + } + private void UploadSave(RomMSave save, Guid mappingID, bool NewSave = false, bool UpdateNotifBar = false) + { + var rom = ROMs.Where(x => x.MappingID == mappingID).SelectMany(y => y.ROMVersions).First(z => z.Id == save.ROMID); + HttpResponseMessage response = null; + + try + { + if (!Uri.IsWellFormedUriString(Settings.RomMHost, UriKind.RelativeOrAbsolute)) + throw new ArgumentException("Host is not a valid URL!"); + + if (!File.Exists($"{save.SaveFolder}/{save.FileName}")) + throw new ArgumentException("Save file does not exist!"); + + MultipartFormDataContent request = new MultipartFormDataContent(); + var savefile = new ByteArrayContent(File.ReadAllBytes($"{save.SaveFolder}/{save.FileName}")); + request.Add(savefile, "saveFile", save.FileName); + + + + if (NewSave) + { + response = HttpClientSingleton.Instance.PostAsync($"{Settings.RomMHost}/api/saves?rom_id={save.ROMID.ToString()}", request).GetAwaiter().GetResult(); + response.EnsureSuccessStatusCode(); + } + else + { + response = HttpClientSingleton.Instance.PutAsync($"{Settings.RomMHost}/api/saves/{save.ID}", request).GetAwaiter().GetResult(); + response.EnsureSuccessStatusCode(); + } + + Stream body = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult(); + using (StreamReader reader = new StreamReader(body)) + { + var data = reader.ReadToEnd(); + + var jsonResponse = JObject.Parse(data); + var savedata = jsonResponse.ToObject(); + + save.ID = savedata.ID; + save.FileSize = savedata.FileSize; + save.LastUpdated = savedata.LastUpdated; + save.IsInSync = SaveSyncStatus.InSync; + rom.Save = save; + + } + + string filesizestring; + + if (save.FileSize > 1000000) + filesizestring = $"({((float)save.FileSize) / 1000 / 1000}MB)"; + else if (save.FileSize > 1000) + filesizestring = $"({((float)save.FileSize) / 1000}KB)"; + else + filesizestring = $"({save.FileSize}B)"; + + Plugin.PlayniteApi.Notifications.Add(new Playnite.SDK.NotificationMessage("RomM.Save.Updated", $"Backed up {save.FileName} to RomM Server! {filesizestring}", Playnite.SDK.NotificationType.Info)); + Settings.UpdateNotifcationBar($"Backed up {save.FileName} to RomM Server! {filesizestring}"); + + SyncLocalSaves(); + SyncRemoteSaves(); + SaveROMRevisions(mappingID); + + } + catch (Exception ex) + { + Plugin.Logger.Error(ex.ToString()); + Settings.UpdateNotifcationBar(ex.Message, true); + + if (response != null && response.StatusCode == System.Net.HttpStatusCode.NotFound) + { + UploadSave(save, mappingID, true); + } + else + { + rom.Save = save; + SyncLocalSaves(); + SyncRemoteSaves(); + SaveROMRevisions(mappingID); + } + } + } + } +} diff --git a/Save/SaveSelector.xaml b/Save/SaveSelector.xaml new file mode 100644 index 0000000..1abc8fd --- /dev/null +++ b/Save/SaveSelector.xaml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Save/SaveSelector.xaml.cs b/Save/SaveSelector.xaml.cs new file mode 100644 index 0000000..850eff3 --- /dev/null +++ b/Save/SaveSelector.xaml.cs @@ -0,0 +1,40 @@ +using Playnite.SDK.Controls; + +using RomM.Models.RomM.Rom; + +using System.Collections.ObjectModel; +using System.Windows; + +namespace RomM.Save +{ + public partial class RomMSaveSelector : PluginUserControl + { + public ObservableCollection Saves { get; set; } + + public bool Cancelled { get; set; } = true; + public bool NeverSave { get; set; } = false; + + public RomMSaveSelector(ObservableCollection saves) + { + Saves = saves; + InitializeComponent(); + } + + private void Click_Never(object sender, RoutedEventArgs e) + { + NeverSave = true; + ((Window)Parent).Close(); + } + + private void Click_DontSave(object sender, RoutedEventArgs e) + { + ((Window)Parent).Close(); + } + + private void Click_Save(object sender, RoutedEventArgs e) + { + Cancelled = false; + ((Window)Parent).Close(); + } + } +} \ No newline at end of file diff --git a/Settings/EmulatorMapping.cs b/Settings/EmulatorMapping.cs index 12fa907..3d750c2 100644 --- a/Settings/EmulatorMapping.cs +++ b/Settings/EmulatorMapping.cs @@ -8,39 +8,30 @@ using System.Xml.Serialization; using RomM.Models.RomM.Platform; using SharpCompress; +using System.Web; namespace RomM.Settings { public class EmulatorMapping : ObservableObject { - [JsonIgnore] - private Guid _mappingId; - [JsonIgnore] - private string _mappingName = ""; - [JsonIgnore] - private bool _enabled = true; - [JsonIgnore] - private bool _autoExtract = false; - [JsonIgnore] - private bool _useM3U = false; - [JsonIgnore] - private Emulator _emulator; - [JsonIgnore] - private Guid _emulatorId; - [JsonIgnore] - private EmulatorProfile _emulatorProfile; - [JsonIgnore] - private IEnumerable _availableProfiles; - [JsonIgnore] - public string _emulatorProfileId; - [JsonIgnore] - private RomMPlatform _emulatedPlatform = new RomMPlatform(); - [JsonIgnore] - private IEnumerable _availablePlatforms; - [JsonIgnore] - public int _romMPlatformId = -1; - [JsonIgnore] - private string _destinationPath = ""; + [JsonIgnore] private Guid _mappingId; + [JsonIgnore] private string _mappingName = ""; + [JsonIgnore] private bool _enabled = true; + [JsonIgnore] private bool _autoExtract = false; + [JsonIgnore] private bool _useM3U = false; + [JsonIgnore] private Emulator _emulator; + [JsonIgnore] private Guid _emulatorId; + [JsonIgnore] private EmulatorProfile _emulatorProfile; + [JsonIgnore] private IEnumerable _availableProfiles; + [JsonIgnore] public string _emulatorProfileId; + [JsonIgnore] private RomMPlatform _emulatedPlatform = new RomMPlatform(); + [JsonIgnore] private IEnumerable _availablePlatforms; + [JsonIgnore] public int _romMPlatformId = -1; + [JsonIgnore] private string _destinationPath = ""; + [JsonIgnore] private string _savePath = ""; + [JsonIgnore] private bool _downloadSaves = false; + [JsonIgnore] private bool _uploadSaves = true; + [JsonIgnore] private string _savefileExtentions = ""; public EmulatorMapping(List romMPlatforms) { @@ -232,11 +223,11 @@ public string DestinationPath { _destinationPath = value; OnPropertyChanged(); - } + } } -[JsonIgnore] - public static IEnumerable AvailableEmulators => SettingsViewModel.Instance.PlayniteAPI.Database.Emulators?.OrderBy(x => x.Name) ?? Enumerable.Empty(); + [JsonIgnore] public static IEnumerable AvailableEmulators => SettingsViewModel.Instance.PlayniteAPI.Database.Emulators?.OrderBy(x => x.Name) ?? Enumerable.Empty(); + [JsonIgnore] public IEnumerable AvailableProfiles { @@ -294,6 +285,45 @@ public string EmulatorBasePathResolved } } + public string GeneralSavePath + { + get => _savePath; + set + { + _savePath = value; + OnPropertyChanged(); + } + } + + public bool DownloadSaveBeforeGame + { + get => _downloadSaves; + set + { + _downloadSaves = value; + OnPropertyChanged(); + } + } + + public bool UploadSaveAfterGame + { + get => _uploadSaves; + set + { + _uploadSaves = value; + OnPropertyChanged(); + } + } + + public string SaveFileExtentions + { + get => _savefileExtentions; + set + { + _savefileExtentions = value; + OnPropertyChanged(); + } + } public IEnumerable GetDescriptionLines() { diff --git a/Settings/Settings.cs b/Settings/Settings.cs index da93ea4..b695a2f 100644 --- a/Settings/Settings.cs +++ b/Settings/Settings.cs @@ -6,7 +6,7 @@ using RomM.Models.RomM; using RomM.Models.RomM.Platform; - +using RomM.Save; using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -23,11 +23,12 @@ namespace RomM.Settings { public class SettingsViewModel : ObservableObject, ISettings { - private readonly Plugin _plugin; + private readonly RomM _plugin; private SettingsViewModel editingClone { get; set; } [JsonIgnore] internal readonly IPlayniteAPI PlayniteAPI; [JsonIgnore] internal readonly IRomM RomM; public static SettingsViewModel Instance { get; private set; } + [JsonIgnore] public SaveController SaveController { get; private set; } #region Backing Variables @@ -129,6 +130,8 @@ public void UpdateNotifcationBar(string Message, bool IsError = false) #endregion + #region Properties + public string RomMHost { get => _romMHost; @@ -272,16 +275,19 @@ public List RomMPlatforms } } } + #endregion public SettingsViewModel(){} - internal SettingsViewModel(Plugin plugin, IRomM romM) + internal SettingsViewModel(RomM plugin, IRomM romM) { RomM = romM; PlayniteAPI = plugin.PlayniteApi; Instance = this; _plugin = plugin; + SaveController = new SaveController(_plugin); + bool forceSave = false; var savedSettings = plugin.LoadPluginSettings(); @@ -304,6 +310,7 @@ internal SettingsViewModel(Plugin plugin, IRomM romM) // ----- These need to stay in this order ----- Mappings = savedSettings.Mappings; + SaveController.Mappings = savedSettings.Mappings; RomMPlatforms = savedSettings.RomMPlatforms; // -------------------------------------------- @@ -426,7 +433,7 @@ public bool TestConnection(bool UpdateNotificationBar = false) RomMUser = "----"; RomMProfileType = "----"; ServerVersion = "---"; - LogManager.GetLogger().Error($"Failed to read response! {ex}"); + _plugin.Logger.Error($"Failed to read response! {ex}"); if (UpdateNotificationBar) UpdateNotifcationBar($"Authentication failed: {ex.Message}", true); @@ -469,6 +476,11 @@ public void EndEdit() private void SavePluginSettings(SettingsViewModel settings) { + if(SaveController.CurrentMapping != null) + { + SaveController.SaveROMRevisions(SaveController.CurrentMapping.MappingId); + } + var setDir = _plugin.GetPluginUserDataPath(); var setFile = Path.Combine(setDir, "config.json"); if (!Directory.Exists(setDir)) diff --git a/Settings/SettingsView.xaml b/Settings/SettingsView.xaml index 56200fa..01e4ce7 100644 --- a/Settings/SettingsView.xaml +++ b/Settings/SettingsView.xaml @@ -2,8 +2,10 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:d="http://schemas.microsoft.com/expression/blend/2008" x:Class="RomM.Settings.SettingsView" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + x:Class="RomM.Settings.SettingsView" xmlns:local="clr-namespace:RomM.Settings" + xmlns:enum="clr-namespace:RomM.Models.RomM.Rom" xmlns:draw="clr-namespace:System.Drawing;assembly=System.Drawing" xmlns:sys="clr-namespace:System;assembly=mscorlib" mc:Ignorable="d" d:DesignHeight="1000" d:DesignWidth="800" Padding="2,0,2,4"> @@ -61,220 +63,590 @@ - - - - + + + + diff --git a/Settings/SettingsView.xaml.cs b/Settings/SettingsView.xaml.cs index 2f6d364..f4708b3 100644 --- a/Settings/SettingsView.xaml.cs +++ b/Settings/SettingsView.xaml.cs @@ -3,6 +3,7 @@ using Playnite.SDK; using RomM.Models.RomM; using RomM.Models.RomM.Platform; +using RomM.Models.RomM.Rom; using System; using System.Collections.Generic; using System.Diagnostics; @@ -16,8 +17,6 @@ namespace RomM.Settings { public partial class SettingsView : UserControl { - private bool InManualCellCommit = false; - public SettingsView() { InitializeComponent(); @@ -106,35 +105,115 @@ private static string GetSelectedFolderPath() return SettingsViewModel.Instance.PlayniteAPI.Dialogs.SelectFolder(); } - private void DataGrid_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e) + private void Click_Browse7zDestination(object sender, RoutedEventArgs e) { - if (!InManualCellCommit && sender is DataGrid grid) + string path; + if ((path = SettingsViewModel.Instance.PlayniteAPI.Dialogs.SelectFile("7Zip Executable|7z.exe")) == null) return; + + SettingsViewModel.Instance.PathTo7z = path; + e.Handled = true; + } + + private void Click_SyncSaves(object sender, RoutedEventArgs e) + { + SettingsViewModel.Instance.SaveController.SyncRemoteSaves(true); + } + + private void Click_SaveDirectory(object sender, RoutedEventArgs e) + { + var mapping = ((FrameworkElement)sender).DataContext as EmulatorMapping; + + if (mapping != null) { - InManualCellCommit = true; + string path = SettingsViewModel.Instance.PlayniteAPI.Dialogs.SelectFolder(); + if (string.IsNullOrEmpty(path)) + return; - // HACK!!!! - // Alternate approach 1: try to find new value here and store that somewhere as the currently selected emu - // Alternate approach 2: the "right" way(?) https://stackoverflow.com/a/34332709 - if (e.Column.Header?.ToString() == "Emulator" || e.Column.Header?.ToString() == "Profile") - { - grid.CommitEdit(DataGridEditingUnit.Row, true); - } + mapping.GeneralSavePath = path; + SettingsViewModel.Instance.SaveController.SyncPotentialSaves(); + } + + e.Handled = true; + } + + private void Click_BrowseSavefile(object sender, RoutedEventArgs e) + { + var save = ((FrameworkElement)sender).DataContext as RomMSave; - InManualCellCommit = false; + string path = SettingsViewModel.Instance.PlayniteAPI.Dialogs.SelectFolder(SettingsViewModel.Instance.SaveController.CurrentMapping.GeneralSavePath); + if (string.IsNullOrEmpty(path)) + return; + + if(!path.StartsWith(SettingsViewModel.Instance.SaveController.CurrentMapping.GeneralSavePath)) + { + SettingsViewModel.Instance.UpdateNotifcationBar("Selected folder is not in the general save directory!", true); + return; } + + save.SaveFolder = path; + e.Handled = true; + } + + private void Click_SyncNow(object sender, RoutedEventArgs e) + { + var save = ((FrameworkElement)sender).DataContext as RomMSave; + SettingsViewModel.Instance.SaveController.SyncSave(save, SettingsViewModel.Instance.SaveController.CurrentMapping.MappingId); + e.Handled = true; } - private void DataGrid_CurrentCellChanged(object sender, EventArgs e) + private void Checked_RemoteEnableSync(object sender, RoutedEventArgs e) { + var save = ((FrameworkElement)sender).DataContext as RomMSave; + SettingsViewModel.Instance.SaveController.RemoteSaveEnabled(save); + e.Handled = true; + } + private void Checked_LocalEnableSync(object sender, RoutedEventArgs e) + { + var save = ((FrameworkElement)sender).DataContext as RomMSave; + SettingsViewModel.Instance.SaveController.LocalSaveEnabled(save); + e.Handled = true; } - private void Click_Browse7zDestination(object sender, RoutedEventArgs e) + private void Click_UploadSave(object sender, RoutedEventArgs e) { - string path; - if ((path = SettingsViewModel.Instance.PlayniteAPI.Dialogs.SelectFile("7Zip Executable|7z.exe")) == null) return; + var possiblesave = ((FrameworkElement)sender).DataContext as PossibleSave; + SettingsViewModel.Instance.SaveController.UploadNewSave(possiblesave); + e.Handled = true; + } - SettingsViewModel.Instance.PathTo7z = path; + private void Click_DeleteSave(object sender, RoutedEventArgs e) + { + var save = ((FrameworkElement)sender).DataContext as RomMSave; + SettingsViewModel.Instance.SaveController.DeleteSave(save); + e.Handled = true; + } + + private void FocusChanged_SaveExtentionsBox(object sender, System.Windows.Input.KeyboardFocusChangedEventArgs e) + { + SettingsViewModel.Instance.SaveController.SyncPotentialSaves(); + e.Handled = true; + } + + private void LostKeyboard_GeneralSavePath(object sender, System.Windows.Input.KeyboardFocusChangedEventArgs e) + { + var mapping = ((FrameworkElement)sender).DataContext as EmulatorMapping; + + if (mapping != null) + { + if (string.IsNullOrEmpty(mapping.GeneralSavePath)) + return; + + SettingsViewModel.Instance.SaveController.SyncPotentialSaves(); + } + + e.Handled = true; + } + + private void Click_RemoveSave(object sender, RoutedEventArgs e) + { + var save = ((FrameworkElement)sender).DataContext as RomMSave; + SettingsViewModel.Instance.SaveController.RemoveSaveEntry(save); e.Handled = true; } } From 42e23835a7fc79e50ce273d95afee636d32ae516 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 8 May 2026 10:37:35 +0100 Subject: [PATCH 26/28] Fix changing save extensions not updating files --- Save/SaveController.cs | 40 +++++++++++-------- Settings/EmulatorMapping.cs | 8 ++-- Settings/SettingsView.xaml | 4 +- Settings/SettingsView.xaml.cs | 73 +++++++++++++++++++++-------------- 4 files changed, 74 insertions(+), 51 deletions(-) diff --git a/Save/SaveController.cs b/Save/SaveController.cs index 0c4f027..b59482a 100644 --- a/Save/SaveController.cs +++ b/Save/SaveController.cs @@ -46,8 +46,10 @@ public EmulatorMapping CurrentMapping get => _currentMapping; set { + SettingsViewModel.Instance.Notify = false; + // Save save data to file - if(_currentMapping != null) + if (_currentMapping != null) SaveROMRevisions(_currentMapping.MappingId); _currentMapping = value; @@ -329,17 +331,17 @@ public void SyncPotentialSaves() if (CurrentMapping != null && !string.IsNullOrEmpty(CurrentMapping.GeneralSavePath) && Directory.Exists(CurrentMapping.GeneralSavePath)) { var newpossiblesaves = new ObservableCollection(); - List fileExtentions = new List(); + List fileExtensions = new List(); - if (!string.IsNullOrEmpty(CurrentMapping.SaveFileExtentions)) + if (!string.IsNullOrEmpty(CurrentMapping.SaveFileExtensions)) { - if(CurrentMapping.SaveFileExtentions.TrimEnd(';').Contains(';')) + if(CurrentMapping.SaveFileExtensions.Contains(';')) { - fileExtentions = CurrentMapping.SaveFileExtentions.TrimEnd(';').Split(';').ToList(); + fileExtensions = CurrentMapping.SaveFileExtensions.Split(';').ToList(); } else { - fileExtentions.Add(CurrentMapping.SaveFileExtentions.TrimEnd(';')); + fileExtensions.Add(CurrentMapping.SaveFileExtensions); } } @@ -347,9 +349,9 @@ public void SyncPotentialSaves() { var possiblesave = new PossibleSave(); - if (fileExtentions.Count != 0) + if (fileExtensions.Count != 0) { - if(fileExtentions.Any(x => Path.GetExtension(file.FullName) == x)) + if(fileExtensions.Any(x => file.Extension.TrimStart('.').ToLower() == x.ToLower())) { possiblesave.File = file; } @@ -483,18 +485,18 @@ public void GameStopped() { var files = new DirectoryInfo(mapping.GeneralSavePath).GetFiles("*.*", SearchOption.AllDirectories); var saveFiles = new ObservableCollection(); - string[] fileExtentions = new string[0]; + string[] fileExtensions = new string[0]; - if (!string.IsNullOrEmpty(CurrentMapping.SaveFileExtentions)) + if (!string.IsNullOrEmpty(CurrentMapping.SaveFileExtensions)) { - if (CurrentMapping.SaveFileExtentions.TrimEnd(';').Contains(';')) + if (CurrentMapping.SaveFileExtensions.TrimEnd(';').Contains(';')) { - fileExtentions = CurrentMapping.SaveFileExtentions.Split(';'); + fileExtensions = CurrentMapping.SaveFileExtensions.Split(';'); } else { - fileExtentions = new string[1]; - fileExtentions[0] = CurrentMapping.SaveFileExtentions; + fileExtensions = new string[1]; + fileExtensions[0] = CurrentMapping.SaveFileExtensions; } } @@ -503,10 +505,10 @@ public void GameStopped() // Check for new or updated possible save files if(!PossibleSaveFiles.Contains(file) || PossibleSaveFiles.Find(x => x.FullName == file.FullName).LastWriteTime != file.LastWriteTime) { - // If mapping has set file extentions filter out files that don't have that extention - if(fileExtentions.Length != 0) + // If mapping has set file Extensions filter out files that don't have that extention + if(fileExtensions.Length != 0) { - if (fileExtentions.Any(x => Path.GetExtension(file.FullName) == x)) + if (fileExtensions.Any(x => Path.GetExtension(file.FullName) == x)) { var save = new PossibleSave(); save.File = file; @@ -700,6 +702,8 @@ private RomMSave FetchSaveInfo(int id) } private void DownloadSave(RomMSave save, Guid mappingID) { + SettingsViewModel.Instance.Notify = false; + try { if (!Uri.IsWellFormedUriString(Settings.RomMHost, UriKind.RelativeOrAbsolute)) @@ -744,6 +748,8 @@ private void DownloadSave(RomMSave save, Guid mappingID) } private void UploadSave(RomMSave save, Guid mappingID, bool NewSave = false, bool UpdateNotifBar = false) { + SettingsViewModel.Instance.Notify = false; + var rom = ROMs.Where(x => x.MappingID == mappingID).SelectMany(y => y.ROMVersions).First(z => z.Id == save.ROMID); HttpResponseMessage response = null; diff --git a/Settings/EmulatorMapping.cs b/Settings/EmulatorMapping.cs index 3d750c2..a2d69f5 100644 --- a/Settings/EmulatorMapping.cs +++ b/Settings/EmulatorMapping.cs @@ -31,7 +31,7 @@ public class EmulatorMapping : ObservableObject [JsonIgnore] private string _savePath = ""; [JsonIgnore] private bool _downloadSaves = false; [JsonIgnore] private bool _uploadSaves = true; - [JsonIgnore] private string _savefileExtentions = ""; + [JsonIgnore] private string _savefileExtensions = ""; public EmulatorMapping(List romMPlatforms) { @@ -315,12 +315,12 @@ public bool UploadSaveAfterGame } } - public string SaveFileExtentions + public string SaveFileExtensions { - get => _savefileExtentions; + get => _savefileExtensions; set { - _savefileExtentions = value; + _savefileExtensions = value.TrimEnd(';'); OnPropertyChanged(); } } diff --git a/Settings/SettingsView.xaml b/Settings/SettingsView.xaml index 01e4ce7..0d1bf68 100644 --- a/Settings/SettingsView.xaml +++ b/Settings/SettingsView.xaml @@ -318,13 +318,13 @@ - +