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 + } +}