From 59c900a9ac936491386e70d9c841e572f2a49ecd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 3 Nov 2016 00:43:41 -0400 Subject: add update check (#154) --- src/StardewModdingAPI/Constants.cs | 4 ++ src/StardewModdingAPI/Framework/GitRelease.cs | 19 +++++++++ src/StardewModdingAPI/Framework/UpdateHelper.cs | 36 +++++++++++++++++ src/StardewModdingAPI/Program.cs | 21 ++++++++++ src/StardewModdingAPI/StardewModdingAPI.csproj | 2 + src/StardewModdingAPI/Version.cs | 51 ++++++++++++++++++++++++- 6 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/GitRelease.cs create mode 100644 src/StardewModdingAPI/Framework/UpdateHelper.cs (limited to 'src/StardewModdingAPI') diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index 6fe81232..b183f8b6 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -10,8 +10,12 @@ namespace StardewModdingAPI /// public static class Constants { + /// SMAPI's current semantic version. public static readonly Version Version = new Version(1, 0, 0, $"alpha-{DateTime.UtcNow.ToString("yyyyMMddHHmm")}"); + /// The GitHub repository to check for updates. + public const string GitHubRepository = "cjsu/SMAPI"; + /// /// Not quite "constant", but it makes more sense for it to be here, at least for now /// diff --git a/src/StardewModdingAPI/Framework/GitRelease.cs b/src/StardewModdingAPI/Framework/GitRelease.cs new file mode 100644 index 00000000..0da57efd --- /dev/null +++ b/src/StardewModdingAPI/Framework/GitRelease.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace StardewModdingAPI.Framework +{ + /// Metadata about a GitHub release tag. + internal class GitRelease + { + /********* + ** Accessors + *********/ + /// The display name. + [JsonProperty("name")] + public string Name { get; set; } + + /// The semantic version string. + [JsonProperty("tag_name")] + public string Tag { get; set; } + } +} \ No newline at end of file diff --git a/src/StardewModdingAPI/Framework/UpdateHelper.cs b/src/StardewModdingAPI/Framework/UpdateHelper.cs new file mode 100644 index 00000000..ddd1d840 --- /dev/null +++ b/src/StardewModdingAPI/Framework/UpdateHelper.cs @@ -0,0 +1,36 @@ +using System.IO; +using System.Net; +using System.Reflection; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace StardewModdingAPI.Framework +{ + /// Provides utility methods for mod updates. + internal class UpdateHelper + { + /********* + ** Public methods + *********/ + /// Get the latest release from a GitHub repository. + /// The name of the repository from which to fetch releases (like "cjsu/SMAPI"). + public static async Task GetLatestVersionAsync(string repository) + { + // build request + // (avoid HttpClient for Mac compatibility) + HttpWebRequest request = WebRequest.CreateHttp($"https://api.github.com/repos/{repository}/releases/latest"); + AssemblyName assembly = typeof(UpdateHelper).Assembly.GetName(); + request.UserAgent = $"{assembly.Name}/{assembly.Version}"; + request.Accept = "application/vnd.github.v3+json"; + + // fetch data + using (WebResponse response = await request.GetResponseAsync()) + using (Stream responseStream = response.GetResponseStream()) + using (StreamReader reader = new StreamReader(responseStream)) + { + string responseText = reader.ReadToEnd(); + return JsonConvert.DeserializeObject(responseText); + } + } + } +} diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index b3f81217..e2576e40 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -10,6 +10,7 @@ using System.Windows.Forms; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Events; +using StardewModdingAPI.Framework; using StardewModdingAPI.Inheritance; using StardewValley; @@ -54,6 +55,7 @@ namespace StardewModdingAPI { Log.AsyncY($"SMAPI {Constants.Version}"); Log.AsyncY($"Stardew Valley {Game1.version} on {Environment.OSVersion}"); + Program.CheckForUpdateAsync(); Program.ConfigureUI(); Program.CreateDirectories(); Program.StartGame(); @@ -91,6 +93,25 @@ namespace StardewModdingAPI throw new FileNotFoundException($"Could not find executable: {GameExecutablePath}"); } + /// Asynchronously check for a new version of SMAPI, and print a message to the console if an update is available. + private static void CheckForUpdateAsync() + { + new Thread(() => + { + try + { + GitRelease release = UpdateHelper.GetLatestVersionAsync(Constants.GitHubRepository).Result; + Version latestVersion = new Version(release.Tag); + if (latestVersion.CompareTo(Constants.Version) > 0) + Log.AsyncColour($"You can update SMAPI from version {Constants.Version} to {latestVersion}", ConsoleColor.Magenta); + } + catch (Exception ex) + { + Log.Debug($"Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.\n{ex}"); + } + }).Start(); + } + /// /// Load Stardev Valley and control features, and launch the game. /// diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 43e00fbc..e79cabf4 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -192,6 +192,8 @@ + + diff --git a/src/StardewModdingAPI/Version.cs b/src/StardewModdingAPI/Version.cs index db5a21d4..b1903d7b 100644 --- a/src/StardewModdingAPI/Version.cs +++ b/src/StardewModdingAPI/Version.cs @@ -1,11 +1,20 @@ using System; +using System.Text.RegularExpressions; using Newtonsoft.Json; namespace StardewModdingAPI { /// A semantic version with an optional release tag. - public struct Version + public struct Version : IComparable { + /********* + ** Properties + *********/ + /// A regular expression matching a semantic version string. + /// Derived from https://github.com/maxhauser/semver. + private static readonly Regex Regex = new Regex(@"^(?\d+)(\.(?\d+))?(\.(?\d+))?(?.*)$", RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture); + + /********* ** Accessors *********/ @@ -43,6 +52,44 @@ namespace StardewModdingAPI this.Build = build; } + /// Construct an instance. + /// The semantic version string. + internal Version(string version) + { + var match = Version.Regex.Match(version); + if (!match.Success) + throw new FormatException($"The input '{version}' is not a semantic version."); + + this.MajorVersion = int.Parse(match.Groups["major"].Value); + this.MinorVersion = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0; + this.PatchVersion = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0; + this.Build = (match.Groups["build"].Success ? match.Groups["build"].Value : "").Trim(' ', '-', '.'); + } + + /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. + /// The version to compare with this instance. + public int CompareTo(Version other) + { + // compare version numbers + if (this.MajorVersion != other.MajorVersion) + return this.MajorVersion - other.MajorVersion; + if (this.MinorVersion != other.MinorVersion) + return this.MinorVersion - other.MinorVersion; + if (this.PatchVersion != other.PatchVersion) + return this.PatchVersion - other.PatchVersion; + + // stable version (without tag) supercedes prerelease (with tag) + bool curHasTag = !string.IsNullOrWhiteSpace(this.Build); + bool otherHasTag = !string.IsNullOrWhiteSpace(other.Build); + if (!curHasTag && otherHasTag) + return 1; + if (curHasTag && !otherHasTag) + return -1; + + // else compare by string + return string.Compare(this.ToString(), other.ToString(), StringComparison.InvariantCultureIgnoreCase); + } + /// Get a string representation of the version. public override string ToString() { @@ -66,7 +113,7 @@ namespace StardewModdingAPI /// The tag to normalise. private string GetNormalisedTag(string tag) { - tag = tag?.Trim().Trim('-'); + tag = tag?.Trim().Trim('-', '.'); if (string.IsNullOrWhiteSpace(tag) || tag == "0") return null; return tag; -- cgit