diff --git a/DotN64.Desktop/AssemblyReleaseStreamAttribute.cs b/DotN64.Desktop/AssemblyReleaseStreamAttribute.cs
new file mode 100644
index 0000000..35e6e9a
--- /dev/null
+++ b/DotN64.Desktop/AssemblyReleaseStreamAttribute.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace DotN64.Desktop
+{
+ [AttributeUsage(AttributeTargets.Assembly)]
+ public class AssemblyReleaseStreamAttribute : Attribute
+ {
+ #region Properties
+ public string Stream { get; set; }
+ #endregion
+
+ #region Constructors
+ public AssemblyReleaseStreamAttribute(string stream)
+ {
+ Stream = stream;
+ }
+ #endregion
+ }
+}
diff --git a/DotN64.Desktop/DotN64.Desktop.csproj b/DotN64.Desktop/DotN64.Desktop.csproj
index e3652b0..8411969 100644
--- a/DotN64.Desktop/DotN64.Desktop.csproj
+++ b/DotN64.Desktop/DotN64.Desktop.csproj
@@ -29,6 +29,8 @@
+
+
@@ -42,6 +44,8 @@
+
+
\ No newline at end of file
diff --git a/DotN64.Desktop/Program.cs b/DotN64.Desktop/Program.cs
index 6785cac..fbb8d42 100644
--- a/DotN64.Desktop/Program.cs
+++ b/DotN64.Desktop/Program.cs
@@ -1,15 +1,39 @@
-using System.IO;
+using System;
+using System.IO;
+using System.Reflection;
+using DotN64.Desktop;
-namespace DotN64
+[assembly: AssemblyTitle(nameof(DotN64))]
+[assembly: AssemblyDescription("Nintendo 64 emulator.")]
+[assembly: AssemblyVersion("0.0.*")]
+[assembly: AssemblyReleaseStream("master")]
+namespace DotN64.Desktop
{
using Diagnostics;
internal static class Program
{
+ #region Properties
+ private static DateTime BuildDate
+ {
+ get
+ {
+ var version = Assembly.GetEntryAssembly().GetName().Version;
+
+ return new DateTime(2000, 1, 1).AddDays(version.Build).AddSeconds(version.Revision * 2);
+ }
+ }
+
+ public static string ReleaseStream { get; } = Assembly.GetExecutingAssembly().GetCustomAttribute().Stream;
+
+ public static Uri Website { get; } = new Uri("https://nabile.duckdns.org/DotN64");
+ #endregion
+
+ #region Methods
private static void Main(string[] args)
{
- var nintendo64 = new Nintendo64();
- Debugger debugger = null;
+ Environment.CurrentDirectory = AppDomain.CurrentDomain.BaseDirectory;
+ var options = new Options();
for (int i = 0; i < args.Length; i++)
{
@@ -18,18 +42,97 @@ namespace DotN64
switch (arg)
{
case "--pif-rom":
- nintendo64.PIF.BootROM = File.ReadAllBytes(args[++i]);
+ options.BootROM = args[++i];
break;
case "--debug":
case "-d":
- debugger = new Debugger(nintendo64);
+ options.UseDebugger = true;
break;
+ case "--update":
+ case "-u":
+ switch (args.Length - 1 > i ? args[++i] : null)
+ {
+ case "check":
+ case "c":
+ Check();
+ return;
+ case "list":
+ case "l":
+ foreach (var stream in Updater.Streams)
+ {
+ Console.WriteLine($"{stream}{(stream == ReleaseStream ? " (current)" : string.Empty)}");
+ }
+ return;
+ case "stream":
+ case "s":
+ Update(args[++i]);
+ return;
+ default:
+ Update();
+ return;
+ }
+ case "--repair":
+ case "-r":
+ Repair();
+ return;
+ case "--help":
+ case "-h":
+ ShowHelp();
+ return;
default:
- nintendo64.Cartridge = Cartridge.FromFile(new FileInfo(arg));
+ options.Cartridge = arg;
break;
}
}
+ if (Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory).Length <= 1) // Fresh install.
+ Repair();
+
+ Run(options);
+ }
+
+ private static bool Check(string releaseStream = null)
+ {
+ var newVersion = Updater.Check(releaseStream);
+
+ if (newVersion == null)
+ {
+ Console.WriteLine("Already up to date.");
+ return false;
+ }
+
+ Console.WriteLine($"New version available: {newVersion}.");
+ return true;
+ }
+
+ private static void Update(string releaseStream = null, bool force = false)
+ {
+ if (!force && !Check(releaseStream))
+ return;
+
+ Console.WriteLine("Downloading update...");
+ Updater.Download(releaseStream);
+
+ Console.WriteLine("Applying update...");
+ Updater.Apply();
+ }
+
+ private static void Repair() => Update(force: true);
+
+ private static void Run(Options options)
+ {
+ var nintendo64 = new Nintendo64();
+ Debugger debugger = null;
+
+ if (options.BootROM != null)
+ nintendo64.PIF.BootROM = File.ReadAllBytes(options.BootROM);
+
+ if (options.UseDebugger)
+ debugger = new Debugger(nintendo64);
+
+ if (options.Cartridge != null)
+ nintendo64.Cartridge = Cartridge.FromFile(new FileInfo(options.Cartridge));
+
nintendo64.PowerOn();
if (debugger == null)
@@ -37,5 +140,44 @@ namespace DotN64
else
debugger.Run(true);
}
+
+ private static void ShowInfo()
+ {
+ var assembly = Assembly.GetEntryAssembly();
+ var title = assembly.GetCustomAttribute().Title;
+ var version = assembly.GetName().Version;
+ var description = assembly.GetCustomAttribute().Description;
+
+ Console.WriteLine($"{title} v{version} ({BuildDate}){(description != null ? $": {description}" : string.Empty)} ({Website})");
+ }
+
+ private static void ShowHelp()
+ {
+ ShowInfo();
+ Console.WriteLine();
+ Console.WriteLine($"Usage: {Path.GetFileName(Assembly.GetEntryAssembly().Location)} [Options] ");
+ Console.WriteLine();
+ Console.WriteLine("ROM image: Opens the file as a game cartridge.");
+ Console.WriteLine("Options:");
+ Console.WriteLine("\t--pif-rom : Loads the PIF's boot ROM into the machine.");
+ Console.WriteLine("\t-d, --debug: Launches the debugger for the Nintendo 64's CPU.");
+ Console.WriteLine("\t-u, --update [action]: Updates the program.");
+ Console.WriteLine("\t[action = 'check', 'c']: Checks for a new update.");
+ Console.WriteLine("\t[action = 'list', 'l']: Lists the release streams available for download.");
+ Console.WriteLine("\t[action = 'stream', 's'] : Downloads an update from the specified release stream.");
+ Console.WriteLine("\t-r, --repair: Repairs the installation by redownloading the full program.");
+ Console.WriteLine("\t-h, --help: Shows this help.");
+ }
+ #endregion
+
+ #region Structures
+ private struct Options
+ {
+ #region Fields
+ public bool UseDebugger;
+ public string BootROM, Cartridge;
+ #endregion
+ }
+ #endregion
}
}
diff --git a/DotN64.Desktop/Updater.cs b/DotN64.Desktop/Updater.cs
new file mode 100644
index 0000000..2bb6783
--- /dev/null
+++ b/DotN64.Desktop/Updater.cs
@@ -0,0 +1,158 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.IO.Compression;
+using System.Net;
+using System.Reflection;
+
+namespace DotN64.Desktop
+{
+ public static class Updater
+ {
+ #region Fields
+ private const string StagingDirectory = ".update", ProjectName = nameof(DotN64), NewFileExtension = ".new";
+ #endregion
+
+ #region Properties
+ private static string InstallDirectory => AppDomain.CurrentDomain.BaseDirectory;
+
+ private static string PlatformSuffix => string.Join(".", Environment.OSVersion.Platform, Environment.Is64BitOperatingSystem ? "64" : "32");
+
+ ///
+ /// Gets the available release streams.
+ ///
+ public static IEnumerable Streams
+ {
+ get
+ {
+ using (var client = new WebClient())
+ using (var reader = new StreamReader(client.OpenRead(new Uri(Program.Website, $"{ProjectName}/Download/streams"))))
+ {
+ while (!reader.EndOfStream)
+ {
+ yield return reader.ReadLine();
+ }
+ }
+ }
+ }
+ #endregion
+
+ #region Methods
+ ///
+ /// Checks for a new update.
+ ///
+ public static Version Check(string releaseStream = null)
+ {
+ if (releaseStream == null)
+ releaseStream = Program.ReleaseStream;
+
+ using (var client = new WebClient())
+ {
+ var remoteVersion = new Version(client.DownloadString(new Uri(Program.Website, $"{ProjectName}/Download/{releaseStream}/version")));
+ var currentVersion = Assembly.GetExecutingAssembly().GetName().Version;
+
+ return remoteVersion > currentVersion ? remoteVersion : null;
+ }
+ }
+
+ ///
+ /// Downloads the latest update.
+ ///
+ public static void Download(string releaseStream = null)
+ {
+ if (releaseStream == null)
+ releaseStream = Program.ReleaseStream;
+
+ var updateDirectory = new DirectoryInfo(Path.Combine(InstallDirectory, StagingDirectory));
+
+ if (updateDirectory.Exists)
+ updateDirectory.Delete(true);
+
+ updateDirectory.Create();
+ updateDirectory.Attributes = FileAttributes.Directory | FileAttributes.Hidden;
+
+ using (var client = new WebClient())
+ {
+ using (var archive = new ZipArchive(client.OpenRead(new Uri(Program.Website, $"{ProjectName}/Download/{releaseStream}/{ProjectName}.zip"))))
+ {
+ archive.ExtractToDirectory(updateDirectory.FullName);
+ }
+
+ try
+ {
+ using (var archive = new ZipArchive(client.OpenRead(new Uri(Program.Website, $"{ProjectName}/Download/{releaseStream}/{ProjectName}.{PlatformSuffix}.zip"))))
+ {
+ archive.ExtractToDirectory(updateDirectory.FullName);
+ }
+ }
+ catch (WebException e) when ((e.Response as HttpWebResponse).StatusCode == HttpStatusCode.NotFound) { }
+ }
+ }
+
+ ///
+ /// Applies the update.
+ ///
+ public static void Apply()
+ {
+ var updateDirectory = new DirectoryInfo(Path.Combine(InstallDirectory, StagingDirectory));
+ var executableName = Path.GetFileName(Assembly.GetEntryAssembly().Location);
+ var isWindow = Environment.OSVersion.Platform == PlatformID.Win32NT;
+ var shouldRestart = false;
+
+ foreach (var file in updateDirectory.GetFiles())
+ {
+ var destination = Path.Combine(InstallDirectory, file.Name);
+
+ if (isWindow && file.Name == executableName)
+ {
+ file.MoveTo(destination + NewFileExtension);
+
+ shouldRestart = true;
+ continue;
+ }
+
+ File.Delete(destination);
+ file.MoveTo(destination);
+ }
+
+ updateDirectory.Delete(true);
+
+ if (isWindow && shouldRestart)
+ ApplyWindowsUpdate(executableName);
+ }
+
+ private static void ApplyWindowsUpdate(string executableName)
+ {
+ var newExecutableName = executableName + NewFileExtension;
+ var scriptFile = new FileInfo("CompleteUpdate.cmd");
+ var processID = Process.GetCurrentProcess().Id;
+
+ using (var writer = scriptFile.CreateText())
+ {
+ writer.WriteLine(":CHECK");
+ writer.WriteLine("timeout /t 1");
+ writer.WriteLine($"tasklist /fi \"pid eq {processID}\" | find \"{processID}\"");
+
+ writer.WriteLine("if errorlevel 1 goto UPDATE");
+
+ writer.WriteLine("goto CHECK");
+
+ writer.WriteLine(":UPDATE");
+ writer.WriteLine($"move /Y \"{newExecutableName}\" \"{executableName}\"");
+ writer.WriteLine($"start \"\" \"{executableName}\"");
+ writer.WriteLine($"del /A:H %0");
+ }
+
+ scriptFile.Attributes |= FileAttributes.Hidden;
+
+ Process.Start(new ProcessStartInfo(scriptFile.FullName)
+ {
+ UseShellExecute = false,
+ CreateNoWindow = true
+ });
+ Environment.Exit(0);
+ }
+ #endregion
+ }
+}