AssM
+ https://github.com/SubZeroPL/AssM/blob/master/LICENSE
+ https://github.com/SubZeroPL/AssM/
+ Assets\assm.ico
-
-
-
-
-
+
+
+
+
+
-
-
+
+
-
-
+
+
+
@@ -40,6 +44,7 @@
+
@@ -53,25 +58,30 @@
+
+
+
+
+
diff --git a/Assets/assm.ico b/Assets/assm.ico
new file mode 100644
index 0000000..3405810
Binary files /dev/null and b/Assets/assm.ico differ
diff --git a/Classes/Constants.cs b/Classes/Constants.cs
index c80e731..844c764 100644
--- a/Classes/Constants.cs
+++ b/Classes/Constants.cs
@@ -29,7 +29,7 @@ private static bool IsWindows() =>
[GeneratedRegex(BinFile)]
public static partial Regex BinFileRegex();
- private const string BinHash = @"BIN \(TRACK (\d{2})\) MD5\: (.+)";
+ private const string BinHash = @"TRACK (\d{2}) MD5\: (.+)";
[GeneratedRegex(BinHash)]
public static partial Regex BinHashRegex();
public const string ReadmeGameTitle = "#gameTitle#";
@@ -53,4 +53,12 @@ private static bool IsWindows() =>
"Generating README",
"Process JSON"
];
+
+ public static readonly string[] AddFolderSteps =
+ [
+ "",
+ "Aggregating image files (folder {0} of {1})",
+ "Adding to the list",
+ "Loading existing data"
+ ];
}
\ No newline at end of file
diff --git a/Classes/Functions.cs b/Classes/Functions.cs
index 3e79d3a..64ca5fc 100644
--- a/Classes/Functions.cs
+++ b/Classes/Functions.cs
@@ -19,7 +19,7 @@ public static class Functions
public static string GetChdName(Game game, Configuration configuration) => configuration.GameIdAsChdName
? $"{game.Id.ToUpper()}.chd"
- : Path.GetFileName(Path.ChangeExtension(game.CuePath, "chd"));
+ : Path.GetFileName(Path.ChangeExtension(game.ImagePath, "chd"));
public static void LoadChdManInfo(string chdPath, Game game)
{
@@ -174,7 +174,7 @@ private static void LoadTrackInfoFromReadme(string readmePath, Game game)
}
var endIndex = Array.FindIndex(lines, line => line.Contains("**Description:**")) - 1;
- var hashes = lines.Skip(startIndex).Take(endIndex - startIndex).Where(item => item.StartsWith("BIN"));
+ var hashes = lines.Skip(startIndex).Take(endIndex - startIndex).Where(item => item.StartsWith("TRACK"));
foreach (var hashLine in hashes)
{
var matches = Constants.BinHashRegex().Matches(hashLine);
@@ -199,11 +199,12 @@ private static void LoadTrackInfoFromReadme(string readmePath, Game game)
}
}
- public static List GetCueFilesInDirectory(string directory)
+ public static List GetCueIsoFilesInDirectory(string directory)
{
- Logger.Debug($"Getting cue files from {directory}");
- var result = Directory.GetFiles(directory).Where(d => Path.GetExtension(d) == ".cue").ToList();
- Directory.GetDirectories(directory).ToList().ForEach(d => result.AddRange(GetCueFilesInDirectory(d)));
+ Logger.Debug($"Getting cue/iso files from {directory}");
+ var result = Directory.GetFiles(directory)
+ .Where(d => Path.GetExtension(d) == ".cue" || Path.GetExtension(d) == ".iso").ToList();
+ Directory.GetDirectories(directory).ToList().ForEach(d => result.AddRange(GetCueIsoFilesInDirectory(d)));
return result;
}
@@ -216,21 +217,22 @@ public static List GetReadmeFilesInDirectory(string directory)
return result;
}
- public static Game? AddGameToList(string cuePath, Configuration configuration, ObservableCollection gameList)
+ public static Game? AddGameToList(string imagePath, Configuration configuration, ObservableCollection gameList)
{
- Logger.Debug($"Adding game to list from {cuePath}");
- var di = DiscInspector.ScanDisc(cuePath);
+ Logger.Debug($"Adding game to list from {imagePath}");
+ var di = DiscInspector.ScanDisc(imagePath);
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - apparently there are games that have no Id in image (like SLPS-00018)
if (di.Data.SerialNumber == null)
{
- Logger.Error($"Failed to add game to list from {cuePath}{Environment.NewLine}Id not present in image");
+ Logger.Error($"Failed to add game to list from {imagePath}{Environment.NewLine}Id not present in image");
return null;
}
- var title = configuration.GetTitleFromCue ? Path.GetFileNameWithoutExtension(cuePath) : di.Data.GameTitle;
+
+ var title = configuration.GetTitleFromCue ? Path.GetFileNameWithoutExtension(imagePath) : di.Data.GameTitle;
var game = new Game
{
Title = title, Id = di.Data.SerialNumber, Platform = di.DetectedDiscType,
- CuePath = cuePath
+ ImagePath = imagePath
};
var existingGame = gameList.FirstOrDefault(g => g.Id == game.Id);
if (existingGame != null)
@@ -238,7 +240,7 @@ public static List GetReadmeFilesInDirectory(string directory)
Logger.Debug($"Game already exists: {existingGame.Title}, updating");
existingGame.Title = game.Title;
existingGame.Platform = game.Platform;
- existingGame.CuePath = game.CuePath;
+ existingGame.ImagePath = game.ImagePath;
existingGame.Id = game.Id;
}
else
@@ -273,4 +275,6 @@ public static void ProcessJson(Configuration configuration, Game game, Action game.ImagePath.EndsWith(".iso", StringComparison.OrdinalIgnoreCase);
}
\ No newline at end of file
diff --git a/Data/Game.cs b/Data/Game.cs
index c02e62f..d77c9f1 100644
--- a/Data/Game.cs
+++ b/Data/Game.cs
@@ -9,7 +9,7 @@ public class Game
{
public string Title { get; set; } = string.Empty;
public string Id { get; set; } = string.Empty;
- public string CuePath { get; set; } = string.Empty;
+ public string ImagePath { get; set; } = string.Empty;
public DetectedDiscType Platform { get; set; } = DetectedDiscType.UnknownFormat;
public bool ReadmeCreated { get; set; }
public bool ChdCreated { get; set; }
@@ -29,6 +29,6 @@ public class ChdData
public string GetTrackInfo()
{
return string.Join(Environment.NewLine,
- TrackInfo.Select(ti => $"BIN (TRACK {ti.TrackNo,2:D2}) MD5: {ti.TrackMD5}{Environment.NewLine}")).Trim();
+ TrackInfo.Select(ti => $"TRACK {ti.TrackNo,2:D2} MD5: {ti.TrackMD5}{Environment.NewLine}")).Trim();
}
}
\ No newline at end of file
diff --git a/README.md b/README.md
index 3555395..1177e5f 100644
--- a/README.md
+++ b/README.md
@@ -3,9 +3,9 @@
It is a piece of software that assists in creating entries for [Arkadyzja savestates repository](https://github.com/ActionPL/duckstation_openbios_savestates). It allows creating and editing README files for games that will have savestates uploaded to the repository. Those savestates will then be available for [Arkadyzja](https://arkadyzja.honmaru.pl/).
## ⚠️ Requirements
-For the application to work [.NET framework 8](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) is required.
+For the application to work, [.NET Framework 8](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) is required.
-Application is developed and tested on Windows 11 OS. In theory should also work on Linux/Mac hovewer it was not tested on those platforms (only a bit on Linux and not at all on Mac) and currently only Win and Linux builds are released (see below).
+Application is developed and tested on Windows 11 OS. It should also work on Linux/Mac hovewer it was not tested (extensively) on those platforms (only a bit on Linux and not at all on Mac) and currently only Win and Linux builds are released (see below).
## 📦 Releases
Each release starting with 1.4.0 has four variants:
@@ -23,12 +23,12 @@ Clicking it will open default browser on the latest release page.
## 🪧 Usage
[](https://github.com/SubZeroPL/AssM/releases)
-- get latest version of the application from [Releases](https://github.com/SubZeroPL/AssM/releases) page
+- get the latest version of the application from the [Releases](https://github.com/SubZeroPL/AssM/releases) page
- unpack to some empty directory
- run the `AssM.exe` file (or `AssM` for Linux)

-First you should select Output directory, where the created README files will be stored. The files are created in a specific directory structure:
+First, you should select the Output directory, where the created README files will be stored. The files are created in a specific directory structure:
`OutputPath\Platform\GameID`.\
The output directory is saved between sessions, so when you start the application next time it will be automatically read and existing entries inserted into the table.\
**You should not modify existing directory structure if you plan to use the output directory again later!**
@@ -40,8 +40,8 @@ When you click `Start processing` the application will go through the list and p
## ⚙️ Configuration

-- Get game title from CUE file name - by default game name is extracted from cue/bin image, this option uses CUE file name instead
-- Use Game ID as name for CHD file - by default game title (as set by previous option) is used for CHD file name, this option uses Game ID instead
+- Get game title from CUE file name - by default, game name is extracted from cue/bin image; this option uses CUE file name instead
+- Use Game ID as the name for CHD file – by default, game title (as set by a previous option) is used for CHD file name; this option uses Game ID instead
Other configuration options should be self-explanatory.
diff --git a/Windows/AddFolderProgressWindow.axaml b/Windows/AddFolderProgressWindow.axaml
index 0ecd337..42e5e7d 100644
--- a/Windows/AddFolderProgressWindow.axaml
+++ b/Windows/AddFolderProgressWindow.axaml
@@ -3,16 +3,16 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:gif="clr-namespace:Avalonia.Labs.Gif;assembly=Avalonia.Labs.Gif"
- mc:Ignorable="d" d:DesignWidth="300"
+ mc:Ignorable="d" d:DesignWidth="400"
x:Class="AssM.Windows.AddFolderProgressWindow"
Closing="Window_OnClosing"
- Width="300" WindowStartupLocation="CenterOwner" SizeToContent="Height" ShowInTaskbar="False" CanResize="False"
+ Width="400" WindowStartupLocation="CenterOwner" SizeToContent="Height" ShowInTaskbar="False" CanResize="False"
Title="Adding folder contents">
-
-
+
+
diff --git a/Windows/AddFolderProgressWindow.axaml.cs b/Windows/AddFolderProgressWindow.axaml.cs
index dbbfa92..d24fb08 100644
--- a/Windows/AddFolderProgressWindow.axaml.cs
+++ b/Windows/AddFolderProgressWindow.axaml.cs
@@ -9,6 +9,14 @@
namespace AssM.Windows;
+internal class AddFolderProgressUpdateObject
+{
+ public string? Dir { get; init; }
+ public int? Count { get; init; }
+ public int? Index { get; init; }
+ public int Step { get; init; }
+}
+
public partial class AddFolderProgressWindow : Window
{
private readonly BackgroundWorker _worker;
@@ -24,43 +32,64 @@ public AddFolderProgressWindow()
private void WorkerOnProgressChanged(object? sender, ProgressChangedEventArgs e)
{
- if (e.UserState is string dir)
- LabelFolderName.Content = dir;
+ if (e.UserState is not AddFolderProgressUpdateObject updateObject) return;
+ LabelStep.Content = Constants.AddFolderSteps[updateObject.Step];
+ if (updateObject.Step == 1)
+ {
+ LabelStep.Content = string.Format(Constants.AddFolderSteps[updateObject.Step], updateObject.Index,
+ updateObject.Count);
+ LabelFolderName.Content = updateObject.Dir;
+ }
}
- public void Process(List dirs, ObservableCollection gameList, Configuration configuration, Action> finishedCallback)
+ public void Process(List dirs, ObservableCollection gameList, Configuration configuration,
+ Action> finishedCallback)
{
var cueFiles = new List();
var errors = new List();
_worker.DoWork += (_, _) =>
{
+ var index = 0;
foreach (var dir in dirs)
{
if (_worker.CancellationPending) return;
- _worker.ReportProgress(0, dir);
- cueFiles.AddRange(Functions.GetCueFilesInDirectory(dir));
+ var progress = index / dirs.Count * 100;
+ _worker.ReportProgress(progress,
+ new AddFolderProgressUpdateObject { Dir = dir, Index = index++, Count = dirs.Count, Step = 1 });
+ cueFiles.AddRange(Functions.GetCueIsoFilesInDirectory(dir));
}
+ index = 0;
foreach (var cueFile in cueFiles)
{
if (_worker.CancellationPending) return;
+ var progress = index / cueFiles.Count * 100;
+ _worker.ReportProgress(progress,
+ new AddFolderProgressUpdateObject
+ { Dir = cueFile, Index = index++, Count = cueFiles.Count, Step = 2 });
var game = Functions.AddGameToList(cueFile, configuration, gameList);
if (game == null)
{
- errors.Add($"Failed to add game to list from {cueFile}{Environment.NewLine}Id not present in image");
+ errors.Add(
+ $"Failed to add game to list from {cueFile}{Environment.NewLine}Id not present in image");
}
}
+ index = 0;
if (string.IsNullOrWhiteSpace(configuration.OutputDirectory)) return;
foreach (var game in gameList)
{
if (_worker.CancellationPending) return;
+ var progress = index / gameList.Count * 100;
+ _worker.ReportProgress(progress,
+ new AddFolderProgressUpdateObject
+ { Dir = game.Title, Index = index++, Count = gameList.Count, Step = 3 });
Functions.LoadExistingData(game, configuration);
}
};
-
+
_worker.RunWorkerCompleted += (_, _) => finishedCallback.Invoke(errors);
-
+
_worker.RunWorkerAsync();
}
diff --git a/Windows/MainWindow.axaml b/Windows/MainWindow.axaml
index c53f6c0..5f65285 100644
--- a/Windows/MainWindow.axaml
+++ b/Windows/MainWindow.axaml
@@ -39,7 +39,7 @@
Process only modified entiesOverwrite existing READMEs
- Get game title from CUE file name
+ Get game title from file nameUse Game ID as name for CHD file
diff --git a/Windows/MainWindow.axaml.cs b/Windows/MainWindow.axaml.cs
index 3eab7c6..d968ff2 100644
--- a/Windows/MainWindow.axaml.cs
+++ b/Windows/MainWindow.axaml.cs
@@ -20,6 +20,8 @@
using MsBox.Avalonia;
using MsBox.Avalonia.Enums;
using NLog;
+using Octokit;
+using ProductHeaderValue = Octokit.ProductHeaderValue;
namespace AssM.Windows;
@@ -86,30 +88,15 @@ private async Task CheckForNewVersion()
{
_logger.Debug("Checking for new version");
var currentVer = GetVersion();
- var client = new HttpClient();
- var request = new HttpRequestMessage
- {
- RequestUri = new Uri(Constants.LatestReleaseLink),
- Method = HttpMethod.Get,
- Headers =
- {
- Accept = { new MediaTypeWithQualityHeaderValue("application/vnd.github+json") }
- }
- };
- request.Headers.UserAgent.Add(new ProductInfoHeaderValue(Assembly.GetExecutingAssembly().GetName().Name!,
- Assembly.GetExecutingAssembly().GetName().Version!.ToString()));
- var response = await client.SendAsync(request);
- if (response.StatusCode != HttpStatusCode.OK) return;
- var json = await response.Content.ReadAsStringAsync();
- var jsonDoc = JsonDocument.Parse(json);
- if (!jsonDoc.RootElement.TryGetProperty("tag_name", out var tagName)) return;
- var tag = tagName.GetString()?.Replace("v", string.Empty);
+ var ghClient = new GitHubClient(new ProductHeaderValue(Assembly.GetEntryAssembly()?.GetName().Name));
+ var latestRelease = await ghClient.Repository.Release.GetLatest("SubZeroPL", "AssM");
+ var tag = latestRelease.TagName.Replace("v", string.Empty);
var versionPresent = Version.TryParse(tag, out var version);
if (!versionPresent || version <= currentVer) return;
_logger.Debug($"New version detected: {tag}");
TextBlockUpdate.Text = $"New version: {version!.Major}.{version.Minor}.{version.Build}";
ButtonUpdate.IsVisible = true;
- ButtonUpdate.Tag = jsonDoc.RootElement.GetProperty("html_url").GetString();
+ ButtonUpdate.Tag = latestRelease.Url;
}
private void AddButton_OnClick(object? sender, RoutedEventArgs e)
@@ -118,7 +105,9 @@ private void AddButton_OnClick(object? sender, RoutedEventArgs e)
{
AllowMultiple = true,
Title = "Select images",
- FileTypeFilter = [new FilePickerFileType("CUE file") { Patterns = ["*.cue"] }]
+ FileTypeFilter = [ new FilePickerFileType("All supported formats (*.cue, *.iso)") { Patterns = ["*.cue", "*.iso"] },
+ new FilePickerFileType("CUE file (*.cue)") { Patterns = ["*.cue"] },
+ new FilePickerFileType("ISO file (*.iso)") { Patterns = ["*.iso"] }]
};
var files = GetTopLevel(this)?.StorageProvider.OpenFilePickerAsync(fpo).GetAwaiter().GetResult() ??
new List();
@@ -130,14 +119,14 @@ private void AddButton_OnClick(object? sender, RoutedEventArgs e)
DataGridGameList.CollectionView.Refresh();
}
- private void AddGame(string cuePath)
+ private void AddGame(string imagePath)
{
- if (string.IsNullOrWhiteSpace(cuePath)) return;
- var game = Functions.AddGameToList(cuePath, Configuration, GameList);
+ if (string.IsNullOrWhiteSpace(imagePath)) return;
+ var game = Functions.AddGameToList(imagePath, Configuration, GameList);
if (game == null)
{
MessageBoxManager.GetMessageBoxStandard("Error",
- $"Failed to add game to list from {cuePath}{Environment.NewLine}Id not present in image",
+ $"Failed to add game to list from {imagePath}{Environment.NewLine}Id not present in image",
ButtonEnum.Ok, MsBox.Avalonia.Enums.Icon.Error, WindowStartupLocation.CenterOwner)
.ShowWindowDialogAsync(this);
return;
@@ -172,7 +161,7 @@ private void AddFolderButton_OnClick(object? sender, RoutedEventArgs e)
.ToList() ?? [];
var progress = new AddFolderProgressWindow();
_ = progress.ShowDialog(this);
- progress.Process(dirs, GameList, Configuration, (errors) =>
+ progress.Process(dirs, GameList, Configuration, errors =>
{
progress.Close();
DataGridGameList.CollectionView.Refresh();
diff --git a/Windows/ProgressWindow.axaml.cs b/Windows/ProgressWindow.axaml.cs
index 55298a2..a091f9a 100644
--- a/Windows/ProgressWindow.axaml.cs
+++ b/Windows/ProgressWindow.axaml.cs
@@ -52,7 +52,8 @@ public void Process(List gameList, Configuration configuration, Action fin
{
for (var i = 0; i < gameList.Count; i++)
{
- _worker.ReportProgress(0);
+ var progress = i / gameList.Count * 100;
+ _worker.ReportProgress(progress);
if (_worker.CancellationPending)
break;
var game = gameList.ElementAt(i);
@@ -64,17 +65,18 @@ public void Process(List gameList, Configuration configuration, Action fin
}
var step = 1;
- _worker.ReportProgress(0,
+ _worker.ReportProgress(progress,
new ProgressUpdateObject
- { Title = game.Title, Count = gameList.Count, Step = step, Index = i + 1 });
+ { Title = game.Title, Count = gameList.Count, Step = step++, Index = i + 1 });
ConvertSingleChd(game);
- _worker.ReportProgress(0, new ProgressUpdateObject { Step = step++ });
+ _worker.ReportProgress(progress,
+ new ProgressUpdateObject { Step = step++, BinNo = 1, BinCount = game.ChdData.TrackInfo.Count });
CalculateTracksMd5(game);
- _worker.ReportProgress(0, new ProgressUpdateObject { Step = step++ });
+ _worker.ReportProgress(progress, new ProgressUpdateObject { Step = step++ });
GetChdManInfo(game);
- _worker.ReportProgress(0, new ProgressUpdateObject { Step = step++ });
+ _worker.ReportProgress(progress, new ProgressUpdateObject { Step = step++ });
GenerateReadme(game);
- _worker.ReportProgress(0, new ProgressUpdateObject { Step = step });
+ _worker.ReportProgress(progress, new ProgressUpdateObject { Step = step });
Functions.ProcessJson(_configuration, game, d => { _worker.ReportProgress((int)d); });
game.Modified = false;
_logger.Debug($"Finished processing game {i + 1}: {game.Title}");
@@ -114,7 +116,7 @@ private void ConvertSingleChd(Game game)
var chdPath = Path.Combine(_configuration.OutputDirectory, Functions.OutputPath(game), chdFile);
_logger.Debug($"CHD path: {chdPath}");
if (File.Exists(chdPath) && _configuration.ChdProcessing != ChdProcessing.GenerateAll) return;
- if (string.IsNullOrWhiteSpace(game.CuePath)) return;
+ if (string.IsNullOrWhiteSpace(game.ImagePath)) return;
Directory.CreateDirectory(Path.GetDirectoryName(chdPath) ?? string.Empty);
var chdmanConvert = new Process
@@ -122,7 +124,7 @@ private void ConvertSingleChd(Game game)
StartInfo = new ProcessStartInfo
{
FileName = Constants.ChdMan,
- Arguments = string.Format(Constants.ChdManConvert, game.CuePath, chdPath),
+ Arguments = string.Format(Constants.ChdManConvert, game.ImagePath, chdPath),
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
@@ -200,20 +202,30 @@ private void CalculateTracksMd5(Game game)
var readmePath = Path.Combine(_configuration.OutputDirectory, Functions.OutputPath(game), Constants.ReadmeFile);
_logger.Debug($"Readme path: {readmePath}");
if (File.Exists(readmePath) && !_configuration.OverwriteExistingReadmes) return;
- var cuefile = game.CuePath;
- _logger.Debug($"Cue path: {cuefile}");
- if (string.IsNullOrWhiteSpace(cuefile)) return;
- _logger.Debug("Adding bin files from cue");
- var lines = File.ReadLines(cuefile);
+ var imagefile = game.ImagePath;
List bins = [];
- bins.AddRange(from line in lines
- where line.StartsWith("FILE")
- select Constants.BinFileRegex().Matches(line)
- into matches
- select matches[0].Groups[1].Value
- into bin
- select Path.Combine(Path.GetDirectoryName(cuefile)!, bin));
- _logger.Debug($"Bin files from cue: {string.Join(',', bins)}");
+ if (Functions.IsIso(game))
+ {
+ _logger.Debug($"Iso path: {imagefile}");
+ if (string.IsNullOrWhiteSpace(imagefile)) return;
+ bins.Add(imagefile);
+ _logger.Debug($"Iso file: {string.Join(',', bins)}");
+ }
+ else
+ {
+ _logger.Debug($"Cue path: {imagefile}");
+ if (string.IsNullOrWhiteSpace(imagefile)) return;
+ _logger.Debug("Adding bin files from cue");
+ var lines = File.ReadLines(imagefile);
+ bins.AddRange(from line in lines
+ where line.StartsWith("FILE")
+ select Constants.BinFileRegex().Matches(line)
+ into matches
+ select matches[0].Groups[1].Value
+ into bin
+ select Path.Combine(Path.GetDirectoryName(imagefile)!, bin));
+ _logger.Debug($"Bin files from cue: {string.Join(',', bins)}");
+ }
if (_worker.CancellationPending) return;
if (game.ChdData.TrackInfo.Count != 0) game.ChdData.TrackInfo.Clear();
@@ -225,7 +237,7 @@ into bin
using var fs = File.OpenRead(bin);
var hash = CalculateMd5HashFromStream(fs);
game.ChdData.TrackInfo.Add((i + 1, hash));
- _worker.ReportProgress((int)Math.Round((double)i / bins.Count * 100.0),
+ _worker.ReportProgress(i / bins.Count * 100,
new ProgressUpdateObject { Step = 2, BinNo = i + 1, BinCount = bins.Count });
_logger.Debug("Done");
}
@@ -255,7 +267,7 @@ private string CalculateMd5HashFromStream(Stream inputStream)
sb.Append(t.ToString("x2"));
}
- return sb.ToString();
+ return sb.ToString().ToUpperInvariant();
}
private void GetChdManInfo(Game game)