diff options
Diffstat (limited to 'src')
29 files changed, 700 insertions, 185 deletions
diff --git a/src/GlobalAssemblyInfo.cs b/src/GlobalAssemblyInfo.cs index 0dfb42bb..7f1fa401 100644 --- a/src/GlobalAssemblyInfo.cs +++ b/src/GlobalAssemblyInfo.cs @@ -2,5 +2,5 @@ using System.Runtime.InteropServices; [assembly: ComVisible(false)] -[assembly: AssemblyVersion("1.4.0.0")] -[assembly: AssemblyFileVersion("1.4.0.0")]
\ No newline at end of file +[assembly: AssemblyVersion("1.5.0.0")] +[assembly: AssemblyFileVersion("1.5.0.0")]
\ No newline at end of file diff --git a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs index 4cef1a12..5f89caf2 100644 --- a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs +++ b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs @@ -55,7 +55,8 @@ namespace StardewModdingApi.Installer { // common "StardewModdingAPI.exe", - "StardewModdingAPI-settings.json", + "StardewModdingAPI.config.json", + "StardewModdingAPI.data.json", "StardewModdingAPI.AssemblyRewriters.dll", "steam_appid.txt", @@ -72,7 +73,8 @@ namespace StardewModdingApi.Installer "StardewModdingAPI.pdb", // obsolete - "Mods/.cache" + "Mods/.cache", // 1.3-1.4 + "StardewModdingAPI-settings.json" // 1.0-1.4 }; diff --git a/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj b/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj index f82bce0f..4e4872b6 100644 --- a/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj +++ b/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj @@ -73,7 +73,8 @@ <Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.exe" DestinationFolder="$(CompiledInstallerPath)\Mono" /> <Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.exe.mdb" DestinationFolder="$(CompiledInstallerPath)\Mono" /> <Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.AssemblyRewriters.dll" DestinationFolder="$(CompiledInstallerPath)\Mono" /> - <Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI-settings.json" DestinationFolder="$(CompiledInstallerPath)\Mono" /> + <Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.config.json" DestinationFolder="$(CompiledInstallerPath)\Mono" /> + <Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.data.json" DestinationFolder="$(CompiledInstallerPath)\Mono" /> <Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\System.Numerics.dll" DestinationFolder="$(CompiledInstallerPath)\Mono" /> <Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\System.Runtime.Caching.dll" DestinationFolder="$(CompiledInstallerPath)\Mono" /> <Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\unix-launcher.sh" DestinationFiles="$(CompiledInstallerPath)\Mono\StardewModdingAPI" /> @@ -87,7 +88,8 @@ <Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.pdb" DestinationFolder="$(CompiledInstallerPath)\Windows" /> <Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.xml" DestinationFolder="$(CompiledInstallerPath)\Windows" /> <Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.AssemblyRewriters.dll" DestinationFolder="$(CompiledInstallerPath)\Windows" /> - <Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI-settings.json" DestinationFolder="$(CompiledInstallerPath)\Windows" /> + <Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.config.json" DestinationFolder="$(CompiledInstallerPath)\Windows" /> + <Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.data.json" DestinationFolder="$(CompiledInstallerPath)\Windows" /> <Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\steam_appid.txt" DestinationFolder="$(CompiledInstallerPath)\Windows" /> <Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="@(CompiledMods)" DestinationFolder="$(CompiledInstallerPath)\Windows\Mods\%(RecursiveDir)" /> </Target> diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index f5b9e70a..a79fd382 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -26,7 +26,11 @@ namespace StardewModdingAPI ** Accessors *********/ /// <summary>SMAPI's current semantic version.</summary> - public static readonly Version Version = new Version(1, 4, 0, null); + [Obsolete("Use " + nameof(Constants) + "." + nameof(ApiVersion))] + public static readonly Version Version = (Version)Constants.ApiVersion; + + /// <summary>SMAPI's current semantic version.</summary> + public static ISemanticVersion ApiVersion => new Version(1, 5, 0, null, suppressDeprecationWarning: true); /// <summary>The minimum supported version of Stardew Valley.</summary> public const string MinimumGameVersion = "1.1"; @@ -56,7 +60,7 @@ namespace StardewModdingAPI public static string ExecutionPath => Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); /// <summary>The title of the SMAPI console window.</summary> - public static string ConsoleTitle => $"Stardew Modding API Console - Version {Constants.Version} - Mods Loaded: {Program.ModsLoaded}"; + public static string ConsoleTitle => $"Stardew Modding API Console - Version {Constants.ApiVersion} - Mods Loaded: {Program.ModsLoaded}"; /// <summary>The directory path in which error logs should be stored.</summary> public static string LogDir => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley", "ErrorLogs"); @@ -64,6 +68,12 @@ namespace StardewModdingAPI /// <summary>The file path to the error log where the latest output should be saved.</summary> public static string LogPath => Path.Combine(Constants.LogDir, "MODDED_ProgramLog.Log_LATEST.txt"); + /// <summary>The file path for the SMAPI configuration file.</summary> + internal static string ApiConfigPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.config.json"); + + /// <summary>The file path for the SMAPI data file containing metadata about known mods.</summary> + internal static string ApiModMetadataPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.data.json"); + /********* ** Protected methods diff --git a/src/StardewModdingAPI/Framework/AssemblyRewriting/CacheEntry.cs b/src/StardewModdingAPI/Framework/AssemblyRewriting/CacheEntry.cs index 3dfbc78c..a747eaa8 100644 --- a/src/StardewModdingAPI/Framework/AssemblyRewriting/CacheEntry.cs +++ b/src/StardewModdingAPI/Framework/AssemblyRewriting/CacheEntry.cs @@ -36,7 +36,7 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting /// <param name="paths">The paths for the cached assembly.</param> /// <param name="hash">The MD5 hash of the original assembly.</param> /// <param name="currentVersion">The current SMAPI version.</param> - public bool IsUpToDate(CachePaths paths, string hash, Version currentVersion) + public bool IsUpToDate(CachePaths paths, string hash, ISemanticVersion currentVersion) { return hash == this.Hash && this.ApiVersion == currentVersion.ToString() diff --git a/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs index 1ceb8ad2..a2c4ac23 100644 --- a/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs @@ -58,7 +58,7 @@ namespace StardewModdingAPI.Framework CachePaths cachePaths = this.GetCachePaths(assemblyPath); { CacheEntry cacheEntry = File.Exists(cachePaths.Metadata) ? JsonConvert.DeserializeObject<CacheEntry>(File.ReadAllText(cachePaths.Metadata)) : null; - if (cacheEntry != null && cacheEntry.IsUpToDate(cachePaths, hash, Constants.Version)) + if (cacheEntry != null && cacheEntry.IsUpToDate(cachePaths, hash, Constants.ApiVersion)) return new RewriteResult(assemblyPath, cachePaths, assemblyBytes, cacheEntry.Hash, cacheEntry.UseCachedAssembly, isNewerThanCache: false); // no rewrite needed } this.Monitor.Log($"Preprocessing {Path.GetFileName(assemblyPath)} for compatibility...", LogLevel.Trace); @@ -99,7 +99,7 @@ namespace StardewModdingAPI.Framework // cache all results foreach (RewriteResult result in results) { - CacheEntry cacheEntry = new CacheEntry(result.Hash, Constants.Version.ToString(), forceCacheAssemblies || result.UseCachedAssembly); + CacheEntry cacheEntry = new CacheEntry(result.Hash, Constants.ApiVersion.ToString(), forceCacheAssemblies || result.UseCachedAssembly); File.WriteAllText(result.CachePaths.Metadata, JsonConvert.SerializeObject(cacheEntry)); if (forceCacheAssemblies || result.UseCachedAssembly) File.WriteAllBytes(result.CachePaths.Assembly, result.AssemblyBytes); diff --git a/src/StardewModdingAPI/Framework/ModRegistry.cs b/src/StardewModdingAPI/Framework/ModRegistry.cs index ba56a447..b593142d 100644 --- a/src/StardewModdingAPI/Framework/ModRegistry.cs +++ b/src/StardewModdingAPI/Framework/ModRegistry.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Reflection; namespace StardewModdingAPI.Framework @@ -11,6 +12,9 @@ namespace StardewModdingAPI.Framework /********* ** Properties *********/ + /// <summary>The registered mod data.</summary> + private readonly List<IMod> Mods = new List<IMod>(); + /// <summary>The friendly mod names treated as deprecation warning sources (assembly full name => mod name).</summary> private readonly IDictionary<string, string> ModNamesByAssembly = new Dictionary<string, string>(); @@ -19,11 +23,17 @@ namespace StardewModdingAPI.Framework ** Public methods *********/ /// <summary>Register a mod as a possible source of deprecation warnings.</summary> - /// <param name="manifest">The mod manifest.</param> - /// <param name="assembly">The mod assembly.</param> - public void Add(Manifest manifest, Assembly assembly) + /// <param name="mod">The mod instance.</param> + public void Add(IMod mod) + { + this.Mods.Add(mod); + this.ModNamesByAssembly[mod.GetType().Assembly.FullName] = mod.ModManifest.Name; + } + + /// <summary>Get all enabled mods.</summary> + public IEnumerable<IMod> GetMods() { - this.ModNamesByAssembly[assembly.FullName] = manifest.Name; + return (from mod in this.Mods select mod); } /// <summary>Get the friendly name for the closest assembly registered as a source of deprecation warnings.</summary> diff --git a/src/StardewModdingAPI/Framework/GitRelease.cs b/src/StardewModdingAPI/Framework/Models/GitRelease.cs index 0da57efd..bc53468f 100644 --- a/src/StardewModdingAPI/Framework/GitRelease.cs +++ b/src/StardewModdingAPI/Framework/Models/GitRelease.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace StardewModdingAPI.Framework +namespace StardewModdingAPI.Framework.Models { /// <summary>Metadata about a GitHub release tag.</summary> internal class GitRelease diff --git a/src/StardewModdingAPI/Framework/Models/IncompatibleMod.cs b/src/StardewModdingAPI/Framework/Models/IncompatibleMod.cs new file mode 100644 index 00000000..9bf06552 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Models/IncompatibleMod.cs @@ -0,0 +1,47 @@ +using System.Text.RegularExpressions; + +namespace StardewModdingAPI.Framework.Models +{ + /// <summary>Contains abstract metadata about an incompatible mod.</summary> + internal class IncompatibleMod + { + /********* + ** Accessors + *********/ + /// <summary>The unique mod ID.</summary> + public string ID { get; set; } + + /// <summary>The mod name.</summary> + public string Name { get; set; } + + /// <summary>The most recent incompatible mod version.</summary> + public string Version { get; set; } + + /// <summary>The URL the user can check for an official updated version.</summary> + public string UpdateUrl { get; set; } + + /// <summary>The URL the user can check for an unofficial updated version.</summary> + public string UnofficialUpdateUrl { get; set; } + + /// <summary>A regular expression matching version strings to consider compatible, even if they technically precede <see cref="Version"/>.</summary> + public string ForceCompatibleVersion { get; set; } + + + /********* + ** Public methods + *********/ + /// <summary>Get whether the specified version is compatible according to this metadata.</summary> + /// <param name="version">The current version of the matching mod.</param> + public bool IsCompatible(ISemanticVersion version) + { + ISemanticVersion incompatibleVersion = new SemanticVersion(this.Version); + + // allow newer versions + if (version.IsNewerThan(incompatibleVersion)) + return true; + + // allow versions matching override + return !string.IsNullOrWhiteSpace(this.ForceCompatibleVersion) && Regex.IsMatch(version.ToString(), this.ForceCompatibleVersion, RegexOptions.IgnoreCase); + } + } +}
\ No newline at end of file diff --git a/src/StardewModdingAPI/Framework/Models/UserSettings.cs b/src/StardewModdingAPI/Framework/Models/UserSettings.cs new file mode 100644 index 00000000..a0074f77 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Models/UserSettings.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Framework.Models +{ + /// <summary>Contains user settings from SMAPI's JSON configuration file.</summary> + internal class UserSettings + { + /********* + ** Accessors + *********/ + /// <summary>Whether to enable development features.</summary> + public bool DeveloperMode { get; set; } + + /// <summary>Whether to check if a newer version of SMAPI is available on startup.</summary> + public bool CheckForUpdates { get; set; } = true; + } +} diff --git a/src/StardewModdingAPI/Framework/Reflection/CacheEntry.cs b/src/StardewModdingAPI/Framework/Reflection/CacheEntry.cs new file mode 100644 index 00000000..30faca37 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Reflection/CacheEntry.cs @@ -0,0 +1,30 @@ +using System.Reflection; + +namespace StardewModdingAPI.Framework.Reflection +{ + /// <summary>A cached member reflection result.</summary> + internal struct CacheEntry + { + /********* + ** Accessors + *********/ + /// <summary>Whether the lookup found a valid match.</summary> + public bool IsValid; + + /// <summary>The reflection data for this member (or <c>null</c> if invalid).</summary> + public MemberInfo MemberInfo; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="isValid">Whether the lookup found a valid match.</param> + /// <param name="memberInfo">The reflection data for this member (or <c>null</c> if invalid).</param> + public CacheEntry(bool isValid, MemberInfo memberInfo) + { + this.IsValid = isValid; + this.MemberInfo = memberInfo; + } + } +} diff --git a/src/StardewModdingAPI/Framework/Reflection/ReflectionHelper.cs b/src/StardewModdingAPI/Framework/Reflection/ReflectionHelper.cs index 38b4e357..edf59b81 100644 --- a/src/StardewModdingAPI/Framework/Reflection/ReflectionHelper.cs +++ b/src/StardewModdingAPI/Framework/Reflection/ReflectionHelper.cs @@ -67,10 +67,17 @@ namespace StardewModdingAPI.Framework.Reflection /// <param name="obj">The object which has the field.</param> /// <param name="name">The field name.</param> /// <param name="required">Whether to throw an exception if the private field is not found.</param> - /// <remarks>This is a shortcut for <see cref="GetPrivateField{TValue}(object,string,bool)"/> followed by <see cref="IPrivateField{TValue}.GetValue"/>.</remarks> + /// <returns>Returns the field value, or the default value for <typeparamref name="TValue"/> if the field wasn't found and <paramref name="required"/> is false.</returns> + /// <remarks> + /// This is a shortcut for <see cref="GetPrivateField{TValue}(object,string,bool)"/> followed by <see cref="IPrivateField{TValue}.GetValue"/>. + /// When <paramref name="required" /> is false, this will return the default value if reflection fails. If you need to check whether the field exists, use <see cref="GetPrivateField{TValue}(object,string,bool)" /> instead. + /// </remarks> public TValue GetPrivateValue<TValue>(object obj, string name, bool required = true) { - return this.GetPrivateField<TValue>(obj, name, required).GetValue(); + IPrivateField<TValue> field = this.GetPrivateField<TValue>(obj, name, required); + return field != null + ? field.GetValue() + : default(TValue); } /// <summary>Get the value of a private static field.</summary> @@ -78,10 +85,17 @@ namespace StardewModdingAPI.Framework.Reflection /// <param name="type">The type which has the field.</param> /// <param name="name">The field name.</param> /// <param name="required">Whether to throw an exception if the private field is not found.</param> - /// <remarks>This is a shortcut for <see cref="GetPrivateField{TValue}(Type,string,bool)"/> followed by <see cref="IPrivateField{TValue}.GetValue"/>.</remarks> + /// <returns>Returns the field value, or the default value for <typeparamref name="TValue"/> if the field wasn't found and <paramref name="required"/> is false.</returns> + /// <remarks> + /// This is a shortcut for <see cref="GetPrivateField{TValue}(Type,string,bool)"/> followed by <see cref="IPrivateField{TValue}.GetValue"/>. + /// When <paramref name="required" /> is false, this will return the default value if reflection fails. If you need to check whether the field exists, use <see cref="GetPrivateField{TValue}(Type,string,bool)" /> instead. + /// </remarks> public TValue GetPrivateValue<TValue>(Type type, string name, bool required = true) { - return this.GetPrivateField<TValue>(type, name, required).GetValue(); + IPrivateField<TValue> field = this.GetPrivateField<TValue>(type, name, required); + return field != null + ? field.GetValue() + : default(TValue); } /**** @@ -228,12 +242,18 @@ namespace StardewModdingAPI.Framework.Reflection { // get from cache if (this.Cache.Contains(key)) - return (TMemberInfo)this.Cache[key]; + { + CacheEntry entry = (CacheEntry)this.Cache[key]; + return entry.IsValid + ? (TMemberInfo)entry.MemberInfo + : default(TMemberInfo); + } // fetch & cache new value TMemberInfo result = fetch(); - this.Cache.Add(key, result, new CacheItemPolicy { SlidingExpiration = this.SlidingCacheExpiry }); + CacheEntry cacheEntry = new CacheEntry(result != null, result); + this.Cache.Add(key, cacheEntry, new CacheItemPolicy { SlidingExpiration = this.SlidingCacheExpiry }); return result; } } -}
\ No newline at end of file +} diff --git a/src/StardewModdingAPI/Framework/UpdateHelper.cs b/src/StardewModdingAPI/Framework/UpdateHelper.cs index ddd1d840..e01e55c8 100644 --- a/src/StardewModdingAPI/Framework/UpdateHelper.cs +++ b/src/StardewModdingAPI/Framework/UpdateHelper.cs @@ -3,6 +3,7 @@ using System.Net; using System.Reflection; using System.Threading.Tasks; using Newtonsoft.Json; +using StardewModdingAPI.Framework.Models; namespace StardewModdingAPI.Framework { diff --git a/src/StardewModdingAPI/Framework/UserSettings.cs b/src/StardewModdingAPI/Framework/UserSettings.cs deleted file mode 100644 index 199d19b3..00000000 --- a/src/StardewModdingAPI/Framework/UserSettings.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace StardewModdingAPI.Framework -{ - /// <summary>Contains user settings from SMAPI's JSON configuration file.</summary> - internal class UserSettings - { - /// <summary>Whether to enable development features.</summary> - public bool DeveloperMode { get; set; } - } -} diff --git a/src/StardewModdingAPI/IManifest.cs b/src/StardewModdingAPI/IManifest.cs new file mode 100644 index 00000000..3e4b7513 --- /dev/null +++ b/src/StardewModdingAPI/IManifest.cs @@ -0,0 +1,27 @@ +namespace StardewModdingAPI +{ + /// <summary>A manifest which describes a mod for SMAPI.</summary> + public interface IManifest + { + /// <summary>The mod name.</summary> + string Name { get; set; } + + /// <summary>A brief description of the mod.</summary> + string Description { get; set; } + + /// <summary>The mod author's name.</summary> + string Author { get; } + + /// <summary>The mod version.</summary> + ISemanticVersion Version { get; set; } + + /// <summary>The minimum SMAPI version required by this mod, if any.</summary> + string MinimumApiVersion { get; set; } + + /// <summary>The unique mod ID.</summary> + string UniqueID { get; set; } + + /// <summary>The name of the DLL in the directory that has the <see cref="Mod.Entry"/> method.</summary> + string EntryDll { get; set; } + } +}
\ No newline at end of file diff --git a/src/StardewModdingAPI/IMod.cs b/src/StardewModdingAPI/IMod.cs new file mode 100644 index 00000000..35ac7c0f --- /dev/null +++ b/src/StardewModdingAPI/IMod.cs @@ -0,0 +1,26 @@ +namespace StardewModdingAPI +{ + /// <summary>The implementation for a Stardew Valley mod.</summary> + public interface IMod + { + /********* + ** Accessors + *********/ + /// <summary>Provides simplified APIs for writing mods.</summary> + IModHelper Helper { get; } + + /// <summary>Writes messages to the console and log file.</summary> + IMonitor Monitor { get; } + + /// <summary>The mod's manifest.</summary> + IManifest ModManifest { get; } + + + /********* + ** Public methods + *********/ + /// <summary>The mod entry point, called after the mod is first loaded.</summary> + /// <param name="helper">Provides simplified APIs for writing mods.</param> + void Entry(IModHelper helper); + } +}
\ No newline at end of file diff --git a/src/StardewModdingAPI/ISemanticVersion.cs b/src/StardewModdingAPI/ISemanticVersion.cs new file mode 100644 index 00000000..3b9bdb44 --- /dev/null +++ b/src/StardewModdingAPI/ISemanticVersion.cs @@ -0,0 +1,38 @@ +using System; + +namespace StardewModdingAPI +{ + /// <summary>A semantic version with an optional release tag.</summary> + public interface ISemanticVersion : IComparable<ISemanticVersion> + { + /********* + ** Accessors + *********/ + /// <summary>The major version incremented for major API changes.</summary> + int MajorVersion { get; } + + /// <summary>The minor version incremented for backwards-compatible changes.</summary> + int MinorVersion { get; } + + /// <summary>The patch version for backwards-compatible bug fixes.</summary> + int PatchVersion { get; } + + /// <summary>An optional build tag.</summary> + string Build { get; } + + + /********* + ** Accessors + *********/ + /// <summary>Get whether this version is older than the specified version.</summary> + /// <param name="other">The version to compare with this instance.</param> + bool IsOlderThan(ISemanticVersion other); + + /// <summary>Get whether this version is newer than the specified version.</summary> + /// <param name="other">The version to compare with this instance.</param> + bool IsNewerThan(ISemanticVersion other); + + /// <summary>Get a string representation of the version.</summary> + string ToString(); + } +}
\ No newline at end of file diff --git a/src/StardewModdingAPI/Inheritance/SGame.cs b/src/StardewModdingAPI/Inheritance/SGame.cs index f70d0696..16803a73 100644 --- a/src/StardewModdingAPI/Inheritance/SGame.cs +++ b/src/StardewModdingAPI/Inheritance/SGame.cs @@ -913,11 +913,17 @@ namespace StardewModdingAPI.Inheritance // raise menu changed if (Game1.activeClickableMenu != this.PreviousActiveMenu) { + // raise events + IClickableMenu previousMenu = this.PreviousActiveMenu; + IClickableMenu newMenu = Game1.activeClickableMenu; if (Game1.activeClickableMenu != null) - MenuEvents.InvokeMenuChanged(this.Monitor, this.PreviousActiveMenu, Game1.activeClickableMenu); + MenuEvents.InvokeMenuChanged(this.Monitor, previousMenu, newMenu); else - MenuEvents.InvokeMenuClosed(this.Monitor, this.PreviousActiveMenu); - this.PreviousActiveMenu = Game1.activeClickableMenu; + MenuEvents.InvokeMenuClosed(this.Monitor, previousMenu); + + // update previous menu + // (if the menu was changed in one of the handlers, deliberately defer detection until the next update so mods can be notified of the new menu change) + this.PreviousActiveMenu = newMenu; } // raise location list changed diff --git a/src/StardewModdingAPI/Inheritance/SObject.cs b/src/StardewModdingAPI/Inheritance/SObject.cs index 4a218f33..eae5424d 100644 --- a/src/StardewModdingAPI/Inheritance/SObject.cs +++ b/src/StardewModdingAPI/Inheritance/SObject.cs @@ -49,7 +49,7 @@ namespace StardewModdingAPI.Inheritance *********/ public SObject() { - Program.DeprecationManager.Warn(nameof(SObject), "0.39.3", DeprecationLevel.Notice); + Program.DeprecationManager.Warn(nameof(SObject), "0.39.3", DeprecationLevel.Info); this.name = "Modded Item Name"; this.Description = "Modded Item Description"; diff --git a/src/StardewModdingAPI/LogWriter.cs b/src/StardewModdingAPI/LogWriter.cs index 11bcf5f3..058fa649 100644 --- a/src/StardewModdingAPI/LogWriter.cs +++ b/src/StardewModdingAPI/LogWriter.cs @@ -55,7 +55,7 @@ namespace StardewModdingAPI /// <summary>Raise a deprecation warning.</summary> private void WarnDeprecated() { - Program.DeprecationManager.Warn($"the {nameof(LogWriter)} class", "1.0", DeprecationLevel.Notice); + Program.DeprecationManager.Warn($"the {nameof(LogWriter)} class", "1.0", DeprecationLevel.Info); } } }
\ No newline at end of file diff --git a/src/StardewModdingAPI/Manifest.cs b/src/StardewModdingAPI/Manifest.cs index bfc66c43..018b31ae 100644 --- a/src/StardewModdingAPI/Manifest.cs +++ b/src/StardewModdingAPI/Manifest.cs @@ -3,21 +3,52 @@ using Newtonsoft.Json; namespace StardewModdingAPI { + /// <summary>Wraps <see cref="Manifest"/> so it can implement <see cref="IManifest"/> without breaking backwards compatibility.</summary> + [Obsolete("Use " + nameof(IManifest) + " or " + nameof(Mod) + "." + nameof(Mod.ModManifest) + " instead")] + internal class ManifestImpl : Manifest, IManifest + { + /// <summary>The mod version.</summary> + public new ISemanticVersion Version + { + get { return base.Version; } + set { base.Version = (Version)value; } + } + } + /// <summary>A manifest which describes a mod for SMAPI.</summary> public class Manifest { /********* ** Accessors *********/ - /// <summary>Whether the manifest defined the deprecated <see cref="Authour"/> field.</summary> - [JsonIgnore] - internal bool UsedAuthourField { get; private set; } - /// <summary>The mod name.</summary> - public virtual string Name { get; set; } = ""; + public string Name { get; set; } + + /// <summary>A brief description of the mod.</summary> + public string Description { get; set; } /// <summary>The mod author's name.</summary> - public virtual string Author { get; set; } = ""; + public string Author { get; set; } + + /// <summary>The mod version.</summary> + public Version Version { get; set; } = new Version(0, 0, 0, "", suppressDeprecationWarning: true); + + /// <summary>The minimum SMAPI version required by this mod, if any.</summary> + public string MinimumApiVersion { get; set; } + + /// <summary>The name of the DLL in the directory that has the <see cref="Mod.Entry"/> method.</summary> + public string EntryDll { get; set; } + + /// <summary>The unique mod ID.</summary> + public string UniqueID { get; set; } + + + /**** + ** Obsolete + ****/ + /// <summary>Whether the manifest defined the deprecated <see cref="Authour"/> field.</summary> + [JsonIgnore] + internal bool UsedAuthourField { get; private set; } /// <summary>Obsolete.</summary> [Obsolete("Use " + nameof(Manifest) + "." + nameof(Manifest.Author) + ".")] @@ -31,23 +62,8 @@ namespace StardewModdingAPI } } - /// <summary>The mod version.</summary> - public virtual Version Version { get; set; } = new Version(0, 0, 0, ""); - - /// <summary>A brief description of the mod.</summary> - public virtual string Description { get; set; } = ""; - - /// <summary>The unique mod ID.</summary> - public virtual string UniqueID { get; set; } = Guid.NewGuid().ToString(); - /// <summary>Whether the mod uses per-save config files.</summary> [Obsolete("Use " + nameof(Mod) + "." + nameof(Mod.Helper) + "." + nameof(IModHelper.ReadConfig) + " instead")] - public virtual bool PerSaveConfigs { get; set; } - - /// <summary>The minimum SMAPI version required by this mod, if any.</summary> - public string MinimumApiVersion { get; set; } - - /// <summary>The name of the DLL in the directory that has the <see cref="Mod.Entry"/> method.</summary> - public virtual string EntryDll { get; set; } = ""; + public bool PerSaveConfigs { get; set; } } } diff --git a/src/StardewModdingAPI/Mod.cs b/src/StardewModdingAPI/Mod.cs index 21551771..d12a7e05 100644 --- a/src/StardewModdingAPI/Mod.cs +++ b/src/StardewModdingAPI/Mod.cs @@ -5,7 +5,7 @@ using StardewModdingAPI.Framework; namespace StardewModdingAPI { /// <summary>The base class for a mod.</summary> - public class Mod + public class Mod : IMod { /********* ** Properties @@ -24,7 +24,18 @@ namespace StardewModdingAPI public IMonitor Monitor { get; internal set; } /// <summary>The mod's manifest.</summary> - public Manifest Manifest { get; internal set; } + [Obsolete("Use " + nameof(Mod) + "." + nameof(ModManifest))] + public Manifest Manifest + { + get + { + Program.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Manifest)}", "1.5", DeprecationLevel.Notice); + return (Manifest)this.ModManifest; + } + } + + /// <summary>The mod's manifest.</summary> + public IManifest ModManifest { get; internal set; } /// <summary>The full path to the mod's directory on the disk.</summary> [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.DirectoryPath) + " instead")] @@ -94,7 +105,7 @@ namespace StardewModdingAPI Program.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.PerSaveConfigFolder)}", "1.0", DeprecationLevel.Notice); Program.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.PathOnDisk)}", "1.0"); // avoid redundant warnings - if (!this.Manifest.PerSaveConfigs) + if (!((Manifest)this.Manifest).PerSaveConfigs) { this.Monitor.Log("Tried to fetch the per-save config folder, but this mod isn't configured to use per-save config files.", LogLevel.Error); return ""; diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 62b9dabd..090098ca 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -14,6 +14,7 @@ using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Events; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.AssemblyRewriting; +using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Inheritance; using StardewValley; using Monitor = StardewModdingAPI.Framework.Monitor; @@ -49,8 +50,8 @@ namespace StardewModdingAPI /// <summary>The core logger for SMAPI.</summary> private static readonly Monitor Monitor = new Monitor("SMAPI", Program.LogFile); - /// <summary>Whether SMAPI is running in developer mode.</summary> - private static bool DeveloperMode; + /// <summary>The user settings for SMAPI.</summary> + private static UserSettings Settings; /// <summary>Tracks whether the game should exit immediately and any pending initialisation should be cancelled.</summary> private static readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource(); @@ -98,28 +99,33 @@ namespace StardewModdingAPI Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB"); // add info header - Program.Monitor.Log($"SMAPI {Constants.Version} with Stardew Valley {Game1.version} on {Environment.OSVersion}", LogLevel.Info); + Program.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Game1.version} on {Environment.OSVersion}", LogLevel.Info); - // load user settings + // initialise user settings { - string settingsFileName = $"{typeof(Program).Assembly.GetName().Name}-settings.json"; - string settingsPath = Path.Combine(Constants.ExecutionPath, settingsFileName); + string settingsPath = Constants.ApiConfigPath; if (File.Exists(settingsPath)) { string json = File.ReadAllText(settingsPath); - UserSettings settings = JsonConvert.DeserializeObject<UserSettings>(json); - Program.DeveloperMode = settings?.DeveloperMode == true; - - if (Program.DeveloperMode) - { - Program.Monitor.ShowTraceInConsole = true; - Program.Monitor.Log($"SMAPI is running in developer mode. The console may be much more verbose. You can disable developer mode by deleting the {settingsFileName} file in the game directory.", LogLevel.Alert); - } + Program.Settings = JsonConvert.DeserializeObject<UserSettings>(json); } + else + Program.Settings = new UserSettings(); + + File.WriteAllText(settingsPath, JsonConvert.SerializeObject(Program.Settings, Formatting.Indented)); } + // add warning headers + if (Program.Settings.DeveloperMode) + { + Program.Monitor.ShowTraceInConsole = true; + Program.Monitor.Log($"You configured SMAPI to run in developer mode. The console may be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing or deleting {Constants.ApiConfigPath}.", LogLevel.Warn); + } + if (!Program.Settings.CheckForUpdates) + Program.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by editing or deleting {Constants.ApiConfigPath}.", LogLevel.Warn); + // initialise legacy log - Log.Monitor = new Monitor("legacy mod", Program.LogFile) { ShowTraceInConsole = Program.DeveloperMode }; + Log.Monitor = new Monitor("legacy mod", Program.LogFile) { ShowTraceInConsole = Program.Settings.DeveloperMode }; Log.ModRegistry = Program.ModRegistry; // hook into & launch the game @@ -145,7 +151,8 @@ namespace StardewModdingAPI } // check for update when game loads - GameEvents.GameLoaded += (sender, e) => Program.CheckForUpdateAsync(); + if (Program.Settings.CheckForUpdates) + GameEvents.GameLoaded += (sender, e) => Program.CheckForUpdateAsync(); // launch game Program.StartGame(); @@ -191,9 +198,9 @@ namespace StardewModdingAPI try { GitRelease release = UpdateHelper.GetLatestVersionAsync(Constants.GitHubRepository).Result; - Version latestVersion = new Version(release.Tag); - if (latestVersion.IsNewerThan(Constants.Version)) - Program.Monitor.Log($"You can update SMAPI from version {Constants.Version} to {latestVersion}", LogLevel.Alert); + ISemanticVersion latestVersion = new SemanticVersion(release.Tag); + if (latestVersion.IsNewerThan(Constants.ApiVersion)) + Program.Monitor.Log($"You can update SMAPI from version {Constants.ApiVersion} to {latestVersion}", LogLevel.Alert); } catch (Exception ex) { @@ -212,7 +219,7 @@ namespace StardewModdingAPI Program.StardewAssembly = Assembly.UnsafeLoadFrom(Program.GameExecutablePath); Program.StardewProgramType = Program.StardewAssembly.GetType("StardewValley.Program", true); Program.StardewGameInfo = Program.StardewProgramType.GetField("gamePtr"); - Game1.version += $"-Z_MODDED | SMAPI {Constants.Version}"; + Game1.version += $"-Z_MODDED | SMAPI {Constants.ApiVersion}"; // add error interceptors #if SMAPI_FOR_WINDOWS @@ -308,11 +315,27 @@ namespace StardewModdingAPI ModAssemblyLoader modAssemblyLoader = new ModAssemblyLoader(Program.CacheDirName, Program.TargetPlatform, Program.Monitor); AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); + // get known incompatible mods + IDictionary<string, IncompatibleMod> incompatibleMods; + try + { + incompatibleMods = File.Exists(Constants.ApiModMetadataPath) + ? JsonConvert.DeserializeObject<IncompatibleMod[]>(File.ReadAllText(Constants.ApiModMetadataPath)).ToDictionary(p => p.ID, p => p) + : new Dictionary<string, IncompatibleMod>(0); + } + catch (Exception ex) + { + incompatibleMods = new Dictionary<string, IncompatibleMod>(); + Program.Monitor.Log($"Couldn't read metadata file at {Constants.ApiModMetadataPath}. SMAPI will still run, but some features may be disabled.\n{ex}", LogLevel.Warn); + } + // load mods foreach (string directory in Directory.GetDirectories(Program.ModPath)) { + string directoryName = new DirectoryInfo(directory).Name; + // ignore internal directory - if (new DirectoryInfo(directory).Name == ".cache") + if (directoryName == ".cache") continue; // check for cancellation @@ -329,13 +352,13 @@ namespace StardewModdingAPI string manifestPath = Path.Combine(directory, "manifest.json"); if (!File.Exists(manifestPath)) { - Program.Monitor.Log($"Ignored folder \"{new DirectoryInfo(directory).Name}\" which doesn't have a manifest.json.", LogLevel.Warn); + Program.Monitor.Log($"Ignored folder \"{directoryName}\" which doesn't have a manifest.json.", LogLevel.Warn); continue; } string errorPrefix = $"Couldn't load mod for manifest '{manifestPath}'"; // read manifest - Manifest manifest; + ManifestImpl manifest; try { // read manifest text @@ -347,7 +370,7 @@ namespace StardewModdingAPI } // deserialise manifest - manifest = helper.ReadJsonFile<Manifest>("manifest.json"); + manifest = helper.ReadJsonFile<ManifestImpl>("manifest.json"); if (manifest == null) { Program.Monitor.Log($"{errorPrefix}: the manifest file does not exist.", LogLevel.Error); @@ -369,19 +392,36 @@ namespace StardewModdingAPI continue; } - // validate version + // validate known incompatible mods + IncompatibleMod compatibility; + if (incompatibleMods.TryGetValue(manifest.UniqueID ?? $"{manifest.Name}|{manifest.Author}|{manifest.EntryDll}", out compatibility)) + { + if (!compatibility.IsCompatible(manifest.Version)) + { + string warning = $"Skipped {compatibility.Name} ≤v{compatibility.Version} because this version is not compatible with the latest version of the game. Please check for a newer version of the mod here:"; + if (!string.IsNullOrWhiteSpace(compatibility.UpdateUrl)) + warning += $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}"; + if (!string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl)) + warning += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}"; + + Program.Monitor.Log(warning, LogLevel.Error); + continue; + } + } + + // validate SMAPI version if (!string.IsNullOrWhiteSpace(manifest.MinimumApiVersion)) { try { - Version minVersion = new Version(manifest.MinimumApiVersion); - if (minVersion.IsNewerThan(Constants.Version)) + ISemanticVersion minVersion = new SemanticVersion(manifest.MinimumApiVersion); + if (minVersion.IsNewerThan(Constants.ApiVersion)) { Program.Monitor.Log($"{errorPrefix}: this mod requires SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod.", LogLevel.Error); continue; } } - catch (FormatException ex) when (ex.Message.Contains("not a semantic version")) + catch (FormatException ex) when (ex.Message.Contains("not a valid semantic version")) { Program.Monitor.Log($"{errorPrefix}: the mod specified an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.Version}.", LogLevel.Error); continue; @@ -462,37 +502,53 @@ namespace StardewModdingAPI continue; } - // hook up mod + // get mod instance + Mod mod; try { + // get implementation TypeInfo modEntryType = modAssembly.DefinedTypes.First(x => x.BaseType == typeof(Mod)); - Mod modEntry = (Mod)modAssembly.CreateInstance(modEntryType.ToString()); - if (modEntry != null) + mod = (Mod)modAssembly.CreateInstance(modEntryType.ToString()); + if (mod == null) { - // track mod - Program.ModRegistry.Add(manifest, modAssembly); - - // hook up mod - modEntry.Manifest = manifest; - modEntry.Helper = helper; - modEntry.Monitor = new Monitor(manifest.Name, Program.LogFile) { ShowTraceInConsole = Program.DeveloperMode }; - modEntry.PathOnDisk = directory; - Program.Monitor.Log($"Loaded mod: {modEntry.Manifest.Name} by {modEntry.Manifest.Author}, v{modEntry.Manifest.Version} | {modEntry.Manifest.Description}", LogLevel.Info); - Program.ModsLoaded += 1; - modEntry.Entry(); // deprecated since 1.0 - modEntry.Entry((ModHelper)modEntry.Helper); // deprecated since 1.1 - modEntry.Entry(modEntry.Helper); // deprecated since 1.1 - - // raise deprecation warning for old Entry() method - if (Program.DeprecationManager.IsVirtualMethodImplemented(modEntryType, typeof(Mod), nameof(Mod.Entry), new[] { typeof(object[]) })) - Program.DeprecationManager.Warn(manifest.Name, $"an old version of {nameof(Mod)}.{nameof(Mod.Entry)}", "1.0", DeprecationLevel.Notice); - if (Program.DeprecationManager.IsVirtualMethodImplemented(modEntryType, typeof(Mod), nameof(Mod.Entry), new[] { typeof(ModHelper) })) - Program.DeprecationManager.Warn(manifest.Name, $"an old version of {nameof(Mod)}.{nameof(Mod.Entry)}", "1.1", DeprecationLevel.Notice); + Program.Monitor.Log($"{errorPrefix}: the mod's entry class could not be instantiated."); + continue; } + + // inject data + mod.ModManifest = manifest; + mod.Helper = helper; + mod.Monitor = new Monitor(manifest.Name, Program.LogFile) { ShowTraceInConsole = Program.Settings.DeveloperMode }; + mod.PathOnDisk = directory; + + // track mod + Program.ModRegistry.Add(mod); + Program.ModsLoaded += 1; + Program.Monitor.Log($"Loaded mod: {manifest.Name} by {manifest.Author}, v{manifest.Version} | {manifest.Description}", LogLevel.Info); } catch (Exception ex) { Program.Monitor.Log($"{errorPrefix}: an error occurred while loading the target DLL.\n{ex.GetLogSummary()}", LogLevel.Error); + continue; + } + + // call mod entry + try + { + // call entry methods + mod.Entry(); // deprecated since 1.0 + mod.Entry((ModHelper)mod.Helper); // deprecated since 1.1 + mod.Entry(mod.Helper); + + // raise deprecation warning for old Entry() methods + if (Program.DeprecationManager.IsVirtualMethodImplemented(mod.GetType(), typeof(Mod), nameof(Mod.Entry), new[] { typeof(object[]) })) + Program.DeprecationManager.Warn(manifest.Name, $"{nameof(Mod)}.{nameof(Mod.Entry)}(object[]) instead of {nameof(Mod)}.{nameof(Mod.Entry)}({nameof(IModHelper)})", "1.0", DeprecationLevel.Notice); + if (Program.DeprecationManager.IsVirtualMethodImplemented(mod.GetType(), typeof(Mod), nameof(Mod.Entry), new[] { typeof(ModHelper) })) + Program.DeprecationManager.Warn(manifest.Name, $"{nameof(Mod)}.{nameof(Mod.Entry)}({nameof(ModHelper)}) instead of {nameof(Mod)}.{nameof(Mod.Entry)}({nameof(IModHelper)})", "1.1", DeprecationLevel.Info); + } + catch (Exception ex) + { + Program.Monitor.Log($"The {manifest.Name} mod failed on entry initialisation. It will still be loaded, but may not function correctly.\n{ex.GetLogSummary()}", LogLevel.Warn); } } diff --git a/src/StardewModdingAPI/SemanticVersion.cs b/src/StardewModdingAPI/SemanticVersion.cs new file mode 100644 index 00000000..daefda51 --- /dev/null +++ b/src/StardewModdingAPI/SemanticVersion.cs @@ -0,0 +1,171 @@ +using System; +using System.Text.RegularExpressions; + +namespace StardewModdingAPI +{ + /// <summary>A semantic version with an optional release tag.</summary> + public class SemanticVersion : ISemanticVersion + { + /********* + ** Properties + *********/ + /// <summary>A regular expression matching a semantic version string.</summary> + /// <remarks>Derived from https://github.com/maxhauser/semver.</remarks> + private static readonly Regex Regex = new Regex(@"^(?<major>\d+)(\.(?<minor>\d+))?(\.(?<patch>\d+))?(?<build>.*)$", RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture); + + /// <summary>The backing field for <see cref="Build"/>.</summary> + private string _build; + + + /********* + ** Accessors + *********/ + /// <summary>The major version incremented for major API changes.</summary> + public int MajorVersion { get; set; } + + /// <summary>The minor version incremented for backwards-compatible changes.</summary> + public int MinorVersion { get; set; } + + /// <summary>The patch version for backwards-compatible bug fixes.</summary> + public int PatchVersion { get; set; } + + /// <summary>An optional build tag.</summary> + public string Build + { + get { return this._build; } + set { this._build = this.GetNormalisedTag(value); } + } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="major">The major version incremented for major API changes.</param> + /// <param name="minor">The minor version incremented for backwards-compatible changes.</param> + /// <param name="patch">The patch version for backwards-compatible bug fixes.</param> + /// <param name="build">An optional build tag.</param> + public SemanticVersion(int major, int minor, int patch, string build = null) + { + this.MajorVersion = major; + this.MinorVersion = minor; + this.PatchVersion = patch; + this.Build = build; + } + + /// <summary>Construct an instance.</summary> + /// <param name="version">The semantic version string.</param> + /// <exception cref="FormatException">The <paramref name="version"/> is not a valid semantic version.</exception> + internal SemanticVersion(string version) + { + var match = SemanticVersion.Regex.Match(version); + if (!match.Success) + throw new FormatException($"The input '{version}' is not a valid 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 : null; + } + + /// <summary>Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version.</summary> + /// <param name="other">The version to compare with this instance.</param> + /// <remarks>The implementation is defined by Semantic Version 2.0 (http://semver.org/).</remarks> + public int CompareTo(ISemanticVersion other) + { + const int same = 0; + const int curNewer = 1; + const int curOlder = -1; + + // compare stable versions + if (this.MajorVersion != other.MajorVersion) + return this.MajorVersion.CompareTo(other.MajorVersion); + if (this.MinorVersion != other.MinorVersion) + return this.MinorVersion.CompareTo(other.MinorVersion); + if (this.PatchVersion != other.PatchVersion) + return this.PatchVersion.CompareTo(other.PatchVersion); + if (this.Build == other.Build) + return same; + + // stable supercedes pre-release + bool curIsStable = string.IsNullOrWhiteSpace(this.Build); + bool otherIsStable = string.IsNullOrWhiteSpace(other.Build); + if (curIsStable) + return curNewer; + if (otherIsStable) + return curOlder; + + // compare two pre-release tag values + string[] curParts = this.Build.Split('.'); + string[] otherParts = other.Build.Split('.'); + for (int i = 0; i < curParts.Length; i++) + { + // longer prerelease tag supercedes if otherwise equal + if (otherParts.Length <= i) + return curNewer; + + // compare if different + if (curParts[i] != otherParts[i]) + { + // compare numerically if possible + { + int curNum, otherNum; + if (int.TryParse(curParts[i], out curNum) && int.TryParse(otherParts[i], out otherNum)) + return curNum.CompareTo(otherNum); + } + + // else compare lexically + return string.Compare(curParts[i], otherParts[i], StringComparison.OrdinalIgnoreCase); + } + } + + // fallback (this should never happen) + return string.Compare(this.ToString(), other.ToString(), StringComparison.InvariantCultureIgnoreCase); + } + + /// <summary>Get whether this version is older than the specified version.</summary> + /// <param name="other">The version to compare with this instance.</param> + public bool IsOlderThan(ISemanticVersion other) + { + return this.CompareTo(other) < 0; + } + + /// <summary>Get whether this version is newer than the specified version.</summary> + /// <param name="other">The version to compare with this instance.</param> + public bool IsNewerThan(ISemanticVersion other) + { + return this.CompareTo(other) > 0; + } + + /// <summary>Get a string representation of the version.</summary> + public override string ToString() + { + // version + string result = this.PatchVersion != 0 + ? $"{this.MajorVersion}.{this.MinorVersion}.{this.PatchVersion}" + : $"{this.MajorVersion}.{this.MinorVersion}"; + + // tag + string tag = this.Build; + if (tag != null) + result += $"-{tag}"; + return result; + } + + + /********* + ** Private methods + *********/ + /// <summary>Get a normalised build tag.</summary> + /// <param name="tag">The tag to normalise.</param> + private string GetNormalisedTag(string tag) + { + tag = tag?.Trim().Trim('-', '.'); + if (string.IsNullOrWhiteSpace(tag)) + return null; + if (tag == "0") + return null; // from incorrect examples in old SMAPI documentation + return tag; + } + } +} diff --git a/src/StardewModdingAPI/StardewModdingAPI-settings.json b/src/StardewModdingAPI/StardewModdingAPI-settings.json deleted file mode 100644 index d27d0141..00000000 --- a/src/StardewModdingAPI/StardewModdingAPI-settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - /* show all deprecation notices in the console? */ - "DeveloperMode": true -} diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json new file mode 100644 index 00000000..2abaf73a --- /dev/null +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -0,0 +1,4 @@ +{ + "DeveloperMode": true, + "CheckForUpdates": true +} diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 1a31b751..07b1ff5e 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -161,17 +161,22 @@ <Compile Include="Framework\DeprecationLevel.cs" /> <Compile Include="Framework\DeprecationManager.cs" /> <Compile Include="Framework\InternalExtensions.cs" /> + <Compile Include="Framework\Models\IncompatibleMod.cs" /> <Compile Include="Framework\ModAssemblyLoader.cs" /> + <Compile Include="Framework\Reflection\CacheEntry.cs" /> <Compile Include="Framework\Reflection\PrivateField.cs" /> <Compile Include="Framework\Reflection\PrivateMethod.cs" /> <Compile Include="Framework\Reflection\ReflectionHelper.cs" /> + <Compile Include="IManifest.cs" /> + <Compile Include="IMod.cs" /> <Compile Include="IModHelper.cs" /> <Compile Include="Framework\LogFileManager.cs" /> + <Compile Include="ISemanticVersion.cs" /> <Compile Include="LogLevel.cs" /> <Compile Include="Framework\ModRegistry.cs" /> <Compile Include="Framework\UpdateHelper.cs" /> - <Compile Include="Framework\GitRelease.cs" /> - <Compile Include="Framework\UserSettings.cs" /> + <Compile Include="Framework\Models\GitRelease.cs" /> + <Compile Include="Framework\Models\UserSettings.cs" /> <Compile Include="IMonitor.cs" /> <Compile Include="Inheritance\ChangeType.cs" /> <Compile Include="Inheritance\ItemStackChange.cs" /> @@ -189,6 +194,7 @@ <Compile Include="IPrivateField.cs" /> <Compile Include="IPrivateMethod.cs" /> <Compile Include="IReflectionHelper.cs" /> + <Compile Include="SemanticVersion.cs" /> <Compile Include="Version.cs" /> </ItemGroup> <ItemGroup> @@ -198,7 +204,10 @@ <None Include="packages.config"> <SubType>Designer</SubType> </None> - <Content Include="StardewModdingAPI-settings.json"> + <Content Include="StardewModdingAPI.config.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </Content> + <Content Include="StardewModdingAPI.data.json"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> <None Include="unix-launcher.sh"> @@ -247,6 +256,8 @@ </PropertyGroup> <Target Name="AfterBuild" Condition="$(Configuration) == 'Debug'"> <Copy SourceFiles="$(TargetDir)\$(TargetName).exe" DestinationFolder="$(GamePath)" /> + <Copy SourceFiles="$(TargetDir)\$(TargetName).config.json" DestinationFolder="$(GamePath)" /> + <Copy SourceFiles="$(TargetDir)\$(TargetName).data.json" DestinationFolder="$(GamePath)" /> <Copy SourceFiles="$(TargetDir)\StardewModdingAPI.AssemblyRewriters.dll" DestinationFolder="$(GamePath)" /> <Copy SourceFiles="$(TargetDir)\$(TargetName).exe.mdb" DestinationFolder="$(GamePath)" Condition="$(OS) != 'Windows_NT'" /> <Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)" Condition="$(OS) == 'Windows_NT'" /> diff --git a/src/StardewModdingAPI/StardewModdingAPI.data.json b/src/StardewModdingAPI/StardewModdingAPI.data.json new file mode 100644 index 00000000..49b45018 --- /dev/null +++ b/src/StardewModdingAPI/StardewModdingAPI.data.json @@ -0,0 +1,41 @@ +/* + + +This file contains advanced metadata for SMAPI. You shouldn't change this file. + + +*/ +[ + { + "ID": "SPDSprinklersMod", + "Name": "Better Sprinklers", + "Version": "2.1", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/41", + "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/125031", + "ForceCompatibleVersion": "^2.1-EntoPatch" + }, + { + "ID": "SPDChestLabel", + "Name": "Chest Label System", + "Version": "1.5", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/242", + "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/125031", + "ForceCompatibleVersion": "^1.5-EntoPatch" + }, + { + "ID": "CJBCheatsMenu", + "Name": "CJB Cheats Menu", + "Version": "1.12", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/4", + "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/125031", + "ForceCompatibleVersion": "^1.12-EntoPatch" + }, + { + "ID": "CJBItemSpawner", + "Name": "CJB Item Spawner", + "Version": "1.5", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/93", + "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/125031", + "ForceCompatibleVersion": "^1.5-EntoPatch" + } +] diff --git a/src/StardewModdingAPI/Version.cs b/src/StardewModdingAPI/Version.cs index bdecc8e0..87cd4f3d 100644 --- a/src/StardewModdingAPI/Version.cs +++ b/src/StardewModdingAPI/Version.cs @@ -1,22 +1,14 @@ using System; -using System.Text.RegularExpressions; using Newtonsoft.Json; using StardewModdingAPI.Framework; namespace StardewModdingAPI { /// <summary>A semantic version with an optional release tag.</summary> - public struct Version : IComparable<Version> + [Obsolete("Use " + nameof(SemanticVersion) + " or " + nameof(Manifest) + "." + nameof(Manifest.Version) + " instead")] + public struct Version : ISemanticVersion { /********* - ** Properties - *********/ - /// <summary>A regular expression matching a semantic version string.</summary> - /// <remarks>Derived from https://github.com/maxhauser/semver.</remarks> - private static readonly Regex Regex = new Regex(@"^(?<major>\d+)(\.(?<minor>\d+))?(\.(?<patch>\d+))?(?<build>.*)$", RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture); - - - /********* ** Accessors *********/ /// <summary>The major version incremented for major API changes.</summary> @@ -39,7 +31,7 @@ namespace StardewModdingAPI get { Program.DeprecationManager.Warn($"{nameof(Version)}.{nameof(Version.VersionString)}", "1.0", DeprecationLevel.Notice); - return this.ToString(); + return this.GetSemanticVersion().ToString(); } } @@ -53,85 +45,77 @@ namespace StardewModdingAPI /// <param name="patch">The patch version for backwards-compatible bug fixes.</param> /// <param name="build">An optional build tag.</param> public Version(int major, int minor, int patch, string build) + : this(major, minor, patch, build, suppressDeprecationWarning: false) + { } + + /// <summary>Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version.</summary> + /// <param name="other">The version to compare with this instance.</param> + public int CompareTo(Version other) { - this.MajorVersion = major; - this.MinorVersion = minor; - this.PatchVersion = patch; - this.Build = build; + return this.GetSemanticVersion().CompareTo(other); } - /// <summary>Construct an instance.</summary> - /// <param name="version">The semantic version string.</param> - internal Version(string version) + /// <summary>Get whether this version is newer than the specified version.</summary> + /// <param name="other">The version to compare with this instance.</param> + [Obsolete("Use " + nameof(ISemanticVersion) + "." + nameof(ISemanticVersion.IsNewerThan) + " instead")] + public bool IsNewerThan(Version other) { - 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(' ', '-', '.'); + return this.GetSemanticVersion().IsNewerThan(other); } - /// <summary>Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version.</summary> + /// <summary>Compares the current instance with another object of the same type and returns an integer that indicates whether the current instance precedes, follows, or occurs in the same position in the sort order as the other object. </summary> + /// <returns>A value that indicates the relative order of the objects being compared. The return value has these meanings: Value Meaning Less than zero This instance precedes <paramref name="other" /> in the sort order. Zero This instance occurs in the same position in the sort order as <paramref name="other" />. Greater than zero This instance follows <paramref name="other" /> in the sort order. </returns> + /// <param name="other">An object to compare with this instance. </param> + int IComparable<ISemanticVersion>.CompareTo(ISemanticVersion other) + { + return this.GetSemanticVersion().CompareTo(other); + } + + /// <summary>Get whether this version is older than the specified version.</summary> /// <param name="other">The version to compare with this instance.</param> - public int CompareTo(Version other) + bool ISemanticVersion.IsOlderThan(ISemanticVersion 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); + return this.GetSemanticVersion().IsOlderThan(other); } /// <summary>Get whether this version is newer than the specified version.</summary> /// <param name="other">The version to compare with this instance.</param> - public bool IsNewerThan(Version other) + bool ISemanticVersion.IsNewerThan(ISemanticVersion other) { - return this.CompareTo(other) > 0; + return this.GetSemanticVersion().IsNewerThan(other); } /// <summary>Get a string representation of the version.</summary> public override string ToString() { - // version - string result = this.PatchVersion != 0 - ? $"{this.MajorVersion}.{this.MinorVersion}.{this.PatchVersion}" - : $"{this.MajorVersion}.{this.MinorVersion}"; - - // tag - string tag = this.GetNormalisedTag(this.Build); - if (tag != null) - result += $"-{tag}"; - return result; + return this.GetSemanticVersion().ToString(); } - /********* ** Private methods *********/ - /// <summary>Get a normalised build tag.</summary> - /// <param name="tag">The tag to normalise.</param> - private string GetNormalisedTag(string tag) + /// <summary>Construct an instance.</summary> + /// <param name="major">The major version incremented for major API changes.</param> + /// <param name="minor">The minor version incremented for backwards-compatible changes.</param> + /// <param name="patch">The patch version for backwards-compatible bug fixes.</param> + /// <param name="build">An optional build tag.</param> + /// <param name="suppressDeprecationWarning">Whether to suppress the deprecation warning.</param> + internal Version(int major, int minor, int patch, string build, bool suppressDeprecationWarning) + { + if (!suppressDeprecationWarning) + Program.DeprecationManager.Warn($"{nameof(Version)}", "1.5", DeprecationLevel.Notice); + + this.MajorVersion = major; + this.MinorVersion = minor; + this.PatchVersion = patch; + this.Build = build; + } + + /// <summary>Get the equivalent semantic version.</summary> + /// <remarks>This is a hack so the struct can wrap <see cref="SemanticVersion"/> without a mutable backing field, which would cause a <see cref="StackOverflowException"/> due to recreating the struct value on each change.</remarks> + private SemanticVersion GetSemanticVersion() { - tag = tag?.Trim().Trim('-', '.'); - if (string.IsNullOrWhiteSpace(tag) || tag == "0") - return null; - return tag; + return new SemanticVersion(this.MajorVersion, this.MinorVersion, this.PatchVersion, this.Build); } } } |