From 9c1617c9ee51a0f6b93242fe8fc789336957460c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 11 Apr 2018 21:15:16 -0400 Subject: drop support for Stardew Valley 1.2 (#453) --- src/SMAPI/StardewModdingAPI.csproj | 3 --- 1 file changed, 3 deletions(-) (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index edddbd2a..cf476193 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -231,7 +231,6 @@ - @@ -248,8 +247,6 @@ - - -- cgit From 5997857064f4d6bb0747e84e1dbd1556c97b7481 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 12 Apr 2018 00:18:32 -0400 Subject: fix various net field conversions in SMAPI code (#453) --- src/SMAPI/Context.cs | 2 +- src/SMAPI/Framework/SGame.cs | 7 ++++--- src/SMAPI/Metadata/CoreAssetPropagator.cs | 27 ++++++++++++++------------- src/SMAPI/StardewModdingAPI.csproj | 3 +++ 4 files changed, 22 insertions(+), 17 deletions(-) (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI/Context.cs b/src/SMAPI/Context.cs index 7ed9b052..79067b2d 100644 --- a/src/SMAPI/Context.cs +++ b/src/SMAPI/Context.cs @@ -35,7 +35,7 @@ namespace StardewModdingAPI ** Internal ****/ /// Whether a player save has been loaded. - internal static bool IsSaveLoaded => Game1.hasLoadedGame && !string.IsNullOrEmpty(Game1.player.name); + internal static bool IsSaveLoaded => Game1.hasLoadedGame && !string.IsNullOrEmpty(Game1.player.Name); /// Whether the game is currently writing to the save file. internal static bool IsSaving => Game1.activeClickableMenu is SaveGameMenu || Game1.activeClickableMenu is ShippingMenu; // saving is performed by SaveGameMenu, but it's wrapped by ShippingMenu on days when the player shipping something diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index b486f8cd..67390882 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -500,9 +500,9 @@ namespace StardewModdingAPI.Framework this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Luck, Game1.player.luckLevel)); // raise player inventory changed - ItemStackChange[] changedItems = this.GetInventoryChanges(Game1.player.items, this.PreviousItems).ToArray(); + ItemStackChange[] changedItems = this.GetInventoryChanges(Game1.player.Items, this.PreviousItems).ToArray(); if (changedItems.Any()) - this.Events.Player_InventoryChanged.Raise(new EventArgsInventoryChanged(Game1.player.items, changedItems.ToList())); + this.Events.Player_InventoryChanged.Raise(new EventArgsInventoryChanged(Game1.player.Items, changedItems.ToList())); // raise current location's object list changed if (this.GetHash(Game1.currentLocation.objects) != this.PreviousLocationObjects) @@ -526,7 +526,7 @@ namespace StardewModdingAPI.Framework this.PreviousForagingLevel = Game1.player.foragingLevel; this.PreviousMiningLevel = Game1.player.miningLevel; this.PreviousLuckLevel = Game1.player.luckLevel; - this.PreviousItems = Game1.player.items.Where(n => n != null).Distinct().ToDictionary(n => n, n => n.Stack); + this.PreviousItems = Game1.player.Items.Where(n => n != null).Distinct().ToDictionary(n => n, n => n.Stack); this.PreviousLocationObjects = this.GetHash(Game1.currentLocation.objects); this.PreviousTime = Game1.timeOfDay; this.PreviousMineLevel = Game1.mine?.mineLevel ?? 0; @@ -633,6 +633,7 @@ namespace StardewModdingAPI.Framework [SuppressMessage("ReSharper", "RedundantCast", Justification = "copied from game code as-is")] [SuppressMessage("ReSharper", "RedundantExplicitNullableCreation", Justification = "copied from game code as-is")] [SuppressMessage("ReSharper", "RedundantTypeArgumentsOfMethod", Justification = "copied from game code as-is")] + [SuppressMessage("SMAPI.CommonErrors", "SMAPI002", Justification = "copied from game code as-is")] private void DrawImpl(GameTime gameTime) { if (Game1.debugMode) diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 01ea24df..37623f32 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -90,12 +90,12 @@ namespace StardewModdingAPI.Metadata return FarmerRenderer.accessoriesTexture = content.Load(key); case "characters\\farmer\\farmer_base": // Farmer - if (Game1.player == null || !Game1.player.isMale) + if (Game1.player == null || !Game1.player.IsMale) return false; return Game1.player.FarmerRenderer = new FarmerRenderer(key); case "characters\\farmer\\farmer_girl_base": // Farmer - if (Game1.player == null || Game1.player.isMale) + if (Game1.player == null || Game1.player.IsMale) return false; return Game1.player.FarmerRenderer = new FarmerRenderer(key); @@ -372,16 +372,16 @@ namespace StardewModdingAPI.Metadata foreach (FarmAnimal animal in animals) { // get expected key - string expectedKey = animal.age < animal.ageWhenMature - ? $"Baby{(animal.type == "Duck" ? "White Chicken" : animal.type)}" + string expectedKey = animal.age.Value < animal.ageWhenMature.Value + ? $"Baby{(animal.type.Value == "Duck" ? "White Chicken" : animal.type.Value)}" : animal.type; - if (animal.showDifferentTextureWhenReadyForHarvest && animal.currentProduce <= 0) + if (animal.showDifferentTextureWhenReadyForHarvest && animal.currentProduce.Value <= 0) expectedKey = $"Sheared{expectedKey}"; expectedKey = $"Animals\\{expectedKey}"; // reload asset if (expectedKey == key) - this.SetSpriteTexture(animal.sprite, texture.Value); + this.SetSpriteTexture(animal.Sprite, texture.Value); } return texture.IsValueCreated; } @@ -397,7 +397,7 @@ namespace StardewModdingAPI.Metadata Building[] buildings = Game1.locations .OfType() .SelectMany(p => p.buildings) - .Where(p => p.buildingType == type) + .Where(p => p.buildingType.Value == type) .ToArray(); // reload buildings @@ -427,7 +427,7 @@ namespace StardewModdingAPI.Metadata from fence in location.Objects.Values.OfType() where fenceType == 1 ? fence.isGate - : fence.whichType == fenceType + : fence.whichType.Value == fenceType select fence ) .ToArray(); @@ -447,7 +447,7 @@ namespace StardewModdingAPI.Metadata { // get NPCs string name = this.GetNpcNameFromFileName(Path.GetFileName(key)); - NPC[] characters = this.GetCharacters().Where(npc => npc.name == name && npc.IsMonster == monster).ToArray(); + NPC[] characters = this.GetCharacters().Where(npc => npc.Name == name && npc.IsMonster == monster).ToArray(); if (!characters.Any()) return false; @@ -466,7 +466,7 @@ namespace StardewModdingAPI.Metadata { // get NPCs string name = this.GetNpcNameFromFileName(Path.GetFileName(key)); - NPC[] villagers = this.GetCharacters().Where(npc => npc.name == name && npc.isVillager()).ToArray(); + NPC[] villagers = this.GetCharacters().Where(npc => npc.Name == name && npc.isVillager()).ToArray(); if (!villagers.Any()) return false; @@ -486,7 +486,7 @@ namespace StardewModdingAPI.Metadata { Tree[] trees = Game1.locations .SelectMany(p => p.terrainFeatures.Values.OfType()) - .Where(tree => tree.treeType == type) + .Where(tree => tree.treeType.Value == type) .ToArray(); if (trees.Any()) @@ -562,8 +562,9 @@ namespace StardewModdingAPI.Metadata { foreach (Building building in buildableLocation.buildings) { - if (building.indoors != null) - yield return building.indoors; + GameLocation indoors = building.indoors; + if (indoors != null) + yield return indoors; } } } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index cf476193..c0d5386a 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -288,6 +288,9 @@ StardewModdingAPI.AssemblyRewriters + + + \ No newline at end of file -- cgit From a3ade7a5126642f42794281057349fa5ff737cdd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 13 Apr 2018 22:41:34 -0400 Subject: split mod DB into a separate file The mod metadata has grown over time, and there's no need to keep it in memory after mod loading. This lets us load the config earlier (since it has a smaller impact on memory usage which affects the game's audio code), and lets us discard the mod metadata when we're done with it. --- build/common.targets | 1 + build/prepare-install-package.targets | 2 + docs/release-notes.md | 1 + src/SMAPI/Constants.cs | 3 + src/SMAPI/Framework/Models/SConfig.cs | 6 - src/SMAPI/Framework/Models/SMetadata.cs | 15 + src/SMAPI/Program.cs | 23 +- src/SMAPI/StardewModdingAPI.config.json | 1837 +---------------------------- src/SMAPI/StardewModdingAPI.csproj | 4 + src/SMAPI/StardewModdingAPI.metadata.json | 1836 ++++++++++++++++++++++++++++ 10 files changed, 1875 insertions(+), 1853 deletions(-) create mode 100644 src/SMAPI/Framework/Models/SMetadata.cs create mode 100644 src/SMAPI/StardewModdingAPI.metadata.json (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/build/common.targets b/build/common.targets index 1230ec15..a6cead8f 100644 --- a/build/common.targets +++ b/build/common.targets @@ -87,6 +87,7 @@ + diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index fca311f2..348cc1bc 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -31,6 +31,7 @@ + @@ -44,6 +45,7 @@ + diff --git a/docs/release-notes.md b/docs/release-notes.md index e68720da..f0202ee1 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -15,6 +15,7 @@ * For SMAPI developers: * Added prerelease versions to the mod update-check API response where available (GitHub only). * Added support for beta releases on the home page. + * Split mod DB out of `StardewModdingAPI.config.json`, so we can load config earlier and reduce unnecessary memory usage later. --> ## 2.5.5 diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index e547dfa6..a098194b 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -72,6 +72,9 @@ namespace StardewModdingAPI /// The file path for the SMAPI configuration file. internal static string ApiConfigPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.config.json"); + /// The file path for the SMAPI metadata file. + internal static string ApiMetadataPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.metadata.json"); + /// The file path to the log where the latest output should be saved. internal static string DefaultLogPath => Path.Combine(Constants.LogDir, "SMAPI-latest.txt"); diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index 17169714..2d6da0fa 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -1,6 +1,3 @@ -using System.Collections.Generic; -using StardewModdingAPI.Framework.ModData; - namespace StardewModdingAPI.Framework.Models { /// The SMAPI configuration settings. @@ -23,8 +20,5 @@ namespace StardewModdingAPI.Framework.Models /// Whether SMAPI should log more information about the game context. public bool VerboseLogging { get; set; } - - /// Extra metadata about mods. - public IDictionary ModData { get; set; } } } diff --git a/src/SMAPI/Framework/Models/SMetadata.cs b/src/SMAPI/Framework/Models/SMetadata.cs new file mode 100644 index 00000000..9ff495e9 --- /dev/null +++ b/src/SMAPI/Framework/Models/SMetadata.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using StardewModdingAPI.Framework.ModData; + +namespace StardewModdingAPI.Framework.Models +{ + /// The SMAPI predefined metadata. + internal class SMetadata + { + /******** + ** Accessors + ********/ + /// Extra metadata about mods. + public IDictionary ModData { get; set; } + } +} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index ff4e9a50..da2c0e8e 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -54,16 +54,15 @@ namespace StardewModdingAPI /// Simplifies access to private game code. private readonly Reflector Reflection = new Reflector(); + /// The SMAPI configuration settings. + private readonly SConfig Settings; + /// The underlying game instance. private SGame GameInstance; /// The underlying content manager. private ContentCore ContentCore => this.GameInstance.ContentCore; - /// The SMAPI configuration settings. - /// This is initialised after the game starts. - private SConfig Settings; - /// Tracks the installed mods. /// This is initialised after the game starts. private readonly ModRegistry ModRegistry = new ModRegistry(); @@ -133,8 +132,14 @@ namespace StardewModdingAPI public Program(bool writeToConsole, string logPath) { // init basics + this.Settings = JsonConvert.DeserializeObject(File.ReadAllText(Constants.ApiConfigPath)); this.LogFile = new LogFileManager(logPath); - this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.CancellationTokenSource) { WriteToConsole = writeToConsole }; + this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.CancellationTokenSource) + { + WriteToConsole = writeToConsole, + ShowTraceInConsole = this.Settings.DeveloperMode, + ShowFullStampInConsole = this.Settings.DeveloperMode + }; this.EventManager = new EventManager(this.Monitor, this.ModRegistry); // hook up events @@ -345,7 +350,6 @@ namespace StardewModdingAPI private void InitialiseAfterGameStart() { // load settings - this.Settings = JsonConvert.DeserializeObject(File.ReadAllText(Constants.ApiConfigPath)); this.GameInstance.VerboseLogging = this.Settings.VerboseLogging; // load core components @@ -361,11 +365,7 @@ namespace StardewModdingAPI // add headers if (this.Settings.DeveloperMode) - { - this.Monitor.ShowTraceInConsole = true; - this.Monitor.ShowFullStampInConsole = true; this.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 {Constants.ApiConfigPath}.", LogLevel.Info); - } if (!this.Settings.CheckForUpdates) this.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 reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn); if (!this.Monitor.WriteToConsole) @@ -377,7 +377,8 @@ namespace StardewModdingAPI this.Monitor.Log("SMAPI found problems in your game's content files which are likely to cause errors or crashes. Consider uninstalling XNB mods or reinstalling the game.", LogLevel.Error); // load mod data - ModDatabase modDatabase = new ModDatabase(this.Settings.ModData, Constants.GetUpdateUrl); + SMetadata metadata = JsonConvert.DeserializeObject(File.ReadAllText(Constants.ApiMetadataPath)); + ModDatabase modDatabase = new ModDatabase(metadata.ModData, Constants.GetUpdateUrl); // load mods { diff --git a/src/SMAPI/StardewModdingAPI.config.json b/src/SMAPI/StardewModdingAPI.config.json index f7082e96..9743894a 100644 --- a/src/SMAPI/StardewModdingAPI.config.json +++ b/src/SMAPI/StardewModdingAPI.config.json @@ -36,1840 +36,5 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha /** * Whether SMAPI should log more information about the game context. */ - "VerboseLogging": false, - - /** - * Metadata about some SMAPI mods used in compatibility, update, and dependency checks. This - * field shouldn't be edited by players in most cases. - * - * Standard fields - * =============== - * The predefined fields are documented below (only 'ID' is required). Each entry's key is the - * default display name for the mod if one isn't available (e.g. in dependency checks). - * - * - ID: the mod's latest unique ID (if any). - * - * - FormerIDs: uniquely identifies the mod across multiple versions, and supports matching - * other fields if no ID was specified. This doesn't include the latest ID, if any. - * Format rules: - * 1. If the mod's ID changed over time, multiple variants can be separated by the '|' - * character. - * 2. Each variant can take one of two forms: - * - A simple string matching the mod's UniqueID value. - * - A JSON structure containing any of four manifest fields (ID, Name, Author, and - * EntryDll) to match. - * - * - MapLocalVersions and MapRemoteVersions correct local manifest versions and remote versions - * during update checks. For example, if the API returns version '1.1-1078' where '1078' is - * intended to be a build number, MapRemoteVersions can map it to '1.1' when comparing to the - * mod's current version. This is only meant to support legacy mods with injected update keys. - * - * Versioned metadata - * ================== - * Each record can also specify extra metadata using the field keys below. - * - * Each key consists of a field name prefixed with any combination of version range and 'Default', - * separated by pipes (whitespace trimmed). For example, 'UpdateKey' will always override, - * 'Default | UpdateKey' will only override if the mod has no update keys, and - * '~1.1 | Default | Name' will do the same up to version 1.1. - * - * The version format is 'min~max' (where either side can be blank for unbounded), or a single - * version number. - * - * These are the valid field names: - * - * - UpdateKey: the update key to set in the mod's manifest. This is used to enable update - * checks for older mods that haven't been updated to use it yet. - * - * - Status: overrides compatibility checks. The possible values are Obsolete (SMAPI won't load - * it because the mod should no longer be used), AssumeBroken (SMAPI won't load it because - * the specified version isn't compatible), or AssumeCompatible (SMAPI will try to load it - * even if it detects incompatible code). - * - * - StatusReasonPhrase: a message to show to the player explaining why the mod can't be loaded - * (if applicable). If blank, will default to a generic not-compatible message. - * - * - AlternativeUrl: a URL where the player can find an unofficial update or alternative if the - * mod is no longer compatible. - */ - "ModData": { - "AccessChestAnywhere": { - "ID": "AccessChestAnywhere", - "MapLocalVersions": { "1.1-1078": "1.1" }, - "Default | UpdateKey": "Nexus:257", - "~1.1 | Status": "AssumeBroken" - }, - - "AdjustArtisanPrices": { - "ID": "ThatNorthernMonkey.AdjustArtisanPrices", - "FormerIDs": "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc", // changed in 0.0.2-pathoschild-update - "MapRemoteVersions": { "0.01": "0.0.1" }, - "Default | UpdateKey": "Chucklefish:3532", - "~0.0.1 | Status": "AssumeBroken" - }, - - "Adjust Monster": { - "ID": "mmanlapat.AdjustMonster", - "Default | UpdateKey": "Nexus:1161" - }, - - "Advanced Location Loader": { - "ID": "Entoarox.AdvancedLocationLoader", - "~1.3.7 | UpdateKey": "Chucklefish:3619", // only enable update checks up to 1.3.7 by request (has its own update-check feature) - "~1.2.10 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Adventure Shop Inventory": { - "ID": "HammurabiAdventureShopInventory", - "Default | UpdateKey": "Chucklefish:4608" - }, - - "AgingMod": { - "ID": "skn.AgingMod", - "Default | UpdateKey": "Nexus:1129", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "All Crops All Seasons": { - "ID": "cantorsdust.AllCropsAllSeasons", - "FormerIDs": "29ee8246-d67b-4242-a340-35a9ae0d5dd7 | community.AllCropsAllSeasons", // changed in 1.3 and 1.5 - "Default | UpdateKey": "Nexus:170" - }, - - "All Professions": { - "ID": "cantorsdust.AllProfessions", - "FormerIDs": "8c37b1a7-4bfb-4916-9d8a-9533e6363ea3 | community.AllProfessions", // changed in 1.2 and 1.3.1 - "Default | UpdateKey": "Nexus:174" - }, - - "Almighty Tool": { - "ID": "439", - "FormerIDs": "{EntryDll: 'AlmightyTool.dll'}", // changed in 1.2.1 - "MapRemoteVersions": { "1.21": "1.2.1" }, - "Default | UpdateKey": "Nexus:439", - "~1.1.1 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Animal Husbandry": { - "ID": "DIGUS.ANIMALHUSBANDRYMOD", - "FormerIDs": "DIGUS.BUTCHER", // changed in 2.0.1 - "Default | UpdateKey": "Nexus:1538" - }, - - "Animal Mood Fix": { - "ID": "GPeters-AnimalMoodFix", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2." - }, - - "Animal Sitter": { - "ID": "jwdred.AnimalSitter", - "FormerIDs": "{EntryDll: 'AnimalSitter.dll'}", // changed in 1.0.9 - "Default | UpdateKey": "Nexus:581", - "~1.0.8 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Ashley Mod": { - "FormerIDs": "{EntryDll: 'AshleyMod.dll'}", - "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "A Tapper's Dream": { - "ID": "ddde5195-8f85-4061-90cc-0d4fd5459358", - "Default | UpdateKey": "Nexus:260", - "~1.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Auto Animal Doors": { - "ID": "AaronTaggart.AutoAnimalDoors", - "Default | UpdateKey": "Nexus:1019" - }, - - "Auto-Eat": { - "ID": "Permamiss.AutoEat", - "FormerIDs": "BALANCEMOD_AutoEat", // changed in 1.1.1 - "Default | UpdateKey": "Nexus:643" - }, - - "AutoFish": { - "ID": "WhiteMind.AF", - "Default | UpdateKey": "Nexus:1895" - }, - - "AutoGate": { - "ID": "AutoGate", - "Default | UpdateKey": "Nexus:820" - }, - - "Automate": { - "ID": "Pathoschild.Automate", - "Default | UpdateKey": "Nexus:1063" - }, - - "Automated Doors": { - "ID": "azah.automated-doors", - "FormerIDs": "1abcfa07-2cf4-4dc3-a6e9-6068b642112b", // changed in 1.4.1 - "Default | UpdateKey": "GitHub:azah/AutomatedDoors" // added in 1.4.2 - }, - - "AutoSpeed": { - "ID": "Omegasis.AutoSpeed", - "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'AutoSpeed'}", // changed in 1.4; disambiguate from other Alpha_Omegasis mods - "Default | UpdateKey": "Nexus:443" // added in 1.4.1 - }, - - "Basic Sprinklers Improved": { - "ID": "lrsk_sdvm_bsi.0117171308", - "MapRemoteVersions": { "1.0.2": "1.0.1-release" }, // manifest not updated - "Default | UpdateKey": "Nexus:833" - }, - - "Better Hay": { - "ID": "cat.betterhay", - "Default | UpdateKey": "Nexus:1430" - }, - - "Better Quality More Seasons": { - "ID": "SB_BQMS", - "Default | UpdateKey": "Nexus:935" - }, - - "Better Quarry": { - "ID": "BetterQuarry", - "Default | UpdateKey": "Nexus:771" - }, - - "Better Ranching": { - "ID": "BetterRanching", - "Default | UpdateKey": "Nexus:859" - }, - - "Better Shipping Box": { - "ID": "Kithio:BetterShippingBox", - "MapLocalVersions": { "1.0.1": "1.0.2" }, - "Default | UpdateKey": "Chucklefish:4302", - "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Better Sprinklers": { - "ID": "Speeder.BetterSprinklers", - "FormerIDs": "SPDSprinklersMod", // changed in 2.3 - "Default | UpdateKey": "Nexus:41", - "~2.3.1-pathoschild-update | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Billboard Anywhere": { - "ID": "Omegasis.BillboardAnywhere", - "FormerIDs": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Billboard Anywhere'}", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis - "Default | UpdateKey": "Nexus:492" // added in 1.4.1 - }, - - "Birthday Mail": { - "ID": "KathrynHazuka.BirthdayMail", - "FormerIDs": "005e02dc-d900-425c-9c68-1ff55c5a295d", // changed in 1.2.3-pathoschild-update - "Default | UpdateKey": "Nexus:276", - "~1.2.2 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Breed Like Rabbits": { - "ID": "dycedarger.breedlikerabbits", - "Default | UpdateKey": "Nexus:948" - }, - - "Build Endurance": { - "ID": "Omegasis.BuildEndurance", - "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildEndurance'}", // changed in 1.4; disambiguate from other Alpha_Omegasis mods - "Default | UpdateKey": "Nexus:445", // added in 1.4.1 - "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Build Health": { - "ID": "Omegasis.BuildHealth", - "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildHealth'}", // changed in 1.4; disambiguate from other Alpha_Omegasis mods - "Default | UpdateKey": "Nexus:446", // added in 1.4.1 - "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Buy Cooking Recipes": { - "ID": "Denifia.BuyRecipes", - "Default | UpdateKey": "Nexus:1126", // added in 1.0.1 (2017-10-04) - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Buy Back Collectables": { - "ID": "Omegasis.BuyBackCollectables", - "FormerIDs": "BuyBackCollectables", // changed in 1.4 - "Default | UpdateKey": "Nexus:507", // added in 1.4.1 - "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Carry Chest": { - "ID": "spacechase0.CarryChest", - "Default | UpdateKey": "Nexus:1333" - }, - - "Casks Anywhere": { - "ID": "CasksAnywhere", - "MapLocalVersions": { "1.1-alpha": "1.1" }, - "Default | UpdateKey": "Nexus:878" - }, - - "Categorize Chests": { - "ID": "CategorizeChests", - "Default | UpdateKey": "Nexus:1300" - }, - - "Chefs Closet": { - "ID": "Duder.ChefsCloset", - "MapLocalVersions": { "1.3-1": "1.3" }, - "Default | UpdateKey": "Nexus:1030" - }, - - "Chest Label System": { - "ID": "Speeder.ChestLabel", - "FormerIDs": "SPDChestLabel", // changed in 1.5.1-pathoschild-update - "Default | UpdateKey": "Nexus:242", - "~1.6 | Status": "AssumeBroken" // broke in SDV 1.1 - }, - - "Chest Pooling": { - "ID": "mralbobo.ChestPooling", - "FormerIDs": "{EntryDll: 'ChestPooling.dll'}", // changed in 1.3 - "Default | UpdateKey": "GitHub:mralbobo/stardew-chest-pooling", - "~1.2 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Chests Anywhere": { - "ID": "Pathoschild.ChestsAnywhere", - "FormerIDs": "ChestsAnywhere", // changed in 1.9 - "Default | UpdateKey": "Nexus:518", - "~1.9-beta | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Choose Baby Gender": { - "FormerIDs": "{EntryDll: 'ChooseBabyGender.dll'}", - "Default | UpdateKey": "Nexus:590", - "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "CJB Automation": { - "ID": "CJBAutomation", - "Default | UpdateKey": "Nexus:211", - "~1.4 | Status": "AssumeBroken", // broke in SDV 1.2 - "~1.4 | AlternativeUrl": "http://www.nexusmods.com/stardewvalley/mods/1063" - }, - - "CJB Cheats Menu": { - "ID": "CJBok.CheatsMenu", - "FormerIDs": "CJBCheatsMenu", // changed in 1.14 - "Default | UpdateKey": "Nexus:4", - "~1.12 | Status": "AssumeBroken" // broke in SDV 1.1 - }, - - "CJB Item Spawner": { - "ID": "CJBok.ItemSpawner", - "FormerIDs": "CJBItemSpawner", // changed in 1.7 - "Default | UpdateKey": "Nexus:93", - "~1.5 | Status": "AssumeBroken" // broke in SDV 1.1 - }, - - "CJB Show Item Sell Price": { - "ID": "CJBok.ShowItemSellPrice", - "FormerIDs": "CJBShowItemSellPrice", // changed in 1.7 - "Default | UpdateKey": "Nexus:5", - "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Clean Farm": { - "ID": "tstaples.CleanFarm", - "Default | UpdateKey": "Nexus:794" - }, - - "Climates of Ferngill": { - "ID": "KoihimeNakamura.ClimatesOfFerngill", - "Default | UpdateKey": "Nexus:604", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Coal Regen": { - "ID": "Blucifer.CoalRegen", - "Default | UpdateKey": "Nexus:1664" - }, - - "Cold Weather Haley": { - "ID": "LordXamon.ColdWeatherHaleyPRO", - "Default | UpdateKey": "Nexus:1169", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Colored Chests": { - "ID": "4befde5c-731c-4853-8e4b-c5cdf946805f", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "colored chests were added in Stardew Valley 1.1." - }, - - "Combat with Farm Implements": { - "ID": "SPDFarmingImplementsInCombat", - "Default | UpdateKey": "Nexus:313", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Community Bundle Item Tooltip": { - "ID": "musbah.bundleTooltip", - "Default | UpdateKey": "Nexus:1329" - }, - - "Concentration on Farming": { - "ID": "punyo.ConcentrationOnFarming", - "Default | UpdateKey": "Nexus:1445" - }, - - "Configurable Machines": { - "ID": "21da6619-dc03-4660-9794-8e5b498f5b97", - "MapLocalVersions": { "1.2-beta": "1.2" }, - "Default | UpdateKey": "Nexus:280" - }, - - "Configurable Shipping Dates": { - "ID": "ConfigurableShippingDates", - "Default | UpdateKey": "Nexus:675", - "~1.1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Cooking Skill": { - "ID": "spacechase0.CookingSkill", - "FormerIDs": "CookingSkill", // changed in 1.0.4–6 - "Default | UpdateKey": "Nexus:522", - "~1.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "CrabNet": { - "ID": "jwdred.CrabNet", - "FormerIDs": "{EntryDll: 'CrabNet.dll'}", // changed in 1.0.5 - "Default | UpdateKey": "Nexus:584", - "~1.0.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Crafting Counter": { - "ID": "lolpcgaming.CraftingCounter", - "Default | UpdateKey": "Nexus:1585" - }, - - "Current Location": { - "ID": "CurrentLocation102120161203", - "Default | UpdateKey": "Nexus:638" - }, - - "Custom Asset Modifier": { - "ID": "Omegasis.CustomAssetModifier", - "Default | UpdateKey": "1836" - }, - - "Custom Critters": { - "ID": "spacechase0.CustomCritters", - "Default | UpdateKey": "Nexus:1255" - }, - - "Custom Crops": { - "ID": "spacechase0.CustomCrops", - "Default | UpdateKey": "Nexus:1592" - }, - - "Custom Element Handler": { - "ID": "Platonymous.CustomElementHandler", - "Default | UpdateKey": "Nexus:1068" // added in 1.3.1 - }, - - "Custom Farming Redux": { - "ID": "Platonymous.CustomFarming", - "Default | UpdateKey": "Nexus:991" // added in 0.6.1 - }, - - "Custom Farming Automate Bridge": { - "ID": "Platonymous.CFAutomate", - "~1.0.1 | Status": "AssumeBroken", // no longer compatible with Automate - "~1.0.1 | AlternativeUrl": "https://www.nexusmods.com/stardewvalley/mods/991" - }, - - "Custom Farm Types": { - "ID": "spacechase0.CustomFarmTypes", - "Default | UpdateKey": "Nexus:1140" - }, - - "Custom Furniture": { - "ID": "Platonymous.CustomFurniture", - "Default | UpdateKey": "Nexus:1254" // added in 0.4.1 - }, - - "Customize Exterior": { - "ID": "spacechase0.CustomizeExterior", - "FormerIDs": "CustomizeExterior", // changed in 1.0.3 - "Default | UpdateKey": "Nexus:1099", - "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Customizable Cart Redux": { - "ID": "KoihimeNakamura.CCR", - "MapLocalVersions": { "1.1-20170917": "1.1" }, - "Default | UpdateKey": "Nexus:1402" - }, - - "Customizable Traveling Cart Days": { - "ID": "TravelingCartYyeahdude", - "Default | UpdateKey": "Nexus:567", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Custom Linens": { - "ID": "Mevima.CustomLinens", - "MapRemoteVersions": { "1.1": "1.0" }, // manifest not updated - "Default | UpdateKey": "Nexus:1027" - }, - - "Custom NPC": { - "ID": "Platonymous.CustomNPC", - "Default | UpdateKey": "Nexus:1607" - }, - - "Custom Shops Redux": { - "ID": "Omegasis.CustomShopReduxGui", - "Default | UpdateKey": "Nexus:1378" // added in 1.4.1 - }, - - "Custom TV": { - "ID": "Platonymous.CustomTV", - "Default | UpdateKey": "Nexus:1139" // added in 1.0.6 - }, - - "Daily Luck Message": { - "ID": "Schematix.DailyLuckMessage", - "Default | UpdateKey": "Nexus:1327" - }, - - "Daily News": { - "ID": "bashNinja.DailyNews", - "Default | UpdateKey": "Nexus:1141", - "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Daily Quest Anywhere": { - "ID": "Omegasis.DailyQuestAnywhere", - "FormerIDs": "DailyQuest", // changed in 1.4 - "Default | UpdateKey": "Nexus:513" // added in 1.4.1 - }, - - "Debug Mode": { - "ID": "Pathoschild.DebugMode", - "FormerIDs": "Pathoschild.Stardew.DebugMode", // changed in 1.4 - "Default | UpdateKey": "Nexus:679" - }, - - "Did You Water Your Crops?": { - "ID": "Nishtra.DidYouWaterYourCrops", - "Default | UpdateKey": "Nexus:1583" - }, - - "Dynamic Checklist": { - "ID": "gunnargolf.DynamicChecklist", - "Default | UpdateKey": "Nexus:1145", // added in 1.0.1-pathoschild-update - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Dynamic Horses": { - "ID": "Bpendragon-DynamicHorses", - "MapRemoteVersions": { "1.2": "1.1-release" }, // manifest not updated - "Default | UpdateKey": "Nexus:874" - }, - - "Dynamic Machines": { - "ID": "DynamicMachines", - "MapLocalVersions": { "1.1": "1.1.1" }, - "Default | UpdateKey": "Nexus:374", - "~1.1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Dynamic NPC Sprites": { - "ID": "BashNinja.DynamicNPCSprites", - "Default | UpdateKey": "Nexus:1183" - }, - - "Easier Farming": { - "ID": "cautiouswafffle.EasierFarming", - "Default | UpdateKey": "Nexus:1426" - }, - - "Empty Hands": { - "ID": "QuicksilverFox.EmptyHands", - "Default | UpdateKey": "Nexus:1176", // added in 1.0.1-pathoschild-update - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Enemy Health Bars": { - "ID": "Speeder.HealthBars", - "FormerIDs": "SPDHealthBar", // changed in 1.7.1-pathoschild-update - "Default | UpdateKey": "Nexus:193", - "~1.7 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Entoarox Framework": { - "ID": "Entoarox.EntoaroxFramework", - "FormerIDs": "eacdb74b-4080-4452-b16b-93773cda5cf9", // changed in ??? - "~2.0.6 | UpdateKey": "Chucklefish:4228", // only enable update checks up to 2.0.6 by request (has its own update-check feature) - "~2.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.5 (error reflecting into SMAPI internals) - }, - - "Expanded Fridge": { - "ID": "Uwazouri.ExpandedFridge", - "Default | UpdateKey": "Nexus:1191" - }, - - "Experience Bars": { - "ID": "spacechase0.ExperienceBars", - "FormerIDs": "ExperienceBars", // changed in 1.0.2 - "Default | UpdateKey": "Nexus:509" - }, - - "Extended Bus System": { - "ID": "ExtendedBusSystem", - "Default | UpdateKey": "Chucklefish:4373" - }, - - "Extended Fridge": { - "ID": "Crystalmir.ExtendedFridge", - "FormerIDs": "Mystra007ExtendedFridge", // changed in 1.0.1 - "Default | UpdateKey": "Nexus:485", - "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Extended Greenhouse": { - "ID": "ExtendedGreenhouse", - "Default | UpdateKey": "Chucklefish:4303", - "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Extended Minecart": { - "ID": "Entoarox.ExtendedMinecart", - "FormerIDs": "{ID:'EntoaroxFurnitureAnywhere', Name:'Extended Minecart'}", // changed in 1.6.1 - "~1.7.1 | UpdateKey": "Chucklefish:4359" // only enable update checks up to 1.7.1 by request (has its own update-check feature) - }, - - "Extended Reach": { - "ID": "spacechase0.ExtendedReach", - "Default | UpdateKey": "Nexus:1493" - }, - - "Fall 28 Snow Day": { - "ID": "Omegasis.Fall28SnowDay", - "FormerIDs": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Fall28 Snow Day'}", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis - "Default | UpdateKey": "Nexus:486", // added in 1.4.1 - "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Farm Automation: Barn Door Automation": { - "FormerIDs": "{EntryDll: 'FarmAutomation.BarnDoorAutomation.dll'}", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Farm Automation: Item Collector": { - "FormerIDs": "{EntryDll: 'FarmAutomation.ItemCollector.dll'}", - "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Farm Automation Unofficial: Item Collector": { - "ID": "Maddy99.FarmAutomation.ItemCollector", - "~0.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Farm Expansion": { - "ID": "Advize.FarmExpansion", - "FormerIDs": "3888bdfd-73f6-4776-8bb7-8ad45aea1915 | AdvizeFarmExpansionMod-2-0 | AdvizeFarmExpansionMod-2-0-5", // changed in 2.0, 2.0.5, and 3.0 - "Default | UpdateKey": "Nexus:130", - "~2.0.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Farm Resource Generator": { - "FormerIDs": "{EntryDll: 'FarmResourceGenerator.dll'}", - "Default | UpdateKey": "Nexus:647", - "~1.0.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Fast Animations": { - "ID": "Pathoschild.FastAnimations", - "Default | UpdateKey": "Nexus:1089" - }, - - "Faster Grass": { - "ID": "IceGladiador.FasterGrass", - "Default | UpdateKey": "Nexus:1772" - }, - - "Faster Paths": { - "ID": "Entoarox.FasterPaths", - "FormerIDs": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Faster Paths'} | 615f85f8-5c89-44ee-aecc-c328f172e413", // changed in 1.2 and 1.3; disambiguate from Shop Expander - "~1.3.3 | UpdateKey": "Chucklefish:3641" // only enable update checks up to 1.3.3 by request (has its own update-check feature) - }, - - "Faster Run": { - "ID": "KathrynHazuka.FasterRun", - "FormerIDs": "{EntryDll: 'FasterRun.dll'}", // changed in 1.1.1-pathoschild-update - "Default | UpdateKey": "Nexus:733", // added in 1.1.1-pathoschild-update - "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Fishing Adjust": { - "ID": "shuaiz.FishingAdjustMod", - "Default | UpdateKey": "Nexus:1350" - }, - - "Fishing Tuner Redux": { - "ID": "HammurabiFishingTunerRedux", - "Default | UpdateKey": "Chucklefish:4578" - }, - - "Fixed Secret Woods Debris": { - "ID": "f4iTh.WoodsDebrisFix", - "Default | UpdateKey": "Nexus:1941" - }, - - "FlorenceMod": { - "FormerIDs": "{EntryDll: 'FlorenceMod.dll'}", - "MapLocalVersions": { "1.0.1": "1.1" }, - "Default | UpdateKey": "Nexus:591", - "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Flower Color Picker": { - "ID": "spacechase0.FlowerColorPicker", - "Default | UpdateKey": "Nexus:1229" - }, - - "Forage at the Farm": { - "ID": "Nishtra.ForageAtTheFarm", - "FormerIDs": "ForageAtTheFarm", // changed in <=1.6 - "Default | UpdateKey": "Nexus:673", - "~1.5.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Furniture Anywhere": { - "ID": "Entoarox.FurnitureAnywhere", - "FormerIDs": "{ID:'EntoaroxFurnitureAnywhere', Name:'Furniture Anywhere'}", // changed in 1.1; disambiguate from Extended Minecart - "~1.1.5 | UpdateKey": "Chucklefish:4324" // only enable update checks up to 1.1.5 by request (has its own update-check feature) - }, - - "Game Reminder": { - "ID": "mmanlapat.GameReminder", - "Default | UpdateKey": "Nexus:1153" - }, - - "Gate Opener": { - "ID": "mralbobo.GateOpener", - "FormerIDs": "{EntryDll: 'GateOpener.dll'}", // changed in 1.1 - "Default | UpdateKey": "GitHub:mralbobo/stardew-gate-opener", - "~1.0.1 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "GenericShopExtender": { - "ID": "GenericShopExtender", - "Default | UpdateKey": "Nexus:814", // added in 0.1.3 - "~0.1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Geode Info Menu": { - "ID": "cat.geodeinfomenu", - "Default | UpdateKey": "Nexus:1448" - }, - - "Get Dressed": { - "ID": "Advize.GetDressed", - "FormerIDs": "{EntryDll: 'GetDressed.dll'}", // changed in 3.3 - "Default | UpdateKey": "Nexus:331", - "~3.3 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Giant Crop Ring": { - "ID": "cat.giantcropring", - "Default | UpdateKey": "Nexus:1182" - }, - - "Gift Taste Helper": { - "ID": "tstaples.GiftTasteHelper", - "FormerIDs": "8008db57-fa67-4730-978e-34b37ef191d6", // changed in 2.5 - "Default | UpdateKey": "Nexus:229", - "~2.3.1 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Grandfather's Gift": { - "ID": "ShadowDragon.GrandfathersGift", - "Default | UpdateKey": "Nexus:985" - }, - - "Happy Animals": { - "ID": "HappyAnimals", - "~1.0.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Happy Birthday (Omegasis)": { - "ID": "Omegasis.HappyBirthday", - "FormerIDs": "{ID:'HappyBirthday', Author:'Alpha_Omegasis'}", // changed in 1.4; disambiguate from Oxyligen's fork - "Default | UpdateKey": "Nexus:520", // added in 1.4.1 - "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Happy Birthday (Oxyligen fork)": { - "FormerIDs": "{ID:'HappyBirthday', Author:'Alpha_Omegasis/Oxyligen'}", // disambiguate from Oxyligen's fork - "Default | UpdateKey": "Nexus:1064" // missing key reported: https://www.nexusmods.com/stardewvalley/mods/1064?tab=bugs - }, - - "Hardcore Mines": { - "ID": "kibbe.hardcore_mines", - "Default | UpdateKey": "Nexus:1674" - }, - - "Harp of Yoba Redux": { - "ID": "Platonymous.HarpOfYobaRedux", - "Default | UpdateKey": "Nexus:914" // added in 2.0.3 - }, - - "Harvest Moon Witch Princess": { - "ID": "Sasara.WitchPrincess", - "Default | UpdateKey": "Nexus:1157" - }, - - "Harvest With Scythe": { - "ID": "965169fd-e1ed-47d0-9f12-b104535fb4bc", - "Default | UpdateKey": "Nexus:236", - "~1.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Horse Whistle (icepuente)": { - "ID": "icepuente.HorseWhistle", - "Default | UpdateKey": "Nexus:1131" - }, - - "Hunger (Yyeadude)": { - "ID": "HungerYyeadude", - "Default | UpdateKey": "Nexus:613" - }, - - "Hunger for Food (Tigerle)": { - "ID": "HungerForFoodByTigerle", - "Default | UpdateKey": "Nexus:810", - "~0.1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Hunger Mod (skn)": { - "ID": "skn.HungerMod", - "MapRemoteVersions": { "1.2.1": "1.0" }, // manifest not updated - "Default | UpdateKey": "Nexus:1127" - }, - - "Idle Pause": { - "ID": "Veleek.IdlePause", - "MapRemoteVersions": { "1.2": "1.1" }, // manifest not updated - "Default | UpdateKey": "Nexus:1092" - }, - - "Improved Quality of Life": { - "ID": "Demiacle.ImprovedQualityOfLife", - "Default | UpdateKey": "Nexus:1025", - "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Instant Geode": { - "ID": "InstantGeode", - "~1.12 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Instant Grow Trees": { - "ID": "cantorsdust.InstantGrowTrees", - "FormerIDs": "dc50c58b-c7d8-4e60-86cc-e27b5d95ee59 | community.InstantGrowTrees", // changed in 1.2 and 1.3.1 - "Default | UpdateKey": "Nexus:173" - }, - - "Interaction Helper": { - "ID": "HammurabiInteractionHelper", - "Default | UpdateKey": "Chucklefish:4640", // added in 1.0.4-pathoschild-update - "~1.0.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Item Auto Stacker": { - "ID": "cat.autostacker", - "MapRemoteVersions": { "1.0.1": "1.0" }, // manifest not updated - "Default | UpdateKey": "Nexus:1184" - }, - - "Jiggly Junimo Bundles": { - "ID": "Greger.JigglyJunimoBundles", - "FormerIDs": "{EntryDll: 'JJB.dll'}", // changed in 1.1.2-pathoschild-update - "Default | UpdateKey": "GitHub:gr3ger/Stardew_JJB" // added in 1.0.4-pathoschild-update - }, - - "Json Assets": { - "ID": "spacechase0.JsonAssets", - "Default | UpdateKey": "Nexus:1720" - }, - - "Junimo Farm": { - "ID": "Platonymous.JunimoFarm", - "MapRemoteVersions": { "1.1.2": "1.1.1" }, // manifest not updated - "Default | UpdateKey": "Nexus:984" // added in 1.1.3 - }, - - "Less Strict Over-Exertion (AntiExhaustion)": { - "ID": "BALANCEMOD_AntiExhaustion", - "MapLocalVersions": { "0.0": "1.1" }, - "Default | UpdateKey": "Nexus:637", - "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Level Extender": { - "ID": "Devin Lematty.Level Extender", - "MapRemoteVersions": { "1.1": "1.0" }, // manifest not updated - "Default | UpdateKey": "Nexus:1471" - }, - - "Level Up Notifications": { - "ID": "Level Up Notifications", - "MapRemoteVersions": { "0.0.1a": "0.0.1" }, - "Default | UpdateKey": "Nexus:855" - }, - - "Location and Music Logging": { - "ID": "Brandy Lover.LMlog", - "Default | UpdateKey": "Nexus:1366" - }, - - "Longevity": { - "ID": "RTGOAT.Longevity", - "MapRemoteVersions": { "1.6.8h": "1.6.8" }, - "Default | UpdateKey": "Nexus:649" - }, - - "Lookup Anything": { - "ID": "Pathoschild.LookupAnything", - "FormerIDs": "LookupAnything", // changed in 1.10.1 - "Default | UpdateKey": "Nexus:541", - "~1.10.1 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Love Bubbles": { - "ID": "LoveBubbles", - "Default | UpdateKey": "Nexus:1318" - }, - - "Loved Labels": { - "ID": "Advize.LovedLabels", - "FormerIDs": "{EntryDll: 'LovedLabels.dll'}", // changed in 2.1 - "Default | UpdateKey": "Nexus:279", - "~2.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Luck Skill": { - "ID": "spacechase0.LuckSkill", - "FormerIDs": "LuckSkill", // changed in 0.1.4 - "Default | UpdateKey": "Nexus:521", - "~0.1.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Mail Framework": { - "ID": "DIGUS.MailFrameworkMod", - "Default | UpdateKey": "Nexus:1536" - }, - - "MailOrderPigs": { - "ID": "jwdred.MailOrderPigs", - "FormerIDs": "{EntryDll: 'MailOrderPigs.dll'}", // changed in 1.0.2 - "Default | UpdateKey": "Nexus:632", - "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Makeshift Multiplayer": { - "ID": "spacechase0.StardewValleyMP", - "FormerIDs": "StardewValleyMP", // changed in 0.3 - "Default | UpdateKey": "Nexus:501", - "~0.3.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Map Image Exporter": { - "ID": "spacechase0.MapImageExporter", - "FormerIDs": "MapImageExporter", // changed in 1.0.2 - "Default | UpdateKey": "Nexus:1073" - }, - - "Message Box [API]? (ChatMod)": { - "ID": "Kithio:ChatMod", - "Default | UpdateKey": "Chucklefish:4296", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Mining at the Farm": { - "ID": "Nishtra.MiningAtTheFarm", - "FormerIDs": "MiningAtTheFarm", // changed in <=1.7 - "Default | UpdateKey": "Nexus:674" - }, - - "Mining With Explosives": { - "ID": "Nishtra.MiningWithExplosives", - "FormerIDs": "MiningWithExplosives", // changed in 1.1 - "Default | UpdateKey": "Nexus:770" - }, - - "Modder Serialization Utility": { - "ID": "SerializerUtils-0-1", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "it's no longer maintained or used." - }, - - "Monster Level Tip": { - "ID": "WhiteMind.MonsterLT", - "Default | UpdateKey": "Nexus:1896" - }, - - "More Animals": { - "ID": "Entoarox.MoreAnimals", - "FormerIDs": "821ce8f6-e629-41ad-9fde-03b54f68b0b6MOREPETS | Entoarox.MorePets", // changed in 1.3 and 2.0 - "~2.0.2 | UpdateKey": "Chucklefish:4288", // only enable update checks up to 2.0.2 by request (has its own update-check feature) - "~1.3.2 | Status": "AssumeBroken" // overhauled for SMAPI 1.11+ compatibility - }, - - "More Artifact Spots": { - "ID": "451", - "Default | UpdateKey": "Nexus:451", - "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "More Map Layers": { - "ID": "Platonymous.MoreMapLayers", - "Default | UpdateKey": "Nexus:1134" // added in 1.1.1 - }, - - "More Rain": { - "ID": "Omegasis.MoreRain", - "FormerIDs": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'More_Rain'}", // changed in 1.5; disambiguate from other mods by Alpha_Omegasis - "Default | UpdateKey": "Nexus:441", // added in 1.5.1 - "~1.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "More Weapons": { - "ID": "Joco80.MoreWeapons", - "Default | UpdateKey": "Nexus:1168" - }, - - "Move Faster": { - "ID": "shuaiz.MoveFasterMod", - "Default | UpdateKey": "Nexus:1351" - }, - - "Multiple Sprites and Portraits On Rotation (File Loading)": { - "ID": "FileLoading", - "MapLocalVersions": { "1.1": "1.12" }, - "Default | UpdateKey": "Nexus:1094", - "~1.12 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Museum Rearranger": { - "ID": "Omegasis.MuseumRearranger", - "FormerIDs": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Museum Rearranger'}", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis - "Default | UpdateKey": "Nexus:428", // added in 1.4.1 - "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Mushroom Level Tip": { - "ID": "WhiteMind.MLT", - "Default | UpdateKey": "Nexus:1894" - }, - - "New Machines": { - "ID": "F70D4FAB-0AB2-4B78-9F1B-AF2CA2236A59", - "Default | UpdateKey": "Chucklefish:3683", - "~4.2.1343 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Night Owl": { - "ID": "Omegasis.NightOwl", - "FormerIDs": "{ID:'SaveAnywhere', Name:'Stardew_NightOwl'}", // changed in 1.4; disambiguate from Save Anywhere - "MapLocalVersions": { "2.1": "1.3" }, // 1.3 had wrong version in manifest - "Default | UpdateKey": "Nexus:433", // added in 1.4.1 - "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "No Crows": { - "ID": "cat.nocrows", - "Default | UpdateKey": "Nexus:1682" - }, - - "No Kids Ever": { - "ID": "Hangy.NoKidsEver", - "Default | UpdateKey": "Nexus:1464" - }, - - "No Debug Mode": { - "ID": "NoDebugMode", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "debug mode was removed in SMAPI 1.0." - }, - - "No Fence Decay": { - "ID": "cat.nofencedecay", - "Default | UpdateKey": "Nexus:1180" - }, - - "No More Pets": { - "ID": "Omegasis.NoMorePets", - "FormerIDs": "NoMorePets", // changed in 1.4 - "Default | UpdateKey": "Nexus:506" // added in 1.4.1 - }, - - "No Rumble Horse": { - "ID": "Xangria.NoRumbleHorse", - "Default | UpdateKey": "Nexus:1779" - }, - - "No Soil Decay": { - "ID": "289dee03-5f38-4d8e-8ffc-e440198e8610", - "Default | UpdateKey": "Nexus:237", - "~0.5 | Status": "AssumeBroken" // broke in SDV 1.2 and uses Assembly.GetExecutingAssembly().Location - }, - - "No Soil Decay Redux": { - "ID": "Platonymous.NoSoilDecayRedux", - "Default | UpdateKey": "Nexus:1084" // added in 1.1.9 - }, - - "NPC Map Locations": { - "ID": "NPCMapLocationsMod", - "Default | UpdateKey": "Nexus:239", - "1.42~1.43 | Status": "AssumeBroken", - "1.42~1.43 | StatusReasonPhrase": "this version has an update check error which crashes the game." - }, - - "NPC Speak": { - "FormerIDs": "{EntryDll: 'NpcEcho.dll'}", - "Default | UpdateKey": "Nexus:694", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Object Time Left": { - "ID": "spacechase0.ObjectTimeLeft", - "Default | UpdateKey": "Nexus:1315" - }, - - "OmniFarm": { - "ID": "PhthaloBlue.OmniFarm", - "FormerIDs": "BlueMod_OmniFarm", // changed in 2.0.2-pathoschild-update - "Default | UpdateKey": "GitHub:lambui/StardewValleyMod_OmniFarm", - "~2.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Out of Season Bonuses (Seasonal Items)": { - "ID": "midoriarmstrong.seasonalitems", - "Default | UpdateKey": "Nexus:1452" - }, - - "Part of the Community": { - "ID": "SB_PotC", - "Default | UpdateKey": "Nexus:923", - "~1.0.8 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "PelicanFiber": { - "ID": "jwdred.PelicanFiber", - "FormerIDs": "{EntryDll: 'PelicanFiber.dll'}", // changed in 3.0.1 - "MapRemoteVersions": { "3.0.2": "3.0.1" }, // didn't change manifest version - "Default | UpdateKey": "Nexus:631", - "~3.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "PelicanTTS": { - "ID": "Platonymous.PelicanTTS", - "Default | UpdateKey": "Nexus:1079", // added in 1.6.1 - "~1.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Persia the Mermaid - Standalone Custom NPC": { - "ID": "63b9f419-7449-42db-ab2e-440b4d05c073", - "Default | UpdateKey": "Nexus:1419" - }, - - "Persistent Game Options": { - "ID": "Xangria.PersistentGameOptions", - "Default | UpdateKey": "Nexus:1778" - }, - - "Persival's BundleMod": { - "FormerIDs": "{EntryDll: 'BundleMod.dll'}", - "Default | UpdateKey": "Nexus:438", - "~1.0 | Status": "AssumeBroken" // broke in SDV 1.1 - }, - - "Plant on Grass": { - "ID": "Demiacle.PlantOnGrass", - "Default | UpdateKey": "Nexus:1026" - }, - - "PyTK - Platonymous Toolkit": { - "ID": "Platonymous.Toolkit", - "Default | UpdateKey": "Nexus:1726" - }, - - "Point-and-Plant": { - "ID": "jwdred.PointAndPlant", - "FormerIDs": "{EntryDll: 'PointAndPlant.dll'}", // changed in 1.0.3 - "Default | UpdateKey": "Nexus:572", - "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Pony Weight Loss Program": { - "ID": "BadNetCode.PonyWeightLossProgram", - "Default | UpdateKey": "Nexus:1232" - }, - - "Portraiture": { - "ID": "Platonymous.Portraiture", - "Default | UpdateKey": "Nexus:999" // added in 1.3.1 - }, - - "Prairie King Made Easy": { - "ID": "Mucchan.PrairieKingMadeEasy", - "FormerIDs": "{EntryDll: 'PrairieKingMadeEasy.dll'}", // changed in 1.0.1 - "Default | UpdateKey": "Chucklefish:3594", - "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Purchasable Recipes": { - "ID": "Paracosm.PurchasableRecipes", - "Default | UpdateKey": "Nexus:1722" - }, - - "Quest Delay": { - "ID": "BadNetCode.QuestDelay", - "Default | UpdateKey": "Nexus:1239" - }, - - "Rain Randomizer": { - "FormerIDs": "{EntryDll: 'RainRandomizer.dll'}", - "~1.0.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Recatch Legendary Fish": { - "ID": "cantorsdust.RecatchLegendaryFish", - "FormerIDs": "b3af8c31-48f0-43cf-8343-3eb08bcfa1f9 | community.RecatchLegendaryFish", // changed in 1.3 and 1.5.1 - "Default | UpdateKey": "Nexus:172" - }, - - "Regeneration": { - "ID": "HammurabiRegeneration", - "Default | UpdateKey": "Chucklefish:4584" - }, - - "Relationship Bar UI": { - "ID": "RelationshipBar", - "Default | UpdateKey": "Nexus:1009" - }, - - "RelationshipsEnhanced": { - "ID": "relationshipsenhanced", - "Default | UpdateKey": "Chucklefish:4435", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Relationship Status": { - "ID": "relationshipstatus", - "MapRemoteVersions": { "1.0.5": "1.0.4" }, // not updated in manifest - "Default | UpdateKey": "Nexus:751", - "~1.0.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Rented Tools": { - "ID": "JarvieK.RentedTools", - "Default | UpdateKey": "Nexus:1307" - }, - - "Replanter": { - "ID": "jwdred.Replanter", - "FormerIDs": "{EntryDll: 'Replanter.dll'}", // changed in 1.0.5 - "Default | UpdateKey": "Nexus:589", - "~1.0.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "ReRegeneration": { - "ID": "lrsk_sdvm_rerg.0925160827", - "MapLocalVersions": { "1.1.2-release": "1.1.2" }, - "Default | UpdateKey": "Chucklefish:4465" - }, - - "Reseed": { - "ID": "Roc.Reseed", - "Default | UpdateKey": "Nexus:887" - }, - - "Reusable Wallpapers and Floors (Wallpaper Retain)": { - "ID": "dae1b553-2e39-43e7-8400-c7c5c836134b", - "Default | UpdateKey": "Nexus:356", - "~1.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Ring of Fire": { - "ID": "Platonymous.RingOfFire", - "Default | UpdateKey": "Nexus:1166" // added in 1.0.1 - }, - - "Rise and Shine": { - "ID": "Yoshify.RiseAndShine", - "FormerIDs": "{EntryDll: 'RiseAndShine.dll'}", // changed in 1.1.1-whisk-update - "Default | UpdateKey": "Nexus:3" - }, - - "Rope Bridge": { - "ID": "RopeBridge", - "Default | UpdateKey": "Nexus:824" - }, - - "Rotate Toolbar": { - "ID": "Pathoschild.RotateToolbar", - "Default | UpdateKey": "Nexus:1100" - }, - - "Rush Orders": { - "ID": "spacechase0.RushOrders", - "FormerIDs": "RushOrders", // changed in 1.1 - "Default | UpdateKey": "Nexus:605", - "~1.1 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Save Anywhere": { - "ID": "Omegasis.SaveAnywhere", - "FormerIDs": "{ID:'SaveAnywhere', Name:'Save Anywhere'}", // changed in 2.5; disambiguate from Night Owl - "Default | UpdateKey": "Nexus:444", // added in 2.6.1 - "~2.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Save Backup": { - "ID": "Omegasis.SaveBackup", - "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'Stardew_Save_Backup'}", // changed in 1.3; disambiguate from other Alpha_Omegasis mods - "Default | UpdateKey": "Nexus:435", // added in 1.3.1 - "~1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Scroll to Blank": { - "ID": "caraxian.scroll.to.blank", - "Default | UpdateKey": "Chucklefish:4405" - }, - - "Scythe Harvesting": { - "ID": "mmanlapat.ScytheHarvesting", - "FormerIDs": "ScytheHarvesting", // changed in 1.6 - "Default | UpdateKey": "Nexus:1106" - }, - - "SDV Twitch": { - "ID": "MTD.SDVTwitch", - "Default | UpdateKey": "Nexus:1760" - }, - - "Seasonal Immersion": { - "ID": "Entoarox.SeasonalImmersion", - "FormerIDs": "EntoaroxSeasonalHouse | EntoaroxSeasonalBuildings | EntoaroxSeasonalImmersion", // changed in 1.1, 1.6 or earlier, and 1.7 - "~1.11 | UpdateKey": "Chucklefish:4262", // only enable update checks up to 1.11 by request (has its own update-check feature) - "~1.8.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Seed Bag": { - "ID": "Platonymous.SeedBag", - "Default | UpdateKey": "Nexus:1133" // added in 1.1.2 - }, - - "Seed Catalogue": { - "ID": "spacechase0.SeedCatalogue", - "Default | UpdateKey": "Nexus:1640" - }, - - "Self Service": { - "ID": "JarvieK.SelfService", - "MapRemoteVersions": { "0.2.1": "0.2" }, // manifest not updated - "Default | UpdateKey": "Nexus:1304" - }, - - "Send Items": { - "ID": "Denifia.SendItems", - "Default | UpdateKey": "Nexus:1087", // added in 1.0.3 (2017-10-04) - "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Shed Notifications (BuildingsNotifications)": { - "ID": "TheCroak.BuildingsNotifications", - "Default | UpdateKey": "Nexus:620", - "~0.4.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Shenandoah Project": { - "ID": "Nishtra.ShenandoahProject", - "FormerIDs": "Shenandoah Project", // changed in 1.2 - "MapRemoteVersions": { "1.1.1": "1.1" }, // not updated in manifest - "Default | UpdateKey": "Nexus:756", - "~1.1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Ship Anywhere": { - "ID": "spacechase0.ShipAnywhere", - "Default | UpdateKey": "Nexus:1379" - }, - - "Shipment Tracker": { - "ID": "7e474181-e1a0-40f9-9c11-d08a3dcefaf3", - "Default | UpdateKey": "Nexus:321", - "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Shop Expander": { - "ID": "Entoarox.ShopExpander", - "FormerIDs": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Shop Expander'} | EntoaroxShopExpander", // changed in 1.5 and 1.5.2; disambiguate from Faster Paths - "MapRemoteVersions": { "1.6.0b": "1.6.0" }, - "~1.6 | UpdateKey": "Chucklefish:4381", // only enable update checks up to 1.6 by request (has its own update-check feature) - "~1.5.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Showcase Mod": { - "ID": "Igorious.Showcase", - "MapLocalVersions": { "0.9-500": "0.9" }, - "Default | UpdateKey": "Chucklefish:4487", - "~0.9 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Shroom Spotter": { - "ID": "TehPers.ShroomSpotter", - "Default | UpdateKey": "Nexus:908" - }, - - "Simple Crop Label": { - "ID": "SimpleCropLabel", - "Default | UpdateKey": "Nexus:314" - }, - - "Simple Sound Manager": { - "ID": "Omegasis.SimpleSoundManager", - "Default | UpdateKey": "Nexus:1410", // added in 1.0.1 - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Simple Sprinklers": { - "ID": "tZed.SimpleSprinkler", - "FormerIDs": "{EntryDll: 'SimpleSprinkler.dll'}", // changed in 1.5 - "Default | UpdateKey": "Nexus:76", - "~1.4 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Siv's Marriage Mod": { - "ID": "6266959802", - "MapLocalVersions": { "0.0": "1.4" }, - "Default | UpdateKey": "Nexus:366", - "~1.2.2 | Status": "AssumeBroken" // broke in SMAPI 1.9 (has multiple Mod instances) - }, - - "Skill Prestige": { - "ID": "alphablackwolf.skillPrestige", - "FormerIDs": "6b843e60-c8fc-4a25-a67b-4a38ac8dcf9b", // changed circa 1.2.3 - "Default | UpdateKey": "Nexus:569", - "~1.0.9 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Skill Prestige: Cooking Adapter": { - "ID": "Alphablackwolf.CookingSkillPrestigeAdapter", - "FormerIDs": "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63", // changed circa 1.1 - "MapRemoteVersions": { "1.2.3": "1.1" }, // manifest not updated - "Default | UpdateKey": "Nexus:569", - "~1.0.9 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Skip Intro": { - "ID": "Pathoschild.SkipIntro", - "FormerIDs": "SkipIntro", // changed in 1.4 - "Default | UpdateKey": "Nexus:533" - }, - - "Skull Cavern Elevator": { - "ID": "SkullCavernElevator", - "Default | UpdateKey": "Nexus:963" - }, - - "Skull Cave Saver": { - "ID": "cantorsdust.SkullCaveSaver", - "FormerIDs": "8ac06349-26f7-4394-806c-95d48fd35774 | community.SkullCaveSaver", // changed in 1.1 and 1.2.2 - "Default | UpdateKey": "Nexus:175" - }, - - "Sleepy Eye": { - "ID": "spacechase0.SleepyEye", - "Default | UpdateKey": "Nexus:1152" - }, - - "Slower Fence Decay": { - "ID": "Speeder.SlowerFenceDecay", - "FormerIDs": "SPDSlowFenceDecay", // changed in 0.5.2-pathoschild-update - "Default | UpdateKey": "Nexus:252", - "~0.5.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Smart Mod": { - "ID": "KuroBear.SmartMod", - "~2.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Solar Eclipse Event": { - "ID": "KoihimeNakamura.SolarEclipseEvent", - "Default | UpdateKey": "Nexus:897", - "MapLocalVersions": { "1.3-20170917": "1.3" }, - "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "SpaceCore": { - "ID": "spacechase0.SpaceCore", - "Default | UpdateKey": "Nexus:1348" - }, - - "Speedster": { - "ID": "Platonymous.Speedster", - "Default | UpdateKey": "Nexus:1102" // added in 1.3.1 - }, - - "Sprinkler Range": { - "ID": "cat.sprinklerrange", - "Default | UpdateKey": "Nexus:1179" - }, - - "Sprinkles": { - "ID": "Platonymous.Sprinkles", - "Default | UpdateKey": "Chucklefish:4592", - "~1.1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Sprint and Dash": { - "ID": "SPDSprintAndDash", - "Default | UpdateKey": "Chucklefish:3531", - "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Sprint and Dash Redux": { - "ID": "littleraskol.SprintAndDashRedux", - "FormerIDs": "lrsk_sdvm_sndr.0921161059", // changed in 1.3 - "Default | UpdateKey": "Chucklefish:4201" - }, - - "Sprinting Mod": { - "FormerIDs": "{EntryDll: 'SprintingMod.dll'}", - "MapLocalVersions": { "1.0": "2.1" }, // not updated in manifest - "Default | UpdateKey": "GitHub:oliverpl/SprintingMod", - "~2.1 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "StackSplitX": { - "ID": "tstaples.StackSplitX", - "FormerIDs": "{EntryDll: 'StackSplitX.dll'}", // changed circa 1.3.1 - "Default | UpdateKey": "Nexus:798", - "~1.2 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "StaminaRegen": { - "FormerIDs": "{EntryDll: 'StaminaRegen.dll'}", - "~1.0.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Stardew Config Menu": { - "ID": "Juice805.StardewConfigMenu", - "Default | UpdateKey": "Nexus:1312" - }, - - "Stardew Content Compatibility Layer (SCCL)": { - "ID": "SCCL", - "Default | UpdateKey": "Nexus:889", - "~0.1 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Stardew Editor Game Integration": { - "ID": "spacechase0.StardewEditor.GameIntegration", - "Default | UpdateKey": "Nexus:1298" - }, - - "Stardew Notification": { - "ID": "stardewnotification", - "Default | UpdateKey": "GitHub:monopandora/StardewNotification", - "~1.7 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Stardew Symphony": { - "ID": "Omegasis.StardewSymphony", - "FormerIDs": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'Stardew_Symphony'}", // changed in 1.4; disambiguate other mods by Alpha_Omegasis - "Default | UpdateKey": "Nexus:425", // added in 1.4.1 - "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "StarDustCore": { - "ID": "StarDustCore", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "it was only used by earlier versions of Save Anywhere, and is no longer used or maintained." - }, - - "Starting Money": { - "ID": "mmanlapat.StartingMoney", - "FormerIDs": "StartingMoney", // changed in 1.1 - "Default | UpdateKey": "Nexus:1138" - }, - - "StashItemsToChest": { - "ID": "BlueMod_StashItemsToChest", - "Default | UpdateKey": "GitHub:lambui/StardewValleyMod_StashItemsToChest", - "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Stephan's Lots of Crops": { - "ID": "stephansstardewcrops", - "MapRemoteVersions": { "1.41": "1.1" }, // manifest not updated - "Default | UpdateKey": "Chucklefish:4314" - }, - - "Stone Bridge Over Pond (PondWithBridge)": { - "FormerIDs": "{EntryDll: 'PondWithBridge.dll'}", - "MapLocalVersions": { "0.0": "1.0" }, - "Default | UpdateKey": "Nexus:316", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Stumps to Hardwood Stumps": { - "ID": "StumpsToHardwoodStumps", - "Default | UpdateKey": "Nexus:691" - }, - - "Super Greenhouse Warp Modifier": { - "ID": "SuperGreenhouse", - "Default | UpdateKey": "Chucklefish:4334", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Swim Almost Anywhere / Swim Suit": { - "ID": "Platonymous.SwimSuit", - "Default | UpdateKey": "Nexus:1215" // added in 0.5.1 - }, - - "Tainted Cellar": { - "ID": "TaintedCellar", - "FormerIDs": "{EntryDll: 'TaintedCellar.dll'}", // changed in 1.1 - "~1.0 | Status": "AssumeBroken" // broke in SDV 1.1 or 1.11 - }, - - "Tapper Ready": { - "ID": "skunkkk.TapperReady", - "Default | UpdateKey": "Nexus:1219" - }, - - "Teh's Fishing Overhaul": { - "ID": "TehPers.FishingOverhaul", - "Default | UpdateKey": "Nexus:866" - }, - - "Teleporter": { - "ID": "Teleporter", - "Default | UpdateKey": "Chucklefish:4374", - "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "The Long Night": { - "ID": "Pathoschild.TheLongNight", - "Default | UpdateKey": "Nexus:1369" - }, - - "Three-heart Dance Partner": { - "ID": "ThreeHeartDancePartner", - "Default | UpdateKey": "Nexus:500", - "~1.0.1 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "TimeFreeze": { - "ID": "Omegasis.TimeFreeze", - "FormerIDs": "4108e859-333c-4fec-a1a7-d2e18c1019fe", // changed in 1.2 - "Default | UpdateKey": "Nexus:973" // added in 1.2.1 - }, - - "Time Reminder": { - "ID": "KoihimeNakamura.TimeReminder", - "MapLocalVersions": { "1.0-20170314": "1.0.2" }, - "Default | UpdateKey": "Nexus:1000" - }, - - "TimeSpeed": { - "ID": "cantorsdust.TimeSpeed", - "FormerIDs": "{EntryDll: 'TimeSpeed.dll'} | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed'} | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed Mod (unofficial)'} | community.TimeSpeed", // changed in 2.0.3, 2.1, and 2.3.3; disambiguate other mods by Alpha_Omegasis - "Default | UpdateKey": "Nexus:169", - "~2.2 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "To Do List": { - "ID": "eleanor.todolist", - "Default | UpdateKey": "Nexus:1630" - }, - - "Tool Charging": { - "ID": "mralbobo.ToolCharging", - "Default | UpdateKey": "GitHub:mralbobo/stardew-tool-charging" - }, - - "TractorMod": { - "ID": "Pathoschild.TractorMod", - "FormerIDs": "BlueMod_TractorMod | PhthaloBlue.TractorMod | community.TractorMod", // changed in 3.2, 4.0 beta, and 4.0 - "Default | UpdateKey": "Nexus:1401" - }, - - "TrainerMod": { - "ID": "SMAPI.TrainerMod", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "replaced by ConsoleCommands, which is added by the SMAPI installer." - }, - - "Tree Transplant": { - "ID": "TreeTransplant", - "Default | UpdateKey": "Nexus:1342" - }, - - "UI Info Suite": { - "ID": "Cdaragorn.UiInfoSuite", - "Default | UpdateKey": "Nexus:1150" - }, - - "UiModSuite": { - "ID": "Demiacle.UiModSuite", - "MapLocalVersions": { "0.5": "1.0" }, // not updated in manifest - "Default | UpdateKey": "Nexus:1023", - "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Variable Grass": { - "ID": "dantheman999.VariableGrass", - "Default | UpdateKey": "GitHub:dantheman999301/StardewMods" - }, - - "Vertical Toolbar": { - "ID": "SB_VerticalToolMenu", - "Default | UpdateKey": "Nexus:943" - }, - - "WakeUp": { - "FormerIDs": "{EntryDll: 'WakeUp.dll'}", - "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Wallpaper Fix": { - "FormerIDs": "{EntryDll: 'WallpaperFix.dll'}", - "Default | UpdateKey": "Chucklefish:4211", - "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "WarpAnimals": { - "ID": "Symen.WarpAnimals", - "Default | UpdateKey": "Nexus:1400" - }, - - "Weather Controller": { - "FormerIDs": "{EntryDll: 'WeatherController.dll'}", - "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "What Farm Cave / WhatAMush": { - "ID": "WhatAMush", - "Default | UpdateKey": "Nexus:1097" - }, - - "WHats Up": { - "ID": "wHatsUp", - "Default | UpdateKey": "Nexus:1082" - }, - - "Winter Grass": { - "ID": "cat.wintergrass", - "Default | UpdateKey": "Nexus:1601" - }, - - "Wonderful Farm Life": { - "FormerIDs": "{EntryDll: 'WonderfulFarmLife.dll'}", - "~1.0 | Status": "AssumeBroken" // broke in SDV 1.1 or 1.11 - }, - - "XmlSerializerRetool": { - "FormerIDs": "{EntryDll: 'XmlSerializerRetool.dll'}", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "it's no longer maintained or used." - }, - - "Xnb Loader": { - "ID": "Entoarox.XnbLoader", - "~1.1.10 | UpdateKey": "Chucklefish:4506", // only enable update checks up to 1.1.10 by request (has its own update-check feature) - "~1.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "zDailyIncrease": { - "ID": "zdailyincrease", - "MapRemoteVersions": { "1.3.5": "1.3.4" }, // not updated in manifest - "Default | UpdateKey": "Chucklefish:4247", - "~1.2 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Zoom Out Extreme": { - "ID": "RockinMods.ZoomMod", - "FormerIDs": "ZoomMod", // changed circa 1.2.1 - "Default | UpdateKey": "Nexus:1326", - "~0.1 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Zoryn's Better RNG": { - "ID": "Zoryn.BetterRNG", - "FormerIDs": "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", - "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Zoryn's Calendar Anywhere": { - "ID": "Zoryn.CalendarAnywhere", - "FormerIDs": "a41c01cd-0437-43eb-944f-78cb5a53002a", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", - "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Zoryn's Durable Fences": { - "ID": "Zoryn.DurableFences", - "FormerIDs": "56d3439c-7b9b-497e-9496-0c4890e8a00e", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods" - }, - - "Zoryn's Health Bars": { - "ID": "Zoryn.HealthBars", - "FormerIDs": "{EntryDll: 'HealthBars.dll'}", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", - "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Zoryn's Fishing Mod": { - "ID": "Zoryn.FishingMod", - "FormerIDs": "fa277b1f-265e-47c3-a84f-cd320cc74949", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods" - }, - - "Zoryn's Junimo Deposit Anywhere": { - "ID": "Zoryn.JunimoDepositAnywhere", - "FormerIDs": "f93a4fe8-cade-4146-9335-b5f82fbbf7bc", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", - "~1.7 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Zoryn's Movement Mod": { - "ID": "Zoryn.MovementModifier", - "FormerIDs": "8a632929-8335-484f-87dd-c29d2ba3215d", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", - "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Zoryn's Regen Mod": { - "ID": "Zoryn.RegenMod", - "FormerIDs": "dfac4383-1b6b-4f33-ae4e-37fc23e5252e", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", - "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 - } - } + "VerboseLogging": false } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index c0d5386a..9dbca475 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -100,6 +100,7 @@ + @@ -262,6 +263,9 @@ Always + + PreserveNewest + diff --git a/src/SMAPI/StardewModdingAPI.metadata.json b/src/SMAPI/StardewModdingAPI.metadata.json new file mode 100644 index 00000000..237c069d --- /dev/null +++ b/src/SMAPI/StardewModdingAPI.metadata.json @@ -0,0 +1,1836 @@ +{ + /** + * Metadata about some SMAPI mods used in compatibility, update, and dependency checks. This + * field shouldn't be edited by players in most cases. + * + * Standard fields + * =============== + * The predefined fields are documented below (only 'ID' is required). Each entry's key is the + * default display name for the mod if one isn't available (e.g. in dependency checks). + * + * - ID: the mod's latest unique ID (if any). + * + * - FormerIDs: uniquely identifies the mod across multiple versions, and supports matching + * other fields if no ID was specified. This doesn't include the latest ID, if any. + * Format rules: + * 1. If the mod's ID changed over time, multiple variants can be separated by the '|' + * character. + * 2. Each variant can take one of two forms: + * - A simple string matching the mod's UniqueID value. + * - A JSON structure containing any of four manifest fields (ID, Name, Author, and + * EntryDll) to match. + * + * - MapLocalVersions and MapRemoteVersions correct local manifest versions and remote versions + * during update checks. For example, if the API returns version '1.1-1078' where '1078' is + * intended to be a build number, MapRemoteVersions can map it to '1.1' when comparing to the + * mod's current version. This is only meant to support legacy mods with injected update keys. + * + * Versioned metadata + * ================== + * Each record can also specify extra metadata using the field keys below. + * + * Each key consists of a field name prefixed with any combination of version range and 'Default', + * separated by pipes (whitespace trimmed). For example, 'UpdateKey' will always override, + * 'Default | UpdateKey' will only override if the mod has no update keys, and + * '~1.1 | Default | Name' will do the same up to version 1.1. + * + * The version format is 'min~max' (where either side can be blank for unbounded), or a single + * version number. + * + * These are the valid field names: + * + * - UpdateKey: the update key to set in the mod's manifest. This is used to enable update + * checks for older mods that haven't been updated to use it yet. + * + * - Status: overrides compatibility checks. The possible values are Obsolete (SMAPI won't load + * it because the mod should no longer be used), AssumeBroken (SMAPI won't load it because + * the specified version isn't compatible), or AssumeCompatible (SMAPI will try to load it + * even if it detects incompatible code). + * + * - StatusReasonPhrase: a message to show to the player explaining why the mod can't be loaded + * (if applicable). If blank, will default to a generic not-compatible message. + * + * - AlternativeUrl: a URL where the player can find an unofficial update or alternative if the + * mod is no longer compatible. + */ + "ModData": { + "AccessChestAnywhere": { + "ID": "AccessChestAnywhere", + "MapLocalVersions": { "1.1-1078": "1.1" }, + "Default | UpdateKey": "Nexus:257", + "~1.1 | Status": "AssumeBroken" + }, + + "AdjustArtisanPrices": { + "ID": "ThatNorthernMonkey.AdjustArtisanPrices", + "FormerIDs": "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc", // changed in 0.0.2-pathoschild-update + "MapRemoteVersions": { "0.01": "0.0.1" }, + "Default | UpdateKey": "Chucklefish:3532", + "~0.0.1 | Status": "AssumeBroken" + }, + + "Adjust Monster": { + "ID": "mmanlapat.AdjustMonster", + "Default | UpdateKey": "Nexus:1161" + }, + + "Advanced Location Loader": { + "ID": "Entoarox.AdvancedLocationLoader", + "~1.3.7 | UpdateKey": "Chucklefish:3619", // only enable update checks up to 1.3.7 by request (has its own update-check feature) + "~1.2.10 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Adventure Shop Inventory": { + "ID": "HammurabiAdventureShopInventory", + "Default | UpdateKey": "Chucklefish:4608" + }, + + "AgingMod": { + "ID": "skn.AgingMod", + "Default | UpdateKey": "Nexus:1129", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "All Crops All Seasons": { + "ID": "cantorsdust.AllCropsAllSeasons", + "FormerIDs": "29ee8246-d67b-4242-a340-35a9ae0d5dd7 | community.AllCropsAllSeasons", // changed in 1.3 and 1.5 + "Default | UpdateKey": "Nexus:170" + }, + + "All Professions": { + "ID": "cantorsdust.AllProfessions", + "FormerIDs": "8c37b1a7-4bfb-4916-9d8a-9533e6363ea3 | community.AllProfessions", // changed in 1.2 and 1.3.1 + "Default | UpdateKey": "Nexus:174" + }, + + "Almighty Tool": { + "ID": "439", + "FormerIDs": "{EntryDll: 'AlmightyTool.dll'}", // changed in 1.2.1 + "MapRemoteVersions": { "1.21": "1.2.1" }, + "Default | UpdateKey": "Nexus:439", + "~1.1.1 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Animal Husbandry": { + "ID": "DIGUS.ANIMALHUSBANDRYMOD", + "FormerIDs": "DIGUS.BUTCHER", // changed in 2.0.1 + "Default | UpdateKey": "Nexus:1538" + }, + + "Animal Mood Fix": { + "ID": "GPeters-AnimalMoodFix", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2." + }, + + "Animal Sitter": { + "ID": "jwdred.AnimalSitter", + "FormerIDs": "{EntryDll: 'AnimalSitter.dll'}", // changed in 1.0.9 + "Default | UpdateKey": "Nexus:581", + "~1.0.8 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Ashley Mod": { + "FormerIDs": "{EntryDll: 'AshleyMod.dll'}", + "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "A Tapper's Dream": { + "ID": "ddde5195-8f85-4061-90cc-0d4fd5459358", + "Default | UpdateKey": "Nexus:260", + "~1.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Auto Animal Doors": { + "ID": "AaronTaggart.AutoAnimalDoors", + "Default | UpdateKey": "Nexus:1019" + }, + + "Auto-Eat": { + "ID": "Permamiss.AutoEat", + "FormerIDs": "BALANCEMOD_AutoEat", // changed in 1.1.1 + "Default | UpdateKey": "Nexus:643" + }, + + "AutoFish": { + "ID": "WhiteMind.AF", + "Default | UpdateKey": "Nexus:1895" + }, + + "AutoGate": { + "ID": "AutoGate", + "Default | UpdateKey": "Nexus:820" + }, + + "Automate": { + "ID": "Pathoschild.Automate", + "Default | UpdateKey": "Nexus:1063" + }, + + "Automated Doors": { + "ID": "azah.automated-doors", + "FormerIDs": "1abcfa07-2cf4-4dc3-a6e9-6068b642112b", // changed in 1.4.1 + "Default | UpdateKey": "GitHub:azah/AutomatedDoors" // added in 1.4.2 + }, + + "AutoSpeed": { + "ID": "Omegasis.AutoSpeed", + "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'AutoSpeed'}", // changed in 1.4; disambiguate from other Alpha_Omegasis mods + "Default | UpdateKey": "Nexus:443" // added in 1.4.1 + }, + + "Basic Sprinklers Improved": { + "ID": "lrsk_sdvm_bsi.0117171308", + "MapRemoteVersions": { "1.0.2": "1.0.1-release" }, // manifest not updated + "Default | UpdateKey": "Nexus:833" + }, + + "Better Hay": { + "ID": "cat.betterhay", + "Default | UpdateKey": "Nexus:1430" + }, + + "Better Quality More Seasons": { + "ID": "SB_BQMS", + "Default | UpdateKey": "Nexus:935" + }, + + "Better Quarry": { + "ID": "BetterQuarry", + "Default | UpdateKey": "Nexus:771" + }, + + "Better Ranching": { + "ID": "BetterRanching", + "Default | UpdateKey": "Nexus:859" + }, + + "Better Shipping Box": { + "ID": "Kithio:BetterShippingBox", + "MapLocalVersions": { "1.0.1": "1.0.2" }, + "Default | UpdateKey": "Chucklefish:4302", + "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Better Sprinklers": { + "ID": "Speeder.BetterSprinklers", + "FormerIDs": "SPDSprinklersMod", // changed in 2.3 + "Default | UpdateKey": "Nexus:41", + "~2.3.1-pathoschild-update | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Billboard Anywhere": { + "ID": "Omegasis.BillboardAnywhere", + "FormerIDs": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Billboard Anywhere'}", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis + "Default | UpdateKey": "Nexus:492" // added in 1.4.1 + }, + + "Birthday Mail": { + "ID": "KathrynHazuka.BirthdayMail", + "FormerIDs": "005e02dc-d900-425c-9c68-1ff55c5a295d", // changed in 1.2.3-pathoschild-update + "Default | UpdateKey": "Nexus:276", + "~1.2.2 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Breed Like Rabbits": { + "ID": "dycedarger.breedlikerabbits", + "Default | UpdateKey": "Nexus:948" + }, + + "Build Endurance": { + "ID": "Omegasis.BuildEndurance", + "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildEndurance'}", // changed in 1.4; disambiguate from other Alpha_Omegasis mods + "Default | UpdateKey": "Nexus:445", // added in 1.4.1 + "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Build Health": { + "ID": "Omegasis.BuildHealth", + "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildHealth'}", // changed in 1.4; disambiguate from other Alpha_Omegasis mods + "Default | UpdateKey": "Nexus:446", // added in 1.4.1 + "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Buy Cooking Recipes": { + "ID": "Denifia.BuyRecipes", + "Default | UpdateKey": "Nexus:1126", // added in 1.0.1 (2017-10-04) + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Buy Back Collectables": { + "ID": "Omegasis.BuyBackCollectables", + "FormerIDs": "BuyBackCollectables", // changed in 1.4 + "Default | UpdateKey": "Nexus:507", // added in 1.4.1 + "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Carry Chest": { + "ID": "spacechase0.CarryChest", + "Default | UpdateKey": "Nexus:1333" + }, + + "Casks Anywhere": { + "ID": "CasksAnywhere", + "MapLocalVersions": { "1.1-alpha": "1.1" }, + "Default | UpdateKey": "Nexus:878" + }, + + "Categorize Chests": { + "ID": "CategorizeChests", + "Default | UpdateKey": "Nexus:1300" + }, + + "Chefs Closet": { + "ID": "Duder.ChefsCloset", + "MapLocalVersions": { "1.3-1": "1.3" }, + "Default | UpdateKey": "Nexus:1030" + }, + + "Chest Label System": { + "ID": "Speeder.ChestLabel", + "FormerIDs": "SPDChestLabel", // changed in 1.5.1-pathoschild-update + "Default | UpdateKey": "Nexus:242", + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.1 + }, + + "Chest Pooling": { + "ID": "mralbobo.ChestPooling", + "FormerIDs": "{EntryDll: 'ChestPooling.dll'}", // changed in 1.3 + "Default | UpdateKey": "GitHub:mralbobo/stardew-chest-pooling", + "~1.2 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Chests Anywhere": { + "ID": "Pathoschild.ChestsAnywhere", + "FormerIDs": "ChestsAnywhere", // changed in 1.9 + "Default | UpdateKey": "Nexus:518", + "~1.9-beta | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Choose Baby Gender": { + "FormerIDs": "{EntryDll: 'ChooseBabyGender.dll'}", + "Default | UpdateKey": "Nexus:590", + "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "CJB Automation": { + "ID": "CJBAutomation", + "Default | UpdateKey": "Nexus:211", + "~1.4 | Status": "AssumeBroken", // broke in SDV 1.2 + "~1.4 | AlternativeUrl": "http://www.nexusmods.com/stardewvalley/mods/1063" + }, + + "CJB Cheats Menu": { + "ID": "CJBok.CheatsMenu", + "FormerIDs": "CJBCheatsMenu", // changed in 1.14 + "Default | UpdateKey": "Nexus:4", + "~1.12 | Status": "AssumeBroken" // broke in SDV 1.1 + }, + + "CJB Item Spawner": { + "ID": "CJBok.ItemSpawner", + "FormerIDs": "CJBItemSpawner", // changed in 1.7 + "Default | UpdateKey": "Nexus:93", + "~1.5 | Status": "AssumeBroken" // broke in SDV 1.1 + }, + + "CJB Show Item Sell Price": { + "ID": "CJBok.ShowItemSellPrice", + "FormerIDs": "CJBShowItemSellPrice", // changed in 1.7 + "Default | UpdateKey": "Nexus:5", + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Clean Farm": { + "ID": "tstaples.CleanFarm", + "Default | UpdateKey": "Nexus:794" + }, + + "Climates of Ferngill": { + "ID": "KoihimeNakamura.ClimatesOfFerngill", + "Default | UpdateKey": "Nexus:604", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Coal Regen": { + "ID": "Blucifer.CoalRegen", + "Default | UpdateKey": "Nexus:1664" + }, + + "Cold Weather Haley": { + "ID": "LordXamon.ColdWeatherHaleyPRO", + "Default | UpdateKey": "Nexus:1169", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Colored Chests": { + "ID": "4befde5c-731c-4853-8e4b-c5cdf946805f", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "colored chests were added in Stardew Valley 1.1." + }, + + "Combat with Farm Implements": { + "ID": "SPDFarmingImplementsInCombat", + "Default | UpdateKey": "Nexus:313", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Community Bundle Item Tooltip": { + "ID": "musbah.bundleTooltip", + "Default | UpdateKey": "Nexus:1329" + }, + + "Concentration on Farming": { + "ID": "punyo.ConcentrationOnFarming", + "Default | UpdateKey": "Nexus:1445" + }, + + "Configurable Machines": { + "ID": "21da6619-dc03-4660-9794-8e5b498f5b97", + "MapLocalVersions": { "1.2-beta": "1.2" }, + "Default | UpdateKey": "Nexus:280" + }, + + "Configurable Shipping Dates": { + "ID": "ConfigurableShippingDates", + "Default | UpdateKey": "Nexus:675", + "~1.1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Cooking Skill": { + "ID": "spacechase0.CookingSkill", + "FormerIDs": "CookingSkill", // changed in 1.0.4–6 + "Default | UpdateKey": "Nexus:522", + "~1.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "CrabNet": { + "ID": "jwdred.CrabNet", + "FormerIDs": "{EntryDll: 'CrabNet.dll'}", // changed in 1.0.5 + "Default | UpdateKey": "Nexus:584", + "~1.0.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Crafting Counter": { + "ID": "lolpcgaming.CraftingCounter", + "Default | UpdateKey": "Nexus:1585" + }, + + "Current Location": { + "ID": "CurrentLocation102120161203", + "Default | UpdateKey": "Nexus:638" + }, + + "Custom Asset Modifier": { + "ID": "Omegasis.CustomAssetModifier", + "Default | UpdateKey": "1836" + }, + + "Custom Critters": { + "ID": "spacechase0.CustomCritters", + "Default | UpdateKey": "Nexus:1255" + }, + + "Custom Crops": { + "ID": "spacechase0.CustomCrops", + "Default | UpdateKey": "Nexus:1592" + }, + + "Custom Element Handler": { + "ID": "Platonymous.CustomElementHandler", + "Default | UpdateKey": "Nexus:1068" // added in 1.3.1 + }, + + "Custom Farming Redux": { + "ID": "Platonymous.CustomFarming", + "Default | UpdateKey": "Nexus:991" // added in 0.6.1 + }, + + "Custom Farming Automate Bridge": { + "ID": "Platonymous.CFAutomate", + "~1.0.1 | Status": "AssumeBroken", // no longer compatible with Automate + "~1.0.1 | AlternativeUrl": "https://www.nexusmods.com/stardewvalley/mods/991" + }, + + "Custom Farm Types": { + "ID": "spacechase0.CustomFarmTypes", + "Default | UpdateKey": "Nexus:1140" + }, + + "Custom Furniture": { + "ID": "Platonymous.CustomFurniture", + "Default | UpdateKey": "Nexus:1254" // added in 0.4.1 + }, + + "Customize Exterior": { + "ID": "spacechase0.CustomizeExterior", + "FormerIDs": "CustomizeExterior", // changed in 1.0.3 + "Default | UpdateKey": "Nexus:1099", + "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Customizable Cart Redux": { + "ID": "KoihimeNakamura.CCR", + "MapLocalVersions": { "1.1-20170917": "1.1" }, + "Default | UpdateKey": "Nexus:1402" + }, + + "Customizable Traveling Cart Days": { + "ID": "TravelingCartYyeahdude", + "Default | UpdateKey": "Nexus:567", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Custom Linens": { + "ID": "Mevima.CustomLinens", + "MapRemoteVersions": { "1.1": "1.0" }, // manifest not updated + "Default | UpdateKey": "Nexus:1027" + }, + + "Custom NPC": { + "ID": "Platonymous.CustomNPC", + "Default | UpdateKey": "Nexus:1607" + }, + + "Custom Shops Redux": { + "ID": "Omegasis.CustomShopReduxGui", + "Default | UpdateKey": "Nexus:1378" // added in 1.4.1 + }, + + "Custom TV": { + "ID": "Platonymous.CustomTV", + "Default | UpdateKey": "Nexus:1139" // added in 1.0.6 + }, + + "Daily Luck Message": { + "ID": "Schematix.DailyLuckMessage", + "Default | UpdateKey": "Nexus:1327" + }, + + "Daily News": { + "ID": "bashNinja.DailyNews", + "Default | UpdateKey": "Nexus:1141", + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Daily Quest Anywhere": { + "ID": "Omegasis.DailyQuestAnywhere", + "FormerIDs": "DailyQuest", // changed in 1.4 + "Default | UpdateKey": "Nexus:513" // added in 1.4.1 + }, + + "Debug Mode": { + "ID": "Pathoschild.DebugMode", + "FormerIDs": "Pathoschild.Stardew.DebugMode", // changed in 1.4 + "Default | UpdateKey": "Nexus:679" + }, + + "Did You Water Your Crops?": { + "ID": "Nishtra.DidYouWaterYourCrops", + "Default | UpdateKey": "Nexus:1583" + }, + + "Dynamic Checklist": { + "ID": "gunnargolf.DynamicChecklist", + "Default | UpdateKey": "Nexus:1145", // added in 1.0.1-pathoschild-update + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Dynamic Horses": { + "ID": "Bpendragon-DynamicHorses", + "MapRemoteVersions": { "1.2": "1.1-release" }, // manifest not updated + "Default | UpdateKey": "Nexus:874" + }, + + "Dynamic Machines": { + "ID": "DynamicMachines", + "MapLocalVersions": { "1.1": "1.1.1" }, + "Default | UpdateKey": "Nexus:374", + "~1.1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Dynamic NPC Sprites": { + "ID": "BashNinja.DynamicNPCSprites", + "Default | UpdateKey": "Nexus:1183" + }, + + "Easier Farming": { + "ID": "cautiouswafffle.EasierFarming", + "Default | UpdateKey": "Nexus:1426" + }, + + "Empty Hands": { + "ID": "QuicksilverFox.EmptyHands", + "Default | UpdateKey": "Nexus:1176", // added in 1.0.1-pathoschild-update + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Enemy Health Bars": { + "ID": "Speeder.HealthBars", + "FormerIDs": "SPDHealthBar", // changed in 1.7.1-pathoschild-update + "Default | UpdateKey": "Nexus:193", + "~1.7 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Entoarox Framework": { + "ID": "Entoarox.EntoaroxFramework", + "FormerIDs": "eacdb74b-4080-4452-b16b-93773cda5cf9", // changed in ??? + "~2.0.6 | UpdateKey": "Chucklefish:4228", // only enable update checks up to 2.0.6 by request (has its own update-check feature) + "~2.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.5 (error reflecting into SMAPI internals) + }, + + "Expanded Fridge": { + "ID": "Uwazouri.ExpandedFridge", + "Default | UpdateKey": "Nexus:1191" + }, + + "Experience Bars": { + "ID": "spacechase0.ExperienceBars", + "FormerIDs": "ExperienceBars", // changed in 1.0.2 + "Default | UpdateKey": "Nexus:509" + }, + + "Extended Bus System": { + "ID": "ExtendedBusSystem", + "Default | UpdateKey": "Chucklefish:4373" + }, + + "Extended Fridge": { + "ID": "Crystalmir.ExtendedFridge", + "FormerIDs": "Mystra007ExtendedFridge", // changed in 1.0.1 + "Default | UpdateKey": "Nexus:485", + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Extended Greenhouse": { + "ID": "ExtendedGreenhouse", + "Default | UpdateKey": "Chucklefish:4303", + "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Extended Minecart": { + "ID": "Entoarox.ExtendedMinecart", + "FormerIDs": "{ID:'EntoaroxFurnitureAnywhere', Name:'Extended Minecart'}", // changed in 1.6.1 + "~1.7.1 | UpdateKey": "Chucklefish:4359" // only enable update checks up to 1.7.1 by request (has its own update-check feature) + }, + + "Extended Reach": { + "ID": "spacechase0.ExtendedReach", + "Default | UpdateKey": "Nexus:1493" + }, + + "Fall 28 Snow Day": { + "ID": "Omegasis.Fall28SnowDay", + "FormerIDs": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Fall28 Snow Day'}", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis + "Default | UpdateKey": "Nexus:486", // added in 1.4.1 + "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Farm Automation: Barn Door Automation": { + "FormerIDs": "{EntryDll: 'FarmAutomation.BarnDoorAutomation.dll'}", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Farm Automation: Item Collector": { + "FormerIDs": "{EntryDll: 'FarmAutomation.ItemCollector.dll'}", + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Farm Automation Unofficial: Item Collector": { + "ID": "Maddy99.FarmAutomation.ItemCollector", + "~0.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Farm Expansion": { + "ID": "Advize.FarmExpansion", + "FormerIDs": "3888bdfd-73f6-4776-8bb7-8ad45aea1915 | AdvizeFarmExpansionMod-2-0 | AdvizeFarmExpansionMod-2-0-5", // changed in 2.0, 2.0.5, and 3.0 + "Default | UpdateKey": "Nexus:130", + "~2.0.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Farm Resource Generator": { + "FormerIDs": "{EntryDll: 'FarmResourceGenerator.dll'}", + "Default | UpdateKey": "Nexus:647", + "~1.0.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Fast Animations": { + "ID": "Pathoschild.FastAnimations", + "Default | UpdateKey": "Nexus:1089" + }, + + "Faster Grass": { + "ID": "IceGladiador.FasterGrass", + "Default | UpdateKey": "Nexus:1772" + }, + + "Faster Paths": { + "ID": "Entoarox.FasterPaths", + "FormerIDs": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Faster Paths'} | 615f85f8-5c89-44ee-aecc-c328f172e413", // changed in 1.2 and 1.3; disambiguate from Shop Expander + "~1.3.3 | UpdateKey": "Chucklefish:3641" // only enable update checks up to 1.3.3 by request (has its own update-check feature) + }, + + "Faster Run": { + "ID": "KathrynHazuka.FasterRun", + "FormerIDs": "{EntryDll: 'FasterRun.dll'}", // changed in 1.1.1-pathoschild-update + "Default | UpdateKey": "Nexus:733", // added in 1.1.1-pathoschild-update + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Fishing Adjust": { + "ID": "shuaiz.FishingAdjustMod", + "Default | UpdateKey": "Nexus:1350" + }, + + "Fishing Tuner Redux": { + "ID": "HammurabiFishingTunerRedux", + "Default | UpdateKey": "Chucklefish:4578" + }, + + "Fixed Secret Woods Debris": { + "ID": "f4iTh.WoodsDebrisFix", + "Default | UpdateKey": "Nexus:1941" + }, + + "FlorenceMod": { + "FormerIDs": "{EntryDll: 'FlorenceMod.dll'}", + "MapLocalVersions": { "1.0.1": "1.1" }, + "Default | UpdateKey": "Nexus:591", + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Flower Color Picker": { + "ID": "spacechase0.FlowerColorPicker", + "Default | UpdateKey": "Nexus:1229" + }, + + "Forage at the Farm": { + "ID": "Nishtra.ForageAtTheFarm", + "FormerIDs": "ForageAtTheFarm", // changed in <=1.6 + "Default | UpdateKey": "Nexus:673", + "~1.5.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Furniture Anywhere": { + "ID": "Entoarox.FurnitureAnywhere", + "FormerIDs": "{ID:'EntoaroxFurnitureAnywhere', Name:'Furniture Anywhere'}", // changed in 1.1; disambiguate from Extended Minecart + "~1.1.5 | UpdateKey": "Chucklefish:4324" // only enable update checks up to 1.1.5 by request (has its own update-check feature) + }, + + "Game Reminder": { + "ID": "mmanlapat.GameReminder", + "Default | UpdateKey": "Nexus:1153" + }, + + "Gate Opener": { + "ID": "mralbobo.GateOpener", + "FormerIDs": "{EntryDll: 'GateOpener.dll'}", // changed in 1.1 + "Default | UpdateKey": "GitHub:mralbobo/stardew-gate-opener", + "~1.0.1 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "GenericShopExtender": { + "ID": "GenericShopExtender", + "Default | UpdateKey": "Nexus:814", // added in 0.1.3 + "~0.1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Geode Info Menu": { + "ID": "cat.geodeinfomenu", + "Default | UpdateKey": "Nexus:1448" + }, + + "Get Dressed": { + "ID": "Advize.GetDressed", + "FormerIDs": "{EntryDll: 'GetDressed.dll'}", // changed in 3.3 + "Default | UpdateKey": "Nexus:331", + "~3.3 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Giant Crop Ring": { + "ID": "cat.giantcropring", + "Default | UpdateKey": "Nexus:1182" + }, + + "Gift Taste Helper": { + "ID": "tstaples.GiftTasteHelper", + "FormerIDs": "8008db57-fa67-4730-978e-34b37ef191d6", // changed in 2.5 + "Default | UpdateKey": "Nexus:229", + "~2.3.1 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Grandfather's Gift": { + "ID": "ShadowDragon.GrandfathersGift", + "Default | UpdateKey": "Nexus:985" + }, + + "Happy Animals": { + "ID": "HappyAnimals", + "~1.0.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Happy Birthday (Omegasis)": { + "ID": "Omegasis.HappyBirthday", + "FormerIDs": "{ID:'HappyBirthday', Author:'Alpha_Omegasis'}", // changed in 1.4; disambiguate from Oxyligen's fork + "Default | UpdateKey": "Nexus:520", // added in 1.4.1 + "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Happy Birthday (Oxyligen fork)": { + "FormerIDs": "{ID:'HappyBirthday', Author:'Alpha_Omegasis/Oxyligen'}", // disambiguate from Oxyligen's fork + "Default | UpdateKey": "Nexus:1064" // missing key reported: https://www.nexusmods.com/stardewvalley/mods/1064?tab=bugs + }, + + "Hardcore Mines": { + "ID": "kibbe.hardcore_mines", + "Default | UpdateKey": "Nexus:1674" + }, + + "Harp of Yoba Redux": { + "ID": "Platonymous.HarpOfYobaRedux", + "Default | UpdateKey": "Nexus:914" // added in 2.0.3 + }, + + "Harvest Moon Witch Princess": { + "ID": "Sasara.WitchPrincess", + "Default | UpdateKey": "Nexus:1157" + }, + + "Harvest With Scythe": { + "ID": "965169fd-e1ed-47d0-9f12-b104535fb4bc", + "Default | UpdateKey": "Nexus:236", + "~1.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Horse Whistle (icepuente)": { + "ID": "icepuente.HorseWhistle", + "Default | UpdateKey": "Nexus:1131" + }, + + "Hunger (Yyeadude)": { + "ID": "HungerYyeadude", + "Default | UpdateKey": "Nexus:613" + }, + + "Hunger for Food (Tigerle)": { + "ID": "HungerForFoodByTigerle", + "Default | UpdateKey": "Nexus:810", + "~0.1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Hunger Mod (skn)": { + "ID": "skn.HungerMod", + "MapRemoteVersions": { "1.2.1": "1.0" }, // manifest not updated + "Default | UpdateKey": "Nexus:1127" + }, + + "Idle Pause": { + "ID": "Veleek.IdlePause", + "MapRemoteVersions": { "1.2": "1.1" }, // manifest not updated + "Default | UpdateKey": "Nexus:1092" + }, + + "Improved Quality of Life": { + "ID": "Demiacle.ImprovedQualityOfLife", + "Default | UpdateKey": "Nexus:1025", + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Instant Geode": { + "ID": "InstantGeode", + "~1.12 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Instant Grow Trees": { + "ID": "cantorsdust.InstantGrowTrees", + "FormerIDs": "dc50c58b-c7d8-4e60-86cc-e27b5d95ee59 | community.InstantGrowTrees", // changed in 1.2 and 1.3.1 + "Default | UpdateKey": "Nexus:173" + }, + + "Interaction Helper": { + "ID": "HammurabiInteractionHelper", + "Default | UpdateKey": "Chucklefish:4640", // added in 1.0.4-pathoschild-update + "~1.0.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Item Auto Stacker": { + "ID": "cat.autostacker", + "MapRemoteVersions": { "1.0.1": "1.0" }, // manifest not updated + "Default | UpdateKey": "Nexus:1184" + }, + + "Jiggly Junimo Bundles": { + "ID": "Greger.JigglyJunimoBundles", + "FormerIDs": "{EntryDll: 'JJB.dll'}", // changed in 1.1.2-pathoschild-update + "Default | UpdateKey": "GitHub:gr3ger/Stardew_JJB" // added in 1.0.4-pathoschild-update + }, + + "Json Assets": { + "ID": "spacechase0.JsonAssets", + "Default | UpdateKey": "Nexus:1720" + }, + + "Junimo Farm": { + "ID": "Platonymous.JunimoFarm", + "MapRemoteVersions": { "1.1.2": "1.1.1" }, // manifest not updated + "Default | UpdateKey": "Nexus:984" // added in 1.1.3 + }, + + "Less Strict Over-Exertion (AntiExhaustion)": { + "ID": "BALANCEMOD_AntiExhaustion", + "MapLocalVersions": { "0.0": "1.1" }, + "Default | UpdateKey": "Nexus:637", + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Level Extender": { + "ID": "Devin Lematty.Level Extender", + "MapRemoteVersions": { "1.1": "1.0" }, // manifest not updated + "Default | UpdateKey": "Nexus:1471" + }, + + "Level Up Notifications": { + "ID": "Level Up Notifications", + "MapRemoteVersions": { "0.0.1a": "0.0.1" }, + "Default | UpdateKey": "Nexus:855" + }, + + "Location and Music Logging": { + "ID": "Brandy Lover.LMlog", + "Default | UpdateKey": "Nexus:1366" + }, + + "Longevity": { + "ID": "RTGOAT.Longevity", + "MapRemoteVersions": { "1.6.8h": "1.6.8" }, + "Default | UpdateKey": "Nexus:649" + }, + + "Lookup Anything": { + "ID": "Pathoschild.LookupAnything", + "FormerIDs": "LookupAnything", // changed in 1.10.1 + "Default | UpdateKey": "Nexus:541", + "~1.10.1 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Love Bubbles": { + "ID": "LoveBubbles", + "Default | UpdateKey": "Nexus:1318" + }, + + "Loved Labels": { + "ID": "Advize.LovedLabels", + "FormerIDs": "{EntryDll: 'LovedLabels.dll'}", // changed in 2.1 + "Default | UpdateKey": "Nexus:279", + "~2.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Luck Skill": { + "ID": "spacechase0.LuckSkill", + "FormerIDs": "LuckSkill", // changed in 0.1.4 + "Default | UpdateKey": "Nexus:521", + "~0.1.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Mail Framework": { + "ID": "DIGUS.MailFrameworkMod", + "Default | UpdateKey": "Nexus:1536" + }, + + "MailOrderPigs": { + "ID": "jwdred.MailOrderPigs", + "FormerIDs": "{EntryDll: 'MailOrderPigs.dll'}", // changed in 1.0.2 + "Default | UpdateKey": "Nexus:632", + "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Makeshift Multiplayer": { + "ID": "spacechase0.StardewValleyMP", + "FormerIDs": "StardewValleyMP", // changed in 0.3 + "Default | UpdateKey": "Nexus:501", + "~0.3.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Map Image Exporter": { + "ID": "spacechase0.MapImageExporter", + "FormerIDs": "MapImageExporter", // changed in 1.0.2 + "Default | UpdateKey": "Nexus:1073" + }, + + "Message Box [API]? (ChatMod)": { + "ID": "Kithio:ChatMod", + "Default | UpdateKey": "Chucklefish:4296", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Mining at the Farm": { + "ID": "Nishtra.MiningAtTheFarm", + "FormerIDs": "MiningAtTheFarm", // changed in <=1.7 + "Default | UpdateKey": "Nexus:674" + }, + + "Mining With Explosives": { + "ID": "Nishtra.MiningWithExplosives", + "FormerIDs": "MiningWithExplosives", // changed in 1.1 + "Default | UpdateKey": "Nexus:770" + }, + + "Modder Serialization Utility": { + "ID": "SerializerUtils-0-1", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "it's no longer maintained or used." + }, + + "Monster Level Tip": { + "ID": "WhiteMind.MonsterLT", + "Default | UpdateKey": "Nexus:1896" + }, + + "More Animals": { + "ID": "Entoarox.MoreAnimals", + "FormerIDs": "821ce8f6-e629-41ad-9fde-03b54f68b0b6MOREPETS | Entoarox.MorePets", // changed in 1.3 and 2.0 + "~2.0.2 | UpdateKey": "Chucklefish:4288", // only enable update checks up to 2.0.2 by request (has its own update-check feature) + "~1.3.2 | Status": "AssumeBroken" // overhauled for SMAPI 1.11+ compatibility + }, + + "More Artifact Spots": { + "ID": "451", + "Default | UpdateKey": "Nexus:451", + "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "More Map Layers": { + "ID": "Platonymous.MoreMapLayers", + "Default | UpdateKey": "Nexus:1134" // added in 1.1.1 + }, + + "More Rain": { + "ID": "Omegasis.MoreRain", + "FormerIDs": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'More_Rain'}", // changed in 1.5; disambiguate from other mods by Alpha_Omegasis + "Default | UpdateKey": "Nexus:441", // added in 1.5.1 + "~1.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "More Weapons": { + "ID": "Joco80.MoreWeapons", + "Default | UpdateKey": "Nexus:1168" + }, + + "Move Faster": { + "ID": "shuaiz.MoveFasterMod", + "Default | UpdateKey": "Nexus:1351" + }, + + "Multiple Sprites and Portraits On Rotation (File Loading)": { + "ID": "FileLoading", + "MapLocalVersions": { "1.1": "1.12" }, + "Default | UpdateKey": "Nexus:1094", + "~1.12 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Museum Rearranger": { + "ID": "Omegasis.MuseumRearranger", + "FormerIDs": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Museum Rearranger'}", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis + "Default | UpdateKey": "Nexus:428", // added in 1.4.1 + "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Mushroom Level Tip": { + "ID": "WhiteMind.MLT", + "Default | UpdateKey": "Nexus:1894" + }, + + "New Machines": { + "ID": "F70D4FAB-0AB2-4B78-9F1B-AF2CA2236A59", + "Default | UpdateKey": "Chucklefish:3683", + "~4.2.1343 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Night Owl": { + "ID": "Omegasis.NightOwl", + "FormerIDs": "{ID:'SaveAnywhere', Name:'Stardew_NightOwl'}", // changed in 1.4; disambiguate from Save Anywhere + "MapLocalVersions": { "2.1": "1.3" }, // 1.3 had wrong version in manifest + "Default | UpdateKey": "Nexus:433", // added in 1.4.1 + "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "No Crows": { + "ID": "cat.nocrows", + "Default | UpdateKey": "Nexus:1682" + }, + + "No Kids Ever": { + "ID": "Hangy.NoKidsEver", + "Default | UpdateKey": "Nexus:1464" + }, + + "No Debug Mode": { + "ID": "NoDebugMode", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "debug mode was removed in SMAPI 1.0." + }, + + "No Fence Decay": { + "ID": "cat.nofencedecay", + "Default | UpdateKey": "Nexus:1180" + }, + + "No More Pets": { + "ID": "Omegasis.NoMorePets", + "FormerIDs": "NoMorePets", // changed in 1.4 + "Default | UpdateKey": "Nexus:506" // added in 1.4.1 + }, + + "No Rumble Horse": { + "ID": "Xangria.NoRumbleHorse", + "Default | UpdateKey": "Nexus:1779" + }, + + "No Soil Decay": { + "ID": "289dee03-5f38-4d8e-8ffc-e440198e8610", + "Default | UpdateKey": "Nexus:237", + "~0.5 | Status": "AssumeBroken" // broke in SDV 1.2 and uses Assembly.GetExecutingAssembly().Location + }, + + "No Soil Decay Redux": { + "ID": "Platonymous.NoSoilDecayRedux", + "Default | UpdateKey": "Nexus:1084" // added in 1.1.9 + }, + + "NPC Map Locations": { + "ID": "NPCMapLocationsMod", + "Default | UpdateKey": "Nexus:239", + "1.42~1.43 | Status": "AssumeBroken", + "1.42~1.43 | StatusReasonPhrase": "this version has an update check error which crashes the game." + }, + + "NPC Speak": { + "FormerIDs": "{EntryDll: 'NpcEcho.dll'}", + "Default | UpdateKey": "Nexus:694", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Object Time Left": { + "ID": "spacechase0.ObjectTimeLeft", + "Default | UpdateKey": "Nexus:1315" + }, + + "OmniFarm": { + "ID": "PhthaloBlue.OmniFarm", + "FormerIDs": "BlueMod_OmniFarm", // changed in 2.0.2-pathoschild-update + "Default | UpdateKey": "GitHub:lambui/StardewValleyMod_OmniFarm", + "~2.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Out of Season Bonuses (Seasonal Items)": { + "ID": "midoriarmstrong.seasonalitems", + "Default | UpdateKey": "Nexus:1452" + }, + + "Part of the Community": { + "ID": "SB_PotC", + "Default | UpdateKey": "Nexus:923", + "~1.0.8 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "PelicanFiber": { + "ID": "jwdred.PelicanFiber", + "FormerIDs": "{EntryDll: 'PelicanFiber.dll'}", // changed in 3.0.1 + "MapRemoteVersions": { "3.0.2": "3.0.1" }, // didn't change manifest version + "Default | UpdateKey": "Nexus:631", + "~3.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "PelicanTTS": { + "ID": "Platonymous.PelicanTTS", + "Default | UpdateKey": "Nexus:1079", // added in 1.6.1 + "~1.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Persia the Mermaid - Standalone Custom NPC": { + "ID": "63b9f419-7449-42db-ab2e-440b4d05c073", + "Default | UpdateKey": "Nexus:1419" + }, + + "Persistent Game Options": { + "ID": "Xangria.PersistentGameOptions", + "Default | UpdateKey": "Nexus:1778" + }, + + "Persival's BundleMod": { + "FormerIDs": "{EntryDll: 'BundleMod.dll'}", + "Default | UpdateKey": "Nexus:438", + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.1 + }, + + "Plant on Grass": { + "ID": "Demiacle.PlantOnGrass", + "Default | UpdateKey": "Nexus:1026" + }, + + "PyTK - Platonymous Toolkit": { + "ID": "Platonymous.Toolkit", + "Default | UpdateKey": "Nexus:1726" + }, + + "Point-and-Plant": { + "ID": "jwdred.PointAndPlant", + "FormerIDs": "{EntryDll: 'PointAndPlant.dll'}", // changed in 1.0.3 + "Default | UpdateKey": "Nexus:572", + "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Pony Weight Loss Program": { + "ID": "BadNetCode.PonyWeightLossProgram", + "Default | UpdateKey": "Nexus:1232" + }, + + "Portraiture": { + "ID": "Platonymous.Portraiture", + "Default | UpdateKey": "Nexus:999" // added in 1.3.1 + }, + + "Prairie King Made Easy": { + "ID": "Mucchan.PrairieKingMadeEasy", + "FormerIDs": "{EntryDll: 'PrairieKingMadeEasy.dll'}", // changed in 1.0.1 + "Default | UpdateKey": "Chucklefish:3594", + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Purchasable Recipes": { + "ID": "Paracosm.PurchasableRecipes", + "Default | UpdateKey": "Nexus:1722" + }, + + "Quest Delay": { + "ID": "BadNetCode.QuestDelay", + "Default | UpdateKey": "Nexus:1239" + }, + + "Rain Randomizer": { + "FormerIDs": "{EntryDll: 'RainRandomizer.dll'}", + "~1.0.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Recatch Legendary Fish": { + "ID": "cantorsdust.RecatchLegendaryFish", + "FormerIDs": "b3af8c31-48f0-43cf-8343-3eb08bcfa1f9 | community.RecatchLegendaryFish", // changed in 1.3 and 1.5.1 + "Default | UpdateKey": "Nexus:172" + }, + + "Regeneration": { + "ID": "HammurabiRegeneration", + "Default | UpdateKey": "Chucklefish:4584" + }, + + "Relationship Bar UI": { + "ID": "RelationshipBar", + "Default | UpdateKey": "Nexus:1009" + }, + + "RelationshipsEnhanced": { + "ID": "relationshipsenhanced", + "Default | UpdateKey": "Chucklefish:4435", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Relationship Status": { + "ID": "relationshipstatus", + "MapRemoteVersions": { "1.0.5": "1.0.4" }, // not updated in manifest + "Default | UpdateKey": "Nexus:751", + "~1.0.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Rented Tools": { + "ID": "JarvieK.RentedTools", + "Default | UpdateKey": "Nexus:1307" + }, + + "Replanter": { + "ID": "jwdred.Replanter", + "FormerIDs": "{EntryDll: 'Replanter.dll'}", // changed in 1.0.5 + "Default | UpdateKey": "Nexus:589", + "~1.0.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "ReRegeneration": { + "ID": "lrsk_sdvm_rerg.0925160827", + "MapLocalVersions": { "1.1.2-release": "1.1.2" }, + "Default | UpdateKey": "Chucklefish:4465" + }, + + "Reseed": { + "ID": "Roc.Reseed", + "Default | UpdateKey": "Nexus:887" + }, + + "Reusable Wallpapers and Floors (Wallpaper Retain)": { + "ID": "dae1b553-2e39-43e7-8400-c7c5c836134b", + "Default | UpdateKey": "Nexus:356", + "~1.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Ring of Fire": { + "ID": "Platonymous.RingOfFire", + "Default | UpdateKey": "Nexus:1166" // added in 1.0.1 + }, + + "Rise and Shine": { + "ID": "Yoshify.RiseAndShine", + "FormerIDs": "{EntryDll: 'RiseAndShine.dll'}", // changed in 1.1.1-whisk-update + "Default | UpdateKey": "Nexus:3" + }, + + "Rope Bridge": { + "ID": "RopeBridge", + "Default | UpdateKey": "Nexus:824" + }, + + "Rotate Toolbar": { + "ID": "Pathoschild.RotateToolbar", + "Default | UpdateKey": "Nexus:1100" + }, + + "Rush Orders": { + "ID": "spacechase0.RushOrders", + "FormerIDs": "RushOrders", // changed in 1.1 + "Default | UpdateKey": "Nexus:605", + "~1.1 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Save Anywhere": { + "ID": "Omegasis.SaveAnywhere", + "FormerIDs": "{ID:'SaveAnywhere', Name:'Save Anywhere'}", // changed in 2.5; disambiguate from Night Owl + "Default | UpdateKey": "Nexus:444", // added in 2.6.1 + "~2.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Save Backup": { + "ID": "Omegasis.SaveBackup", + "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'Stardew_Save_Backup'}", // changed in 1.3; disambiguate from other Alpha_Omegasis mods + "Default | UpdateKey": "Nexus:435", // added in 1.3.1 + "~1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Scroll to Blank": { + "ID": "caraxian.scroll.to.blank", + "Default | UpdateKey": "Chucklefish:4405" + }, + + "Scythe Harvesting": { + "ID": "mmanlapat.ScytheHarvesting", + "FormerIDs": "ScytheHarvesting", // changed in 1.6 + "Default | UpdateKey": "Nexus:1106" + }, + + "SDV Twitch": { + "ID": "MTD.SDVTwitch", + "Default | UpdateKey": "Nexus:1760" + }, + + "Seasonal Immersion": { + "ID": "Entoarox.SeasonalImmersion", + "FormerIDs": "EntoaroxSeasonalHouse | EntoaroxSeasonalBuildings | EntoaroxSeasonalImmersion", // changed in 1.1, 1.6 or earlier, and 1.7 + "~1.11 | UpdateKey": "Chucklefish:4262", // only enable update checks up to 1.11 by request (has its own update-check feature) + "~1.8.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Seed Bag": { + "ID": "Platonymous.SeedBag", + "Default | UpdateKey": "Nexus:1133" // added in 1.1.2 + }, + + "Seed Catalogue": { + "ID": "spacechase0.SeedCatalogue", + "Default | UpdateKey": "Nexus:1640" + }, + + "Self Service": { + "ID": "JarvieK.SelfService", + "MapRemoteVersions": { "0.2.1": "0.2" }, // manifest not updated + "Default | UpdateKey": "Nexus:1304" + }, + + "Send Items": { + "ID": "Denifia.SendItems", + "Default | UpdateKey": "Nexus:1087", // added in 1.0.3 (2017-10-04) + "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Shed Notifications (BuildingsNotifications)": { + "ID": "TheCroak.BuildingsNotifications", + "Default | UpdateKey": "Nexus:620", + "~0.4.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Shenandoah Project": { + "ID": "Nishtra.ShenandoahProject", + "FormerIDs": "Shenandoah Project", // changed in 1.2 + "MapRemoteVersions": { "1.1.1": "1.1" }, // not updated in manifest + "Default | UpdateKey": "Nexus:756", + "~1.1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Ship Anywhere": { + "ID": "spacechase0.ShipAnywhere", + "Default | UpdateKey": "Nexus:1379" + }, + + "Shipment Tracker": { + "ID": "7e474181-e1a0-40f9-9c11-d08a3dcefaf3", + "Default | UpdateKey": "Nexus:321", + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Shop Expander": { + "ID": "Entoarox.ShopExpander", + "FormerIDs": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Shop Expander'} | EntoaroxShopExpander", // changed in 1.5 and 1.5.2; disambiguate from Faster Paths + "MapRemoteVersions": { "1.6.0b": "1.6.0" }, + "~1.6 | UpdateKey": "Chucklefish:4381", // only enable update checks up to 1.6 by request (has its own update-check feature) + "~1.5.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Showcase Mod": { + "ID": "Igorious.Showcase", + "MapLocalVersions": { "0.9-500": "0.9" }, + "Default | UpdateKey": "Chucklefish:4487", + "~0.9 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Shroom Spotter": { + "ID": "TehPers.ShroomSpotter", + "Default | UpdateKey": "Nexus:908" + }, + + "Simple Crop Label": { + "ID": "SimpleCropLabel", + "Default | UpdateKey": "Nexus:314" + }, + + "Simple Sound Manager": { + "ID": "Omegasis.SimpleSoundManager", + "Default | UpdateKey": "Nexus:1410", // added in 1.0.1 + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Simple Sprinklers": { + "ID": "tZed.SimpleSprinkler", + "FormerIDs": "{EntryDll: 'SimpleSprinkler.dll'}", // changed in 1.5 + "Default | UpdateKey": "Nexus:76", + "~1.4 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Siv's Marriage Mod": { + "ID": "6266959802", + "MapLocalVersions": { "0.0": "1.4" }, + "Default | UpdateKey": "Nexus:366", + "~1.2.2 | Status": "AssumeBroken" // broke in SMAPI 1.9 (has multiple Mod instances) + }, + + "Skill Prestige": { + "ID": "alphablackwolf.skillPrestige", + "FormerIDs": "6b843e60-c8fc-4a25-a67b-4a38ac8dcf9b", // changed circa 1.2.3 + "Default | UpdateKey": "Nexus:569", + "~1.0.9 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Skill Prestige: Cooking Adapter": { + "ID": "Alphablackwolf.CookingSkillPrestigeAdapter", + "FormerIDs": "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63", // changed circa 1.1 + "MapRemoteVersions": { "1.2.3": "1.1" }, // manifest not updated + "Default | UpdateKey": "Nexus:569", + "~1.0.9 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Skip Intro": { + "ID": "Pathoschild.SkipIntro", + "FormerIDs": "SkipIntro", // changed in 1.4 + "Default | UpdateKey": "Nexus:533" + }, + + "Skull Cavern Elevator": { + "ID": "SkullCavernElevator", + "Default | UpdateKey": "Nexus:963" + }, + + "Skull Cave Saver": { + "ID": "cantorsdust.SkullCaveSaver", + "FormerIDs": "8ac06349-26f7-4394-806c-95d48fd35774 | community.SkullCaveSaver", // changed in 1.1 and 1.2.2 + "Default | UpdateKey": "Nexus:175" + }, + + "Sleepy Eye": { + "ID": "spacechase0.SleepyEye", + "Default | UpdateKey": "Nexus:1152" + }, + + "Slower Fence Decay": { + "ID": "Speeder.SlowerFenceDecay", + "FormerIDs": "SPDSlowFenceDecay", // changed in 0.5.2-pathoschild-update + "Default | UpdateKey": "Nexus:252", + "~0.5.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Smart Mod": { + "ID": "KuroBear.SmartMod", + "~2.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Solar Eclipse Event": { + "ID": "KoihimeNakamura.SolarEclipseEvent", + "Default | UpdateKey": "Nexus:897", + "MapLocalVersions": { "1.3-20170917": "1.3" }, + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "SpaceCore": { + "ID": "spacechase0.SpaceCore", + "Default | UpdateKey": "Nexus:1348" + }, + + "Speedster": { + "ID": "Platonymous.Speedster", + "Default | UpdateKey": "Nexus:1102" // added in 1.3.1 + }, + + "Sprinkler Range": { + "ID": "cat.sprinklerrange", + "Default | UpdateKey": "Nexus:1179" + }, + + "Sprinkles": { + "ID": "Platonymous.Sprinkles", + "Default | UpdateKey": "Chucklefish:4592", + "~1.1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Sprint and Dash": { + "ID": "SPDSprintAndDash", + "Default | UpdateKey": "Chucklefish:3531", + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Sprint and Dash Redux": { + "ID": "littleraskol.SprintAndDashRedux", + "FormerIDs": "lrsk_sdvm_sndr.0921161059", // changed in 1.3 + "Default | UpdateKey": "Chucklefish:4201" + }, + + "Sprinting Mod": { + "FormerIDs": "{EntryDll: 'SprintingMod.dll'}", + "MapLocalVersions": { "1.0": "2.1" }, // not updated in manifest + "Default | UpdateKey": "GitHub:oliverpl/SprintingMod", + "~2.1 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "StackSplitX": { + "ID": "tstaples.StackSplitX", + "FormerIDs": "{EntryDll: 'StackSplitX.dll'}", // changed circa 1.3.1 + "Default | UpdateKey": "Nexus:798", + "~1.2 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "StaminaRegen": { + "FormerIDs": "{EntryDll: 'StaminaRegen.dll'}", + "~1.0.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Stardew Config Menu": { + "ID": "Juice805.StardewConfigMenu", + "Default | UpdateKey": "Nexus:1312" + }, + + "Stardew Content Compatibility Layer (SCCL)": { + "ID": "SCCL", + "Default | UpdateKey": "Nexus:889", + "~0.1 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Stardew Editor Game Integration": { + "ID": "spacechase0.StardewEditor.GameIntegration", + "Default | UpdateKey": "Nexus:1298" + }, + + "Stardew Notification": { + "ID": "stardewnotification", + "Default | UpdateKey": "GitHub:monopandora/StardewNotification", + "~1.7 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Stardew Symphony": { + "ID": "Omegasis.StardewSymphony", + "FormerIDs": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'Stardew_Symphony'}", // changed in 1.4; disambiguate other mods by Alpha_Omegasis + "Default | UpdateKey": "Nexus:425", // added in 1.4.1 + "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "StarDustCore": { + "ID": "StarDustCore", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "it was only used by earlier versions of Save Anywhere, and is no longer used or maintained." + }, + + "Starting Money": { + "ID": "mmanlapat.StartingMoney", + "FormerIDs": "StartingMoney", // changed in 1.1 + "Default | UpdateKey": "Nexus:1138" + }, + + "StashItemsToChest": { + "ID": "BlueMod_StashItemsToChest", + "Default | UpdateKey": "GitHub:lambui/StardewValleyMod_StashItemsToChest", + "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Stephan's Lots of Crops": { + "ID": "stephansstardewcrops", + "MapRemoteVersions": { "1.41": "1.1" }, // manifest not updated + "Default | UpdateKey": "Chucklefish:4314" + }, + + "Stone Bridge Over Pond (PondWithBridge)": { + "FormerIDs": "{EntryDll: 'PondWithBridge.dll'}", + "MapLocalVersions": { "0.0": "1.0" }, + "Default | UpdateKey": "Nexus:316", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Stumps to Hardwood Stumps": { + "ID": "StumpsToHardwoodStumps", + "Default | UpdateKey": "Nexus:691" + }, + + "Super Greenhouse Warp Modifier": { + "ID": "SuperGreenhouse", + "Default | UpdateKey": "Chucklefish:4334", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Swim Almost Anywhere / Swim Suit": { + "ID": "Platonymous.SwimSuit", + "Default | UpdateKey": "Nexus:1215" // added in 0.5.1 + }, + + "Tainted Cellar": { + "ID": "TaintedCellar", + "FormerIDs": "{EntryDll: 'TaintedCellar.dll'}", // changed in 1.1 + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.1 or 1.11 + }, + + "Tapper Ready": { + "ID": "skunkkk.TapperReady", + "Default | UpdateKey": "Nexus:1219" + }, + + "Teh's Fishing Overhaul": { + "ID": "TehPers.FishingOverhaul", + "Default | UpdateKey": "Nexus:866" + }, + + "Teleporter": { + "ID": "Teleporter", + "Default | UpdateKey": "Chucklefish:4374", + "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "The Long Night": { + "ID": "Pathoschild.TheLongNight", + "Default | UpdateKey": "Nexus:1369" + }, + + "Three-heart Dance Partner": { + "ID": "ThreeHeartDancePartner", + "Default | UpdateKey": "Nexus:500", + "~1.0.1 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "TimeFreeze": { + "ID": "Omegasis.TimeFreeze", + "FormerIDs": "4108e859-333c-4fec-a1a7-d2e18c1019fe", // changed in 1.2 + "Default | UpdateKey": "Nexus:973" // added in 1.2.1 + }, + + "Time Reminder": { + "ID": "KoihimeNakamura.TimeReminder", + "MapLocalVersions": { "1.0-20170314": "1.0.2" }, + "Default | UpdateKey": "Nexus:1000" + }, + + "TimeSpeed": { + "ID": "cantorsdust.TimeSpeed", + "FormerIDs": "{EntryDll: 'TimeSpeed.dll'} | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed'} | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed Mod (unofficial)'} | community.TimeSpeed", // changed in 2.0.3, 2.1, and 2.3.3; disambiguate other mods by Alpha_Omegasis + "Default | UpdateKey": "Nexus:169", + "~2.2 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "To Do List": { + "ID": "eleanor.todolist", + "Default | UpdateKey": "Nexus:1630" + }, + + "Tool Charging": { + "ID": "mralbobo.ToolCharging", + "Default | UpdateKey": "GitHub:mralbobo/stardew-tool-charging" + }, + + "TractorMod": { + "ID": "Pathoschild.TractorMod", + "FormerIDs": "BlueMod_TractorMod | PhthaloBlue.TractorMod | community.TractorMod", // changed in 3.2, 4.0 beta, and 4.0 + "Default | UpdateKey": "Nexus:1401" + }, + + "TrainerMod": { + "ID": "SMAPI.TrainerMod", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "replaced by ConsoleCommands, which is added by the SMAPI installer." + }, + + "Tree Transplant": { + "ID": "TreeTransplant", + "Default | UpdateKey": "Nexus:1342" + }, + + "UI Info Suite": { + "ID": "Cdaragorn.UiInfoSuite", + "Default | UpdateKey": "Nexus:1150" + }, + + "UiModSuite": { + "ID": "Demiacle.UiModSuite", + "MapLocalVersions": { "0.5": "1.0" }, // not updated in manifest + "Default | UpdateKey": "Nexus:1023", + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Variable Grass": { + "ID": "dantheman999.VariableGrass", + "Default | UpdateKey": "GitHub:dantheman999301/StardewMods" + }, + + "Vertical Toolbar": { + "ID": "SB_VerticalToolMenu", + "Default | UpdateKey": "Nexus:943" + }, + + "WakeUp": { + "FormerIDs": "{EntryDll: 'WakeUp.dll'}", + "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Wallpaper Fix": { + "FormerIDs": "{EntryDll: 'WallpaperFix.dll'}", + "Default | UpdateKey": "Chucklefish:4211", + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "WarpAnimals": { + "ID": "Symen.WarpAnimals", + "Default | UpdateKey": "Nexus:1400" + }, + + "Weather Controller": { + "FormerIDs": "{EntryDll: 'WeatherController.dll'}", + "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "What Farm Cave / WhatAMush": { + "ID": "WhatAMush", + "Default | UpdateKey": "Nexus:1097" + }, + + "WHats Up": { + "ID": "wHatsUp", + "Default | UpdateKey": "Nexus:1082" + }, + + "Winter Grass": { + "ID": "cat.wintergrass", + "Default | UpdateKey": "Nexus:1601" + }, + + "Wonderful Farm Life": { + "FormerIDs": "{EntryDll: 'WonderfulFarmLife.dll'}", + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.1 or 1.11 + }, + + "XmlSerializerRetool": { + "FormerIDs": "{EntryDll: 'XmlSerializerRetool.dll'}", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "it's no longer maintained or used." + }, + + "Xnb Loader": { + "ID": "Entoarox.XnbLoader", + "~1.1.10 | UpdateKey": "Chucklefish:4506", // only enable update checks up to 1.1.10 by request (has its own update-check feature) + "~1.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "zDailyIncrease": { + "ID": "zdailyincrease", + "MapRemoteVersions": { "1.3.5": "1.3.4" }, // not updated in manifest + "Default | UpdateKey": "Chucklefish:4247", + "~1.2 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoom Out Extreme": { + "ID": "RockinMods.ZoomMod", + "FormerIDs": "ZoomMod", // changed circa 1.2.1 + "Default | UpdateKey": "Nexus:1326", + "~0.1 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoryn's Better RNG": { + "ID": "Zoryn.BetterRNG", + "FormerIDs": "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoryn's Calendar Anywhere": { + "ID": "Zoryn.CalendarAnywhere", + "FormerIDs": "a41c01cd-0437-43eb-944f-78cb5a53002a", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoryn's Durable Fences": { + "ID": "Zoryn.DurableFences", + "FormerIDs": "56d3439c-7b9b-497e-9496-0c4890e8a00e", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods" + }, + + "Zoryn's Health Bars": { + "ID": "Zoryn.HealthBars", + "FormerIDs": "{EntryDll: 'HealthBars.dll'}", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoryn's Fishing Mod": { + "ID": "Zoryn.FishingMod", + "FormerIDs": "fa277b1f-265e-47c3-a84f-cd320cc74949", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods" + }, + + "Zoryn's Junimo Deposit Anywhere": { + "ID": "Zoryn.JunimoDepositAnywhere", + "FormerIDs": "f93a4fe8-cade-4146-9335-b5f82fbbf7bc", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", + "~1.7 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoryn's Movement Mod": { + "ID": "Zoryn.MovementModifier", + "FormerIDs": "8a632929-8335-484f-87dd-c29d2ba3215d", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoryn's Regen Mod": { + "ID": "Zoryn.RegenMod", + "FormerIDs": "dfac4383-1b6b-4f33-ae4e-37fc23e5252e", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 + } + } +} -- cgit From 504733dec7d629335b83841af38cd5da91d5231f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 13 Apr 2018 23:00:49 -0400 Subject: fix console color scheme for PowerShell, and make it configurable --- docs/release-notes.md | 1 + src/SMAPI/Framework/Models/MonitorColorScheme.cs | 15 ++++++ src/SMAPI/Framework/Models/SConfig.cs | 3 ++ src/SMAPI/Framework/Monitor.cs | 65 ++++++++++++++---------- src/SMAPI/Program.cs | 4 +- src/SMAPI/StardewModdingAPI.config.json | 10 +++- src/SMAPI/StardewModdingAPI.csproj | 1 + 7 files changed, 70 insertions(+), 29 deletions(-) create mode 100644 src/SMAPI/Framework/Models/MonitorColorScheme.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/docs/release-notes.md b/docs/release-notes.md index f0202ee1..817fcd47 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -5,6 +5,7 @@ * Added support for Stardew Valley 1.3+; no longer compatible with earlier versions. * Added `Context.IsMultiplayer` and `Context.IsMainPlayer` flags. * Fixed SMAPI update checks not showing newer beta versions when using a beta version. + * Fixed console color scheme for PowerShell and added override option to `StardewModdingAPI.config.json`. * For modders: * Added code analysis to mod build config package to flag common issues as warnings. diff --git a/src/SMAPI/Framework/Models/MonitorColorScheme.cs b/src/SMAPI/Framework/Models/MonitorColorScheme.cs new file mode 100644 index 00000000..d8289d08 --- /dev/null +++ b/src/SMAPI/Framework/Models/MonitorColorScheme.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Framework.Models +{ + /// A monitor color scheme to use. + internal enum MonitorColorScheme + { + /// Choose a color scheme automatically. + AutoDetect, + + /// Use lighter text colors that look better on a black or dark background. + DarkBackground, + + /// Use darker text colors that look better on a white or light background. + LightBackground + } +} diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index 2d6da0fa..b504f38b 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -20,5 +20,8 @@ namespace StardewModdingAPI.Framework.Models /// Whether SMAPI should log more information about the game context. public bool VerboseLogging { get; set; } + + /// The console color scheme to use. + public MonitorColorScheme ColorScheme { get; set; } } } diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index bf338386..da025ab9 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using StardewModdingAPI.Framework.Logging; +using StardewModdingAPI.Framework.Models; namespace StardewModdingAPI.Framework { @@ -25,7 +26,7 @@ namespace StardewModdingAPI.Framework private static readonly int MaxLevelLength = (from level in Enum.GetValues(typeof(LogLevel)).Cast() select level.ToString().Length).Max(); /// The console text color for each log level. - private static readonly IDictionary Colors = Monitor.GetConsoleColorScheme(); + private readonly IDictionary Colors; /// Propagates notification that SMAPI should exit. private readonly CancellationTokenSource ExitTokenSource; @@ -58,13 +59,15 @@ namespace StardewModdingAPI.Framework /// Manages access to the console output. /// The log file to which to write messages. /// Propagates notification that SMAPI should exit. - public Monitor(string source, ConsoleInterceptionManager consoleManager, LogFileManager logFile, CancellationTokenSource exitTokenSource) + /// The console color scheme to use. + public Monitor(string source, ConsoleInterceptionManager consoleManager, LogFileManager logFile, CancellationTokenSource exitTokenSource, MonitorColorScheme colorScheme) { // validate if (string.IsNullOrWhiteSpace(source)) throw new ArgumentException("The log source cannot be empty."); // initialise + this.Colors = Monitor.GetConsoleColorScheme(colorScheme); this.Source = source; this.LogFile = logFile ?? throw new ArgumentNullException(nameof(logFile), "The log file manager cannot be null."); this.ConsoleManager = consoleManager; @@ -76,7 +79,7 @@ namespace StardewModdingAPI.Framework /// The log severity level. public void Log(string message, LogLevel level = LogLevel.Debug) { - this.LogImpl(this.Source, message, level, Monitor.Colors[level]); + this.LogImpl(this.Source, message, level, this.Colors[level]); } /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. @@ -145,32 +148,41 @@ namespace StardewModdingAPI.Framework } /// Get the color scheme to use for the current console. - private static IDictionary GetConsoleColorScheme() + /// The console color scheme to use. + private static IDictionary GetConsoleColorScheme(MonitorColorScheme colorScheme) { - // scheme for dark console background - if (Monitor.IsDark(Console.BackgroundColor)) - { - return new Dictionary - { - [LogLevel.Trace] = ConsoleColor.DarkGray, - [LogLevel.Debug] = ConsoleColor.DarkGray, - [LogLevel.Info] = ConsoleColor.White, - [LogLevel.Warn] = ConsoleColor.Yellow, - [LogLevel.Error] = ConsoleColor.Red, - [LogLevel.Alert] = ConsoleColor.Magenta - }; - } + // auto detect color scheme + if (colorScheme == MonitorColorScheme.AutoDetect) + colorScheme = Monitor.IsDark(Console.BackgroundColor) ? MonitorColorScheme.DarkBackground : MonitorColorScheme.LightBackground; - // scheme for light console background - return new Dictionary + // get colors for scheme + switch (colorScheme) { - [LogLevel.Trace] = ConsoleColor.DarkGray, - [LogLevel.Debug] = ConsoleColor.DarkGray, - [LogLevel.Info] = ConsoleColor.Black, - [LogLevel.Warn] = ConsoleColor.DarkYellow, - [LogLevel.Error] = ConsoleColor.Red, - [LogLevel.Alert] = ConsoleColor.DarkMagenta - }; + case MonitorColorScheme.DarkBackground: + return new Dictionary + { + [LogLevel.Trace] = ConsoleColor.DarkGray, + [LogLevel.Debug] = ConsoleColor.DarkGray, + [LogLevel.Info] = ConsoleColor.White, + [LogLevel.Warn] = ConsoleColor.Yellow, + [LogLevel.Error] = ConsoleColor.Red, + [LogLevel.Alert] = ConsoleColor.Magenta + }; + + case MonitorColorScheme.LightBackground: + return new Dictionary + { + [LogLevel.Trace] = ConsoleColor.DarkGray, + [LogLevel.Debug] = ConsoleColor.DarkGray, + [LogLevel.Info] = ConsoleColor.Black, + [LogLevel.Warn] = ConsoleColor.DarkYellow, + [LogLevel.Error] = ConsoleColor.Red, + [LogLevel.Alert] = ConsoleColor.DarkMagenta + }; + + default: + throw new NotSupportedException($"Unknown color scheme '{colorScheme}'."); + } } /// Get whether a console color should be considered dark, which is subjectively defined as 'white looks better than black on this text'. @@ -182,6 +194,7 @@ namespace StardewModdingAPI.Framework case ConsoleColor.Black: case ConsoleColor.Blue: case ConsoleColor.DarkBlue: + case ConsoleColor.DarkMagenta: // Powershell case ConsoleColor.DarkRed: case ConsoleColor.Red: return true; diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index da2c0e8e..36310fed 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -134,7 +134,7 @@ namespace StardewModdingAPI // init basics this.Settings = JsonConvert.DeserializeObject(File.ReadAllText(Constants.ApiConfigPath)); this.LogFile = new LogFileManager(logPath); - this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.CancellationTokenSource) + this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.CancellationTokenSource, this.Settings.ColorScheme) { WriteToConsole = writeToConsole, ShowTraceInConsole = this.Settings.DeveloperMode, @@ -1149,7 +1149,7 @@ namespace StardewModdingAPI /// The name of the module which will log messages with this instance. private Monitor GetSecondaryMonitor(string name) { - return new Monitor(name, this.ConsoleManager, this.LogFile, this.CancellationTokenSource) + return new Monitor(name, this.ConsoleManager, this.LogFile, this.CancellationTokenSource, this.Settings.ColorScheme) { WriteToConsole = this.Monitor.WriteToConsole, ShowTraceInConsole = this.Settings.DeveloperMode, diff --git a/src/SMAPI/StardewModdingAPI.config.json b/src/SMAPI/StardewModdingAPI.config.json index 9743894a..06c63e8b 100644 --- a/src/SMAPI/StardewModdingAPI.config.json +++ b/src/SMAPI/StardewModdingAPI.config.json @@ -36,5 +36,13 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha /** * Whether SMAPI should log more information about the game context. */ - "VerboseLogging": false + "VerboseLogging": false, + + /** + * The console color theme to use. The possible values are: + * - AutoDetect: SMAPI will assume a light background on Mac, and detect the background color automatically on Linux or Mac. + * - LightBackground: use darker text colors that look better on a white or light background. + * - DarkBackground: use lighter text colors that look better on a black or dark background. + */ + "ColorScheme": "AutoDetect" } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 9dbca475..6e9d0d9c 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -123,6 +123,7 @@ + -- cgit From 45f4f85b7e74e0cffd345310d6aabc95c12dac26 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 13 Apr 2018 23:47:24 -0400 Subject: add MacOS detection --- build/common.targets | 9 ++ docs/release-notes.md | 1 + src/SMAPI.Common/EnvironmentUtility.cs | 97 ++++++++++++++++++++++ src/SMAPI.Common/Platform.cs | 15 ++++ .../StardewModdingAPI.Common.projitems | 2 + src/SMAPI.Installer/Enums/Platform.cs | 12 --- src/SMAPI.Installer/InteractiveInstaller.cs | 28 ++----- .../StardewModdingAPI.Installer.csproj | 1 - src/SMAPI/Constants.cs | 11 +-- src/SMAPI/Framework/Content/ContentCache.cs | 2 +- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 1 + src/SMAPI/Framework/ModLoading/Platform.cs | 12 --- .../Framework/ModLoading/PlatformAssemblyMap.cs | 1 + src/SMAPI/Program.cs | 22 +---- src/SMAPI/StardewModdingAPI.csproj | 2 - 15 files changed, 140 insertions(+), 76 deletions(-) create mode 100644 src/SMAPI.Common/EnvironmentUtility.cs create mode 100644 src/SMAPI.Common/Platform.cs delete mode 100644 src/SMAPI.Installer/Enums/Platform.cs delete mode 100644 src/SMAPI/Framework/ModLoading/Platform.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/build/common.targets b/build/common.targets index a6cead8f..95885299 100644 --- a/build/common.targets +++ b/build/common.targets @@ -26,6 +26,10 @@ $(DefineConstants);SMAPI_FOR_WINDOWS + + + + False @@ -38,6 +42,8 @@ False + + $(GamePath)\Netcode.dll False @@ -58,11 +64,14 @@ $(DefineConstants);SMAPI_FOR_UNIX + $(GamePath)\MonoGame.Framework.dll False False + + $(GamePath)\StardewValley.exe False diff --git a/docs/release-notes.md b/docs/release-notes.md index 817fcd47..40f404d3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -14,6 +14,7 @@ * Fixed issue where assets didn't reload correctly when the player switches language. * For SMAPI developers: + * Added MacOS detection to `Constants.Platform`. * Added prerelease versions to the mod update-check API response where available (GitHub only). * Added support for beta releases on the home page. * Split mod DB out of `StardewModdingAPI.config.json`, so we can load config earlier and reduce unnecessary memory usage later. diff --git a/src/SMAPI.Common/EnvironmentUtility.cs b/src/SMAPI.Common/EnvironmentUtility.cs new file mode 100644 index 00000000..b8a6d775 --- /dev/null +++ b/src/SMAPI.Common/EnvironmentUtility.cs @@ -0,0 +1,97 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +#if SMAPI_FOR_WINDOWS +using System.Management; +#endif +using System.Runtime.InteropServices; + +namespace StardewModdingAPI.Common +{ + /// Provides methods for fetching environment information. + internal static class EnvironmentUtility + { + /********* + ** Properties + *********/ + /// Get the OS name from the system uname command. + /// The buffer to fill with the resulting string. + [DllImport("libc")] + static extern int uname(IntPtr buffer); + + + /********* + ** Public methods + *********/ + /// Detect the current OS. + public static Platform DetectPlatform() + { + switch (Environment.OSVersion.Platform) + { + case PlatformID.MacOSX: + return Platform.Mac; + + case PlatformID.Unix: + return EnvironmentUtility.IsRunningMac() + ? Platform.Mac + : Platform.Linux; + + default: + return Platform.Windows; + } + } + + + /// Get the human-readable OS name and version. + /// The current platform. + [SuppressMessage("ReSharper", "EmptyGeneralCatchClause", Justification = "Error suppressed deliberately to fallback to default behaviour.")] + public static string GetFriendlyPlatformName(Platform platform) + { +#if SMAPI_FOR_WINDOWS + try + { + return new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem") + .Get() + .Cast() + .Select(entry => entry.GetPropertyValue("Caption").ToString()) + .FirstOrDefault(); + } + catch { } +#endif + return (platform == Platform.Mac ? "MacOS " : "") + Environment.OSVersion; + } + + + /********* + ** Private methods + *********/ + /// Detect whether the code is running on Mac. + /// + /// This code is derived from the Mono project (see System.Windows.Forms/System.Windows.Forms/XplatUI.cs). It detects Mac by calling the + /// uname system command and checking the response, which is always 'Darwin' for MacOS. + /// + private static bool IsRunningMac() + { + IntPtr buffer = IntPtr.Zero; + try + { + buffer = Marshal.AllocHGlobal(8192); + if (uname(buffer) == 0) + { + string os = Marshal.PtrToStringAnsi(buffer); + return os == "Darwin"; + } + return false; + } + catch + { + return false; // default to Linux + } + finally + { + if (buffer != IntPtr.Zero) + Marshal.FreeHGlobal(buffer); + } + } + } +} diff --git a/src/SMAPI.Common/Platform.cs b/src/SMAPI.Common/Platform.cs new file mode 100644 index 00000000..08b4545f --- /dev/null +++ b/src/SMAPI.Common/Platform.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Common +{ + /// The game's platform version. + internal enum Platform + { + /// The Linux version of the game. + Linux, + + /// The Mac version of the game. + Mac, + + /// The Windows version of the game. + Windows + } +} diff --git a/src/SMAPI.Common/StardewModdingAPI.Common.projitems b/src/SMAPI.Common/StardewModdingAPI.Common.projitems index 223b0d5c..0b89f092 100644 --- a/src/SMAPI.Common/StardewModdingAPI.Common.projitems +++ b/src/SMAPI.Common/StardewModdingAPI.Common.projitems @@ -9,6 +9,8 @@ StardewModdingAPI.Common + + diff --git a/src/SMAPI.Installer/Enums/Platform.cs b/src/SMAPI.Installer/Enums/Platform.cs deleted file mode 100644 index 9bcaa3c3..00000000 --- a/src/SMAPI.Installer/Enums/Platform.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace StardewModdingApi.Installer.Enums -{ - /// The game's platform version. - internal enum Platform - { - /// The Linux/Mac version of the game. - Mono, - - /// The Windows version of the game. - Windows - } -} \ No newline at end of file diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index b7e698ad..01076573 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -27,7 +27,8 @@ namespace StardewModdingApi.Installer { switch (platform) { - case Platform.Mono: + case Platform.Linux: + case Platform.Mac: { string home = Environment.GetEnvironmentVariable("HOME"); @@ -140,7 +141,7 @@ namespace StardewModdingApi.Installer /**** ** Get platform & set window title ****/ - Platform platform = this.DetectPlatform(); + Platform platform = EnvironmentUtility.DetectPlatform(); Console.Title = $"SMAPI {new SemanticVersionImpl(this.GetType().Assembly.GetName().Version)} installer on {platform}"; Console.WriteLine(); @@ -182,7 +183,7 @@ namespace StardewModdingApi.Installer DirectoryInfo modsDir = new DirectoryInfo(Path.Combine(installDir.FullName, "Mods")); var paths = new { - executable = Path.Combine(installDir.FullName, platform == Platform.Mono ? "StardewValley.exe" : "Stardew Valley.exe"), + executable = Path.Combine(installDir.FullName, platform == Platform.Windows ? "Stardew Valley.exe" : "StardewValley.exe"), unixSmapiLauncher = Path.Combine(installDir.FullName, "StardewModdingAPI"), unixLauncher = Path.Combine(installDir.FullName, "StardewValley"), unixLauncherBackup = Path.Combine(installDir.FullName, "StardewValley-original") @@ -271,7 +272,7 @@ namespace StardewModdingApi.Installer ** Always uninstall old files ****/ // restore game launcher - if (platform == Platform.Mono && File.Exists(paths.unixLauncherBackup)) + if ((platform == Platform.Linux || platform == Platform.Mac) && File.Exists(paths.unixLauncherBackup)) { this.PrintDebug("Removing SMAPI launcher..."); this.InteractivelyDelete(paths.unixLauncher); @@ -304,7 +305,7 @@ namespace StardewModdingApi.Installer } // replace mod launcher (if possible) - if (platform == Platform.Mono) + if (platform == Platform.Linux || platform == Platform.Mac) { this.PrintDebug("Safely replacing game launcher..."); if (!File.Exists(paths.unixLauncherBackup)) @@ -379,21 +380,6 @@ namespace StardewModdingApi.Installer /********* ** Private methods *********/ - /// Detect the game's platform. - /// The platform is not supported. - private Platform DetectPlatform() - { - switch (Environment.OSVersion.Platform) - { - case PlatformID.MacOSX: - case PlatformID.Unix: - return Platform.Mono; - - default: - return Platform.Windows; - } - } - /// Test whether the current console supports color formatting. private static bool GetConsoleSupportsColor() { @@ -635,7 +621,7 @@ namespace StardewModdingApi.Installer // normalise path if (platform == Platform.Windows) path = path.Replace("\"", ""); // in Windows, quotes are used to escape spaces and aren't part of the file path - if (platform == Platform.Mono) + if (platform == Platform.Linux || platform == Platform.Mac) path = path.Replace("\\ ", " "); // in Linux/Mac, spaces in paths may be escaped if copied from the command line if (path.StartsWith("~/")) { diff --git a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj index a575e06f..7a71bef9 100644 --- a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj +++ b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj @@ -40,7 +40,6 @@ Properties\GlobalAssemblyInfo.cs - diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index a098194b..1faaf656 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using StardewModdingAPI.Common; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.ModLoading; using StardewValley; @@ -91,12 +92,7 @@ namespace StardewModdingAPI internal static ISemanticVersion GameVersion { get; } = new GameVersion(Constants.GetGameVersion()); /// The target game platform. - internal static Platform TargetPlatform { get; } = -#if SMAPI_FOR_WINDOWS - Platform.Windows; -#else - Platform.Mono; -#endif + internal static Platform TargetPlatform { get; } = EnvironmentUtility.DetectPlatform(); /********* @@ -111,7 +107,8 @@ namespace StardewModdingAPI Assembly[] targetAssemblies; switch (targetPlatform) { - case Platform.Mono: + case Platform.Linux: + case Platform.Mac: removeAssemblyReferences = new[] { "Stardew Valley", diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs index 533da398..d95de4fe 100644 --- a/src/SMAPI/Framework/Content/ContentCache.cs +++ b/src/SMAPI/Framework/Content/ContentCache.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; using Microsoft.Xna.Framework; -using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Common; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Utilities; using StardewValley; diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index a60f63da..bf9b6450 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using Mono.Cecil; using Mono.Cecil.Cil; +using StardewModdingAPI.Common; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Metadata; diff --git a/src/SMAPI/Framework/ModLoading/Platform.cs b/src/SMAPI/Framework/ModLoading/Platform.cs deleted file mode 100644 index 45e881c4..00000000 --- a/src/SMAPI/Framework/ModLoading/Platform.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace StardewModdingAPI.Framework.ModLoading -{ - /// The game's platform version. - internal enum Platform - { - /// The Linux/Mac version of the game. - Mono, - - /// The Windows version of the game. - Windows - } -} diff --git a/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs b/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs index 463f45e8..9499b538 100644 --- a/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs +++ b/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using Mono.Cecil; +using StardewModdingAPI.Common; namespace StardewModdingAPI.Framework.ModLoading { diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 36310fed..27f7b4d5 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -10,10 +10,10 @@ using System.Security; using System.Text.RegularExpressions; using System.Threading; #if SMAPI_FOR_WINDOWS -using System.Management; using System.Windows.Forms; #endif using Newtonsoft.Json; +using StardewModdingAPI.Common; using StardewModdingAPI.Common.Models; using StardewModdingAPI.Events; using StardewModdingAPI.Framework; @@ -165,7 +165,7 @@ namespace StardewModdingAPI try { // init logging - this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {this.GetFriendlyPlatformName()}", LogLevel.Info); + this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {EnvironmentUtility.GetFriendlyPlatformName(Constants.TargetPlatform)}", LogLevel.Info); this.Monitor.Log($"Mods go here: {Constants.ModPath}"); this.Monitor.Log($"Log started at {DateTime.UtcNow:s} UTC", LogLevel.Trace); @@ -1157,24 +1157,6 @@ namespace StardewModdingAPI }; } - /// Get a human-readable name for the current platform. - [SuppressMessage("ReSharper", "EmptyGeneralCatchClause", Justification = "Error suppressed deliberately to fallback to default behaviour.")] - private string GetFriendlyPlatformName() - { -#if SMAPI_FOR_WINDOWS - try - { - return new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem") - .Get() - .Cast() - .Select(entry => entry.GetPropertyValue("Caption").ToString()) - .FirstOrDefault(); - } - catch { } -#endif - return Environment.OSVersion.ToString(); - } - /// Log a message if verbose mode is enabled. /// The message to log. private void VerboseLog(string message) diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 6e9d0d9c..9b4a496e 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -72,7 +72,6 @@ - True @@ -110,7 +109,6 @@ - -- cgit From eead352af26d0fcc5cac147d0eb5ec384854d931 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 21 Apr 2018 20:37:17 -0400 Subject: rewrite world/player state tracking (#453) --- docs/release-notes.md | 6 +- src/SMAPI/Framework/SGame.cs | 279 +++++++++------------ .../StateTracking/Comparers/EquatableComparer.cs | 32 +++ .../Comparers/ObjectReferenceComparer.cs | 29 +++ .../FieldWatchers/BaseDisposableWatcher.cs | 36 +++ .../FieldWatchers/ComparableWatcher.cs | 62 +++++ .../FieldWatchers/NetDictionaryWatcher.cs | 103 ++++++++ .../StateTracking/FieldWatchers/NetValueWatcher.cs | 83 ++++++ .../FieldWatchers/ObservableCollectionWatcher.cs | 86 +++++++ .../StateTracking/FieldWatchers/WatcherFactory.cs | 54 ++++ .../Framework/StateTracking/ICollectionWatcher.cs | 17 ++ .../Framework/StateTracking/IDictionaryWatcher.cs | 7 + src/SMAPI/Framework/StateTracking/IValueWatcher.cs | 15 ++ src/SMAPI/Framework/StateTracking/IWatcher.cs | 24 ++ src/SMAPI/Framework/StateTracking/PlayerTracker.cs | 202 +++++++++++++++ src/SMAPI/StardewModdingAPI.csproj | 13 + 16 files changed, 884 insertions(+), 164 deletions(-) create mode 100644 src/SMAPI/Framework/StateTracking/Comparers/EquatableComparer.cs create mode 100644 src/SMAPI/Framework/StateTracking/Comparers/ObjectReferenceComparer.cs create mode 100644 src/SMAPI/Framework/StateTracking/FieldWatchers/BaseDisposableWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/FieldWatchers/NetDictionaryWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/FieldWatchers/NetValueWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs create mode 100644 src/SMAPI/Framework/StateTracking/ICollectionWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/IDictionaryWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/IValueWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/IWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/PlayerTracker.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/docs/release-notes.md b/docs/release-notes.md index d0b6d332..73fe0710 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,5 +1,4 @@ # Release notes - + * Overhauled world/player state tracking: + * much more efficient than previous method; + * uses net field events where available; + * lays groundwork for tracking events for multiple players. ## 2.5.5 * For players: diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 4ec46e5c..cea86dfb 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -1,6 +1,6 @@ using System; -using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; @@ -13,6 +13,8 @@ using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.StateTracking; +using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewModdingAPI.Framework.Utilities; using StardewValley; using StardewValley.BellsAndWhistles; @@ -22,6 +24,7 @@ using StardewValley.Tools; using xTile.Dimensions; using xTile.Layers; using SFarmer = StardewValley.Farmer; +using SObject = StardewValley.Object; namespace StardewModdingAPI.Framework { @@ -60,6 +63,9 @@ namespace StardewModdingAPI.Framework /// Skipping a few frames ensures the game finishes initialising the world before mods try to change it. private int AfterLoadTimer = 5; + /// Whether the after-load events were raised for this session. + private bool RaisedAfterLoadEvent; + /// Whether the game is returning to the menu. private bool IsExitingToTitle; @@ -75,50 +81,26 @@ namespace StardewModdingAPI.Framework /// The player input as of the previous tick. private InputState PreviousInput = new InputState(); - /// The window size value at last check. - private Point PreviousWindowSize; - - /// The save ID at last check. - private ulong PreviousSaveID; - - /// A hash of at last check. - private int PreviousGameLocations; - - /// A hash of the current location's at last check. - private int PreviousLocationObjects; - - /// The player's inventory at last check. - private IDictionary PreviousItems; - - /// The player's combat skill level at last check. - private int PreviousCombatLevel; - - /// The player's farming skill level at last check. - private int PreviousFarmingLevel; - - /// The player's fishing skill level at last check. - private int PreviousFishingLevel; - - /// The player's foraging skill level at last check. - private int PreviousForagingLevel; + /// The underlying watchers for convenience. These are accessible individually as separate properties. + private readonly List Watchers = new List(); - /// The player's mining skill level at last check. - private int PreviousMiningLevel; + /// Tracks changes to the window size. + private readonly IValueWatcher WindowSizeWatcher; - /// The player's luck skill level at last check. - private int PreviousLuckLevel; + /// Tracks changes to the current player. + private PlayerTracker CurrentPlayerTracker; - /// The player's location at last check. - private GameLocation PreviousGameLocation; + /// Tracks changes to the time of day (in 24-hour military format). + private readonly IValueWatcher TimeWatcher; - /// The active game menu at last check. - private IClickableMenu PreviousActiveMenu; + /// Tracks changes to the save ID. + private readonly IValueWatcher SaveIdWatcher; - /// The mine level at last check. - private int PreviousMineLevel; + /// Tracks changes to the location list. + private readonly ICollectionWatcher LocationsWatcher; - /// The time of day (in 24-hour military format) at last check. - private int PreviousTime; + /// Tracks changes to . + private readonly IValueWatcher ActiveMenuWatcher; /// The previous content locale. private LocalizedContentManager.LanguageCode? PreviousLocale; @@ -156,7 +138,10 @@ namespace StardewModdingAPI.Framework /// A callback to invoke after the game finishes initialising. internal SGame(IMonitor monitor, Reflector reflection, EventManager eventManager, Action onGameInitialised) { - // initialise + // init XNA + Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; + + // init SMAPI this.Monitor = monitor; this.Events = eventManager; this.FirstUpdate = true; @@ -165,8 +150,21 @@ namespace StardewModdingAPI.Framework if (this.ContentCore == null) // shouldn't happen since CreateContentManager is called first, but let's init here just in case this.ContentCore = new ContentCore(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, this.Monitor, reflection); - // set XNA option required by Stardew Valley - Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; + // init watchers + Game1.locations = new ObservableCollection(); + this.SaveIdWatcher = WatcherFactory.ForEquatable(() => Game1.hasLoadedGame ? Game1.uniqueIDForThisGame : 0); + this.WindowSizeWatcher = WatcherFactory.ForEquatable(() => new Point(Game1.viewport.Width, Game1.viewport.Height)); + this.TimeWatcher = WatcherFactory.ForEquatable(() => Game1.timeOfDay); + this.ActiveMenuWatcher = WatcherFactory.ForReference(() => Game1.activeClickableMenu); + this.LocationsWatcher = WatcherFactory.ForObservableCollection((ObservableCollection)Game1.locations); + this.Watchers.AddRange(new IWatcher[] + { + this.SaveIdWatcher, + this.WindowSizeWatcher, + this.TimeWatcher, + this.ActiveMenuWatcher, + this.LocationsWatcher + }); } /**** @@ -203,31 +201,58 @@ namespace StardewModdingAPI.Framework return; } - // While a background new-day task is in progress, the game skips its own update logic - // and defers to the XNA Update method. Running mod code in parallel to the background - // update is risky, because data changes can conflict (e.g. collection changed during - // enumeration errors) and data may change unexpectedly from one mod instruction to the - // next. + // While a background task is in progress, the game may make changes to the game + // state while mods are running their code. This is risky, because data changes can + // conflict (e.g. collection changed during enumeration errors) and data may change + // unexpectedly from one mod instruction to the next. // // Therefore we can just run Game1.Update here without raising any SMAPI events. There's // a small chance that the task will finish after we defer but before the game checks, // which means technically events should be raised, but the effects of missing one // update tick are neglible and not worth the complications of bypassing Game1.Update. - if (Game1._newDayTask != null) + if (Game1._newDayTask != null || Game1.gameMode == Game1.loadingMode) { base.Update(gameTime); this.Events.Specialised_UnvalidatedUpdateTick.Raise(); return; } - // game is asynchronously loading a save, block mod events to avoid conflicts - if (Game1.gameMode == Game1.loadingMode) + /********* + ** Update context + *********/ + if (Context.IsSaveLoaded && !SaveGame.IsProcessing /*still loading save*/ && this.AfterLoadTimer >= 0) { - base.Update(gameTime); - this.Events.Specialised_UnvalidatedUpdateTick.Raise(); - return; + if (Game1.dayOfMonth != 0) // wait until new-game intro finishes (world not fully initialised yet) + this.AfterLoadTimer--; + Context.IsWorldReady = this.AfterLoadTimer <= 0; } + /********* + ** Update watchers + *********/ + // reset player + if (Context.IsWorldReady) + { + if (this.CurrentPlayerTracker == null || this.CurrentPlayerTracker.Player != Game1.player) + { + this.CurrentPlayerTracker?.Dispose(); + this.CurrentPlayerTracker = new PlayerTracker(Game1.player); + } + } + else + { + if (this.CurrentPlayerTracker != null) + { + this.CurrentPlayerTracker.Dispose(); + this.CurrentPlayerTracker = null; + } + } + + // update values + foreach (IWatcher watcher in this.Watchers) + watcher.Update(); + this.CurrentPlayerTracker?.Update(); + /********* ** Save events + suppress events during save *********/ @@ -300,19 +325,12 @@ namespace StardewModdingAPI.Framework /********* ** After load events *********/ - if (Context.IsSaveLoaded && !SaveGame.IsProcessing /*still loading save*/ && this.AfterLoadTimer >= 0) + if (!this.RaisedAfterLoadEvent && Context.IsWorldReady) { - if (Game1.dayOfMonth != 0) // wait until new-game intro finishes (world not fully initialised yet) - this.AfterLoadTimer--; - - if (this.AfterLoadTimer == 0) - { - this.Monitor.Log($"Context: loaded saved game '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); - Context.IsWorldReady = true; - - this.Events.Save_AfterLoad.Raise(); - this.Events.Time_AfterDayStarted.Raise(); - } + this.RaisedAfterLoadEvent = true; + this.Monitor.Log($"Context: loaded saved game '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); + this.Events.Save_AfterLoad.Raise(); + this.Events.Time_AfterDayStarted.Raise(); } /********* @@ -339,11 +357,10 @@ namespace StardewModdingAPI.Framework // event because we need to notify mods after the game handles the resize, so the // game's metadata (like Game1.viewport) are updated. That's a bit complicated // since the game adds & removes its own handler on the fly. - if (Game1.viewport.Width != this.PreviousWindowSize.X || Game1.viewport.Height != this.PreviousWindowSize.Y) + if (this.WindowSizeWatcher.IsChanged) { - Point size = new Point(Game1.viewport.Width, Game1.viewport.Height); this.Events.Graphics_Resize.Raise(); - this.PreviousWindowSize = size; + this.WindowSizeWatcher.Reset(); } /********* @@ -431,10 +448,11 @@ namespace StardewModdingAPI.Framework /********* ** Menu events *********/ - if (Game1.activeClickableMenu != this.PreviousActiveMenu) + if (this.ActiveMenuWatcher.IsChanged) { - IClickableMenu previousMenu = this.PreviousActiveMenu; - IClickableMenu newMenu = Game1.activeClickableMenu; + IClickableMenu previousMenu = this.ActiveMenuWatcher.PreviousValue; + IClickableMenu newMenu = this.ActiveMenuWatcher.CurrentValue; + this.ActiveMenuWatcher.Reset(); // reset here so a mod changing the menu will be raised as a new event afterwards // log context if (this.VerboseLogging) @@ -452,10 +470,6 @@ namespace StardewModdingAPI.Framework this.Events.Menu_Changed.Raise(new EventArgsClickableMenuChanged(previousMenu, newMenu)); else this.Events.Menu_Closed.Raise(new EventArgsClickableMenuClosed(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; } /********* @@ -463,70 +477,56 @@ namespace StardewModdingAPI.Framework *********/ if (Context.IsWorldReady) { + // update player info + PlayerTracker curPlayer = this.CurrentPlayerTracker; + // raise current location changed - // ReSharper disable once PossibleUnintendedReferenceComparison - if (Game1.currentLocation != this.PreviousGameLocation) + if (curPlayer.TryGetNewLocation(out GameLocation newLocation)) { if (this.VerboseLogging) - this.Monitor.Log($"Context: set location to {Game1.currentLocation?.Name ?? "(none)"}.", LogLevel.Trace); - this.Events.Location_CurrentLocationChanged.Raise(new EventArgsCurrentLocationChanged(this.PreviousGameLocation, Game1.currentLocation)); + this.Monitor.Log($"Context: set location to {newLocation.Name}.", LogLevel.Trace); + this.Events.Location_CurrentLocationChanged.Raise(new EventArgsCurrentLocationChanged(curPlayer.LocationWatcher.PreviousValue, newLocation)); } // raise location list changed - if (this.GetHash(Game1.locations) != this.PreviousGameLocations) + if (this.LocationsWatcher.IsChanged) this.Events.Location_LocationsChanged.Raise(new EventArgsGameLocationsChanged(Game1.locations)); // raise events that shouldn't be triggered on initial load - if (Game1.uniqueIDForThisGame == this.PreviousSaveID) + if (!this.SaveIdWatcher.IsChanged) { // raise player leveled up a skill - if (Game1.player.combatLevel != this.PreviousCombatLevel) - this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Combat, Game1.player.combatLevel)); - if (Game1.player.farmingLevel != this.PreviousFarmingLevel) - this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Farming, Game1.player.farmingLevel)); - if (Game1.player.fishingLevel != this.PreviousFishingLevel) - this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Fishing, Game1.player.fishingLevel)); - if (Game1.player.foragingLevel != this.PreviousForagingLevel) - this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Foraging, Game1.player.foragingLevel)); - if (Game1.player.miningLevel != this.PreviousMiningLevel) - this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Mining, Game1.player.miningLevel)); - if (Game1.player.luckLevel != this.PreviousLuckLevel) - this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Luck, Game1.player.luckLevel)); + foreach (KeyValuePair> pair in curPlayer.GetChangedSkills()) + this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(pair.Key, pair.Value.CurrentValue)); // raise player inventory changed - ItemStackChange[] changedItems = this.GetInventoryChanges(Game1.player.Items, this.PreviousItems).ToArray(); + ItemStackChange[] changedItems = curPlayer.GetInventoryChanges().ToArray(); if (changedItems.Any()) this.Events.Player_InventoryChanged.Raise(new EventArgsInventoryChanged(Game1.player.Items, changedItems.ToList())); // raise current location's object list changed - if (this.GetHash(Game1.currentLocation.objects) != this.PreviousLocationObjects) - this.Events.Location_LocationObjectsChanged.Raise(new EventArgsLocationObjectsChanged(Game1.currentLocation.objects.FieldDict)); + if (curPlayer.TryGetLocationChanges(out IDictionaryWatcher _)) + this.Events.Location_LocationObjectsChanged.Raise(new EventArgsLocationObjectsChanged(curPlayer.GetCurrentLocation().objects.FieldDict)); // raise time changed - if (Game1.timeOfDay != this.PreviousTime) - this.Events.Time_TimeOfDayChanged.Raise(new EventArgsIntChanged(this.PreviousTime, Game1.timeOfDay)); + if (this.TimeWatcher.IsChanged) + this.Events.Time_TimeOfDayChanged.Raise(new EventArgsIntChanged(this.TimeWatcher.PreviousValue, this.TimeWatcher.CurrentValue)); // raise mine level changed - if (Game1.mine != null && Game1.mine.mineLevel != this.PreviousMineLevel) - this.Events.Mine_LevelChanged.Raise(new EventArgsMineLevelChanged(this.PreviousMineLevel, Game1.mine.mineLevel)); + if (curPlayer.TryGetNewMineLevel(out int mineLevel)) + { + this.Monitor.Log("curPlayer mine level changed", LogLevel.Alert); + this.Events.Mine_LevelChanged.Raise(new EventArgsMineLevelChanged(curPlayer.MineLevelWatcher.PreviousValue, mineLevel)); + } } - - // update state - this.PreviousGameLocations = this.GetHash(Game1.locations); - this.PreviousGameLocation = Game1.currentLocation; - this.PreviousCombatLevel = Game1.player.combatLevel; - this.PreviousFarmingLevel = Game1.player.farmingLevel; - this.PreviousFishingLevel = Game1.player.fishingLevel; - this.PreviousForagingLevel = Game1.player.foragingLevel; - this.PreviousMiningLevel = Game1.player.miningLevel; - this.PreviousLuckLevel = Game1.player.luckLevel; - this.PreviousItems = Game1.player.Items.Where(n => n != null).Distinct().ToDictionary(n => n, n => n.Stack); - this.PreviousLocationObjects = this.GetHash(Game1.currentLocation.objects); - this.PreviousTime = Game1.timeOfDay; - this.PreviousMineLevel = Game1.mine?.mineLevel ?? 0; - this.PreviousSaveID = Game1.uniqueIDForThisGame; } + // update state + this.CurrentPlayerTracker?.Reset(); + this.LocationsWatcher.Reset(); + this.SaveIdWatcher.Reset(); + this.TimeWatcher.Reset(); + /********* ** Game update *********/ @@ -982,7 +982,7 @@ namespace StardewModdingAPI.Framework } Game1.drawPlayerHeldObject(Game1.player); } -label_140: + label_140: if ((Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && ((!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool) && (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null))) Game1.drawTool(Game1.player); if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null) @@ -1204,52 +1204,7 @@ label_140: { Context.IsWorldReady = false; this.AfterLoadTimer = 5; - this.PreviousSaveID = 0; - } - - - - /// Get the player inventory changes between two states. - /// The player's current inventory. - /// The player's previous inventory. - private IEnumerable GetInventoryChanges(IEnumerable current, IDictionary previous) - { - current = current.Where(n => n != null).ToArray(); - foreach (Item item in current) - { - // stack size changed - if (previous != null && previous.ContainsKey(item)) - { - if (previous[item] != item.Stack) - yield return new ItemStackChange { Item = item, StackChange = item.Stack - previous[item], ChangeType = ChangeType.StackChange }; - } - - // new item - else - yield return new ItemStackChange { Item = item, StackChange = item.Stack, ChangeType = ChangeType.Added }; - } - - // removed items - if (previous != null) - { - foreach (var entry in previous) - { - if (current.Any(i => i == entry.Key)) - continue; - - yield return new ItemStackChange { Item = entry.Key, StackChange = -entry.Key.Stack, ChangeType = ChangeType.Removed }; - } - } - } - - /// Get a hash value for an enumeration. - /// The enumeration of items to hash. - private int GetHash(IEnumerable enumerable) - { - int hash = 0; - foreach (object v in enumerable) - hash ^= v.GetHashCode(); - return hash; + this.RaisedAfterLoadEvent = false; } /// Raise the if there are any listeners. diff --git a/src/SMAPI/Framework/StateTracking/Comparers/EquatableComparer.cs b/src/SMAPI/Framework/StateTracking/Comparers/EquatableComparer.cs new file mode 100644 index 00000000..a96ffdb6 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/Comparers/EquatableComparer.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace StardewModdingAPI.Framework.StateTracking.Comparers +{ + /// Compares instances using . + /// The value type. + internal class EquatableComparer : IEqualityComparer where T : IEquatable + { + /********* + ** Public methods + *********/ + /// Determines whether the specified objects are equal. + /// true if the specified objects are equal; otherwise, false. + /// The first object to compare. + /// The second object to compare. + public bool Equals(T x, T y) + { + if (x == null) + return y == null; + return x.Equals(y); + } + + /// Get a hash code for the specified object. + /// The value. + public int GetHashCode(T obj) + { + return RuntimeHelpers.GetHashCode(obj); + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/Comparers/ObjectReferenceComparer.cs b/src/SMAPI/Framework/StateTracking/Comparers/ObjectReferenceComparer.cs new file mode 100644 index 00000000..ef9adafb --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/Comparers/ObjectReferenceComparer.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace StardewModdingAPI.Framework.StateTracking.Comparers +{ + /// A comparer which considers two references equal if they point to the same instance. + /// The value type. + internal class ObjectReferenceComparer : IEqualityComparer + { + /********* + ** Public methods + *********/ + /// Determines whether the specified objects are equal. + /// true if the specified objects are equal; otherwise, false. + /// The first object to compare. + /// The second object to compare. + public bool Equals(T x, T y) + { + return object.ReferenceEquals(x, y); + } + + /// Get a hash code for the specified object. + /// The value. + public int GetHashCode(T obj) + { + return RuntimeHelpers.GetHashCode(obj); + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/BaseDisposableWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/BaseDisposableWatcher.cs new file mode 100644 index 00000000..40ec6c57 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/BaseDisposableWatcher.cs @@ -0,0 +1,36 @@ +using System; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// The base implementation for a disposable watcher. + internal abstract class BaseDisposableWatcher : IDisposable + { + /********* + ** Properties + *********/ + /// Whether the watcher has been disposed. + protected bool IsDisposed { get; private set; } + + + /********* + ** Public methods + *********/ + /// Stop watching the field and release all references. + public virtual void Dispose() + { + this.IsDisposed = true; + } + + + /********* + ** Protected methods + *********/ + /// Throw an exception if the watcher is disposed. + /// The watcher is disposed. + protected void AssertNotDisposed() + { + if (this.IsDisposed) + throw new ObjectDisposedException(this.GetType().Name); + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableWatcher.cs new file mode 100644 index 00000000..d51fc2ac --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableWatcher.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// A watcher which detects changes to a value using a specified instance. + internal class ComparableWatcher : IValueWatcher + { + /********* + ** Properties + *********/ + /// Get the current value. + private readonly Func GetValue; + + /// The equality comparer. + private readonly IEqualityComparer Comparer; + + + /********* + ** Accessors + *********/ + /// The field value at the last reset. + public T PreviousValue { get; private set; } + + /// The latest value. + public T CurrentValue { get; private set; } + + /// Whether the value changed since the last reset. + public bool IsChanged { get; private set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Get the current value. + /// The equality comparer which indicates whether two values are the same. + public ComparableWatcher(Func getValue, IEqualityComparer comparer) + { + this.GetValue = getValue; + this.Comparer = comparer; + this.PreviousValue = getValue(); + } + + /// Update the current value if needed. + public void Update() + { + this.CurrentValue = this.GetValue(); + this.IsChanged = !this.Comparer.Equals(this.PreviousValue, this.CurrentValue); + } + + /// Set the current value as the baseline. + public void Reset() + { + this.PreviousValue = this.CurrentValue; + this.IsChanged = false; + } + + /// Release any references if needed when the field is no longer needed. + public void Dispose() { } + } +} diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetDictionaryWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetDictionaryWatcher.cs new file mode 100644 index 00000000..7a2bf84e --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetDictionaryWatcher.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; +using Netcode; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// A watcher which detects changes to a net dictionary field. + /// The dictionary key type. + /// The dictionary value type. + /// The net type equivalent to . + /// The serializable dictionary type that can store the keys and values. + /// The net field instance type. + internal class NetDictionaryWatcher : BaseDisposableWatcher, IDictionaryWatcher + where TField : class, INetObject, new() + where TSerialDict : IDictionary, new() + where TSelf : NetDictionary + { + /********* + ** Properties + *********/ + /// The pairs added since the last reset. + private readonly IDictionary PairsAdded = new Dictionary(); + + /// The pairs demoved since the last reset. + private readonly IDictionary PairsRemoved = new Dictionary(); + + /// The field being watched. + private readonly NetDictionary Field; + + + /********* + ** Accessors + *********/ + /// Whether the collection changed since the last reset. + public bool IsChanged => this.PairsAdded.Count > 0 || this.PairsRemoved.Count > 0; + + /// The values added since the last reset. + public IEnumerable> Added => this.PairsAdded; + + /// The values removed since the last reset. + public IEnumerable> Removed => this.PairsRemoved; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The field to watch. + public NetDictionaryWatcher(NetDictionary field) + { + this.Field = field; + + field.OnValueAdded += this.OnValueAdded; + field.OnValueRemoved += this.OnValueRemoved; + } + + /// Update the current value if needed. + public void Update() + { + this.AssertNotDisposed(); + } + + /// Set the current value as the baseline. + public void Reset() + { + this.AssertNotDisposed(); + + this.PairsAdded.Clear(); + this.PairsRemoved.Clear(); + } + + /// Stop watching the field and release all references. + public override void Dispose() + { + if (!this.IsDisposed) + { + this.Field.OnValueAdded -= this.OnValueAdded; + this.Field.OnValueRemoved -= this.OnValueRemoved; + } + base.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// A callback invoked when an entry is added to the dictionary. + /// The entry key. + /// The entry value. + private void OnValueAdded(TKey key, TValue value) + { + this.PairsAdded[key] = value; + } + + /// A callback invoked when an entry is removed from the dictionary. + /// The entry key. + /// The entry value. + private void OnValueRemoved(TKey key, TValue value) + { + if (!this.PairsRemoved.ContainsKey(key)) + this.PairsRemoved[key] = value; + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetValueWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetValueWatcher.cs new file mode 100644 index 00000000..188ed9f3 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetValueWatcher.cs @@ -0,0 +1,83 @@ +using Netcode; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// A watcher which detects changes to a net value field. + internal class NetValueWatcher : BaseDisposableWatcher, IValueWatcher where TSelf : NetFieldBase + { + /********* + ** Properties + *********/ + /// The field being watched. + private readonly NetFieldBase Field; + + + /********* + ** Accessors + *********/ + /// Whether the value changed since the last reset. + public bool IsChanged { get; private set; } + + /// The field value at the last reset. + public T PreviousValue { get; private set; } + + /// The latest value. + public T CurrentValue { get; private set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The field to watch. + public NetValueWatcher(NetFieldBase field) + { + this.Field = field; + this.PreviousValue = field.Value; + this.CurrentValue = field.Value; + + field.fieldChangeVisibleEvent += this.OnValueChanged; + field.fieldChangeEvent += this.OnValueChanged; + } + + /// Update the current value if needed. + public void Update() + { + this.AssertNotDisposed(); + } + + /// Set the current value as the baseline. + public void Reset() + { + this.AssertNotDisposed(); + + this.PreviousValue = this.CurrentValue; + this.IsChanged = false; + } + + /// Stop watching the field and release all references. + public override void Dispose() + { + if (!this.IsDisposed) + { + this.Field.fieldChangeEvent -= this.OnValueChanged; + this.Field.fieldChangeVisibleEvent -= this.OnValueChanged; + } + base.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// A callback invoked when the field's value changes. + /// The field being watched. + /// The old field value. + /// The new field value. + private void OnValueChanged(TSelf field, T oldValue, T newValue) + { + this.CurrentValue = newValue; + this.IsChanged = true; + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs new file mode 100644 index 00000000..34a97097 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// A watcher which detects changes to an observable collection. + internal class ObservableCollectionWatcher : BaseDisposableWatcher, ICollectionWatcher + { + /********* + ** Properties + *********/ + /// The field being watched. + private readonly ObservableCollection Field; + + /// The pairs added since the last reset. + private readonly List AddedImpl = new List(); + + /// The pairs demoved since the last reset. + private readonly List RemovedImpl = new List(); + + + /********* + ** Accessors + *********/ + /// Whether the collection changed since the last reset. + public bool IsChanged => this.AddedImpl.Count > 0 || this.RemovedImpl.Count > 0; + + /// The values added since the last reset. + public IEnumerable Added => this.AddedImpl; + + /// The values removed since the last reset. + public IEnumerable Removed => this.RemovedImpl; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The field to watch. + public ObservableCollectionWatcher(ObservableCollection field) + { + this.Field = field; + field.CollectionChanged += this.OnCollectionChanged; + } + + /// Update the current value if needed. + public void Update() + { + this.AssertNotDisposed(); + } + + /// Set the current value as the baseline. + public void Reset() + { + this.AssertNotDisposed(); + + this.AddedImpl.Clear(); + this.RemovedImpl.Clear(); + } + + /// Stop watching the field and release all references. + public override void Dispose() + { + if (!this.IsDisposed) + this.Field.CollectionChanged -= this.OnCollectionChanged; + base.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// A callback invoked when an entry is added or removed from the collection. + /// The event sender. + /// The event arguments. + private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.NewItems != null) + this.AddedImpl.AddRange(e.NewItems.Cast()); + if (e.OldItems != null) + this.RemovedImpl.AddRange(e.OldItems.Cast()); + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs new file mode 100644 index 00000000..bf261bb5 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Netcode; +using StardewModdingAPI.Framework.StateTracking.Comparers; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// Provides convenience wrappers for creating watchers. + internal static class WatcherFactory + { + /********* + ** Public methods + *********/ + /// Get a watcher for an value. + /// The value type. + /// Get the current value. + public static ComparableWatcher ForEquatable(Func getValue) where T : IEquatable + { + return new ComparableWatcher(getValue, new EquatableComparer()); + } + + /// Get a watcher which detects when an object reference changes. + /// The value type. + /// Get the current value. + public static ComparableWatcher ForReference(Func getValue) + { + return new ComparableWatcher(getValue, new ObjectReferenceComparer()); + } + + /// Get a watcher for an observable collection. + /// The value type. + /// The observable collection. + public static ObservableCollectionWatcher ForObservableCollection(ObservableCollection collection) + { + return new ObservableCollectionWatcher(collection); + } + + /// Get a watcher for a net dictionary. + /// The dictionary key type. + /// The dictionary value type. + /// The net type equivalent to . + /// The serializable dictionary type that can store the keys and values. + /// The net field instance type. + /// The net field. + public static NetDictionaryWatcher ForNetDictionary(NetDictionary field) + where TField : class, INetObject, new() + where TSerialDict : IDictionary, new() + where TSelf : NetDictionary + { + return new NetDictionaryWatcher(field); + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/ICollectionWatcher.cs b/src/SMAPI/Framework/StateTracking/ICollectionWatcher.cs new file mode 100644 index 00000000..7a7759e3 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/ICollectionWatcher.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.StateTracking +{ + /// A watcher which tracks changes to a collection. + internal interface ICollectionWatcher : IWatcher + { + /********* + ** Accessors + *********/ + /// The values added since the last reset. + IEnumerable Added { get; } + + /// The values removed since the last reset. + IEnumerable Removed { get; } + } +} diff --git a/src/SMAPI/Framework/StateTracking/IDictionaryWatcher.cs b/src/SMAPI/Framework/StateTracking/IDictionaryWatcher.cs new file mode 100644 index 00000000..691ed377 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/IDictionaryWatcher.cs @@ -0,0 +1,7 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.StateTracking +{ + /// A watcher which tracks changes to a dictionary. + internal interface IDictionaryWatcher : ICollectionWatcher> { } +} diff --git a/src/SMAPI/Framework/StateTracking/IValueWatcher.cs b/src/SMAPI/Framework/StateTracking/IValueWatcher.cs new file mode 100644 index 00000000..4afca972 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/IValueWatcher.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Framework.StateTracking +{ + /// A watcher which tracks changes to a value. + internal interface IValueWatcher : IWatcher + { + /********* + ** Accessors + *********/ + /// The field value at the last reset. + T PreviousValue { get; } + + /// The latest value. + T CurrentValue { get; } + } +} diff --git a/src/SMAPI/Framework/StateTracking/IWatcher.cs b/src/SMAPI/Framework/StateTracking/IWatcher.cs new file mode 100644 index 00000000..8c7fa51c --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/IWatcher.cs @@ -0,0 +1,24 @@ +using System; + +namespace StardewModdingAPI.Framework.StateTracking +{ + /// A watcher which detects changes to something. + internal interface IWatcher : IDisposable + { + /********* + ** Accessors + *********/ + /// Whether the value changed since the last reset. + bool IsChanged { get; } + + + /********* + ** Methods + *********/ + /// Update the current value if needed. + void Update(); + + /// Set the current value as the baseline. + void Reset(); + } +} diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs new file mode 100644 index 00000000..81e074ec --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.StateTracking.FieldWatchers; +using StardewValley; +using StardewValley.Locations; +using SFarmer = StardewValley.Farmer; +using SObject = StardewValley.Object; + +namespace StardewModdingAPI.Framework.StateTracking +{ + /// Tracks changes to a player's data. + internal class PlayerTracker : IDisposable + { + /********* + ** Properties + *********/ + /// The player's inventory as of the last reset. + private IDictionary PreviousInventory; + + /// The player's inventory change as of the last update. + private IDictionary CurrentInventory; + + /// The player's last valid location. + private GameLocation LastValidLocation; + + /// The underlying watchers. + private readonly List Watchers = new List(); + + + /********* + ** Accessors + *********/ + /// The player being tracked. + public SFarmer Player { get; } + + /// The player's current location. + public IValueWatcher LocationWatcher { get; } + + /// Tracks changes to the player's current location's objects. + public IDictionaryWatcher LocationObjectsWatcher { get; private set; } + + /// The player's current mine level. + public IValueWatcher MineLevelWatcher { get; } + + /// Tracks changes to the player's skill levels. + public IDictionary> SkillWatchers { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player to track. + public PlayerTracker(SFarmer player) + { + // init player data + this.Player = player; + this.PreviousInventory = this.GetInventory(); + + // init trackers + this.LocationWatcher = WatcherFactory.ForReference(this.GetCurrentLocation); + this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(this.GetCurrentLocation().objects); + this.MineLevelWatcher = WatcherFactory.ForEquatable(() => this.LastValidLocation is MineShaft mine ? mine.mineLevel : 0); + this.SkillWatchers = new Dictionary> + { + [EventArgsLevelUp.LevelType.Combat] = WatcherFactory.ForEquatable(() => player.combatLevel), + [EventArgsLevelUp.LevelType.Farming] = WatcherFactory.ForEquatable(() => player.farmingLevel), + [EventArgsLevelUp.LevelType.Fishing] = WatcherFactory.ForEquatable(() => player.fishingLevel), + [EventArgsLevelUp.LevelType.Foraging] = WatcherFactory.ForEquatable(() => player.foragingLevel), + [EventArgsLevelUp.LevelType.Luck] = WatcherFactory.ForEquatable(() => player.luckLevel), + [EventArgsLevelUp.LevelType.Mining] = WatcherFactory.ForEquatable(() => player.miningLevel) + }; + + // track watchers for convenience + this.Watchers.AddRange(new IWatcher[] + { + this.LocationWatcher, + this.LocationObjectsWatcher, + this.MineLevelWatcher + }); + this.Watchers.AddRange(this.SkillWatchers.Values); + } + + /// Update the current values if needed. + public void Update() + { + // update valid location + this.LastValidLocation = this.GetCurrentLocation(); + + // update watchers + foreach (IWatcher watcher in this.Watchers) + watcher.Update(); + + // replace location objects watcher + if (this.LocationWatcher.IsChanged) + { + this.Watchers.Remove(this.LocationObjectsWatcher); + this.LocationObjectsWatcher.Dispose(); + + this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(this.GetCurrentLocation().objects); + this.Watchers.Add(this.LocationObjectsWatcher); + } + + // update inventory + this.CurrentInventory = this.GetInventory(); + } + + /// Reset all trackers so their current values are the baseline. + public void Reset() + { + foreach (IWatcher watcher in this.Watchers) + watcher.Reset(); + + this.PreviousInventory = this.CurrentInventory; + } + + /// Get the player's current location, ignoring temporary null values. + /// The game will set to null in some cases, e.g. when they're a secondary player in multiplayer and transition to a location that hasn't been synced yet. While that's happening, this returns the player's last valid location instead. + public GameLocation GetCurrentLocation() + { + return this.Player.currentLocation ?? this.LastValidLocation; + } + + /// Get the player inventory changes between two states. + public IEnumerable GetInventoryChanges() + { + IDictionary previous = this.PreviousInventory; + IDictionary current = this.GetInventory(); + foreach (Item item in previous.Keys.Union(current.Keys)) + { + if (!previous.TryGetValue(item, out int prevStack)) + yield return new ItemStackChange { Item = item, StackChange = item.Stack, ChangeType = ChangeType.Added }; + else if (!current.TryGetValue(item, out int newStack)) + yield return new ItemStackChange { Item = item, StackChange = -item.Stack, ChangeType = ChangeType.Removed }; + else if (prevStack != newStack) + yield return new ItemStackChange { Item = item, StackChange = newStack - prevStack, ChangeType = ChangeType.StackChange }; + } + } + + /// Get the player skill levels which changed. + public IEnumerable>> GetChangedSkills() + { + return this.SkillWatchers.Where(p => p.Value.IsChanged); + } + + /// Get the player's new location if it changed. + /// The player's current location. + /// Returns whether it changed. + public bool TryGetNewLocation(out GameLocation location) + { + location = this.LocationWatcher.CurrentValue; + return this.LocationWatcher.IsChanged; + } + + /// Get object changes to the player's current location if they there as of the last reset. + /// The object change watcher. + /// Returns whether it changed. + public bool TryGetLocationChanges(out IDictionaryWatcher watcher) + { + if (this.LocationWatcher.IsChanged) + { + watcher = null; + return false; + } + + watcher = this.LocationObjectsWatcher; + return watcher.IsChanged; + } + + /// Get the player's new mine level if it changed. + /// The player's current mine level. + /// Returns whether it changed. + public bool TryGetNewMineLevel(out int mineLevel) + { + mineLevel = this.MineLevelWatcher.CurrentValue; + return this.MineLevelWatcher.IsChanged; + } + + /// Stop watching the player fields and release all references. + public void Dispose() + { + foreach (IWatcher watcher in this.Watchers) + watcher.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// Get the player's current inventory. + private IDictionary GetInventory() + { + return this.Player.Items + .Where(n => n != null) + .Distinct() + .ToDictionary(n => n, n => n.Stack); + } + } +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 9b4a496e..5fe3e32c 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -131,6 +131,19 @@ + + + + + + + + + + + + + -- cgit From 5e7eaf9f75d72d8cbb338c35b43f2974440b3456 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 22 Apr 2018 19:59:03 -0400 Subject: rewrite input suppression (#453) This lets SMAPI intercept all input using the new Game1.hooks in SDV 1.3.0.32. However, intercepting mouse clicks needs a few more changes in the game code. --- docs/release-notes.md | 3 +- src/SMAPI/Events/EventArgsInput.cs | 109 ++-------------- src/SMAPI/Framework/Input/GamePadStateBuilder.cs | 153 +++++++++++++++++++++++ src/SMAPI/Framework/Input/InputState.cs | 78 +++++++++--- src/SMAPI/Framework/SGame.cs | 78 +++++++++++- src/SMAPI/SModHooks.cs | 48 +++++++ src/SMAPI/StardewModdingAPI.csproj | 2 + 7 files changed, 357 insertions(+), 114 deletions(-) create mode 100644 src/SMAPI/Framework/Input/GamePadStateBuilder.cs create mode 100644 src/SMAPI/SModHooks.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/docs/release-notes.md b/docs/release-notes.md index ee2d0aa1..565ed58c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -22,7 +22,8 @@ * Added prerelease versions to the mod update-check API response where available (GitHub only). * Added support for beta releases on the home page. * Split mod DB out of `StardewModdingAPI.config.json`, so we can load config earlier and reduce unnecessary memory usage later. - * Overhauled world/player state tracking: + * Rewrote input suppression using new SDV 1.3 APIs. + * Rewrote world/player state tracking: * much more efficient than previous method; * uses net field events where available; * lays groundwork for tracking events for multiple players. diff --git a/src/SMAPI/Events/EventArgsInput.cs b/src/SMAPI/Events/EventArgsInput.cs index 0cf0828b..d60f4017 100644 --- a/src/SMAPI/Events/EventArgsInput.cs +++ b/src/SMAPI/Events/EventArgsInput.cs @@ -1,14 +1,18 @@ using System; -using System.Linq; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Input; -using StardewValley; +using System.Collections.Generic; namespace StardewModdingAPI.Events { /// Event arguments when a button is pressed or released. public class EventArgsInput : EventArgs { + /********* + ** Properties + *********/ + /// The buttons to suppress. + private readonly HashSet SuppressButtons; + + /********* ** Accessors *********/ @@ -25,7 +29,7 @@ namespace StardewModdingAPI.Events public bool IsUseToolButton { get; } /// Whether a mod has indicated the key was already handled. - public bool IsSuppressed { get; private set; } + public bool IsSuppressed => this.SuppressButtons.Contains(this.Button); /********* @@ -36,12 +40,14 @@ namespace StardewModdingAPI.Events /// The cursor position. /// Whether the input should trigger actions on the affected tile. /// Whether the input should use tools on the affected tile. - public EventArgsInput(SButton button, ICursorPosition cursor, bool isActionButton, bool isUseToolButton) + /// The buttons to suppress. + public EventArgsInput(SButton button, ICursorPosition cursor, bool isActionButton, bool isUseToolButton, HashSet suppressButtons) { this.Button = button; this.Cursor = cursor; this.IsActionButton = isActionButton; this.IsUseToolButton = isUseToolButton; + this.SuppressButtons = suppressButtons; } /// Prevent the game from handling the current button press. This doesn't prevent other mods from receiving the event. @@ -54,96 +60,7 @@ namespace StardewModdingAPI.Events /// The button to suppress. public void SuppressButton(SButton button) { - if (button == this.Button) - this.IsSuppressed = true; - - // keyboard - if (button.TryGetKeyboard(out Keys key)) - Game1.oldKBState = new KeyboardState(Game1.oldKBState.GetPressedKeys().Union(new[] { key }).ToArray()); - - // controller - else if (button.TryGetController(out Buttons controllerButton)) - { - var newState = GamePad.GetState(PlayerIndex.One); - var thumbsticks = Game1.oldPadState.ThumbSticks; - var triggers = Game1.oldPadState.Triggers; - var buttons = Game1.oldPadState.Buttons; - var dpad = Game1.oldPadState.DPad; - - switch (controllerButton) - { - // d-pad - case Buttons.DPadDown: - dpad = new GamePadDPad(dpad.Up, newState.DPad.Down, dpad.Left, dpad.Right); - break; - case Buttons.DPadLeft: - dpad = new GamePadDPad(dpad.Up, dpad.Down, newState.DPad.Left, dpad.Right); - break; - case Buttons.DPadRight: - dpad = new GamePadDPad(dpad.Up, dpad.Down, dpad.Left, newState.DPad.Right); - break; - case Buttons.DPadUp: - dpad = new GamePadDPad(newState.DPad.Up, dpad.Down, dpad.Left, dpad.Right); - break; - - // trigger - case Buttons.LeftTrigger: - triggers = new GamePadTriggers(newState.Triggers.Left, triggers.Right); - break; - case Buttons.RightTrigger: - triggers = new GamePadTriggers(triggers.Left, newState.Triggers.Right); - break; - - // thumbstick - case Buttons.LeftThumbstickDown: - case Buttons.LeftThumbstickLeft: - case Buttons.LeftThumbstickRight: - case Buttons.LeftThumbstickUp: - thumbsticks = new GamePadThumbSticks(newState.ThumbSticks.Left, thumbsticks.Right); - break; - case Buttons.RightThumbstickDown: - case Buttons.RightThumbstickLeft: - case Buttons.RightThumbstickRight: - case Buttons.RightThumbstickUp: - thumbsticks = new GamePadThumbSticks(newState.ThumbSticks.Right, thumbsticks.Left); - break; - - // buttons - default: - var mask = - (buttons.A == ButtonState.Pressed ? Buttons.A : 0) - | (buttons.B == ButtonState.Pressed ? Buttons.B : 0) - | (buttons.Back == ButtonState.Pressed ? Buttons.Back : 0) - | (buttons.BigButton == ButtonState.Pressed ? Buttons.BigButton : 0) - | (buttons.LeftShoulder == ButtonState.Pressed ? Buttons.LeftShoulder : 0) - | (buttons.LeftStick == ButtonState.Pressed ? Buttons.LeftStick : 0) - | (buttons.RightShoulder == ButtonState.Pressed ? Buttons.RightShoulder : 0) - | (buttons.RightStick == ButtonState.Pressed ? Buttons.RightStick : 0) - | (buttons.Start == ButtonState.Pressed ? Buttons.Start : 0) - | (buttons.X == ButtonState.Pressed ? Buttons.X : 0) - | (buttons.Y == ButtonState.Pressed ? Buttons.Y : 0); - mask = mask ^ controllerButton; - buttons = new GamePadButtons(mask); - break; - } - - Game1.oldPadState = new GamePadState(thumbsticks, triggers, buttons, dpad); - } - - // mouse - else if (button == SButton.MouseLeft || button == SButton.MouseMiddle || button == SButton.MouseRight || button == SButton.MouseX1 || button == SButton.MouseX2) - { - Game1.oldMouseState = new MouseState( - x: Game1.oldMouseState.X, - y: Game1.oldMouseState.Y, - scrollWheel: Game1.oldMouseState.ScrollWheelValue, - leftButton: button == SButton.MouseLeft ? ButtonState.Pressed : Game1.oldMouseState.LeftButton, - middleButton: button == SButton.MouseMiddle ? ButtonState.Pressed : Game1.oldMouseState.MiddleButton, - rightButton: button == SButton.MouseRight ? ButtonState.Pressed : Game1.oldMouseState.RightButton, - xButton1: button == SButton.MouseX1 ? ButtonState.Pressed : Game1.oldMouseState.XButton1, - xButton2: button == SButton.MouseX2 ? ButtonState.Pressed : Game1.oldMouseState.XButton2 - ); - } + this.SuppressButtons.Add(button); } } } diff --git a/src/SMAPI/Framework/Input/GamePadStateBuilder.cs b/src/SMAPI/Framework/Input/GamePadStateBuilder.cs new file mode 100644 index 00000000..5eeb7ef6 --- /dev/null +++ b/src/SMAPI/Framework/Input/GamePadStateBuilder.cs @@ -0,0 +1,153 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; + +namespace StardewModdingAPI.Framework.Input +{ + /// An abstraction for manipulating controller state. + internal class GamePadStateBuilder + { + /********* + ** Properties + *********/ + /// The current button states. + private readonly IDictionary ButtonStates; + + /// The left trigger value. + private float LeftTrigger; + + /// The right trigger value. + private float RightTrigger; + + /// The left thumbstick position. + private Vector2 LeftStickPos; + + /// The left thumbstick position. + private Vector2 RightStickPos; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The initial controller state. + public GamePadStateBuilder(GamePadState state) + { + this.ButtonStates = new Dictionary + { + [SButton.DPadUp] = state.DPad.Up, + [SButton.DPadDown] = state.DPad.Down, + [SButton.DPadLeft] = state.DPad.Left, + [SButton.DPadRight] = state.DPad.Right, + + [SButton.ControllerA] = state.Buttons.A, + [SButton.ControllerB] = state.Buttons.B, + [SButton.ControllerX] = state.Buttons.X, + [SButton.ControllerY] = state.Buttons.Y, + [SButton.LeftStick] = state.Buttons.LeftStick, + [SButton.RightStick] = state.Buttons.RightStick, + [SButton.LeftShoulder] = state.Buttons.LeftShoulder, + [SButton.RightShoulder] = state.Buttons.RightShoulder, + [SButton.ControllerBack] = state.Buttons.Back, + [SButton.ControllerStart] = state.Buttons.Start, + [SButton.BigButton] = state.Buttons.BigButton + }; + this.LeftTrigger = state.Triggers.Left; + this.RightTrigger = state.Triggers.Right; + this.LeftStickPos = state.ThumbSticks.Left; + this.RightStickPos = state.ThumbSticks.Right; + } + + /// Mark all matching buttons unpressed. + /// The buttons. + public void SuppressButtons(IEnumerable buttons) + { + foreach (SButton button in buttons) + this.SuppressButton(button); + } + + /// Mark a button unpressed. + /// The button. + public void SuppressButton(SButton button) + { + switch (button) + { + // left thumbstick + case SButton.LeftThumbstickUp: + if (this.LeftStickPos.Y > 0) + this.LeftStickPos.Y = 0; + break; + case SButton.LeftThumbstickDown: + if (this.LeftStickPos.Y < 0) + this.LeftStickPos.Y = 0; + break; + case SButton.LeftThumbstickLeft: + if (this.LeftStickPos.X < 0) + this.LeftStickPos.X = 0; + break; + case SButton.LeftThumbstickRight: + if (this.LeftStickPos.X > 0) + this.LeftStickPos.X = 0; + break; + + // right thumbstick + case SButton.RightThumbstickUp: + if (this.RightStickPos.Y > 0) + this.RightStickPos.Y = 0; + break; + case SButton.RightThumbstickDown: + if (this.RightStickPos.Y < 0) + this.RightStickPos.Y = 0; + break; + case SButton.RightThumbstickLeft: + if (this.RightStickPos.X < 0) + this.RightStickPos.X = 0; + break; + case SButton.RightThumbstickRight: + if (this.RightStickPos.X > 0) + this.RightStickPos.X = 0; + break; + + // triggers + case SButton.LeftTrigger: + this.LeftTrigger = 0; + break; + case SButton.RightTrigger: + this.RightTrigger = 0; + break; + + // buttons + default: + if (this.ButtonStates.ContainsKey(button)) + this.ButtonStates[button] = ButtonState.Released; + break; + } + } + + /// Construct an equivalent gamepad state. + public GamePadState ToGamePadState() + { + return new GamePadState( + leftThumbStick: this.LeftStickPos, + rightThumbStick: this.RightStickPos, + leftTrigger: this.LeftTrigger, + rightTrigger: this.RightTrigger, + buttons: this.GetPressedButtons().ToArray() + ); + } + + /********* + ** Private methods + *********/ + /// Get all pressed buttons. + private IEnumerable GetPressedButtons() + { + foreach (var pair in this.ButtonStates) + { + if (pair.Value == ButtonState.Pressed && pair.Key.TryGetController(out Buttons button)) + yield return button; + } + } + } +} diff --git a/src/SMAPI/Framework/Input/InputState.cs b/src/SMAPI/Framework/Input/InputState.cs index 7c8676e9..62337a6c 100644 --- a/src/SMAPI/Framework/Input/InputState.cs +++ b/src/SMAPI/Framework/Input/InputState.cs @@ -9,6 +9,16 @@ namespace StardewModdingAPI.Framework.Input /// A summary of input changes during an update frame. internal class InputState { + /********* + ** Accessors + *********/ + /// The maximum amount of direction to ignore for the left thumbstick. + private const float LeftThumbstickDeadZone = 0.2f; + + /// The maximum amount of direction to ignore for the right thumbstick. + private const float RightThumbstickDeadZone = 0f; + + /********* ** Accessors *********/ @@ -48,7 +58,7 @@ namespace StardewModdingAPI.Framework.Input this.MousePosition = new Point((int)(mouseState.X * (1.0 / Game1.options.zoomLevel)), (int)(mouseState.Y * (1.0 / Game1.options.zoomLevel))); // derived from Game1::getMouseX // get button states - SButton[] down = InputState.GetPressedButtons(keyboardState, mouseState, controllerState).ToArray(); + SButton[] down = this.GetPressedButtons(keyboardState, mouseState, controllerState).ToArray(); foreach (SButton button in down) this.ActiveButtons[button] = this.GetStatus(previousState.GetStatus(button), isDown: true); foreach (KeyValuePair prev in previousState.ActiveButtons) @@ -109,7 +119,8 @@ namespace StardewModdingAPI.Framework.Input /// The keyboard state. /// The mouse state. /// The controller state. - private static IEnumerable GetPressedButtons(KeyboardState keyboard, MouseState mouse, GamePadState controller) + /// Thumbstick direction logic derived from . + private IEnumerable GetPressedButtons(KeyboardState keyboard, MouseState mouse, GamePadState controller) { // keyboard foreach (Keys key in keyboard.GetPressedKeys()) @@ -130,28 +141,23 @@ namespace StardewModdingAPI.Framework.Input // controller if (controller.IsConnected) { + // main buttons if (controller.Buttons.A == ButtonState.Pressed) yield return SButton.ControllerA; if (controller.Buttons.B == ButtonState.Pressed) yield return SButton.ControllerB; - if (controller.Buttons.Back == ButtonState.Pressed) - yield return SButton.ControllerBack; - if (controller.Buttons.BigButton == ButtonState.Pressed) - yield return SButton.BigButton; - if (controller.Buttons.LeftShoulder == ButtonState.Pressed) - yield return SButton.LeftShoulder; + if (controller.Buttons.X == ButtonState.Pressed) + yield return SButton.ControllerX; + if (controller.Buttons.Y == ButtonState.Pressed) + yield return SButton.ControllerY; if (controller.Buttons.LeftStick == ButtonState.Pressed) yield return SButton.LeftStick; - if (controller.Buttons.RightShoulder == ButtonState.Pressed) - yield return SButton.RightShoulder; if (controller.Buttons.RightStick == ButtonState.Pressed) yield return SButton.RightStick; if (controller.Buttons.Start == ButtonState.Pressed) yield return SButton.ControllerStart; - if (controller.Buttons.X == ButtonState.Pressed) - yield return SButton.ControllerX; - if (controller.Buttons.Y == ButtonState.Pressed) - yield return SButton.ControllerY; + + // directional pad if (controller.DPad.Up == ButtonState.Pressed) yield return SButton.DPadUp; if (controller.DPad.Down == ButtonState.Pressed) @@ -160,11 +166,55 @@ namespace StardewModdingAPI.Framework.Input yield return SButton.DPadLeft; if (controller.DPad.Right == ButtonState.Pressed) yield return SButton.DPadRight; + + // secondary buttons + if (controller.Buttons.Back == ButtonState.Pressed) + yield return SButton.ControllerBack; + if (controller.Buttons.BigButton == ButtonState.Pressed) + yield return SButton.BigButton; + + // shoulders + if (controller.Buttons.LeftShoulder == ButtonState.Pressed) + yield return SButton.LeftShoulder; + if (controller.Buttons.RightShoulder == ButtonState.Pressed) + yield return SButton.RightShoulder; + + // triggers if (controller.Triggers.Left > 0.2f) yield return SButton.LeftTrigger; if (controller.Triggers.Right > 0.2f) yield return SButton.RightTrigger; + + // left thumbstick direction + if (controller.ThumbSticks.Left.Y > InputState.LeftThumbstickDeadZone) + yield return SButton.LeftThumbstickUp; + if (controller.ThumbSticks.Left.Y < -InputState.LeftThumbstickDeadZone) + yield return SButton.LeftThumbstickDown; + if (controller.ThumbSticks.Left.X > InputState.LeftThumbstickDeadZone) + yield return SButton.LeftThumbstickRight; + if (controller.ThumbSticks.Left.X < -InputState.LeftThumbstickDeadZone) + yield return SButton.LeftThumbstickLeft; + + // right thumbstick direction + if (this.IsRightThumbstickOutsideDeadZone(controller.ThumbSticks.Right)) + { + if (controller.ThumbSticks.Right.Y > 0) + yield return SButton.RightThumbstickUp; + if (controller.ThumbSticks.Right.Y < 0) + yield return SButton.RightThumbstickDown; + if (controller.ThumbSticks.Right.X > 0) + yield return SButton.RightThumbstickRight; + if (controller.ThumbSticks.Right.X < 0) + yield return SButton.RightThumbstickLeft; + } } } + + /// Get whether the right thumbstick should be considered outside the dead zone. + /// The right thumbstick value. + private bool IsRightThumbstickOutsideDeadZone(Vector2 direction) + { + return direction.Length() > 0.9f; + } } } diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 35e027d8..3b9a159f 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -120,6 +120,9 @@ namespace StardewModdingAPI.Framework /// Simplifies access to private game code. private readonly Reflector Reflection; + /// The buttons to suppress when the game next handles input. Each button is suppressed until it's released. + private readonly HashSet SuppressButtons = new HashSet(); + /********* ** Accessors @@ -154,6 +157,7 @@ namespace StardewModdingAPI.Framework this.OnGameExiting = onGameExiting; if (this.ContentCore == null) // shouldn't happen since CreateContentManager is called first, but let's init here just in case this.ContentCore = new ContentCore(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, this.Monitor, reflection); + Game1.hooks = new SModHooks(this.UpdateControlInput); // init watchers Game1.locations = new ObservableCollection(); @@ -420,7 +424,7 @@ namespace StardewModdingAPI.Framework if (status == InputStatus.Pressed) { - this.Events.Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton())); + this.Events.Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton(), this.SuppressButtons)); // legacy events if (button.TryGetKeyboard(out Keys key)) @@ -438,7 +442,7 @@ namespace StardewModdingAPI.Framework } else if (status == InputStatus.Released) { - this.Events.Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton())); + this.Events.Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton(), this.SuppressButtons)); // legacy events if (button.TryGetKeyboard(out Keys key)) @@ -539,7 +543,7 @@ namespace StardewModdingAPI.Framework if (curPlayer.TryGetLocationChanges(out IDictionaryWatcher _)) { if (this.VerboseLogging) - this.Monitor.Log($"Context: current location objects changed.", LogLevel.Trace); + this.Monitor.Log("Context: current location objects changed.", LogLevel.Trace); this.Events.Location_LocationObjectsChanged.Raise(new EventArgsLocationObjectsChanged(curPlayer.GetCurrentLocation().objects.FieldDict)); } @@ -619,6 +623,17 @@ namespace StardewModdingAPI.Framework } } + /// Read the current input state for handling. + /// The game's keyboard state for the current tick. + /// The game's mouse state for the current tick. + /// The game's controller state for the current tick. + /// The game's default logic. + public void UpdateControlInput(ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState, Action defaultLogic) + { + this.ApplySuppression(ref keyboardState, ref mouseState, ref gamePadState); + defaultLogic(); + } + /// The method called to draw everything to the screen. /// A snapshot of the game timing state. protected override void Draw(GameTime gameTime) @@ -1240,6 +1255,63 @@ namespace StardewModdingAPI.Framework /**** ** Methods ****/ + /// Apply input suppression for the given input states. + /// The game's keyboard state for the current tick. + /// The game's mouse state for the current tick. + /// The game's controller state for the current tick. + private void ApplySuppression(ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState) + { + // stop suppressing buttons once released + if (this.SuppressButtons.Count != 0) + { + InputState inputState = new InputState(this.PreviousInput, gamePadState, keyboardState, mouseState); + this.SuppressButtons.RemoveWhere(p => !inputState.IsDown(p)); + } + if (this.SuppressButtons.Count == 0) + return; + + // gather info + HashSet keyboardButtons = new HashSet(); + HashSet controllerButtons = new HashSet(); + HashSet mouseButtons = new HashSet(); + foreach (SButton button in this.SuppressButtons) + { + if (button == SButton.MouseLeft || button == SButton.MouseMiddle || button == SButton.MouseRight || button == SButton.MouseX1 || button == SButton.MouseX2) + mouseButtons.Add(button); + else if (button.TryGetKeyboard(out Keys key)) + keyboardButtons.Add(key); + else if (gamePadState.IsConnected && button.TryGetController(out Buttons _)) + controllerButtons.Add(button); + } + + // suppress keyboard keys + if (keyboardState.GetPressedKeys().Any() && keyboardButtons.Any()) + keyboardState = new KeyboardState(keyboardState.GetPressedKeys().Except(keyboardButtons).ToArray()); + + // suppress controller keys + if (gamePadState.IsConnected && controllerButtons.Any()) + { + GamePadStateBuilder builder = new GamePadStateBuilder(gamePadState); + builder.SuppressButtons(controllerButtons); + gamePadState = builder.ToGamePadState(); + } + + // suppress mouse buttons + if (mouseButtons.Any()) + { + mouseState = new MouseState( + x: mouseState.X, + y: mouseState.Y, + scrollWheel: mouseState.ScrollWheelValue, + leftButton: mouseButtons.Contains(SButton.MouseLeft) ? ButtonState.Pressed : mouseState.LeftButton, + middleButton: mouseButtons.Contains(SButton.MouseMiddle) ? ButtonState.Pressed : mouseState.MiddleButton, + rightButton: mouseButtons.Contains(SButton.MouseRight) ? ButtonState.Pressed : mouseState.RightButton, + xButton1: mouseButtons.Contains(SButton.MouseX1) ? ButtonState.Pressed : mouseState.XButton1, + xButton2: mouseButtons.Contains(SButton.MouseX2) ? ButtonState.Pressed : mouseState.XButton2 + ); + } + } + /// Perform any cleanup needed when the player unloads a save and returns to the title screen. private void CleanupAfterReturnToTitle() { diff --git a/src/SMAPI/SModHooks.cs b/src/SMAPI/SModHooks.cs new file mode 100644 index 00000000..1b88d606 --- /dev/null +++ b/src/SMAPI/SModHooks.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.Xna.Framework.Input; +using StardewValley; + +namespace StardewModdingAPI +{ + /// Intercepts predefined Stardew Valley mod hooks. + internal class SModHooks : ModHooks + { + /********* + ** Delegates + *********/ + /// A delegate invoked by the hook. + /// The game's keyboard state for the current tick. + /// The game's mouse state for the current tick. + /// The game's controller state for the current tick. + /// The game's default logic. + public delegate void UpdateControlInputDelegate(ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState, Action action); + + + /********* + ** Properties + *********/ + /// The callback for . + private readonly UpdateControlInputDelegate UpdateControlInputHandler; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The callback for . + public SModHooks(UpdateControlInputDelegate updateControlInputHandler) + { + this.UpdateControlInputHandler = updateControlInputHandler; + } + + /// A hook invoked before the game processes player input. + /// The game's keyboard state for the current tick. + /// The game's mouse state for the current tick. + /// The game's controller state for the current tick. + /// The game's default logic. + public override void OnGame1_UpdateControlInput(ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState, Action action) + { + this.UpdateControlInputHandler(ref keyboardState, ref mouseState, ref gamePadState, action); + } + } +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 5fe3e32c..d7719e27 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -91,6 +91,7 @@ + @@ -262,6 +263,7 @@ + -- cgit From 3fcf58fcb5abcaf56dd7385fa7ef504ec9e90c5c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 25 Apr 2018 02:47:07 -0400 Subject: rewrite input suppression again (#453) This uses the new Game1.input in SDV 1.3.0.37 to override the game's input more consistently, though it still doesn't intercept clicks correctly yet. --- src/SMAPI/Framework/Input/InputState.cs | 220 ------------------- src/SMAPI/Framework/Input/SInputState.cs | 359 +++++++++++++++++++++++++++++++ src/SMAPI/Framework/SGame.cs | 113 ++-------- src/SMAPI/SModHooks.cs | 48 ----- src/SMAPI/StardewModdingAPI.csproj | 3 +- 5 files changed, 376 insertions(+), 367 deletions(-) delete mode 100644 src/SMAPI/Framework/Input/InputState.cs create mode 100644 src/SMAPI/Framework/Input/SInputState.cs delete mode 100644 src/SMAPI/SModHooks.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI/Framework/Input/InputState.cs b/src/SMAPI/Framework/Input/InputState.cs deleted file mode 100644 index 62337a6c..00000000 --- a/src/SMAPI/Framework/Input/InputState.cs +++ /dev/null @@ -1,220 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Input; -using StardewValley; - -namespace StardewModdingAPI.Framework.Input -{ - /// A summary of input changes during an update frame. - internal class InputState - { - /********* - ** Accessors - *********/ - /// The maximum amount of direction to ignore for the left thumbstick. - private const float LeftThumbstickDeadZone = 0.2f; - - /// The maximum amount of direction to ignore for the right thumbstick. - private const float RightThumbstickDeadZone = 0f; - - - /********* - ** Accessors - *********/ - /// The underlying controller state. - public GamePadState ControllerState { get; } - - /// The underlying keyboard state. - public KeyboardState KeyboardState { get; } - - /// The underlying mouse state. - public MouseState MouseState { get; } - - /// The mouse position on the screen adjusted for the zoom level. - public Point MousePosition { get; } - - /// The buttons which were pressed, held, or released. - public IDictionary ActiveButtons { get; } = new Dictionary(); - - - /********* - ** Public methods - *********/ - /// Construct an empty instance. - public InputState() { } - - /// Construct an instance. - /// The previous input state. - /// The current controller state. - /// The current keyboard state. - /// The current mouse state. - public InputState(InputState previousState, GamePadState controllerState, KeyboardState keyboardState, MouseState mouseState) - { - // init properties - this.ControllerState = controllerState; - this.KeyboardState = keyboardState; - this.MouseState = mouseState; - this.MousePosition = new Point((int)(mouseState.X * (1.0 / Game1.options.zoomLevel)), (int)(mouseState.Y * (1.0 / Game1.options.zoomLevel))); // derived from Game1::getMouseX - - // get button states - SButton[] down = this.GetPressedButtons(keyboardState, mouseState, controllerState).ToArray(); - foreach (SButton button in down) - this.ActiveButtons[button] = this.GetStatus(previousState.GetStatus(button), isDown: true); - foreach (KeyValuePair prev in previousState.ActiveButtons) - { - if (prev.Value.IsDown() && !this.ActiveButtons.ContainsKey(prev.Key)) - this.ActiveButtons[prev.Key] = InputStatus.Released; - } - } - - /// Get the status of a button. - /// The button to check. - public InputStatus GetStatus(SButton button) - { - return this.ActiveButtons.TryGetValue(button, out InputStatus status) ? status : InputStatus.None; - } - - /// Get whether a given button was pressed or held. - /// The button to check. - public bool IsDown(SButton button) - { - return this.GetStatus(button).IsDown(); - } - - /// Get whether any of the given buttons were pressed or held. - /// The buttons to check. - public bool IsAnyDown(InputButton[] buttons) - { - return buttons.Any(button => this.IsDown(button.ToSButton())); - } - - /// Get the current input state. - /// The previous input state. - public static InputState GetState(InputState previousState) - { - GamePadState controllerState = GamePad.GetState(PlayerIndex.One); - KeyboardState keyboardState = Keyboard.GetState(); - MouseState mouseState = Mouse.GetState(); - - return new InputState(previousState, controllerState, keyboardState, mouseState); - } - - /********* - ** Private methods - *********/ - /// Get the status of a button. - /// The previous button status. - /// Whether the button is currently down. - public InputStatus GetStatus(InputStatus oldStatus, bool isDown) - { - if (isDown && oldStatus.IsDown()) - return InputStatus.Held; - if (isDown) - return InputStatus.Pressed; - return InputStatus.Released; - } - - /// Get the buttons pressed in the given stats. - /// The keyboard state. - /// The mouse state. - /// The controller state. - /// Thumbstick direction logic derived from . - private IEnumerable GetPressedButtons(KeyboardState keyboard, MouseState mouse, GamePadState controller) - { - // keyboard - foreach (Keys key in keyboard.GetPressedKeys()) - yield return key.ToSButton(); - - // mouse - if (mouse.LeftButton == ButtonState.Pressed) - yield return SButton.MouseLeft; - if (mouse.RightButton == ButtonState.Pressed) - yield return SButton.MouseRight; - if (mouse.MiddleButton == ButtonState.Pressed) - yield return SButton.MouseMiddle; - if (mouse.XButton1 == ButtonState.Pressed) - yield return SButton.MouseX1; - if (mouse.XButton2 == ButtonState.Pressed) - yield return SButton.MouseX2; - - // controller - if (controller.IsConnected) - { - // main buttons - if (controller.Buttons.A == ButtonState.Pressed) - yield return SButton.ControllerA; - if (controller.Buttons.B == ButtonState.Pressed) - yield return SButton.ControllerB; - if (controller.Buttons.X == ButtonState.Pressed) - yield return SButton.ControllerX; - if (controller.Buttons.Y == ButtonState.Pressed) - yield return SButton.ControllerY; - if (controller.Buttons.LeftStick == ButtonState.Pressed) - yield return SButton.LeftStick; - if (controller.Buttons.RightStick == ButtonState.Pressed) - yield return SButton.RightStick; - if (controller.Buttons.Start == ButtonState.Pressed) - yield return SButton.ControllerStart; - - // directional pad - if (controller.DPad.Up == ButtonState.Pressed) - yield return SButton.DPadUp; - if (controller.DPad.Down == ButtonState.Pressed) - yield return SButton.DPadDown; - if (controller.DPad.Left == ButtonState.Pressed) - yield return SButton.DPadLeft; - if (controller.DPad.Right == ButtonState.Pressed) - yield return SButton.DPadRight; - - // secondary buttons - if (controller.Buttons.Back == ButtonState.Pressed) - yield return SButton.ControllerBack; - if (controller.Buttons.BigButton == ButtonState.Pressed) - yield return SButton.BigButton; - - // shoulders - if (controller.Buttons.LeftShoulder == ButtonState.Pressed) - yield return SButton.LeftShoulder; - if (controller.Buttons.RightShoulder == ButtonState.Pressed) - yield return SButton.RightShoulder; - - // triggers - if (controller.Triggers.Left > 0.2f) - yield return SButton.LeftTrigger; - if (controller.Triggers.Right > 0.2f) - yield return SButton.RightTrigger; - - // left thumbstick direction - if (controller.ThumbSticks.Left.Y > InputState.LeftThumbstickDeadZone) - yield return SButton.LeftThumbstickUp; - if (controller.ThumbSticks.Left.Y < -InputState.LeftThumbstickDeadZone) - yield return SButton.LeftThumbstickDown; - if (controller.ThumbSticks.Left.X > InputState.LeftThumbstickDeadZone) - yield return SButton.LeftThumbstickRight; - if (controller.ThumbSticks.Left.X < -InputState.LeftThumbstickDeadZone) - yield return SButton.LeftThumbstickLeft; - - // right thumbstick direction - if (this.IsRightThumbstickOutsideDeadZone(controller.ThumbSticks.Right)) - { - if (controller.ThumbSticks.Right.Y > 0) - yield return SButton.RightThumbstickUp; - if (controller.ThumbSticks.Right.Y < 0) - yield return SButton.RightThumbstickDown; - if (controller.ThumbSticks.Right.X > 0) - yield return SButton.RightThumbstickRight; - if (controller.ThumbSticks.Right.X < 0) - yield return SButton.RightThumbstickLeft; - } - } - } - - /// Get whether the right thumbstick should be considered outside the dead zone. - /// The right thumbstick value. - private bool IsRightThumbstickOutsideDeadZone(Vector2 direction) - { - return direction.Length() > 0.9f; - } - } -} diff --git a/src/SMAPI/Framework/Input/SInputState.cs b/src/SMAPI/Framework/Input/SInputState.cs new file mode 100644 index 00000000..62defa9f --- /dev/null +++ b/src/SMAPI/Framework/Input/SInputState.cs @@ -0,0 +1,359 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; +using StardewValley; + +#pragma warning disable 809 // obsolete override of non-obsolete method (this is deliberate) +namespace StardewModdingAPI.Framework.Input +{ + /// A summary of input changes during an update frame. + internal sealed class SInputState : InputState + { + /********* + ** Accessors + *********/ + /// The maximum amount of direction to ignore for the left thumbstick. + private const float LeftThumbstickDeadZone = 0.2f; + + + /********* + ** Accessors + *********/ + /// The controller state as of the last update. + public GamePadState RealController { get; private set; } + + /// The keyboard state as of the last update. + public KeyboardState RealKeyboard { get; private set; } + + /// The mouse state as of the last update. + public MouseState RealMouse { get; private set; } + + /// A derivative of which suppresses the buttons in . + public GamePadState SuppressedController { get; private set; } + + /// A derivative of which suppresses the buttons in . + public KeyboardState SuppressedKeyboard { get; private set; } + + /// A derivative of which suppresses the buttons in . + public MouseState SuppressedMouse { get; private set; } + + /// The mouse position on the screen adjusted for the zoom level. + public Point MousePosition { get; private set; } + + /// The buttons which were pressed, held, or released. + public IDictionary ActiveButtons { get; private set; } = new Dictionary(); + + /// The buttons to suppress when the game next handles input. Each button is suppressed until it's released. + public HashSet SuppressButtons { get; } = new HashSet(); + + + /********* + ** Public methods + *********/ + /// Get a copy of the current state. + public SInputState Clone() + { + return new SInputState + { + ActiveButtons = this.ActiveButtons, + RealController = this.RealController, + RealKeyboard = this.RealKeyboard, + RealMouse = this.RealMouse, + MousePosition = this.MousePosition + }; + } + + /// This method is called by the game, and does nothing since SMAPI will already have updated by that point. + [Obsolete("This method should only be called by the game itself.")] + public override void Update() { } + + /// Update the current button statuses for the given tick. + public void TrueUpdate() + { + try + { + // get new states + GamePadState realController = GamePad.GetState(PlayerIndex.One); + KeyboardState realKeyboard = Keyboard.GetState(); + MouseState realMouse = Mouse.GetState(); + Point mousePosition = new Point((int)(this.RealMouse.X * (1.0 / Game1.options.zoomLevel)), (int)(this.RealMouse.Y * (1.0 / Game1.options.zoomLevel))); // derived from Game1::getMouseX + var activeButtons = this.DeriveStatuses(this.ActiveButtons, realKeyboard, realMouse, realController); + + // get suppressed states + GamePadState suppressedController = realController; + KeyboardState suppressedKeyboard = realKeyboard; + MouseState suppressedMouse = realMouse; + if (this.SuppressButtons.Count > 0) + this.UpdateSuppression(activeButtons, ref suppressedKeyboard, ref suppressedMouse, ref suppressedController); + + // update + this.ActiveButtons = activeButtons; + this.RealController = realController; + this.RealKeyboard = realKeyboard; + this.RealMouse = realMouse; + this.SuppressedController = suppressedController; + this.SuppressedKeyboard = suppressedKeyboard; + this.SuppressedMouse = suppressedMouse; + this.MousePosition = mousePosition; + } + catch (InvalidOperationException) + { + // GetState() may crash for some players if window doesn't have focus but game1.IsActive == true + } + } + + /// Get the gamepad state visible to the game. + [Obsolete("This method should only be called by the game itself.")] + public override GamePadState GetGamePadState() + { + return this.ShouldSuppressNow() + ? this.SuppressedController + : this.RealController; + } + + /// Get the keyboard state visible to the game. + [Obsolete("This method should only be called by the game itself.")] + public override KeyboardState GetKeyboardState() + { + return this.ShouldSuppressNow() + ? this.SuppressedKeyboard + : this.RealKeyboard; + } + + /// Get the keyboard state visible to the game. + [Obsolete("This method should only be called by the game itself.")] + public override MouseState GetMouseState() + { + return this.ShouldSuppressNow() + ? this.SuppressedMouse + : this.RealMouse; + } + + /// Get whether a given button was pressed or held. + /// The button to check. + public bool IsDown(SButton button) + { + return this.GetStatus(this.ActiveButtons, button).IsDown(); + } + + /// Get whether any of the given buttons were pressed or held. + /// The buttons to check. + public bool IsAnyDown(InputButton[] buttons) + { + return buttons.Any(button => this.IsDown(button.ToSButton())); + } + + /// Apply input suppression for the given input states. + /// The current button states to check. + /// The game's keyboard state for the current tick. + /// The game's mouse state for the current tick. + /// The game's controller state for the current tick. + public void UpdateSuppression(IDictionary activeButtons, ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState) + { + // stop suppressing buttons once released + if (this.SuppressButtons.Count != 0) + this.SuppressButtons.RemoveWhere(p => !this.GetStatus(activeButtons, p).IsDown()); + if (this.SuppressButtons.Count == 0) + return; + + // gather info + HashSet keyboardButtons = new HashSet(); + HashSet controllerButtons = new HashSet(); + HashSet mouseButtons = new HashSet(); + foreach (SButton button in this.SuppressButtons) + { + if (button == SButton.MouseLeft || button == SButton.MouseMiddle || button == SButton.MouseRight || button == SButton.MouseX1 || button == SButton.MouseX2) + mouseButtons.Add(button); + else if (button.TryGetKeyboard(out Keys key)) + keyboardButtons.Add(key); + else if (gamePadState.IsConnected && button.TryGetController(out Buttons _)) + controllerButtons.Add(button); + } + + // suppress keyboard keys + if (keyboardState.GetPressedKeys().Any() && keyboardButtons.Any()) + keyboardState = new KeyboardState(keyboardState.GetPressedKeys().Except(keyboardButtons).ToArray()); + + // suppress controller keys + if (gamePadState.IsConnected && controllerButtons.Any()) + { + GamePadStateBuilder builder = new GamePadStateBuilder(gamePadState); + builder.SuppressButtons(controllerButtons); + gamePadState = builder.ToGamePadState(); + } + + // suppress mouse buttons + if (mouseButtons.Any()) + { + mouseState = new MouseState( + x: mouseState.X, + y: mouseState.Y, + scrollWheel: mouseState.ScrollWheelValue, + leftButton: mouseButtons.Contains(SButton.MouseLeft) ? ButtonState.Pressed : mouseState.LeftButton, + middleButton: mouseButtons.Contains(SButton.MouseMiddle) ? ButtonState.Pressed : mouseState.MiddleButton, + rightButton: mouseButtons.Contains(SButton.MouseRight) ? ButtonState.Pressed : mouseState.RightButton, + xButton1: mouseButtons.Contains(SButton.MouseX1) ? ButtonState.Pressed : mouseState.XButton1, + xButton2: mouseButtons.Contains(SButton.MouseX2) ? ButtonState.Pressed : mouseState.XButton2 + ); + } + } + + + /********* + ** Private methods + *********/ + /// Whether input should be suppressed in the current context. + private bool ShouldSuppressNow() + { + return Game1.chatBox != null && !Game1.chatBox.isActive(); + } + + /// Get the status of all pressed or released buttons relative to their previous status. + /// The previous button statuses. + /// The keyboard state. + /// The mouse state. + /// The controller state. + private IDictionary DeriveStatuses(IDictionary previousStatuses, KeyboardState keyboard, MouseState mouse, GamePadState controller) + { + IDictionary activeButtons = new Dictionary(); + + // handle pressed keys + SButton[] down = this.GetPressedButtons(keyboard, mouse, controller).ToArray(); + foreach (SButton button in down) + activeButtons[button] = this.DeriveStatus(this.GetStatus(previousStatuses, button), isDown: true); + + // handle released keys + foreach (KeyValuePair prev in activeButtons) + { + if (prev.Value.IsDown() && !activeButtons.ContainsKey(prev.Key)) + activeButtons[prev.Key] = InputStatus.Released; + } + + return activeButtons; + } + + /// Get the status of a button relative to its previous status. + /// The previous button status. + /// Whether the button is currently down. + private InputStatus DeriveStatus(InputStatus oldStatus, bool isDown) + { + if (isDown && oldStatus.IsDown()) + return InputStatus.Held; + if (isDown) + return InputStatus.Pressed; + return InputStatus.Released; + } + + /// Get the status of a button. + /// The current button states to check. + /// The button to check. + private InputStatus GetStatus(IDictionary activeButtons, SButton button) + { + return activeButtons.TryGetValue(button, out InputStatus status) ? status : InputStatus.None; + } + + /// Get the buttons pressed in the given stats. + /// The keyboard state. + /// The mouse state. + /// The controller state. + /// Thumbstick direction logic derived from . + private IEnumerable GetPressedButtons(KeyboardState keyboard, MouseState mouse, GamePadState controller) + { + // keyboard + foreach (Keys key in keyboard.GetPressedKeys()) + yield return key.ToSButton(); + + // mouse + if (mouse.LeftButton == ButtonState.Pressed) + yield return SButton.MouseLeft; + if (mouse.RightButton == ButtonState.Pressed) + yield return SButton.MouseRight; + if (mouse.MiddleButton == ButtonState.Pressed) + yield return SButton.MouseMiddle; + if (mouse.XButton1 == ButtonState.Pressed) + yield return SButton.MouseX1; + if (mouse.XButton2 == ButtonState.Pressed) + yield return SButton.MouseX2; + + // controller + if (controller.IsConnected) + { + // main buttons + if (controller.Buttons.A == ButtonState.Pressed) + yield return SButton.ControllerA; + if (controller.Buttons.B == ButtonState.Pressed) + yield return SButton.ControllerB; + if (controller.Buttons.X == ButtonState.Pressed) + yield return SButton.ControllerX; + if (controller.Buttons.Y == ButtonState.Pressed) + yield return SButton.ControllerY; + if (controller.Buttons.LeftStick == ButtonState.Pressed) + yield return SButton.LeftStick; + if (controller.Buttons.RightStick == ButtonState.Pressed) + yield return SButton.RightStick; + if (controller.Buttons.Start == ButtonState.Pressed) + yield return SButton.ControllerStart; + + // directional pad + if (controller.DPad.Up == ButtonState.Pressed) + yield return SButton.DPadUp; + if (controller.DPad.Down == ButtonState.Pressed) + yield return SButton.DPadDown; + if (controller.DPad.Left == ButtonState.Pressed) + yield return SButton.DPadLeft; + if (controller.DPad.Right == ButtonState.Pressed) + yield return SButton.DPadRight; + + // secondary buttons + if (controller.Buttons.Back == ButtonState.Pressed) + yield return SButton.ControllerBack; + if (controller.Buttons.BigButton == ButtonState.Pressed) + yield return SButton.BigButton; + + // shoulders + if (controller.Buttons.LeftShoulder == ButtonState.Pressed) + yield return SButton.LeftShoulder; + if (controller.Buttons.RightShoulder == ButtonState.Pressed) + yield return SButton.RightShoulder; + + // triggers + if (controller.Triggers.Left > 0.2f) + yield return SButton.LeftTrigger; + if (controller.Triggers.Right > 0.2f) + yield return SButton.RightTrigger; + + // left thumbstick direction + if (controller.ThumbSticks.Left.Y > SInputState.LeftThumbstickDeadZone) + yield return SButton.LeftThumbstickUp; + if (controller.ThumbSticks.Left.Y < -SInputState.LeftThumbstickDeadZone) + yield return SButton.LeftThumbstickDown; + if (controller.ThumbSticks.Left.X > SInputState.LeftThumbstickDeadZone) + yield return SButton.LeftThumbstickRight; + if (controller.ThumbSticks.Left.X < -SInputState.LeftThumbstickDeadZone) + yield return SButton.LeftThumbstickLeft; + + // right thumbstick direction + if (this.IsRightThumbstickOutsideDeadZone(controller.ThumbSticks.Right)) + { + if (controller.ThumbSticks.Right.Y > 0) + yield return SButton.RightThumbstickUp; + if (controller.ThumbSticks.Right.Y < 0) + yield return SButton.RightThumbstickDown; + if (controller.ThumbSticks.Right.X > 0) + yield return SButton.RightThumbstickRight; + if (controller.ThumbSticks.Right.X < 0) + yield return SButton.RightThumbstickLeft; + } + } + } + + /// Get whether the right thumbstick should be considered outside the dead zone. + /// The right thumbstick value. + private bool IsRightThumbstickOutsideDeadZone(Vector2 direction) + { + return direction.Length() > 0.9f; + } + } +} diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 4f5bd96b..182b90fc 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -53,6 +53,9 @@ namespace StardewModdingAPI.Framework /// Manages SMAPI events for mods. private readonly EventManager Events; + /// Manages input visible to the game. + private SInputState Input => (SInputState)Game1.input; + /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second @@ -78,9 +81,6 @@ namespace StardewModdingAPI.Framework /**** ** Game state ****/ - /// The player input as of the previous tick. - private InputState PreviousInput = new InputState(); - /// The underlying watchers for convenience. These are accessible individually as separate properties. private readonly List Watchers = new List(); @@ -120,9 +120,6 @@ namespace StardewModdingAPI.Framework /// Simplifies access to private game code. private readonly Reflector Reflection; - /// The buttons to suppress when the game next handles input. Each button is suppressed until it's released. - private readonly HashSet SuppressButtons = new HashSet(); - /********* ** Accessors @@ -157,7 +154,7 @@ namespace StardewModdingAPI.Framework this.OnGameExiting = onGameExiting; if (this.ContentCore == null) // shouldn't happen since CreateContentManager is called first, but let's init here just in case this.ContentCore = new ContentCore(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, this.Monitor, reflection); - Game1.hooks = new SModHooks(this.UpdateControlInput); + Game1.input = new SInputState(); // init watchers Game1.locations = new ObservableCollection(); @@ -289,7 +286,7 @@ namespace StardewModdingAPI.Framework if (this.FirstUpdate) this.OnGameInitialised(); - + /********* ** Update context *********/ @@ -390,16 +387,9 @@ namespace StardewModdingAPI.Framework *********/ if (Game1.game1.IsActive) { - // get input state - InputState inputState; - try - { - inputState = InputState.GetState(this.PreviousInput); - } - catch (InvalidOperationException) // GetState() may crash for some players if window doesn't have focus but game1.IsActive == true - { - inputState = this.PreviousInput; - } + SInputState previousInputState = this.Input.Clone(); + SInputState inputState = this.Input; + inputState.TrueUpdate(); // raise events bool isChatInput = Game1.IsChatting || (Context.IsMultiplayer && Context.IsWorldReady && Game1.activeClickableMenu == null && Game1.currentMinigame == null && inputState.IsAnyDown(Game1.options.chatButton)); @@ -425,7 +415,7 @@ namespace StardewModdingAPI.Framework if (status == InputStatus.Pressed) { - this.Events.Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton(), this.SuppressButtons)); + this.Events.Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton(), inputState.SuppressButtons)); // legacy events if (button.TryGetKeyboard(out Keys key)) @@ -436,14 +426,14 @@ namespace StardewModdingAPI.Framework else if (button.TryGetController(out Buttons controllerButton)) { if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) - this.Events.Control_ControllerTriggerPressed.Raise(new EventArgsControllerTriggerPressed(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.ControllerState.Triggers.Left : inputState.ControllerState.Triggers.Right)); + this.Events.Control_ControllerTriggerPressed.Raise(new EventArgsControllerTriggerPressed(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.RealController.Triggers.Left : inputState.RealController.Triggers.Right)); else this.Events.Control_ControllerButtonPressed.Raise(new EventArgsControllerButtonPressed(PlayerIndex.One, controllerButton)); } } else if (status == InputStatus.Released) { - this.Events.Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton(), this.SuppressButtons)); + this.Events.Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton(), inputState.SuppressButtons)); // legacy events if (button.TryGetKeyboard(out Keys key)) @@ -454,7 +444,7 @@ namespace StardewModdingAPI.Framework else if (button.TryGetController(out Buttons controllerButton)) { if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) - this.Events.Control_ControllerTriggerReleased.Raise(new EventArgsControllerTriggerReleased(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.ControllerState.Triggers.Left : inputState.ControllerState.Triggers.Right)); + this.Events.Control_ControllerTriggerReleased.Raise(new EventArgsControllerTriggerReleased(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.RealController.Triggers.Left : inputState.RealController.Triggers.Right)); else this.Events.Control_ControllerButtonReleased.Raise(new EventArgsControllerButtonReleased(PlayerIndex.One, controllerButton)); } @@ -462,14 +452,11 @@ namespace StardewModdingAPI.Framework } // raise legacy state-changed events - if (inputState.KeyboardState != this.PreviousInput.KeyboardState) - this.Events.Control_KeyboardChanged.Raise(new EventArgsKeyboardStateChanged(this.PreviousInput.KeyboardState, inputState.KeyboardState)); - if (inputState.MouseState != this.PreviousInput.MouseState) - this.Events.Control_MouseChanged.Raise(new EventArgsMouseStateChanged(this.PreviousInput.MouseState, inputState.MouseState, this.PreviousInput.MousePosition, inputState.MousePosition)); + if (inputState.RealKeyboard != previousInputState.RealKeyboard) + this.Events.Control_KeyboardChanged.Raise(new EventArgsKeyboardStateChanged(previousInputState.RealKeyboard, inputState.RealKeyboard)); + if (inputState.RealMouse != previousInputState.RealMouse) + this.Events.Control_MouseChanged.Raise(new EventArgsMouseStateChanged(previousInputState.RealMouse, inputState.RealMouse, previousInputState.MousePosition, inputState.MousePosition)); } - - // track state - this.PreviousInput = inputState; } /********* @@ -624,17 +611,6 @@ namespace StardewModdingAPI.Framework } } - /// Read the current input state for handling. - /// The game's keyboard state for the current tick. - /// The game's mouse state for the current tick. - /// The game's controller state for the current tick. - /// The game's default logic. - public void UpdateControlInput(ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState, Action defaultLogic) - { - this.ApplySuppression(ref keyboardState, ref mouseState, ref gamePadState); - defaultLogic(); - } - /// The method called to draw everything to the screen. /// A snapshot of the game timing state. protected override void Draw(GameTime gameTime) @@ -1256,63 +1232,6 @@ namespace StardewModdingAPI.Framework /**** ** Methods ****/ - /// Apply input suppression for the given input states. - /// The game's keyboard state for the current tick. - /// The game's mouse state for the current tick. - /// The game's controller state for the current tick. - private void ApplySuppression(ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState) - { - // stop suppressing buttons once released - if (this.SuppressButtons.Count != 0) - { - InputState inputState = new InputState(this.PreviousInput, gamePadState, keyboardState, mouseState); - this.SuppressButtons.RemoveWhere(p => !inputState.IsDown(p)); - } - if (this.SuppressButtons.Count == 0) - return; - - // gather info - HashSet keyboardButtons = new HashSet(); - HashSet controllerButtons = new HashSet(); - HashSet mouseButtons = new HashSet(); - foreach (SButton button in this.SuppressButtons) - { - if (button == SButton.MouseLeft || button == SButton.MouseMiddle || button == SButton.MouseRight || button == SButton.MouseX1 || button == SButton.MouseX2) - mouseButtons.Add(button); - else if (button.TryGetKeyboard(out Keys key)) - keyboardButtons.Add(key); - else if (gamePadState.IsConnected && button.TryGetController(out Buttons _)) - controllerButtons.Add(button); - } - - // suppress keyboard keys - if (keyboardState.GetPressedKeys().Any() && keyboardButtons.Any()) - keyboardState = new KeyboardState(keyboardState.GetPressedKeys().Except(keyboardButtons).ToArray()); - - // suppress controller keys - if (gamePadState.IsConnected && controllerButtons.Any()) - { - GamePadStateBuilder builder = new GamePadStateBuilder(gamePadState); - builder.SuppressButtons(controllerButtons); - gamePadState = builder.ToGamePadState(); - } - - // suppress mouse buttons - if (mouseButtons.Any()) - { - mouseState = new MouseState( - x: mouseState.X, - y: mouseState.Y, - scrollWheel: mouseState.ScrollWheelValue, - leftButton: mouseButtons.Contains(SButton.MouseLeft) ? ButtonState.Pressed : mouseState.LeftButton, - middleButton: mouseButtons.Contains(SButton.MouseMiddle) ? ButtonState.Pressed : mouseState.MiddleButton, - rightButton: mouseButtons.Contains(SButton.MouseRight) ? ButtonState.Pressed : mouseState.RightButton, - xButton1: mouseButtons.Contains(SButton.MouseX1) ? ButtonState.Pressed : mouseState.XButton1, - xButton2: mouseButtons.Contains(SButton.MouseX2) ? ButtonState.Pressed : mouseState.XButton2 - ); - } - } - /// Perform any cleanup needed when the player unloads a save and returns to the title screen. private void CleanupAfterReturnToTitle() { diff --git a/src/SMAPI/SModHooks.cs b/src/SMAPI/SModHooks.cs deleted file mode 100644 index 1b88d606..00000000 --- a/src/SMAPI/SModHooks.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using Microsoft.Xna.Framework.Input; -using StardewValley; - -namespace StardewModdingAPI -{ - /// Intercepts predefined Stardew Valley mod hooks. - internal class SModHooks : ModHooks - { - /********* - ** Delegates - *********/ - /// A delegate invoked by the hook. - /// The game's keyboard state for the current tick. - /// The game's mouse state for the current tick. - /// The game's controller state for the current tick. - /// The game's default logic. - public delegate void UpdateControlInputDelegate(ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState, Action action); - - - /********* - ** Properties - *********/ - /// The callback for . - private readonly UpdateControlInputDelegate UpdateControlInputHandler; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The callback for . - public SModHooks(UpdateControlInputDelegate updateControlInputHandler) - { - this.UpdateControlInputHandler = updateControlInputHandler; - } - - /// A hook invoked before the game processes player input. - /// The game's keyboard state for the current tick. - /// The game's mouse state for the current tick. - /// The game's controller state for the current tick. - /// The game's default logic. - public override void OnGame1_UpdateControlInput(ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState, Action action) - { - this.UpdateControlInputHandler(ref keyboardState, ref mouseState, ref gamePadState, action); - } - } -} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index d7719e27..560d7bf4 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -92,7 +92,7 @@ - + @@ -263,7 +263,6 @@ - -- cgit From adda9611c73163270cbfcd34d6617560f81d54b0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Apr 2018 18:49:13 -0400 Subject: add multiplayer sync events (#479) --- src/SMAPI/Events/MultiplayerEvents.cs | 58 ++++++++++++++++++++++++++++++ src/SMAPI/Framework/Events/EventManager.cs | 20 +++++++++++ src/SMAPI/Framework/SGame.cs | 1 + src/SMAPI/Framework/SMultiplayer.cs | 47 ++++++++++++++++++++++++ src/SMAPI/Program.cs | 1 + src/SMAPI/StardewModdingAPI.csproj | 2 ++ 6 files changed, 129 insertions(+) create mode 100644 src/SMAPI/Events/MultiplayerEvents.cs create mode 100644 src/SMAPI/Framework/SMultiplayer.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI/Events/MultiplayerEvents.cs b/src/SMAPI/Events/MultiplayerEvents.cs new file mode 100644 index 00000000..f96ecba5 --- /dev/null +++ b/src/SMAPI/Events/MultiplayerEvents.cs @@ -0,0 +1,58 @@ +using System; +using StardewModdingAPI.Framework.Events; + +namespace StardewModdingAPI.Events +{ + /// Events raised during the multiplayer sync process. + public static class MultiplayerEvents + { + /********* + ** Properties + *********/ + /// The core event manager. + private static EventManager EventManager; + + + /********* + ** Events + *********/ + /// Raised before the game syncs changes from other players. + public static event EventHandler BeforeMainSync + { + add => MultiplayerEvents.EventManager.Multiplayer_BeforeMainSync.Add(value); + remove => MultiplayerEvents.EventManager.Multiplayer_BeforeMainSync.Remove(value); + } + + /// Raised after the game syncs changes from other players. + public static event EventHandler AfterMainSync + { + add => MultiplayerEvents.EventManager.Multiplayer_AfterMainSync.Add(value); + remove => MultiplayerEvents.EventManager.Multiplayer_AfterMainSync.Remove(value); + } + + /// Raised before the game broadcasts changes to other players. + public static event EventHandler BeforeMainBroadcast + { + add => MultiplayerEvents.EventManager.Multiplayer_BeforeMainBroadcast.Add(value); + remove => MultiplayerEvents.EventManager.Multiplayer_BeforeMainBroadcast.Remove(value); + } + + /// Raised after the game broadcasts changes to other players. + public static event EventHandler AfterMainBroadcast + { + add => MultiplayerEvents.EventManager.Multiplayer_AfterMainBroadcast.Add(value); + remove => MultiplayerEvents.EventManager.Multiplayer_AfterMainBroadcast.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Initialise the events. + /// The core event manager. + internal static void Init(EventManager eventManager) + { + MultiplayerEvents.EventManager = eventManager; + } + } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index d7c89a76..87ff760f 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -125,6 +125,21 @@ namespace StardewModdingAPI.Framework.Events /// Raised after a game menu is closed. public readonly ManagedEvent Menu_Closed; + /**** + ** MultiplayerEvents + ****/ + /// Raised before the game syncs changes from other players. + public readonly ManagedEvent Multiplayer_BeforeMainSync; + + /// Raised after the game syncs changes from other players. + public readonly ManagedEvent Multiplayer_AfterMainSync; + + /// Raised before the game broadcasts changes to other players. + public readonly ManagedEvent Multiplayer_BeforeMainBroadcast; + + /// Raised after the game broadcasts changes to other players. + public readonly ManagedEvent Multiplayer_AfterMainBroadcast; + /**** ** MineEvents ****/ @@ -228,6 +243,11 @@ namespace StardewModdingAPI.Framework.Events this.Menu_Changed = ManageEventOf(nameof(MenuEvents), nameof(MenuEvents.MenuChanged)); this.Menu_Closed = ManageEventOf(nameof(MenuEvents), nameof(MenuEvents.MenuClosed)); + this.Multiplayer_BeforeMainBroadcast = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.BeforeMainBroadcast)); + this.Multiplayer_AfterMainBroadcast = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.AfterMainBroadcast)); + this.Multiplayer_BeforeMainSync = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.BeforeMainSync)); + this.Multiplayer_AfterMainSync = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.AfterMainSync)); + this.Mine_LevelChanged = ManageEventOf(nameof(MineEvents), nameof(MineEvents.MineLevelChanged)); this.Player_InventoryChanged = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.InventoryChanged)); diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 7ea83957..89bf18ef 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -155,6 +155,7 @@ namespace StardewModdingAPI.Framework if (this.ContentCore == null) // shouldn't happen since CreateContentManager is called first, but let's init here just in case this.ContentCore = new ContentCore(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, this.Monitor, reflection); Game1.input = new SInputState(); + Game1.multiplayer = new SMultiplayer(monitor, eventManager); // init watchers Game1.locations = new ObservableCollection(); diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs new file mode 100644 index 00000000..687b1922 --- /dev/null +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -0,0 +1,47 @@ +using StardewModdingAPI.Framework.Events; +using StardewValley; + +namespace StardewModdingAPI.Framework +{ + /// SMAPI's implementation of the game's core multiplayer logic. + internal class SMultiplayer : Multiplayer + { + /********* + ** Properties + *********/ + /// Encapsulates monitoring and logging. + private readonly IMonitor Monitor; + + /// Manages SMAPI events. + private readonly EventManager EventManager; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Encapsulates monitoring and logging. + /// Manages SMAPI events. + public SMultiplayer(IMonitor monitor, EventManager eventManager) + { + this.Monitor = monitor; + this.EventManager = eventManager; + } + + /// Handle sync messages from other players and perform other initial sync logic. + public override void UpdateEarly() + { + this.EventManager.Multiplayer_BeforeMainSync.Raise(); + base.UpdateEarly(); + this.EventManager.Multiplayer_AfterMainSync.Raise(); + } + + /// Broadcast sync messages to other players and perform other final sync logic. + public override void UpdateLate(bool forceSync = false) + { + this.EventManager.Multiplayer_BeforeMainBroadcast.Raise(); + base.UpdateLate(forceSync); + this.EventManager.Multiplayer_AfterMainBroadcast.Raise(); + } + } +} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index dc9e5308..2325e79a 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -157,6 +157,7 @@ namespace StardewModdingAPI LocationEvents.Init(this.EventManager); MenuEvents.Init(this.EventManager); MineEvents.Init(this.EventManager); + MultiplayerEvents.Init(this.EventManager); PlayerEvents.Init(this.EventManager); SaveEvents.Init(this.EventManager); SpecialisedEvents.Init(this.EventManager); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 560d7bf4..f6a16e5f 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -85,6 +85,7 @@ Properties\GlobalAssemblyInfo.cs + @@ -132,6 +133,7 @@ + -- cgit From a625e9bed71c6398a18ec0f5d41d7f8135660efd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 28 Apr 2018 13:30:24 -0400 Subject: add initial multiplayer API (#480) --- src/SMAPI/Framework/ModHelpers/ModHelper.cs | 6 ++++- .../Framework/ModHelpers/MultiplayerHelper.cs | 31 ++++++++++++++++++++++ src/SMAPI/Framework/SGame.cs | 3 +++ src/SMAPI/IModHelper.cs | 3 +++ src/SMAPI/IMultiplayerHelper.cs | 9 +++++++ src/SMAPI/Program.cs | 3 ++- src/SMAPI/StardewModdingAPI.csproj | 2 ++ 7 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs create mode 100644 src/SMAPI/IMultiplayerHelper.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index b5758d21..1f37a1be 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -45,6 +45,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// An API for managing console commands. public ICommandHelper ConsoleCommands { get; } + /// Provides multiplayer utilities. + public IMultiplayerHelper Multiplayer { get; } + /// An API for reading translations stored in the mod's i18n folder, with one file per locale (like en.json) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json). public ITranslationHelper Translation { get; } @@ -66,7 +69,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Manages deprecation warnings. /// An argument is null or empty. /// The path does not exist on disk. - public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, IContentHelper contentHelper, ICommandHelper commandHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, ITranslationHelper translationHelper, IEnumerable contentPacks, Func createContentPack, DeprecationManager deprecationManager) + public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, IContentHelper contentHelper, ICommandHelper commandHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper, IEnumerable contentPacks, Func createContentPack, DeprecationManager deprecationManager) : base(modID) { // validate directory @@ -82,6 +85,7 @@ namespace StardewModdingAPI.Framework.ModHelpers this.ModRegistry = modRegistry ?? throw new ArgumentNullException(nameof(modRegistry)); this.ConsoleCommands = commandHelper ?? throw new ArgumentNullException(nameof(commandHelper)); this.Reflection = reflectionHelper ?? throw new ArgumentNullException(nameof(reflectionHelper)); + this.Multiplayer = multiplayer ?? throw new ArgumentNullException(nameof(multiplayer)); this.Translation = translationHelper ?? throw new ArgumentNullException(nameof(translationHelper)); this.ContentPacks = contentPacks.ToArray(); this.CreateContentPack = createContentPack; diff --git a/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs b/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs new file mode 100644 index 00000000..7a8da1d0 --- /dev/null +++ b/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs @@ -0,0 +1,31 @@ +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// Provides multiplayer utilities. + internal class MultiplayerHelper : BaseHelper, IMultiplayerHelper + { + /********* + ** Properties + *********/ + /// SMAPI's core multiplayer utility. + private readonly SMultiplayer Multiplayer; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique ID of the relevant mod. + /// SMAPI's core multiplayer utility. + public MultiplayerHelper(string modID, SMultiplayer multiplayer) + : base(modID) + { + this.Multiplayer = multiplayer; + } + + /// Get a new multiplayer ID. + public long GetNewID() + { + return this.Multiplayer.getNewID(); + } + } +} diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 7578473b..b206879c 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -126,6 +126,9 @@ namespace StardewModdingAPI.Framework /// SMAPI's content manager. public ContentCore ContentCore { get; private set; } + /// The game's core multiplayer utility. + public SMultiplayer Multiplayer => (SMultiplayer)Game1.multiplayer; + /// Whether SMAPI should log more information about the game context. public bool VerboseLogging { get; set; } diff --git a/src/SMAPI/IModHelper.cs b/src/SMAPI/IModHelper.cs index e9554fdc..5e39161d 100644 --- a/src/SMAPI/IModHelper.cs +++ b/src/SMAPI/IModHelper.cs @@ -21,6 +21,9 @@ namespace StardewModdingAPI /// Metadata about loaded mods. IModRegistry ModRegistry { get; } + /// Provides multiplayer utilities. + IMultiplayerHelper Multiplayer { get; } + /// An API for managing console commands. ICommandHelper ConsoleCommands { get; } diff --git a/src/SMAPI/IMultiplayerHelper.cs b/src/SMAPI/IMultiplayerHelper.cs new file mode 100644 index 00000000..ac00b970 --- /dev/null +++ b/src/SMAPI/IMultiplayerHelper.cs @@ -0,0 +1,9 @@ +namespace StardewModdingAPI +{ + /// Provides multiplayer utilities. + public interface IMultiplayerHelper : IModLinked + { + /// Get a new multiplayer ID. + long GetNewID(); + } +} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 2325e79a..eaeb5f1d 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -807,6 +807,7 @@ namespace StardewModdingAPI IContentHelper contentHelper = new ContentHelper(contentCore, contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor); + IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(manifest.UniqueID, this.GameInstance.Multiplayer); ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentCore.GetLocale(), contentCore.Language); IContentPack CreateTransitionalContentPack(string packDirPath, IManifest packManifest) @@ -817,7 +818,7 @@ namespace StardewModdingAPI return new ContentPack(packDirPath, packManifest, packContentHelper, this.JsonHelper); } - modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); + modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); } // get mod instance diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index f6a16e5f..e0125c9b 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -102,6 +102,7 @@ + @@ -151,6 +152,7 @@ + -- cgit From 009a387526ee10b18d0ed3030d6e8868edf17203 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 1 May 2018 18:44:39 -0400 Subject: unify SMAPI.AssemblyRewriters and SMAPI.Common projects --- build/common.targets | 2 +- build/prepare-install-package.targets | 4 +- docs/technical-docs.md | 6 +- .../Properties/AssemblyInfo.cs | 4 - src/SMAPI.AssemblyRewriters/SpriteBatchMethods.cs | 59 ------ .../StardewModdingAPI.AssemblyRewriters.csproj | 44 ----- src/SMAPI.Common/EnvironmentUtility.cs | 112 ------------ src/SMAPI.Common/Models/ModInfoModel.cs | 56 ------ src/SMAPI.Common/Models/ModSeachModel.cs | 37 ---- src/SMAPI.Common/Platform.cs | 15 -- src/SMAPI.Common/SemanticVersionImpl.cs | 199 --------------------- .../StardewModdingAPI.Common.projitems | 21 --- src/SMAPI.Common/StardewModdingAPI.Common.shproj | 13 -- src/SMAPI.Installer/InteractiveInstaller.cs | 5 +- .../StardewModdingAPI.Installer.csproj | 9 +- src/SMAPI.Internal/EnvironmentUtility.cs | 112 ++++++++++++ src/SMAPI.Internal/Models/ModInfoModel.cs | 56 ++++++ src/SMAPI.Internal/Models/ModSeachModel.cs | 37 ++++ src/SMAPI.Internal/Platform.cs | 15 ++ src/SMAPI.Internal/Properties/AssemblyInfo.cs | 9 + .../RewriteFacades/SpriteBatchMethods.cs | 59 ++++++ src/SMAPI.Internal/SemanticVersionImpl.cs | 199 +++++++++++++++++++++ .../StardewModdingAPI.Internal.csproj | 49 +++++ .../Framework/ModFileManager.cs | 2 +- .../StardewModdingAPI.ModBuildConfig.csproj | 7 +- src/SMAPI.Web/Controllers/IndexController.cs | 2 +- src/SMAPI.Web/Controllers/ModsApiController.cs | 2 +- src/SMAPI.Web/Framework/LogParsing/LogParser.cs | 2 +- .../Framework/ModRepositories/BaseRepository.cs | 2 +- .../ModRepositories/ChucklefishRepository.cs | 2 +- .../Framework/ModRepositories/GitHubRepository.cs | 2 +- .../Framework/ModRepositories/IModRepository.cs | 2 +- .../Framework/ModRepositories/NexusRepository.cs | 2 +- src/SMAPI.Web/Framework/VersionConstraint.cs | 2 +- src/SMAPI.Web/StardewModdingAPI.Web.csproj | 4 +- src/SMAPI.sln | 12 +- src/SMAPI/Constants.cs | 2 +- src/SMAPI/Framework/Content/ContentCache.cs | 2 +- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 2 +- .../Framework/ModLoading/PlatformAssemblyMap.cs | 2 +- src/SMAPI/Framework/Monitor.cs | 2 +- src/SMAPI/Framework/WebApiClient.cs | 2 +- src/SMAPI/Metadata/InstructionMetadata.cs | 2 +- src/SMAPI/Program.cs | 4 +- src/SMAPI/SemanticVersion.cs | 2 +- src/SMAPI/StardewModdingAPI.csproj | 5 +- 46 files changed, 585 insertions(+), 605 deletions(-) delete mode 100644 src/SMAPI.AssemblyRewriters/Properties/AssemblyInfo.cs delete mode 100644 src/SMAPI.AssemblyRewriters/SpriteBatchMethods.cs delete mode 100644 src/SMAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj delete mode 100644 src/SMAPI.Common/EnvironmentUtility.cs delete mode 100644 src/SMAPI.Common/Models/ModInfoModel.cs delete mode 100644 src/SMAPI.Common/Models/ModSeachModel.cs delete mode 100644 src/SMAPI.Common/Platform.cs delete mode 100644 src/SMAPI.Common/SemanticVersionImpl.cs delete mode 100644 src/SMAPI.Common/StardewModdingAPI.Common.projitems delete mode 100644 src/SMAPI.Common/StardewModdingAPI.Common.shproj create mode 100644 src/SMAPI.Internal/EnvironmentUtility.cs create mode 100644 src/SMAPI.Internal/Models/ModInfoModel.cs create mode 100644 src/SMAPI.Internal/Models/ModSeachModel.cs create mode 100644 src/SMAPI.Internal/Platform.cs create mode 100644 src/SMAPI.Internal/Properties/AssemblyInfo.cs create mode 100644 src/SMAPI.Internal/RewriteFacades/SpriteBatchMethods.cs create mode 100644 src/SMAPI.Internal/SemanticVersionImpl.cs create mode 100644 src/SMAPI.Internal/StardewModdingAPI.Internal.csproj (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/build/common.targets b/build/common.targets index 588eea1b..54e24c74 100644 --- a/build/common.targets +++ b/build/common.targets @@ -98,7 +98,7 @@ - + diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index 8d10fc2e..8410f60e 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -29,7 +29,7 @@ - + @@ -43,7 +43,7 @@ - + diff --git a/docs/technical-docs.md b/docs/technical-docs.md index 52c3f96d..a988eefc 100644 --- a/docs/technical-docs.md +++ b/docs/technical-docs.md @@ -78,8 +78,9 @@ on the wiki for the first-time setup. Mono.Cecil.dll Newtonsoft.Json.dll StardewModdingAPI - StardewModdingAPI.AssemblyRewriters.dll StardewModdingAPI.config.json + StardewModdingAPI.Internal.dll + StardewModdingAPI.metadata.json StardewModdingAPI.exe StardewModdingAPI.pdb StardewModdingAPI.xml @@ -91,8 +92,9 @@ on the wiki for the first-time setup. Mods/* Mono.Cecil.dll Newtonsoft.Json.dll - StardewModdingAPI.AssemblyRewriters.dll StardewModdingAPI.config.json + StardewModdingAPI.Internal.dll + StardewModdingAPI.metadata.json StardewModdingAPI.exe StardewModdingAPI.pdb StardewModdingAPI.xml diff --git a/src/SMAPI.AssemblyRewriters/Properties/AssemblyInfo.cs b/src/SMAPI.AssemblyRewriters/Properties/AssemblyInfo.cs deleted file mode 100644 index f456a30d..00000000 --- a/src/SMAPI.AssemblyRewriters/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,4 +0,0 @@ -using System.Reflection; - -[assembly: AssemblyTitle("SMAPI.AssemblyRewriters")] -[assembly: AssemblyDescription("Contains internal SMAPI classes used during assembly rewriting that need to be public for technical reasons, but shouldn't be visible to modders.")] diff --git a/src/SMAPI.AssemblyRewriters/SpriteBatchMethods.cs b/src/SMAPI.AssemblyRewriters/SpriteBatchMethods.cs deleted file mode 100644 index a7f100f2..00000000 --- a/src/SMAPI.AssemblyRewriters/SpriteBatchMethods.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; - -namespace StardewModdingAPI.AssemblyRewriters -{ - /// Provides method signatures that can be injected into mod code for compatibility between Linux/Mac or Windows. - public class SpriteBatchMethods : SpriteBatch - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public SpriteBatchMethods(GraphicsDevice graphicsDevice) : base(graphicsDevice) { } - - - /**** - ** MonoGame signatures - ****/ - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Linux/Mac.")] - public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix? matrix) - { - base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, matrix ?? Matrix.Identity); - } - - /**** - ** XNA signatures - ****/ - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] - public new void Begin() - { - base.Begin(); - } - - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] - public new void Begin(SpriteSortMode sortMode, BlendState blendState) - { - base.Begin(sortMode, blendState); - } - - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] - public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState) - { - base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState); - } - - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] - public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect) - { - base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect); - } - - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] - public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix transformMatrix) - { - base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, transformMatrix); - } - } -} diff --git a/src/SMAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj b/src/SMAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj deleted file mode 100644 index 651b822d..00000000 --- a/src/SMAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj +++ /dev/null @@ -1,44 +0,0 @@ - - - - - Debug - x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49} - Library - Properties - StardewModdingAPI.AssemblyRewriters - StardewModdingAPI.AssemblyRewriters - v4.5 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - Properties\GlobalAssemblyInfo.cs - - - - - - - \ No newline at end of file diff --git a/src/SMAPI.Common/EnvironmentUtility.cs b/src/SMAPI.Common/EnvironmentUtility.cs deleted file mode 100644 index 9d9e91e6..00000000 --- a/src/SMAPI.Common/EnvironmentUtility.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -#if SMAPI_FOR_WINDOWS -using System.Management; -#endif -using System.Runtime.InteropServices; - -namespace StardewModdingAPI.Common -{ - /// Provides methods for fetching environment information. - internal static class EnvironmentUtility - { - /********* - ** Properties - *********/ - /// Get the OS name from the system uname command. - /// The buffer to fill with the resulting string. - [DllImport("libc")] - static extern int uname(IntPtr buffer); - - - /********* - ** Public methods - *********/ - /// Detect the current OS. - public static Platform DetectPlatform() - { - switch (Environment.OSVersion.Platform) - { - case PlatformID.MacOSX: - return Platform.Mac; - - case PlatformID.Unix: - return EnvironmentUtility.IsRunningMac() - ? Platform.Mac - : Platform.Linux; - - default: - return Platform.Windows; - } - } - - - /// Get the human-readable OS name and version. - /// The current platform. - [SuppressMessage("ReSharper", "EmptyGeneralCatchClause", Justification = "Error suppressed deliberately to fallback to default behaviour.")] - public static string GetFriendlyPlatformName(Platform platform) - { -#if SMAPI_FOR_WINDOWS - try - { - return new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem") - .Get() - .Cast() - .Select(entry => entry.GetPropertyValue("Caption").ToString()) - .FirstOrDefault(); - } - catch { } -#endif - return (platform == Platform.Mac ? "MacOS " : "") + Environment.OSVersion; - } - - /// Get the name of the Stardew Valley executable. - /// The current platform. - public static string GetExecutableName(Platform platform) - { - return platform == Platform.Windows - ? "Stardew Valley.exe" - : "StardewValley.exe"; - } - - /// Get whether the platform uses Mono. - /// The current platform. - public static bool IsMono(this Platform platform) - { - return platform == Platform.Linux || platform == Platform.Mac; - } - - /********* - ** Private methods - *********/ - /// Detect whether the code is running on Mac. - /// - /// This code is derived from the Mono project (see System.Windows.Forms/System.Windows.Forms/XplatUI.cs). It detects Mac by calling the - /// uname system command and checking the response, which is always 'Darwin' for MacOS. - /// - private static bool IsRunningMac() - { - IntPtr buffer = IntPtr.Zero; - try - { - buffer = Marshal.AllocHGlobal(8192); - if (uname(buffer) == 0) - { - string os = Marshal.PtrToStringAnsi(buffer); - return os == "Darwin"; - } - return false; - } - catch - { - return false; // default to Linux - } - finally - { - if (buffer != IntPtr.Zero) - Marshal.FreeHGlobal(buffer); - } - } - } -} diff --git a/src/SMAPI.Common/Models/ModInfoModel.cs b/src/SMAPI.Common/Models/ModInfoModel.cs deleted file mode 100644 index 48df235a..00000000 --- a/src/SMAPI.Common/Models/ModInfoModel.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace StardewModdingAPI.Common.Models -{ - /// Generic metadata about a mod. - internal class ModInfoModel - { - /********* - ** Accessors - *********/ - /// The mod name. - public string Name { get; set; } - - /// The semantic version for the mod's latest release. - public string Version { get; set; } - - /// The semantic version for the mod's latest preview release, if available and different from . - public string PreviewVersion { get; set; } - - /// The mod's web URL. - public string Url { get; set; } - - /// The error message indicating why the mod is invalid (if applicable). - public string Error { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an empty instance. - public ModInfoModel() - { - // needed for JSON deserialising - } - - /// Construct an instance. - /// The mod name. - /// The semantic version for the mod's latest release. - /// The semantic version for the mod's latest preview release, if available and different from . - /// The mod's web URL. - /// The error message indicating why the mod is invalid (if applicable). - public ModInfoModel(string name, string version, string url, string previewVersion = null, string error = null) - { - this.Name = name; - this.Version = version; - this.PreviewVersion = previewVersion; - this.Url = url; - this.Error = error; // mainly initialised here for the JSON deserialiser - } - - /// Construct an instance. - /// The error message indicating why the mod is invalid. - public ModInfoModel(string error) - { - this.Error = error; - } - } -} diff --git a/src/SMAPI.Common/Models/ModSeachModel.cs b/src/SMAPI.Common/Models/ModSeachModel.cs deleted file mode 100644 index 3c33d0b6..00000000 --- a/src/SMAPI.Common/Models/ModSeachModel.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace StardewModdingAPI.Common.Models -{ - /// Specifies mods whose update-check info to fetch. - internal class ModSearchModel - { - /********* - ** Accessors - *********/ - /// The namespaced mod keys to search. - public string[] ModKeys { get; set; } - - /// Whether to allow non-semantic versions, instead of returning an error for those. - public bool AllowInvalidVersions { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an empty instance. - public ModSearchModel() - { - // needed for JSON deserialising - } - - /// Construct an instance. - /// The namespaced mod keys to search. - /// Whether to allow non-semantic versions, instead of returning an error for those. - public ModSearchModel(IEnumerable modKeys, bool allowInvalidVersions) - { - this.ModKeys = modKeys.ToArray(); - this.AllowInvalidVersions = allowInvalidVersions; - } - } -} diff --git a/src/SMAPI.Common/Platform.cs b/src/SMAPI.Common/Platform.cs deleted file mode 100644 index 08b4545f..00000000 --- a/src/SMAPI.Common/Platform.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace StardewModdingAPI.Common -{ - /// The game's platform version. - internal enum Platform - { - /// The Linux version of the game. - Linux, - - /// The Mac version of the game. - Mac, - - /// The Windows version of the game. - Windows - } -} diff --git a/src/SMAPI.Common/SemanticVersionImpl.cs b/src/SMAPI.Common/SemanticVersionImpl.cs deleted file mode 100644 index 084f56a3..00000000 --- a/src/SMAPI.Common/SemanticVersionImpl.cs +++ /dev/null @@ -1,199 +0,0 @@ -using System; -using System.Text.RegularExpressions; - -namespace StardewModdingAPI.Common -{ - /// A low-level implementation of a semantic version with an optional release tag. - /// The implementation is defined by Semantic Version 2.0 (http://semver.org/). - internal class SemanticVersionImpl - { - /********* - ** Accessors - *********/ - /// The major version incremented for major API changes. - public int Major { get; } - - /// The minor version incremented for backwards-compatible changes. - public int Minor { get; } - - /// The patch version for backwards-compatible bug fixes. - public int Patch { get; } - - /// An optional prerelease tag. - public string Tag { get; } - - /// A regex pattern matching a version within a larger string. - internal const string UnboundedVersionPattern = @"(?>(?0|[1-9]\d*))\.(?>(?0|[1-9]\d*))(?>(?:\.(?0|[1-9]\d*))?)(?:-(?(?>[a-z0-9]+[\-\.]?)+))?"; - - /// A regular expression matching a semantic version string. - /// - /// This pattern is derived from the BNF documentation in the semver repo, - /// with three important deviations intended to support Stardew Valley mod conventions: - /// - allows short-form "x.y" versions; - /// - allows hyphens in prerelease tags as synonyms for dots (like "-unofficial-update.3"); - /// - doesn't allow '+build' suffixes. - /// - internal static readonly Regex Regex = new Regex($@"^{SemanticVersionImpl.UnboundedVersionPattern}$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture); - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The major version incremented for major API changes. - /// The minor version incremented for backwards-compatible changes. - /// The patch version for backwards-compatible bug fixes. - /// An optional prerelease tag. - public SemanticVersionImpl(int major, int minor, int patch, string tag = null) - { - this.Major = major; - this.Minor = minor; - this.Patch = patch; - this.Tag = this.GetNormalisedTag(tag); - } - - /// Construct an instance. - /// The assembly version. - /// The is null. - public SemanticVersionImpl(Version version) - { - if (version == null) - throw new ArgumentNullException(nameof(version), "The input version can't be null."); - - this.Major = version.Major; - this.Minor = version.Minor; - this.Patch = version.Build; - } - - /// Construct an instance. - /// The semantic version string. - /// The is null. - /// The is not a valid semantic version. - public SemanticVersionImpl(string version) - { - // parse - if (version == null) - throw new ArgumentNullException(nameof(version), "The input version string can't be null."); - var match = SemanticVersionImpl.Regex.Match(version.Trim()); - if (!match.Success) - throw new FormatException($"The input '{version}' isn't a valid semantic version."); - - // initialise - this.Major = int.Parse(match.Groups["major"].Value); - this.Minor = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0; - this.Patch = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0; - this.Tag = match.Groups["prerelease"].Success ? this.GetNormalisedTag(match.Groups["prerelease"].Value) : null; - } - - /// 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. - /// The value is null. - public int CompareTo(SemanticVersionImpl other) - { - if (other == null) - throw new ArgumentNullException(nameof(other)); - return this.CompareTo(other.Major, other.Minor, other.Patch, other.Tag); - } - - - /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. - /// The major version to compare with this instance. - /// The minor version to compare with this instance. - /// The patch version to compare with this instance. - /// The prerelease tag to compare with this instance. - public int CompareTo(int otherMajor, int otherMinor, int otherPatch, string otherTag) - { - const int same = 0; - const int curNewer = 1; - const int curOlder = -1; - - // compare stable versions - if (this.Major != otherMajor) - return this.Major.CompareTo(otherMajor); - if (this.Minor != otherMinor) - return this.Minor.CompareTo(otherMinor); - if (this.Patch != otherPatch) - return this.Patch.CompareTo(otherPatch); - if (this.Tag == otherTag) - return same; - - // stable supercedes pre-release - bool curIsStable = string.IsNullOrWhiteSpace(this.Tag); - bool otherIsStable = string.IsNullOrWhiteSpace(otherTag); - if (curIsStable) - return curNewer; - if (otherIsStable) - return curOlder; - - // compare two pre-release tag values - string[] curParts = this.Tag.Split('.', '-'); - string[] otherParts = otherTag.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 - { - if (int.TryParse(curParts[i], out int curNum) && int.TryParse(otherParts[i], out int 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(), new SemanticVersionImpl(otherMajor, otherMinor, otherPatch, otherTag).ToString(), StringComparison.InvariantCultureIgnoreCase); - } - - /// Get a string representation of the version. - public override string ToString() - { - // version - string result = this.Patch != 0 - ? $"{this.Major}.{this.Minor}.{this.Patch}" - : $"{this.Major}.{this.Minor}"; - - // tag - string tag = this.Tag; - if (tag != null) - result += $"-{tag}"; - return result; - } - - /// Parse a version string without throwing an exception if it fails. - /// The version string. - /// The parsed representation. - /// Returns whether parsing the version succeeded. - internal static bool TryParse(string version, out SemanticVersionImpl parsed) - { - try - { - parsed = new SemanticVersionImpl(version); - return true; - } - catch - { - parsed = null; - return false; - } - } - - - /********* - ** Private methods - *********/ - /// Get a normalised build tag. - /// The tag to normalise. - private string GetNormalisedTag(string tag) - { - tag = tag?.Trim(); - return !string.IsNullOrWhiteSpace(tag) ? tag : null; - } - } -} diff --git a/src/SMAPI.Common/StardewModdingAPI.Common.projitems b/src/SMAPI.Common/StardewModdingAPI.Common.projitems deleted file mode 100644 index 0b89f092..00000000 --- a/src/SMAPI.Common/StardewModdingAPI.Common.projitems +++ /dev/null @@ -1,21 +0,0 @@ - - - - $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - true - 2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc - - - StardewModdingAPI.Common - - - - - - - - - - - - \ No newline at end of file diff --git a/src/SMAPI.Common/StardewModdingAPI.Common.shproj b/src/SMAPI.Common/StardewModdingAPI.Common.shproj deleted file mode 100644 index 0ef29144..00000000 --- a/src/SMAPI.Common/StardewModdingAPI.Common.shproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - 2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc - 14.0 - - - - - - - - diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 0d602b57..c0bc8f2c 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -7,7 +7,7 @@ using System.Reflection; using System.Threading; using Microsoft.Win32; using StardewModdingApi.Installer.Enums; -using StardewModdingAPI.Common; +using StardewModdingAPI.Internal; namespace StardewModdingApi.Installer { @@ -83,7 +83,7 @@ namespace StardewModdingApi.Installer yield return GetInstallPath("StardewModdingAPI.exe"); yield return GetInstallPath("StardewModdingAPI.config.json"); yield return GetInstallPath("StardewModdingAPI.data.json"); - yield return GetInstallPath("StardewModdingAPI.AssemblyRewriters.dll"); + yield return GetInstallPath("StardewModdingAPI.Internal.dll"); yield return GetInstallPath("System.ValueTuple.dll"); yield return GetInstallPath("steam_appid.txt"); @@ -102,6 +102,7 @@ namespace StardewModdingApi.Installer yield return GetInstallPath(Path.Combine("Mods", "TrainerMod")); // *–2.0 (renamed to ConsoleCommands) yield return GetInstallPath("Mono.Cecil.Rocks.dll"); // 1.3–1.8 yield return GetInstallPath("StardewModdingAPI-settings.json"); // 1.0-1.4 + yield return GetInstallPath("StardewModdingAPI.AssemblyRewriters.dll"); // 1.3-2.5.5 if (modsDir.Exists) { foreach (DirectoryInfo modDir in modsDir.EnumerateDirectories()) diff --git a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj index 7a71bef9..4f849b9b 100644 --- a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj +++ b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj @@ -9,7 +9,7 @@ Properties StardewModdingAPI.Installer StardewModdingAPI.Installer - v4.0 + v4.5 512 true @@ -57,7 +57,12 @@ PreserveNewest - + + + {10db0676-9fc1-4771-a2c8-e2519f091e49} + StardewModdingAPI.Internal + + diff --git a/src/SMAPI.Internal/EnvironmentUtility.cs b/src/SMAPI.Internal/EnvironmentUtility.cs new file mode 100644 index 00000000..a3581898 --- /dev/null +++ b/src/SMAPI.Internal/EnvironmentUtility.cs @@ -0,0 +1,112 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +#if SMAPI_FOR_WINDOWS +using System.Management; +#endif +using System.Runtime.InteropServices; + +namespace StardewModdingAPI.Internal +{ + /// Provides methods for fetching environment information. + internal static class EnvironmentUtility + { + /********* + ** Properties + *********/ + /// Get the OS name from the system uname command. + /// The buffer to fill with the resulting string. + [DllImport("libc")] + static extern int uname(IntPtr buffer); + + + /********* + ** Public methods + *********/ + /// Detect the current OS. + public static Platform DetectPlatform() + { + switch (Environment.OSVersion.Platform) + { + case PlatformID.MacOSX: + return Platform.Mac; + + case PlatformID.Unix: + return EnvironmentUtility.IsRunningMac() + ? Platform.Mac + : Platform.Linux; + + default: + return Platform.Windows; + } + } + + + /// Get the human-readable OS name and version. + /// The current platform. + [SuppressMessage("ReSharper", "EmptyGeneralCatchClause", Justification = "Error suppressed deliberately to fallback to default behaviour.")] + public static string GetFriendlyPlatformName(Platform platform) + { +#if SMAPI_FOR_WINDOWS + try + { + return new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem") + .Get() + .Cast() + .Select(entry => entry.GetPropertyValue("Caption").ToString()) + .FirstOrDefault(); + } + catch { } +#endif + return (platform == Platform.Mac ? "MacOS " : "") + Environment.OSVersion; + } + + /// Get the name of the Stardew Valley executable. + /// The current platform. + public static string GetExecutableName(Platform platform) + { + return platform == Platform.Windows + ? "Stardew Valley.exe" + : "StardewValley.exe"; + } + + /// Get whether the platform uses Mono. + /// The current platform. + public static bool IsMono(this Platform platform) + { + return platform == Platform.Linux || platform == Platform.Mac; + } + + /********* + ** Private methods + *********/ + /// Detect whether the code is running on Mac. + /// + /// This code is derived from the Mono project (see System.Windows.Forms/System.Windows.Forms/XplatUI.cs). It detects Mac by calling the + /// uname system command and checking the response, which is always 'Darwin' for MacOS. + /// + private static bool IsRunningMac() + { + IntPtr buffer = IntPtr.Zero; + try + { + buffer = Marshal.AllocHGlobal(8192); + if (EnvironmentUtility.uname(buffer) == 0) + { + string os = Marshal.PtrToStringAnsi(buffer); + return os == "Darwin"; + } + return false; + } + catch + { + return false; // default to Linux + } + finally + { + if (buffer != IntPtr.Zero) + Marshal.FreeHGlobal(buffer); + } + } + } +} diff --git a/src/SMAPI.Internal/Models/ModInfoModel.cs b/src/SMAPI.Internal/Models/ModInfoModel.cs new file mode 100644 index 00000000..725c88bb --- /dev/null +++ b/src/SMAPI.Internal/Models/ModInfoModel.cs @@ -0,0 +1,56 @@ +namespace StardewModdingAPI.Internal.Models +{ + /// Generic metadata about a mod. + internal class ModInfoModel + { + /********* + ** Accessors + *********/ + /// The mod name. + public string Name { get; set; } + + /// The semantic version for the mod's latest release. + public string Version { get; set; } + + /// The semantic version for the mod's latest preview release, if available and different from . + public string PreviewVersion { get; set; } + + /// The mod's web URL. + public string Url { get; set; } + + /// The error message indicating why the mod is invalid (if applicable). + public string Error { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public ModInfoModel() + { + // needed for JSON deserialising + } + + /// Construct an instance. + /// The mod name. + /// The semantic version for the mod's latest release. + /// The semantic version for the mod's latest preview release, if available and different from . + /// The mod's web URL. + /// The error message indicating why the mod is invalid (if applicable). + public ModInfoModel(string name, string version, string url, string previewVersion = null, string error = null) + { + this.Name = name; + this.Version = version; + this.PreviewVersion = previewVersion; + this.Url = url; + this.Error = error; // mainly initialised here for the JSON deserialiser + } + + /// Construct an instance. + /// The error message indicating why the mod is invalid. + public ModInfoModel(string error) + { + this.Error = error; + } + } +} diff --git a/src/SMAPI.Internal/Models/ModSeachModel.cs b/src/SMAPI.Internal/Models/ModSeachModel.cs new file mode 100644 index 00000000..fac72135 --- /dev/null +++ b/src/SMAPI.Internal/Models/ModSeachModel.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Internal.Models +{ + /// Specifies mods whose update-check info to fetch. + internal class ModSearchModel + { + /********* + ** Accessors + *********/ + /// The namespaced mod keys to search. + public string[] ModKeys { get; set; } + + /// Whether to allow non-semantic versions, instead of returning an error for those. + public bool AllowInvalidVersions { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public ModSearchModel() + { + // needed for JSON deserialising + } + + /// Construct an instance. + /// The namespaced mod keys to search. + /// Whether to allow non-semantic versions, instead of returning an error for those. + public ModSearchModel(IEnumerable modKeys, bool allowInvalidVersions) + { + this.ModKeys = modKeys.ToArray(); + this.AllowInvalidVersions = allowInvalidVersions; + } + } +} diff --git a/src/SMAPI.Internal/Platform.cs b/src/SMAPI.Internal/Platform.cs new file mode 100644 index 00000000..81ca5c1f --- /dev/null +++ b/src/SMAPI.Internal/Platform.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Internal +{ + /// The game's platform version. + internal enum Platform + { + /// The Linux version of the game. + Linux, + + /// The Mac version of the game. + Mac, + + /// The Windows version of the game. + Windows + } +} diff --git a/src/SMAPI.Internal/Properties/AssemblyInfo.cs b/src/SMAPI.Internal/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..b314b353 --- /dev/null +++ b/src/SMAPI.Internal/Properties/AssemblyInfo.cs @@ -0,0 +1,9 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +[assembly: AssemblyTitle("SMAPI.Internal")] +[assembly: AssemblyDescription("Contains internal SMAPI code that's shared between its projects.")] +[assembly: InternalsVisibleTo("StardewModdingAPI")] +[assembly: InternalsVisibleTo("StardewModdingAPI.ModBuildConfig")] +[assembly: InternalsVisibleTo("StardewModdingAPI.Installer")] +[assembly: InternalsVisibleTo("StardewModdingAPI.Web")] diff --git a/src/SMAPI.Internal/RewriteFacades/SpriteBatchMethods.cs b/src/SMAPI.Internal/RewriteFacades/SpriteBatchMethods.cs new file mode 100644 index 00000000..5e5d117e --- /dev/null +++ b/src/SMAPI.Internal/RewriteFacades/SpriteBatchMethods.cs @@ -0,0 +1,59 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace StardewModdingAPI.Internal.RewriteFacades +{ + /// Provides method signatures that can be injected into mod code for compatibility between Linux/Mac or Windows. + public class SpriteBatchMethods : SpriteBatch + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public SpriteBatchMethods(GraphicsDevice graphicsDevice) : base(graphicsDevice) { } + + + /**** + ** MonoGame signatures + ****/ + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Linux/Mac.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix? matrix) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, matrix ?? Matrix.Identity); + } + + /**** + ** XNA signatures + ****/ + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin() + { + base.Begin(); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState) + { + base.Begin(sortMode, blendState); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix transformMatrix) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, transformMatrix); + } + } +} diff --git a/src/SMAPI.Internal/SemanticVersionImpl.cs b/src/SMAPI.Internal/SemanticVersionImpl.cs new file mode 100644 index 00000000..6da16336 --- /dev/null +++ b/src/SMAPI.Internal/SemanticVersionImpl.cs @@ -0,0 +1,199 @@ +using System; +using System.Text.RegularExpressions; + +namespace StardewModdingAPI.Internal +{ + /// A low-level implementation of a semantic version with an optional release tag. + /// The implementation is defined by Semantic Version 2.0 (http://semver.org/). + internal class SemanticVersionImpl + { + /********* + ** Accessors + *********/ + /// The major version incremented for major API changes. + public int Major { get; } + + /// The minor version incremented for backwards-compatible changes. + public int Minor { get; } + + /// The patch version for backwards-compatible bug fixes. + public int Patch { get; } + + /// An optional prerelease tag. + public string Tag { get; } + + /// A regex pattern matching a version within a larger string. + internal const string UnboundedVersionPattern = @"(?>(?0|[1-9]\d*))\.(?>(?0|[1-9]\d*))(?>(?:\.(?0|[1-9]\d*))?)(?:-(?(?>[a-z0-9]+[\-\.]?)+))?"; + + /// A regular expression matching a semantic version string. + /// + /// This pattern is derived from the BNF documentation in the semver repo, + /// with three important deviations intended to support Stardew Valley mod conventions: + /// - allows short-form "x.y" versions; + /// - allows hyphens in prerelease tags as synonyms for dots (like "-unofficial-update.3"); + /// - doesn't allow '+build' suffixes. + /// + internal static readonly Regex Regex = new Regex($@"^{SemanticVersionImpl.UnboundedVersionPattern}$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture); + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The major version incremented for major API changes. + /// The minor version incremented for backwards-compatible changes. + /// The patch version for backwards-compatible bug fixes. + /// An optional prerelease tag. + public SemanticVersionImpl(int major, int minor, int patch, string tag = null) + { + this.Major = major; + this.Minor = minor; + this.Patch = patch; + this.Tag = this.GetNormalisedTag(tag); + } + + /// Construct an instance. + /// The assembly version. + /// The is null. + public SemanticVersionImpl(Version version) + { + if (version == null) + throw new ArgumentNullException(nameof(version), "The input version can't be null."); + + this.Major = version.Major; + this.Minor = version.Minor; + this.Patch = version.Build; + } + + /// Construct an instance. + /// The semantic version string. + /// The is null. + /// The is not a valid semantic version. + public SemanticVersionImpl(string version) + { + // parse + if (version == null) + throw new ArgumentNullException(nameof(version), "The input version string can't be null."); + var match = SemanticVersionImpl.Regex.Match(version.Trim()); + if (!match.Success) + throw new FormatException($"The input '{version}' isn't a valid semantic version."); + + // initialise + this.Major = int.Parse(match.Groups["major"].Value); + this.Minor = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0; + this.Patch = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0; + this.Tag = match.Groups["prerelease"].Success ? this.GetNormalisedTag(match.Groups["prerelease"].Value) : null; + } + + /// 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. + /// The value is null. + public int CompareTo(SemanticVersionImpl other) + { + if (other == null) + throw new ArgumentNullException(nameof(other)); + return this.CompareTo(other.Major, other.Minor, other.Patch, other.Tag); + } + + + /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. + /// The major version to compare with this instance. + /// The minor version to compare with this instance. + /// The patch version to compare with this instance. + /// The prerelease tag to compare with this instance. + public int CompareTo(int otherMajor, int otherMinor, int otherPatch, string otherTag) + { + const int same = 0; + const int curNewer = 1; + const int curOlder = -1; + + // compare stable versions + if (this.Major != otherMajor) + return this.Major.CompareTo(otherMajor); + if (this.Minor != otherMinor) + return this.Minor.CompareTo(otherMinor); + if (this.Patch != otherPatch) + return this.Patch.CompareTo(otherPatch); + if (this.Tag == otherTag) + return same; + + // stable supercedes pre-release + bool curIsStable = string.IsNullOrWhiteSpace(this.Tag); + bool otherIsStable = string.IsNullOrWhiteSpace(otherTag); + if (curIsStable) + return curNewer; + if (otherIsStable) + return curOlder; + + // compare two pre-release tag values + string[] curParts = this.Tag.Split('.', '-'); + string[] otherParts = otherTag.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 + { + if (int.TryParse(curParts[i], out int curNum) && int.TryParse(otherParts[i], out int 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(), new SemanticVersionImpl(otherMajor, otherMinor, otherPatch, otherTag).ToString(), StringComparison.InvariantCultureIgnoreCase); + } + + /// Get a string representation of the version. + public override string ToString() + { + // version + string result = this.Patch != 0 + ? $"{this.Major}.{this.Minor}.{this.Patch}" + : $"{this.Major}.{this.Minor}"; + + // tag + string tag = this.Tag; + if (tag != null) + result += $"-{tag}"; + return result; + } + + /// Parse a version string without throwing an exception if it fails. + /// The version string. + /// The parsed representation. + /// Returns whether parsing the version succeeded. + internal static bool TryParse(string version, out SemanticVersionImpl parsed) + { + try + { + parsed = new SemanticVersionImpl(version); + return true; + } + catch + { + parsed = null; + return false; + } + } + + + /********* + ** Private methods + *********/ + /// Get a normalised build tag. + /// The tag to normalise. + private string GetNormalisedTag(string tag) + { + tag = tag?.Trim(); + return !string.IsNullOrWhiteSpace(tag) ? tag : null; + } + } +} diff --git a/src/SMAPI.Internal/StardewModdingAPI.Internal.csproj b/src/SMAPI.Internal/StardewModdingAPI.Internal.csproj new file mode 100644 index 00000000..6e7fa368 --- /dev/null +++ b/src/SMAPI.Internal/StardewModdingAPI.Internal.csproj @@ -0,0 +1,49 @@ + + + + + Debug + x86 + {10DB0676-9FC1-4771-A2C8-E2519F091E49} + Library + Properties + StardewModdingAPI.Internal + StardewModdingAPI.Internal + v4.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + Properties\GlobalAssemblyInfo.cs + + + + + + + + + + + + \ No newline at end of file diff --git a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs index 64262dc2..ba2e671d 100644 --- a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs +++ b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Web.Script.Serialization; -using StardewModdingAPI.Common; +using StardewModdingAPI.Internal; namespace StardewModdingAPI.ModBuildConfig.Framework { diff --git a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj index 2e3ba356..02564409 100644 --- a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj +++ b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj @@ -55,7 +55,12 @@ - + + + {10db0676-9fc1-4771-a2c8-e2519f091e49} + StardewModdingAPI.Internal + + \ No newline at end of file diff --git a/src/SMAPI.Web/Controllers/IndexController.cs b/src/SMAPI.Web/Controllers/IndexController.cs index 0464e50a..92b4f2c0 100644 --- a/src/SMAPI.Web/Controllers/IndexController.cs +++ b/src/SMAPI.Web/Controllers/IndexController.cs @@ -3,7 +3,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; -using StardewModdingAPI.Common; +using StardewModdingAPI.Internal; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.ViewModels; diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 24517263..fc90d067 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; -using StardewModdingAPI.Common.Models; +using StardewModdingAPI.Internal.Models; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.Framework.Clients.Nexus; diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs index f49fb05c..b5603bd9 100644 --- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs +++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; -using StardewModdingAPI.Common; +using StardewModdingAPI.Internal; using StardewModdingAPI.Web.Framework.LogParsing.Models; namespace StardewModdingAPI.Web.Framework.LogParsing diff --git a/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs index edb00454..bd27d624 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs @@ -1,6 +1,6 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; -using StardewModdingAPI.Common.Models; +using StardewModdingAPI.Internal.Models; namespace StardewModdingAPI.Web.Framework.ModRepositories { diff --git a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs index 3e5a4272..2782e2b9 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using StardewModdingAPI.Common.Models; +using StardewModdingAPI.Internal.Models; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; namespace StardewModdingAPI.Web.Framework.ModRepositories diff --git a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs index 59eb8cd1..b12b24e2 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using StardewModdingAPI.Common.Models; +using StardewModdingAPI.Internal.Models; using StardewModdingAPI.Web.Framework.Clients.GitHub; namespace StardewModdingAPI.Web.Framework.ModRepositories diff --git a/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs index 4496400c..79fe8f87 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using StardewModdingAPI.Common.Models; +using StardewModdingAPI.Internal.Models; namespace StardewModdingAPI.Web.Framework.ModRepositories { diff --git a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs index 6411ad4c..87a87ab7 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using StardewModdingAPI.Common.Models; +using StardewModdingAPI.Internal.Models; using StardewModdingAPI.Web.Framework.Clients.Nexus; namespace StardewModdingAPI.Web.Framework.ModRepositories diff --git a/src/SMAPI.Web/Framework/VersionConstraint.cs b/src/SMAPI.Web/Framework/VersionConstraint.cs index cffb1092..1502f5d8 100644 --- a/src/SMAPI.Web/Framework/VersionConstraint.cs +++ b/src/SMAPI.Web/Framework/VersionConstraint.cs @@ -1,5 +1,5 @@ using Microsoft.AspNetCore.Routing.Constraints; -using StardewModdingAPI.Common; +using StardewModdingAPI.Internal; namespace StardewModdingAPI.Web.Framework { diff --git a/src/SMAPI.Web/StardewModdingAPI.Web.csproj b/src/SMAPI.Web/StardewModdingAPI.Web.csproj index e2eee8a8..bc337e7e 100644 --- a/src/SMAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/SMAPI.Web/StardewModdingAPI.Web.csproj @@ -22,6 +22,8 @@ - + + + diff --git a/src/SMAPI.sln b/src/SMAPI.sln index d84ce589..ff953751 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -24,7 +24,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Installer {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} = {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.AssemblyRewriters", "SMAPI.AssemblyRewriters\StardewModdingAPI.AssemblyRewriters.csproj", "{10DB0676-9FC1-4771-A2C8-E2519F091E49}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Internal", "SMAPI.Internal\StardewModdingAPI.Internal.csproj", "{10DB0676-9FC1-4771-A2C8-E2519F091E49}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Tests", "SMAPI.Tests\StardewModdingAPI.Tests.csproj", "{36CCB19E-92EB-48C7-9615-98EEFD45109B}" EndProject @@ -32,8 +32,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Web", "SM EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Internal", "Internal", "{82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11}" EndProject -Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "StardewModdingAPI.Common", "SMAPI.Common\StardewModdingAPI.Common.shproj", "{2AA02FB6-FF03-41CF-A215-2EE60AB4F5DC}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{EB35A917-67B9-4EFA-8DFC-4FB49B3949BB}" ProjectSection(SolutionItems) = preProject ..\docs\CONTRIBUTING.md = ..\docs\CONTRIBUTING.md @@ -61,12 +59,6 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.ModBuildConfig.Analyzer.Tests", "SMAPI.ModBuildConfig.Analyzer.Tests\SMAPI.ModBuildConfig.Analyzer.Tests.csproj", "{0CF97929-B0D0-4D73-B7BF-4FF7191035F9}" EndProject Global - GlobalSection(SharedMSBuildProjectFiles) = preSolution - SMAPI.Common\StardewModdingAPI.Common.projitems*{2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc}*SharedItemsImports = 13 - SMAPI.Common\StardewModdingAPI.Common.projitems*{443ddf81-6aaf-420a-a610-3459f37e5575}*SharedItemsImports = 4 - SMAPI.Common\StardewModdingAPI.Common.projitems*{ea4f1e80-743f-4a1d-9757-ae66904a196a}*SharedItemsImports = 4 - SMAPI.Common\StardewModdingAPI.Common.projitems*{f1a573b0-f436-472c-ae29-0b91ea6b9f8f}*SharedItemsImports = 4 - EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|Mixed Platforms = Debug|Mixed Platforms @@ -177,9 +169,7 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {10DB0676-9FC1-4771-A2C8-E2519F091E49} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} {36CCB19E-92EB-48C7-9615-98EEFD45109B} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} - {2AA02FB6-FF03-41CF-A215-2EE60AB4F5DC} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} {EB35A917-67B9-4EFA-8DFC-4FB49B3949BB} = {86C452BE-D2D8-45B4-B63F-E329EB06CEDA} {09CF91E5-5BAB-4650-A200-E5EA9A633046} = {86C452BE-D2D8-45B4-B63F-E329EB06CEDA} {0CF97929-B0D0-4D73-B7BF-4FF7191035F9} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 9f2ebdb2..0116ac42 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -3,9 +3,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; -using StardewModdingAPI.Common; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Internal; using StardewValley; namespace StardewModdingAPI diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs index d95de4fe..8851fc7d 100644 --- a/src/SMAPI/Framework/Content/ContentCache.cs +++ b/src/SMAPI/Framework/Content/ContentCache.cs @@ -3,9 +3,9 @@ using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; using Microsoft.Xna.Framework; -using StardewModdingAPI.Common; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Utilities; +using StardewModdingAPI.Internal; using StardewValley; namespace StardewModdingAPI.Framework.Content diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index feaee047..6c0e1c14 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -5,8 +5,8 @@ using System.Linq; using System.Reflection; using Mono.Cecil; using Mono.Cecil.Cil; -using StardewModdingAPI.Common; using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Internal; using StardewModdingAPI.Metadata; namespace StardewModdingAPI.Framework.ModLoading diff --git a/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs b/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs index 9499b538..f0a28b4a 100644 --- a/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs +++ b/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using Mono.Cecil; -using StardewModdingAPI.Common; +using StardewModdingAPI.Internal; namespace StardewModdingAPI.Framework.ModLoading { diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index cc511ed4..73915824 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -2,9 +2,9 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; -using StardewModdingAPI.Common; using StardewModdingAPI.Framework.Logging; using StardewModdingAPI.Framework.Models; +using StardewModdingAPI.Internal; namespace StardewModdingAPI.Framework { diff --git a/src/SMAPI/Framework/WebApiClient.cs b/src/SMAPI/Framework/WebApiClient.cs index 7f0122cf..e33b2681 100644 --- a/src/SMAPI/Framework/WebApiClient.cs +++ b/src/SMAPI/Framework/WebApiClient.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Net; using Newtonsoft.Json; -using StardewModdingAPI.Common.Models; +using StardewModdingAPI.Internal.Models; namespace StardewModdingAPI.Framework { diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 0b532a18..c7abfbef 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -1,10 +1,10 @@ using System.Collections.Generic; using Microsoft.Xna.Framework.Graphics; -using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.ModLoading.Finders; using StardewModdingAPI.Framework.ModLoading.Rewriters; +using StardewModdingAPI.Internal.RewriteFacades; using StardewValley; namespace StardewModdingAPI.Metadata diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index acff0545..6b7c1ad3 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -14,8 +14,6 @@ using System.Threading; using System.Windows.Forms; #endif using Newtonsoft.Json; -using StardewModdingAPI.Common; -using StardewModdingAPI.Common.Models; using StardewModdingAPI.Events; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.Events; @@ -28,6 +26,8 @@ using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; using StardewModdingAPI.Framework.Utilities; +using StardewModdingAPI.Internal; +using StardewModdingAPI.Internal.Models; using StardewValley; using Monitor = StardewModdingAPI.Framework.Monitor; using SObject = StardewValley.Object; diff --git a/src/SMAPI/SemanticVersion.cs b/src/SMAPI/SemanticVersion.cs index 4826c947..0f2a5cb0 100644 --- a/src/SMAPI/SemanticVersion.cs +++ b/src/SMAPI/SemanticVersion.cs @@ -1,6 +1,6 @@ using System; using Newtonsoft.Json; -using StardewModdingAPI.Common; +using StardewModdingAPI.Internal; namespace StardewModdingAPI { diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index e0125c9b..a06056f9 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -302,11 +302,10 @@ false - - + {10db0676-9fc1-4771-a2c8-e2519f091e49} - StardewModdingAPI.AssemblyRewriters + StardewModdingAPI.Internal -- cgit From b1a24452eef782332d699ef8193c01e0da8ffa01 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 1 May 2018 19:15:56 -0400 Subject: add public platform constant for mods --- docs/release-notes.md | 1 + src/SMAPI/Constants.cs | 5 ++++- src/SMAPI/Framework/Content/ContentCache.cs | 2 +- src/SMAPI/Framework/Monitor.cs | 2 +- src/SMAPI/GamePlatform.cs | 17 +++++++++++++++++ src/SMAPI/Program.cs | 4 ++-- src/SMAPI/StardewModdingAPI.csproj | 1 + 7 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 src/SMAPI/GamePlatform.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/docs/release-notes.md b/docs/release-notes.md index 8064a34e..cde9cbc2 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,6 +11,7 @@ * For modders: * Added code analysis to mod build config package to flag common issues as warnings. * Added `Context.IsMultiplayer` and `Context.IsMainPlayer` flags. + * Added `Constants.TargetPlatform` which says whether the game is running on Linux, Mac, or Windows. * Fixed assets loaded by temporary content managers not being editable by mods. * Fixed assets not reloaded consistently when the player switches language. * Fixed console command input not saved to the log. diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 0116ac42..2ef26704 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -46,6 +46,9 @@ namespace StardewModdingAPI /// The maximum supported version of Stardew Valley. public static ISemanticVersion MaximumGameVersion { get; } = null; + /// The target game platform. + public static GamePlatform TargetPlatform => (GamePlatform)Constants.Platform; + /// The path to the game folder. public static string ExecutionPath { get; } = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); @@ -92,7 +95,7 @@ namespace StardewModdingAPI internal static ISemanticVersion GameVersion { get; } = new GameVersion(Constants.GetGameVersion()); /// The target game platform. - internal static Platform TargetPlatform { get; } = EnvironmentUtility.DetectPlatform(); + internal static Platform Platform { get; } = EnvironmentUtility.DetectPlatform(); /********* diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs index 8851fc7d..c2818cdd 100644 --- a/src/SMAPI/Framework/Content/ContentCache.cs +++ b/src/SMAPI/Framework/Content/ContentCache.cs @@ -53,7 +53,7 @@ namespace StardewModdingAPI.Framework.Content this.Cache = reflection.GetField>(contentManager, "loadedAssets").GetValue(); // get key normalisation logic - if (Constants.TargetPlatform == Platform.Windows) + if (Constants.Platform == Platform.Windows) { IReflectedMethod method = reflection.GetMethod(typeof(TitleContainer), "GetCleanPath"); this.NormaliseAssetNameForPlatform = path => method.Invoke(path); diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index 73915824..8df2e59b 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -167,7 +167,7 @@ namespace StardewModdingAPI.Framework // auto detect color scheme if (colorScheme == MonitorColorScheme.AutoDetect) { - if (Constants.TargetPlatform == Platform.Mac) + if (Constants.Platform == Platform.Mac) colorScheme = MonitorColorScheme.LightBackground; // MacOS doesn't provide console background color info, but it's usually white. else colorScheme = Monitor.IsDark(Console.BackgroundColor) ? MonitorColorScheme.DarkBackground : MonitorColorScheme.LightBackground; diff --git a/src/SMAPI/GamePlatform.cs b/src/SMAPI/GamePlatform.cs new file mode 100644 index 00000000..3bd74462 --- /dev/null +++ b/src/SMAPI/GamePlatform.cs @@ -0,0 +1,17 @@ +using StardewModdingAPI.Internal; + +namespace StardewModdingAPI +{ + /// The game's platform version. + public enum GamePlatform + { + /// The Linux version of the game. + Linux = Platform.Linux, + + /// The Mac version of the game. + Mac = Platform.Mac, + + /// The Windows version of the game. + Windows = Platform.Windows + } +} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 6b7c1ad3..ba3e238d 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -172,7 +172,7 @@ namespace StardewModdingAPI try { // init logging - this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {EnvironmentUtility.GetFriendlyPlatformName(Constants.TargetPlatform)}", LogLevel.Info); + this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info); this.Monitor.Log($"Mods go here: {Constants.ModPath}"); this.Monitor.Log($"Log started at {DateTime.UtcNow:s} UTC", LogLevel.Trace); @@ -741,7 +741,7 @@ namespace StardewModdingAPI ); // get assembly loaders - AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor, this.Settings.DeveloperMode); + AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor, this.Settings.DeveloperMode); AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); InterfaceProxyFactory proxyFactory = new InterfaceProxyFactory(); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index a06056f9..31bfd6fd 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -262,6 +262,7 @@ + -- cgit From b3e8f957e27839a1392b63689e7daf03862540c4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 2 May 2018 21:04:46 -0400 Subject: reorganise to avoid errors deploying web app, fix WMI error in Linux installer --- build/build.targets | 92 ++++++++++++++++ build/common.targets | 122 --------------------- build/constants.targets | 25 +++++ .../StardewModdingAPI.Installer.csproj | 3 +- .../RewriteFacades/SpriteBatchMethods.cs | 59 ---------- .../StardewModdingAPI.Internal.csproj | 5 +- .../StardewModdingAPI.ModBuildConfig.csproj | 1 + .../StardewModdingAPI.Mods.ConsoleCommands.csproj | 3 +- src/SMAPI.Tests/StardewModdingAPI.Tests.csproj | 3 +- src/SMAPI.sln | 3 +- .../Framework/RewriteFacades/SpriteBatchMethods.cs | 60 ++++++++++ src/SMAPI/Metadata/InstructionMetadata.cs | 2 +- src/SMAPI/StardewModdingAPI.csproj | 4 +- 13 files changed, 193 insertions(+), 189 deletions(-) create mode 100644 build/build.targets delete mode 100644 build/common.targets create mode 100644 build/constants.targets delete mode 100644 src/SMAPI.Internal/RewriteFacades/SpriteBatchMethods.cs create mode 100644 src/SMAPI/Framework/RewriteFacades/SpriteBatchMethods.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/build/build.targets b/build/build.targets new file mode 100644 index 00000000..270fd95d --- /dev/null +++ b/build/build.targets @@ -0,0 +1,92 @@ + + + + + + + + False + + + False + + + False + + + False + + + + + $(GamePath)\Netcode.dll + False + + + $(GamePath)\Stardew Valley.exe + False + + + $(GamePath)\xTile.dll + False + False + + + + + + + + $(GamePath)\MonoGame.Framework.dll + False + False + + + + + $(GamePath)\StardewValley.exe + False + + + $(GamePath)\xTile.dll + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Program + $(GamePath)\StardewModdingAPI.exe + $(GamePath) + + + + + diff --git a/build/common.targets b/build/common.targets deleted file mode 100644 index 54e24c74..00000000 --- a/build/common.targets +++ /dev/null @@ -1,122 +0,0 @@ - - - - - - - - - $(HOME)/GOG Games/Stardew Valley/game - $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley - $(HOME)/.steam/steam/steamapps/common/Stardew Valley - - /Applications/Stardew Valley.app/Contents/MacOS - $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS - - C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley - C:\Program Files (x86)\GOG Galaxy\Games\Stardew Valley - C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32)) - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32)) - - - - - - - $(DefineConstants);SMAPI_FOR_WINDOWS - - - - - - - - False - - - False - - - False - - - False - - - - - $(GamePath)\Netcode.dll - False - - - $(GamePath)\Stardew Valley.exe - False - - - $(GamePath)\xTile.dll - False - False - - - - - - $(DefineConstants);SMAPI_FOR_UNIX - - - - - $(GamePath)\MonoGame.Framework.dll - False - False - - - - - $(GamePath)\StardewValley.exe - False - - - $(GamePath)\xTile.dll - False - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Program - $(GamePath)\StardewModdingAPI.exe - $(GamePath) - - - - - diff --git a/build/constants.targets b/build/constants.targets new file mode 100644 index 00000000..3e10462a --- /dev/null +++ b/build/constants.targets @@ -0,0 +1,25 @@ + + + + + + + + + $(HOME)/GOG Games/Stardew Valley/game + $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley + $(HOME)/.steam/steam/steamapps/common/Stardew Valley + + /Applications/Stardew Valley.app/Contents/MacOS + $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS + + C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley + C:\Program Files (x86)\GOG Galaxy\Games\Stardew Valley + C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32)) + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32)) + + + $(DefineConstants);SMAPI_FOR_WINDOWS + + diff --git a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj index 4f849b9b..dd349e09 100644 --- a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj +++ b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj @@ -64,6 +64,7 @@ - + + \ No newline at end of file diff --git a/src/SMAPI.Internal/RewriteFacades/SpriteBatchMethods.cs b/src/SMAPI.Internal/RewriteFacades/SpriteBatchMethods.cs deleted file mode 100644 index 5e5d117e..00000000 --- a/src/SMAPI.Internal/RewriteFacades/SpriteBatchMethods.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; - -namespace StardewModdingAPI.Internal.RewriteFacades -{ - /// Provides method signatures that can be injected into mod code for compatibility between Linux/Mac or Windows. - public class SpriteBatchMethods : SpriteBatch - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public SpriteBatchMethods(GraphicsDevice graphicsDevice) : base(graphicsDevice) { } - - - /**** - ** MonoGame signatures - ****/ - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Linux/Mac.")] - public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix? matrix) - { - base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, matrix ?? Matrix.Identity); - } - - /**** - ** XNA signatures - ****/ - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] - public new void Begin() - { - base.Begin(); - } - - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] - public new void Begin(SpriteSortMode sortMode, BlendState blendState) - { - base.Begin(sortMode, blendState); - } - - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] - public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState) - { - base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState); - } - - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] - public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect) - { - base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect); - } - - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] - public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix transformMatrix) - { - base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, transformMatrix); - } - } -} diff --git a/src/SMAPI.Internal/StardewModdingAPI.Internal.csproj b/src/SMAPI.Internal/StardewModdingAPI.Internal.csproj index 6e7fa368..a87dcb0a 100644 --- a/src/SMAPI.Internal/StardewModdingAPI.Internal.csproj +++ b/src/SMAPI.Internal/StardewModdingAPI.Internal.csproj @@ -31,6 +31,8 @@ + + @@ -41,9 +43,8 @@ - - + \ No newline at end of file diff --git a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj index 02564409..c6d25c19 100644 --- a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj +++ b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj @@ -62,5 +62,6 @@ + \ No newline at end of file diff --git a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj index d1f72c6c..15c1c9a8 100644 --- a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj +++ b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj @@ -94,5 +94,6 @@ - + + \ No newline at end of file diff --git a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj index 0c793817..001b006c 100644 --- a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj +++ b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj @@ -78,7 +78,8 @@ - + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. diff --git a/src/SMAPI.sln b/src/SMAPI.sln index ff953751..282d8397 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -43,7 +43,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{EB35A917-6 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{09CF91E5-5BAB-4650-A200-E5EA9A633046}" ProjectSection(SolutionItems) = preProject - ..\build\common.targets = ..\build\common.targets + ..\build\build.targets = ..\build\build.targets + ..\build\constants.targets = ..\build\constants.targets ..\build\GlobalAssemblyInfo.cs = ..\build\GlobalAssemblyInfo.cs ..\build\prepare-install-package.targets = ..\build\prepare-install-package.targets ..\build\prepare-nuget-package.targets = ..\build\prepare-nuget-package.targets diff --git a/src/SMAPI/Framework/RewriteFacades/SpriteBatchMethods.cs b/src/SMAPI/Framework/RewriteFacades/SpriteBatchMethods.cs new file mode 100644 index 00000000..5dd21b92 --- /dev/null +++ b/src/SMAPI/Framework/RewriteFacades/SpriteBatchMethods.cs @@ -0,0 +1,60 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace StardewModdingAPI.Framework.RewriteFacades +{ + /// Provides method signatures that can be injected into mod code for compatibility between Linux/Mac or Windows. + /// This is public to support SMAPI rewriting and should not be referenced directly by mods. + public class SpriteBatchMethods : SpriteBatch + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public SpriteBatchMethods(GraphicsDevice graphicsDevice) : base(graphicsDevice) { } + + + /**** + ** MonoGame signatures + ****/ + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Linux/Mac.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix? matrix) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, matrix ?? Matrix.Identity); + } + + /**** + ** XNA signatures + ****/ + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin() + { + base.Begin(); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState) + { + base.Begin(sortMode, blendState); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix transformMatrix) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, transformMatrix); + } + } +} diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index c7abfbef..1063ae71 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -4,7 +4,7 @@ using StardewModdingAPI.Events; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.ModLoading.Finders; using StardewModdingAPI.Framework.ModLoading.Rewriters; -using StardewModdingAPI.Internal.RewriteFacades; +using StardewModdingAPI.Framework.RewriteFacades; using StardewValley; namespace StardewModdingAPI.Metadata diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 31bfd6fd..ca6459fe 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -127,6 +127,7 @@ + @@ -313,5 +314,6 @@ - + + \ No newline at end of file -- cgit From 60040854a3b95ab220a42c087bda7f9011dda8ca Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 3 May 2018 01:38:08 -0400 Subject: switch back to shared project due to installer issues --- build/build.targets | 92 --------------- build/common.targets | 125 +++++++++++++++++++++ build/constants.targets | 25 ----- .../StardewModdingAPI.Installer.csproj | 10 +- src/SMAPI.Internal/Properties/AssemblyInfo.cs | 9 -- src/SMAPI.Internal/SMAPI.Internal.projitems | 18 +++ .../StardewModdingAPI.Internal.csproj | 50 --------- .../StardewModdingAPI.Internal.shproj | 13 +++ .../StardewModdingAPI.ModBuildConfig.csproj | 9 +- .../StardewModdingAPI.Mods.ConsoleCommands.csproj | 3 +- src/SMAPI.Web/StardewModdingAPI.Web.csproj | 4 +- src/SMAPI.sln | 23 ++-- src/SMAPI/StardewModdingAPI.csproj | 10 +- 13 files changed, 173 insertions(+), 218 deletions(-) delete mode 100644 build/build.targets create mode 100644 build/common.targets delete mode 100644 build/constants.targets delete mode 100644 src/SMAPI.Internal/Properties/AssemblyInfo.cs create mode 100644 src/SMAPI.Internal/SMAPI.Internal.projitems delete mode 100644 src/SMAPI.Internal/StardewModdingAPI.Internal.csproj create mode 100644 src/SMAPI.Internal/StardewModdingAPI.Internal.shproj (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/build/build.targets b/build/build.targets deleted file mode 100644 index 270fd95d..00000000 --- a/build/build.targets +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - - - False - - - False - - - False - - - False - - - - - $(GamePath)\Netcode.dll - False - - - $(GamePath)\Stardew Valley.exe - False - - - $(GamePath)\xTile.dll - False - False - - - - - - - - $(GamePath)\MonoGame.Framework.dll - False - False - - - - - $(GamePath)\StardewValley.exe - False - - - $(GamePath)\xTile.dll - False - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Program - $(GamePath)\StardewModdingAPI.exe - $(GamePath) - - - - - diff --git a/build/common.targets b/build/common.targets new file mode 100644 index 00000000..44c2c071 --- /dev/null +++ b/build/common.targets @@ -0,0 +1,125 @@ + + + + + + + + + $(HOME)/GOG Games/Stardew Valley/game + $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley + $(HOME)/.steam/steam/steamapps/common/Stardew Valley + + /Applications/Stardew Valley.app/Contents/MacOS + $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS + + C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley + C:\Program Files (x86)\GOG Galaxy\Games\Stardew Valley + C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32)) + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32)) + + + $(DefineConstants);SMAPI_FOR_WINDOWS + + + + + + + + + + + + + + + + False + + + False + + + False + + + False + + + + + $(GamePath)\Netcode.dll + False + + + $(GamePath)\Stardew Valley.exe + False + + + $(GamePath)\xTile.dll + False + False + + + + + + + + $(GamePath)\MonoGame.Framework.dll + False + False + + + + + $(GamePath)\StardewValley.exe + False + + + $(GamePath)\xTile.dll + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Program + $(GamePath)\StardewModdingAPI.exe + $(GamePath) + + + + + diff --git a/build/constants.targets b/build/constants.targets deleted file mode 100644 index 3e10462a..00000000 --- a/build/constants.targets +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - $(HOME)/GOG Games/Stardew Valley/game - $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley - $(HOME)/.steam/steam/steamapps/common/Stardew Valley - - /Applications/Stardew Valley.app/Contents/MacOS - $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS - - C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley - C:\Program Files (x86)\GOG Galaxy\Games\Stardew Valley - C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32)) - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32)) - - - $(DefineConstants);SMAPI_FOR_WINDOWS - - diff --git a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj index dd349e09..2ad7e82a 100644 --- a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj +++ b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj @@ -57,14 +57,8 @@ PreserveNewest - - - {10db0676-9fc1-4771-a2c8-e2519f091e49} - StardewModdingAPI.Internal - - + - - + \ No newline at end of file diff --git a/src/SMAPI.Internal/Properties/AssemblyInfo.cs b/src/SMAPI.Internal/Properties/AssemblyInfo.cs deleted file mode 100644 index b314b353..00000000 --- a/src/SMAPI.Internal/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; - -[assembly: AssemblyTitle("SMAPI.Internal")] -[assembly: AssemblyDescription("Contains internal SMAPI code that's shared between its projects.")] -[assembly: InternalsVisibleTo("StardewModdingAPI")] -[assembly: InternalsVisibleTo("StardewModdingAPI.ModBuildConfig")] -[assembly: InternalsVisibleTo("StardewModdingAPI.Installer")] -[assembly: InternalsVisibleTo("StardewModdingAPI.Web")] diff --git a/src/SMAPI.Internal/SMAPI.Internal.projitems b/src/SMAPI.Internal/SMAPI.Internal.projitems new file mode 100644 index 00000000..764cb12e --- /dev/null +++ b/src/SMAPI.Internal/SMAPI.Internal.projitems @@ -0,0 +1,18 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 85208f8d-6fd1-4531-be05-7142490f59fe + + + SMAPI.Internal + + + + + + + + + \ No newline at end of file diff --git a/src/SMAPI.Internal/StardewModdingAPI.Internal.csproj b/src/SMAPI.Internal/StardewModdingAPI.Internal.csproj deleted file mode 100644 index a87dcb0a..00000000 --- a/src/SMAPI.Internal/StardewModdingAPI.Internal.csproj +++ /dev/null @@ -1,50 +0,0 @@ - - - - - Debug - x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49} - Library - Properties - StardewModdingAPI.Internal - StardewModdingAPI.Internal - v4.5 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - Properties\GlobalAssemblyInfo.cs - - - - - - - - - - - \ No newline at end of file diff --git a/src/SMAPI.Internal/StardewModdingAPI.Internal.shproj b/src/SMAPI.Internal/StardewModdingAPI.Internal.shproj new file mode 100644 index 00000000..a098828a --- /dev/null +++ b/src/SMAPI.Internal/StardewModdingAPI.Internal.shproj @@ -0,0 +1,13 @@ + + + + 85208f8d-6fd1-4531-be05-7142490f59fe + 14.0 + + + + + + + + diff --git a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj index c6d25c19..e0ea876f 100644 --- a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj +++ b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj @@ -55,13 +55,8 @@ - - - {10db0676-9fc1-4771-a2c8-e2519f091e49} - StardewModdingAPI.Internal - - + - + \ No newline at end of file diff --git a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj index 15c1c9a8..d1f72c6c 100644 --- a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj +++ b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj @@ -94,6 +94,5 @@ - - + \ No newline at end of file diff --git a/src/SMAPI.Web/StardewModdingAPI.Web.csproj b/src/SMAPI.Web/StardewModdingAPI.Web.csproj index bc337e7e..e4678269 100644 --- a/src/SMAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/SMAPI.Web/StardewModdingAPI.Web.csproj @@ -22,8 +22,6 @@ - - - + diff --git a/src/SMAPI.sln b/src/SMAPI.sln index 282d8397..c8760dcb 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -24,8 +24,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Installer {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} = {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Internal", "SMAPI.Internal\StardewModdingAPI.Internal.csproj", "{10DB0676-9FC1-4771-A2C8-E2519F091E49}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Tests", "SMAPI.Tests\StardewModdingAPI.Tests.csproj", "{36CCB19E-92EB-48C7-9615-98EEFD45109B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Web", "SMAPI.Web\StardewModdingAPI.Web.csproj", "{A308F679-51A3-4006-92D5-BAEC7EBD01A1}" @@ -43,8 +41,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{EB35A917-6 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{09CF91E5-5BAB-4650-A200-E5EA9A633046}" ProjectSection(SolutionItems) = preProject - ..\build\build.targets = ..\build\build.targets - ..\build\constants.targets = ..\build\constants.targets + ..\build\common.targets = ..\build\common.targets ..\build\GlobalAssemblyInfo.cs = ..\build\GlobalAssemblyInfo.cs ..\build\prepare-install-package.targets = ..\build\prepare-install-package.targets ..\build\prepare-nuget-package.targets = ..\build\prepare-nuget-package.targets @@ -59,7 +56,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.ModBuildC EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.ModBuildConfig.Analyzer.Tests", "SMAPI.ModBuildConfig.Analyzer.Tests\SMAPI.ModBuildConfig.Analyzer.Tests.csproj", "{0CF97929-B0D0-4D73-B7BF-4FF7191035F9}" EndProject +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "StardewModdingAPI.Internal", "SMAPI.Internal\StardewModdingAPI.Internal.shproj", "{85208F8D-6FD1-4531-BE05-7142490F59FE}" +EndProject Global + GlobalSection(SharedMSBuildProjectFiles) = preSolution + SMAPI.Internal\SMAPI.Internal.projitems*{443ddf81-6aaf-420a-a610-3459f37e5575}*SharedItemsImports = 4 + SMAPI.Internal\SMAPI.Internal.projitems*{85208f8d-6fd1-4531-be05-7142490f59fe}*SharedItemsImports = 13 + SMAPI.Internal\SMAPI.Internal.projitems*{ea4f1e80-743f-4a1d-9757-ae66904a196a}*SharedItemsImports = 4 + SMAPI.Internal\SMAPI.Internal.projitems*{f1a573b0-f436-472c-ae29-0b91ea6b9f8f}*SharedItemsImports = 4 + EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|Mixed Platforms = Debug|Mixed Platforms @@ -99,16 +104,6 @@ Global {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Mixed Platforms.Build.0 = Release|x86 {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|x86.ActiveCfg = Release|x86 {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|x86.Build.0 = Release|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|Any CPU.ActiveCfg = Debug|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|Mixed Platforms.Build.0 = Debug|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|x86.ActiveCfg = Debug|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|x86.Build.0 = Debug|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|Any CPU.ActiveCfg = Release|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|Mixed Platforms.ActiveCfg = Release|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|Mixed Platforms.Build.0 = Release|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|x86.ActiveCfg = Release|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|x86.Build.0 = Release|x86 {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Any CPU.ActiveCfg = Debug|x86 {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Mixed Platforms.Build.0 = Debug|x86 diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index ca6459fe..1822b134 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -304,16 +304,10 @@ false - - - {10db0676-9fc1-4771-a2c8-e2519f091e49} - StardewModdingAPI.Internal - - + - - + \ No newline at end of file -- cgit From 8051862c7bd2fe498657eef4bb102b5ca33390a6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 4 May 2018 20:44:20 -0400 Subject: add LocationEvents.ObjectsChanged event --- docs/release-notes.md | 1 + .../Events/EventArgsLocationObjectsChanged.cs | 26 ++++++-- src/SMAPI/Events/LocationEvents.cs | 8 +++ src/SMAPI/Framework/Events/EventManager.cs | 6 ++ src/SMAPI/Framework/SGame.cs | 64 +++++++++++++++---- .../Framework/StateTracking/LocationTracker.cs | 71 ++++++++++++++++++++++ src/SMAPI/StardewModdingAPI.csproj | 1 + 7 files changed, 162 insertions(+), 15 deletions(-) create mode 100644 src/SMAPI/Framework/StateTracking/LocationTracker.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/docs/release-notes.md b/docs/release-notes.md index 85776a06..558ed004 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,6 +11,7 @@ * For modders: * Added code analysis to mod build config package to flag common issues as warnings. + * Added `LocationEvents.ObjectsChanged`, raised when an object is added/removed in any location. * Added `Context.IsMultiplayer` and `Context.IsMainPlayer` flags. * Added `Constants.TargetPlatform` which says whether the game is running on Linux, Mac, or Windows. * Added `semanticVersion.IsPrerelease()` method. diff --git a/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs b/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs index de6bd365..a6138ddb 100644 --- a/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs +++ b/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs @@ -1,28 +1,46 @@ using System; using System.Collections.Generic; +using System.Linq; using Microsoft.Xna.Framework; using Netcode; -using Object = StardewValley.Object; +using StardewValley; +using SObject = StardewValley.Object; namespace StardewModdingAPI.Events { - /// Event arguments for a event. + /// Event arguments for a or event. public class EventArgsLocationObjectsChanged : EventArgs { /********* ** Accessors *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The objects added to the list. + public IEnumerable> Added { get; } + + /// The objects removed from the list. + public IEnumerable> Removed { get; } + /// The current list of objects in the current location. - public IDictionary> NewObjects { get; } + [Obsolete("Use " + nameof(EventArgsLocationObjectsChanged.Added))] + public IDictionary> NewObjects { get; } /********* ** Public methods *********/ /// Construct an instance. + /// The location which changed. + /// The objects added to the list. + /// The objects removed from the list. /// The current list of objects in the current location. - public EventArgsLocationObjectsChanged(IDictionary> newObjects) + public EventArgsLocationObjectsChanged(GameLocation location, IEnumerable> added, IEnumerable> removed, IDictionary> newObjects) { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); this.NewObjects = newObjects; } } diff --git a/src/SMAPI/Events/LocationEvents.cs b/src/SMAPI/Events/LocationEvents.cs index 81d13e9f..3ed09136 100644 --- a/src/SMAPI/Events/LocationEvents.cs +++ b/src/SMAPI/Events/LocationEvents.cs @@ -31,12 +31,20 @@ namespace StardewModdingAPI.Events } /// Raised after the list of objects in the current location changes (e.g. an object is added or removed). + [Obsolete("Use " + nameof(LocationEvents) + "." + nameof(LocationEvents.ObjectsChanged) + " instead")] public static event EventHandler LocationObjectsChanged { add => LocationEvents.EventManager.Location_LocationObjectsChanged.Add(value); remove => LocationEvents.EventManager.Location_LocationObjectsChanged.Remove(value); } + /// Raised after the list of objects in a location changes (e.g. an object is added or removed). + public static event EventHandler ObjectsChanged + { + add => LocationEvents.EventManager.Location_ObjectsChanged.Add(value); + remove => LocationEvents.EventManager.Location_ObjectsChanged.Remove(value); + } + /********* ** Public methods diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 87ff760f..9030ba97 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -1,3 +1,4 @@ +using System; using System.Diagnostics.CodeAnalysis; using Microsoft.Xna.Framework.Input; using StardewModdingAPI.Events; @@ -114,8 +115,12 @@ namespace StardewModdingAPI.Framework.Events public readonly ManagedEvent Location_LocationsChanged; /// Raised after the list of objects in the current location changes (e.g. an object is added or removed). + [Obsolete] public readonly ManagedEvent Location_LocationObjectsChanged; + /// Raised after the list of objects in a location changes (e.g. an object is added or removed). + public readonly ManagedEvent Location_ObjectsChanged; + /**** ** MenuEvents ****/ @@ -239,6 +244,7 @@ namespace StardewModdingAPI.Framework.Events this.Location_CurrentLocationChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.CurrentLocationChanged)); this.Location_LocationsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.LocationsChanged)); this.Location_LocationObjectsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.LocationObjectsChanged)); + this.Location_ObjectsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.ObjectsChanged)); this.Menu_Changed = ManageEventOf(nameof(MenuEvents), nameof(MenuEvents.MenuChanged)); this.Menu_Closed = ManageEventOf(nameof(MenuEvents), nameof(MenuEvents.MenuClosed)); diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 78d07fbf..d2ba85f8 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -93,7 +93,10 @@ namespace StardewModdingAPI.Framework private readonly IValueWatcher SaveIdWatcher; /// Tracks changes to the location list. - private readonly ICollectionWatcher LocationsWatcher; + private readonly ICollectionWatcher LocationListWatcher; + + /// Tracks changes to each location. + private readonly IDictionary LocationWatchers = new Dictionary(); /// Tracks changes to . private readonly IValueWatcher ActiveMenuWatcher; @@ -162,14 +165,14 @@ namespace StardewModdingAPI.Framework this.WindowSizeWatcher = WatcherFactory.ForEquatable(() => new Point(Game1.viewport.Width, Game1.viewport.Height)); this.TimeWatcher = WatcherFactory.ForEquatable(() => Game1.timeOfDay); this.ActiveMenuWatcher = WatcherFactory.ForReference(() => Game1.activeClickableMenu); - this.LocationsWatcher = WatcherFactory.ForObservableCollection((ObservableCollection)Game1.locations); + this.LocationListWatcher = WatcherFactory.ForObservableCollection((ObservableCollection)Game1.locations); this.Watchers.AddRange(new IWatcher[] { this.SaveIdWatcher, this.WindowSizeWatcher, this.TimeWatcher, this.ActiveMenuWatcher, - this.LocationsWatcher + this.LocationListWatcher }); } @@ -349,6 +352,22 @@ namespace StardewModdingAPI.Framework watcher.Update(); this.CurrentPlayerTracker?.Update(); + // update location watchers + if (this.LocationListWatcher.IsChanged) + { + foreach (GameLocation location in this.LocationListWatcher.Removed.Union(this.LocationListWatcher.Added)) + { + if (this.LocationWatchers.TryGetValue(location, out LocationTracker watcher)) + { + this.Watchers.Remove(watcher); + this.LocationWatchers.Remove(location); + watcher.Dispose(); + } + } + foreach (GameLocation location in this.LocationListWatcher.Added) + this.LocationWatchers[location] = new LocationTracker(location); + } + /********* ** Locale changed events *********/ @@ -509,12 +528,12 @@ namespace StardewModdingAPI.Framework } // raise location list changed - if (this.LocationsWatcher.IsChanged) + if (this.LocationListWatcher.IsChanged) { if (this.VerboseLogging) { - string added = this.LocationsWatcher.Added.Any() ? string.Join(", ", this.LocationsWatcher.Added.Select(p => p.Name)) : "none"; - string removed = this.LocationsWatcher.Removed.Any() ? string.Join(", ", this.LocationsWatcher.Removed.Select(p => p.Name)) : "none"; + string added = this.LocationListWatcher.Added.Any() ? string.Join(", ", this.LocationListWatcher.Added.Select(p => p.Name)) : "none"; + string removed = this.LocationListWatcher.Removed.Any() ? string.Join(", ", this.LocationListWatcher.Removed.Select(p => p.Name)) : "none"; this.Monitor.Log($"Context: location list changed (added {added}; removed {removed}).", LogLevel.Trace); } @@ -524,6 +543,20 @@ namespace StardewModdingAPI.Framework // raise events that shouldn't be triggered on initial load if (!this.SaveIdWatcher.IsChanged) { + // raise location objects changed + foreach (LocationTracker watcher in this.LocationWatchers.Values) + { + if (watcher.LocationObjectsWatcher.IsChanged) + { + GameLocation location = watcher.Location; + var added = watcher.LocationObjectsWatcher.Added; + var removed = watcher.LocationObjectsWatcher.Removed; + watcher.Reset(); + + this.Events.Location_ObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, added, removed, watcher.Location.netObjects.FieldDict)); + } + } + // raise player leveled up a skill foreach (KeyValuePair> pair in curPlayer.GetChangedSkills()) { @@ -542,12 +575,16 @@ namespace StardewModdingAPI.Framework } // raise current location's object list changed - if (curPlayer.TryGetLocationChanges(out IDictionaryWatcher _)) { - if (this.VerboseLogging) - this.Monitor.Log("Context: current location objects changed.", LogLevel.Trace); + if (curPlayer.TryGetLocationChanges(out IDictionaryWatcher watcher)) + { + if (this.VerboseLogging) + this.Monitor.Log("Context: current location objects changed.", LogLevel.Trace); + + GameLocation location = curPlayer.GetCurrentLocation(); - this.Events.Location_LocationObjectsChanged.Raise(new EventArgsLocationObjectsChanged(curPlayer.GetCurrentLocation().netObjects.FieldDict)); + this.Events.Location_LocationObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, watcher.Added, watcher.Removed, location.netObjects.FieldDict)); + } } // raise time changed @@ -570,9 +607,14 @@ namespace StardewModdingAPI.Framework // update state this.CurrentPlayerTracker?.Reset(); - this.LocationsWatcher.Reset(); + this.LocationListWatcher.Reset(); this.SaveIdWatcher.Reset(); this.TimeWatcher.Reset(); + if (!Context.IsWorldReady) + { + foreach (LocationTracker watcher in this.LocationWatchers.Values) + watcher.Reset(); + } /********* ** Game update diff --git a/src/SMAPI/Framework/StateTracking/LocationTracker.cs b/src/SMAPI/Framework/StateTracking/LocationTracker.cs new file mode 100644 index 00000000..8cf4e7a2 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/LocationTracker.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.StateTracking.FieldWatchers; +using StardewValley; +using Object = StardewValley.Object; + +namespace StardewModdingAPI.Framework.StateTracking +{ + /// Tracks changes to a location's data. + internal class LocationTracker : IWatcher + { + /********* + ** Properties + *********/ + /// The underlying watchers. + private readonly List Watchers = new List(); + + + /********* + ** Accessors + *********/ + /// Whether the value changed since the last reset. + public bool IsChanged => this.Watchers.Any(p => p.IsChanged); + + /// The tracked location. + public GameLocation Location { get; } + + /// Tracks changes to the current location's objects. + public IDictionaryWatcher LocationObjectsWatcher { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location to track. + public LocationTracker(GameLocation location) + { + this.Location = location; + + // init watchers + this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(location.netObjects); + this.Watchers.AddRange(new[] + { + this.LocationObjectsWatcher + }); + } + + /// Stop watching the player fields and release all references. + public void Dispose() + { + foreach (IWatcher watcher in this.Watchers) + watcher.Dispose(); + } + + /// Update the current value if needed. + public void Update() + { + foreach (IWatcher watcher in this.Watchers) + watcher.Update(); + } + + /// Set the current value as the baseline. + public void Reset() + { + foreach (IWatcher watcher in this.Watchers) + watcher.Reset(); + } + } +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 1822b134..9f04887c 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -148,6 +148,7 @@ + -- cgit From b8fd3aedfe884741bdda8c68398427f875585456 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 5 May 2018 01:31:06 -0400 Subject: rewrite location events for multiplayer --- docs/release-notes.md | 12 +- .../Events/EventArgsCurrentLocationChanged.cs | 31 --- src/SMAPI/Events/EventArgsGameLocationsChanged.cs | 27 --- src/SMAPI/Events/EventArgsIntChanged.cs | 3 +- .../Events/EventArgsLocationBuildingsChanged.cs | 39 ++++ .../Events/EventArgsLocationObjectsChanged.cs | 18 +- src/SMAPI/Events/EventArgsLocationsChanged.cs | 33 +++ src/SMAPI/Events/EventArgsPlayerWarped.cs | 32 +++ src/SMAPI/Events/LocationEvents.cs | 20 +- src/SMAPI/Events/PlayerEvents.cs | 10 +- src/SMAPI/Framework/Events/EventManager.cs | 22 +- src/SMAPI/Framework/SGame.cs | 158 +++++++-------- .../FieldWatchers/NetCollectionWatcher.cs | 93 +++++++++ .../StateTracking/FieldWatchers/WatcherFactory.cs | 8 + .../Framework/StateTracking/LocationTracker.cs | 21 +- src/SMAPI/Framework/StateTracking/PlayerTracker.cs | 32 --- .../StateTracking/WorldLocationsTracker.cs | 221 +++++++++++++++++++++ src/SMAPI/StardewModdingAPI.csproj | 7 +- 18 files changed, 566 insertions(+), 221 deletions(-) delete mode 100644 src/SMAPI/Events/EventArgsCurrentLocationChanged.cs delete mode 100644 src/SMAPI/Events/EventArgsGameLocationsChanged.cs create mode 100644 src/SMAPI/Events/EventArgsLocationBuildingsChanged.cs create mode 100644 src/SMAPI/Events/EventArgsLocationsChanged.cs create mode 100644 src/SMAPI/Events/EventArgsPlayerWarped.cs create mode 100644 src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/docs/release-notes.md b/docs/release-notes.md index 00ed6e9c..ece388c7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,7 +12,11 @@ * For modders: * Added code analysis to mod build config package to flag common issues as warnings. - * Added `LocationEvents.ObjectsChanged`, raised when an object is added/removed in any location. + * Replaced `LocationEvents` with a more powerful set of events for multiplayer: + * now raised for all locations; + * now includes added/removed building interiors; + * each event now provides a list of added/removed values; + * added buildings-changed event. * Added `Context.IsMultiplayer` and `Context.IsMainPlayer` flags. * Added `Constants.TargetPlatform` which says whether the game is running on Linux, Mac, or Windows. * Added `semanticVersion.IsPrerelease()` method. @@ -21,8 +25,10 @@ * Fixed assets not reloaded consistently when the player switches language. * Fixed console command input not saved to the log. * Fixed `helper.ModRegistry.GetApi` interface validation errors not mentioning which interface caused the issue. - * **Breaking change**: dropped some deprecated APIs. - * **Breaking change**: mods can't intercept chatbox input, including the game's hotkeys to toggle the chatbox (default `T` and `?`). + * **Breaking changes** (see [migration guide](https://stardewvalleywiki.com/Modding:Migrate_to_Stardew_Valley_1.3)): + * dropped some deprecated APIs; + * `LocationEvents` have been rewritten (see above); + * mods can't intercept chatbox input, including the game's hotkeys to toggle the chatbox (default `T` and `?`). * In console commands: * Added `player_add name`, which lets you add items to your inventory by name instead of ID. diff --git a/src/SMAPI/Events/EventArgsCurrentLocationChanged.cs b/src/SMAPI/Events/EventArgsCurrentLocationChanged.cs deleted file mode 100644 index 25d3ebf3..00000000 --- a/src/SMAPI/Events/EventArgsCurrentLocationChanged.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using StardewValley; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsCurrentLocationChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The player's current location. - public GameLocation NewLocation { get; } - - /// The player's previous location. - public GameLocation PriorLocation { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The player's previous location. - /// The player's current location. - public EventArgsCurrentLocationChanged(GameLocation priorLocation, GameLocation newLocation) - { - this.NewLocation = newLocation; - this.PriorLocation = priorLocation; - } - } -} diff --git a/src/SMAPI/Events/EventArgsGameLocationsChanged.cs b/src/SMAPI/Events/EventArgsGameLocationsChanged.cs deleted file mode 100644 index 78ba38fa..00000000 --- a/src/SMAPI/Events/EventArgsGameLocationsChanged.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Collections.Generic; -using StardewValley; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsGameLocationsChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The current list of game locations. - public IList NewLocations { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The current list of game locations. - public EventArgsGameLocationsChanged(IList newLocations) - { - this.NewLocations = newLocations; - } - } -} diff --git a/src/SMAPI/Events/EventArgsIntChanged.cs b/src/SMAPI/Events/EventArgsIntChanged.cs index 0c742d12..a018695c 100644 --- a/src/SMAPI/Events/EventArgsIntChanged.cs +++ b/src/SMAPI/Events/EventArgsIntChanged.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace StardewModdingAPI.Events { @@ -14,6 +14,7 @@ namespace StardewModdingAPI.Events /// The current value. public int NewInt { get; } + /********* ** Public methods *********/ diff --git a/src/SMAPI/Events/EventArgsLocationBuildingsChanged.cs b/src/SMAPI/Events/EventArgsLocationBuildingsChanged.cs new file mode 100644 index 00000000..e8184ebe --- /dev/null +++ b/src/SMAPI/Events/EventArgsLocationBuildingsChanged.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; +using StardewValley.Buildings; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsLocationBuildingsChanged : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The buildings added to the location. + public IEnumerable Added { get; } + + /// The buildings removed from the location. + public IEnumerable Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The buildings added to the location. + /// The buildings removed from the location. + public EventArgsLocationBuildingsChanged(GameLocation location, IEnumerable added, IEnumerable removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs b/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs index a6138ddb..410ef6e6 100644 --- a/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs +++ b/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs @@ -8,7 +8,7 @@ using SObject = StardewValley.Object; namespace StardewModdingAPI.Events { - /// Event arguments for a or event. + /// Event arguments for a event. public class EventArgsLocationObjectsChanged : EventArgs { /********* @@ -17,31 +17,25 @@ namespace StardewModdingAPI.Events /// The location which changed. public GameLocation Location { get; } - /// The objects added to the list. + /// The objects added to the location. public IEnumerable> Added { get; } - /// The objects removed from the list. + /// The objects removed from the location. public IEnumerable> Removed { get; } - /// The current list of objects in the current location. - [Obsolete("Use " + nameof(EventArgsLocationObjectsChanged.Added))] - public IDictionary> NewObjects { get; } - /********* ** Public methods *********/ /// Construct an instance. /// The location which changed. - /// The objects added to the list. - /// The objects removed from the list. - /// The current list of objects in the current location. - public EventArgsLocationObjectsChanged(GameLocation location, IEnumerable> added, IEnumerable> removed, IDictionary> newObjects) + /// The objects added to the location. + /// The objects removed from the location. + public EventArgsLocationObjectsChanged(GameLocation location, IEnumerable> added, IEnumerable> removed) { this.Location = location; this.Added = added.ToArray(); this.Removed = removed.ToArray(); - this.NewObjects = newObjects; } } } diff --git a/src/SMAPI/Events/EventArgsLocationsChanged.cs b/src/SMAPI/Events/EventArgsLocationsChanged.cs new file mode 100644 index 00000000..20984f45 --- /dev/null +++ b/src/SMAPI/Events/EventArgsLocationsChanged.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsLocationsChanged : EventArgs + { + /********* + ** Accessors + *********/ + /// The added locations. + public IEnumerable Added { get; } + + /// The removed locations. + public IEnumerable Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The added locations. + /// The removed locations. + public EventArgsLocationsChanged(IEnumerable added, IEnumerable removed) + { + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Events/EventArgsPlayerWarped.cs b/src/SMAPI/Events/EventArgsPlayerWarped.cs new file mode 100644 index 00000000..93026aea --- /dev/null +++ b/src/SMAPI/Events/EventArgsPlayerWarped.cs @@ -0,0 +1,32 @@ +using System; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsPlayerWarped : EventArgs + { + /********* + ** Accessors + *********/ + /// The player's previous location. + public GameLocation PriorLocation { get; } + + /// The player's current location. + public GameLocation NewLocation { get; } + + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player's previous location. + /// The player's current location. + public EventArgsPlayerWarped(GameLocation priorLocation, GameLocation newLocation) + { + this.NewLocation = newLocation; + this.PriorLocation = priorLocation; + } + } +} diff --git a/src/SMAPI/Events/LocationEvents.cs b/src/SMAPI/Events/LocationEvents.cs index 3ed09136..ff75c619 100644 --- a/src/SMAPI/Events/LocationEvents.cs +++ b/src/SMAPI/Events/LocationEvents.cs @@ -16,29 +16,21 @@ namespace StardewModdingAPI.Events /********* ** Events *********/ - /// Raised after the player warps to a new location. - public static event EventHandler CurrentLocationChanged - { - add => LocationEvents.EventManager.Location_CurrentLocationChanged.Add(value); - remove => LocationEvents.EventManager.Location_CurrentLocationChanged.Remove(value); - } - /// Raised after a game location is added or removed. - public static event EventHandler LocationsChanged + public static event EventHandler LocationsChanged { add => LocationEvents.EventManager.Location_LocationsChanged.Add(value); remove => LocationEvents.EventManager.Location_LocationsChanged.Remove(value); } - /// Raised after the list of objects in the current location changes (e.g. an object is added or removed). - [Obsolete("Use " + nameof(LocationEvents) + "." + nameof(LocationEvents.ObjectsChanged) + " instead")] - public static event EventHandler LocationObjectsChanged + /// Raised after buildings are added or removed in a location. + public static event EventHandler BuildingsChanged { - add => LocationEvents.EventManager.Location_LocationObjectsChanged.Add(value); - remove => LocationEvents.EventManager.Location_LocationObjectsChanged.Remove(value); + add => LocationEvents.EventManager.Location_BuildingsChanged.Add(value); + remove => LocationEvents.EventManager.Location_BuildingsChanged.Remove(value); } - /// Raised after the list of objects in a location changes (e.g. an object is added or removed). + /// Raised after objects are added or removed in a location. public static event EventHandler ObjectsChanged { add => LocationEvents.EventManager.Location_ObjectsChanged.Add(value); diff --git a/src/SMAPI/Events/PlayerEvents.cs b/src/SMAPI/Events/PlayerEvents.cs index 84a7ff63..6e7050e3 100644 --- a/src/SMAPI/Events/PlayerEvents.cs +++ b/src/SMAPI/Events/PlayerEvents.cs @@ -23,13 +23,21 @@ namespace StardewModdingAPI.Events remove => PlayerEvents.EventManager.Player_InventoryChanged.Remove(value); } - /// Raised after the player levels up a skill. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. + /// Raised after the player levels up a skill. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. public static event EventHandler LeveledUp { add => PlayerEvents.EventManager.Player_LeveledUp.Add(value); remove => PlayerEvents.EventManager.Player_LeveledUp.Remove(value); } + /// Raised after the player warps to a new location. + public static event EventHandler Warped + { + add => PlayerEvents.EventManager.Player_Warped.Add(value); + remove => PlayerEvents.EventManager.Player_Warped.Remove(value); + } + + /********* ** Public methods diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 9030ba97..84036127 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -108,17 +108,13 @@ namespace StardewModdingAPI.Framework.Events /**** ** LocationEvents ****/ - /// Raised after the player warps to a new location. - public readonly ManagedEvent Location_CurrentLocationChanged; - /// Raised after a game location is added or removed. - public readonly ManagedEvent Location_LocationsChanged; + public readonly ManagedEvent Location_LocationsChanged; - /// Raised after the list of objects in the current location changes (e.g. an object is added or removed). - [Obsolete] - public readonly ManagedEvent Location_LocationObjectsChanged; + /// Raised after buildings are added or removed in a location. + public readonly ManagedEvent Location_BuildingsChanged; - /// Raised after the list of objects in a location changes (e.g. an object is added or removed). + /// Raised after objects are added or removed in a location. public readonly ManagedEvent Location_ObjectsChanged; /**** @@ -160,6 +156,10 @@ namespace StardewModdingAPI.Framework.Events /// Raised after the player levels up a skill. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. public readonly ManagedEvent Player_LeveledUp; + /// Raised after the player warps to a new location. + public readonly ManagedEvent Player_Warped; + + /**** ** SaveEvents ****/ @@ -241,9 +241,8 @@ namespace StardewModdingAPI.Framework.Events this.Input_ButtonPressed = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonPressed)); this.Input_ButtonReleased = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonReleased)); - this.Location_CurrentLocationChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.CurrentLocationChanged)); - this.Location_LocationsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.LocationsChanged)); - this.Location_LocationObjectsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.LocationObjectsChanged)); + this.Location_LocationsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.LocationsChanged)); + this.Location_BuildingsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.BuildingsChanged)); this.Location_ObjectsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.ObjectsChanged)); this.Menu_Changed = ManageEventOf(nameof(MenuEvents), nameof(MenuEvents.MenuChanged)); @@ -258,6 +257,7 @@ namespace StardewModdingAPI.Framework.Events this.Player_InventoryChanged = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.InventoryChanged)); this.Player_LeveledUp = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.LeveledUp)); + this.Player_Warped = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.Warped)); this.Save_BeforeCreate = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.BeforeCreate)); this.Save_AfterCreate = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterCreate)); diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index d2ba85f8..c8c30834 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -23,7 +23,6 @@ using StardewValley.Menus; using StardewValley.Tools; using xTile.Dimensions; using xTile.Layers; -using SObject = StardewValley.Object; namespace StardewModdingAPI.Framework { @@ -92,11 +91,8 @@ namespace StardewModdingAPI.Framework /// Tracks changes to the save ID. private readonly IValueWatcher SaveIdWatcher; - /// Tracks changes to the location list. - private readonly ICollectionWatcher LocationListWatcher; - - /// Tracks changes to each location. - private readonly IDictionary LocationWatchers = new Dictionary(); + /// Tracks changes to the game's locations. + private readonly WorldLocationsTracker LocationsWatcher; /// Tracks changes to . private readonly IValueWatcher ActiveMenuWatcher; @@ -165,14 +161,14 @@ namespace StardewModdingAPI.Framework this.WindowSizeWatcher = WatcherFactory.ForEquatable(() => new Point(Game1.viewport.Width, Game1.viewport.Height)); this.TimeWatcher = WatcherFactory.ForEquatable(() => Game1.timeOfDay); this.ActiveMenuWatcher = WatcherFactory.ForReference(() => Game1.activeClickableMenu); - this.LocationListWatcher = WatcherFactory.ForObservableCollection((ObservableCollection)Game1.locations); + this.LocationsWatcher = new WorldLocationsTracker((ObservableCollection)Game1.locations); this.Watchers.AddRange(new IWatcher[] { this.SaveIdWatcher, this.WindowSizeWatcher, this.TimeWatcher, this.ActiveMenuWatcher, - this.LocationListWatcher + this.LocationsWatcher }); } @@ -351,22 +347,7 @@ namespace StardewModdingAPI.Framework foreach (IWatcher watcher in this.Watchers) watcher.Update(); this.CurrentPlayerTracker?.Update(); - - // update location watchers - if (this.LocationListWatcher.IsChanged) - { - foreach (GameLocation location in this.LocationListWatcher.Removed.Union(this.LocationListWatcher.Added)) - { - if (this.LocationWatchers.TryGetValue(location, out LocationTracker watcher)) - { - this.Watchers.Remove(watcher); - this.LocationWatchers.Remove(location); - watcher.Dispose(); - } - } - foreach (GameLocation location in this.LocationListWatcher.Added) - this.LocationWatchers[location] = new LocationTracker(location); - } + this.LocationsWatcher.Update(); /********* ** Locale changed events @@ -516,45 +497,86 @@ namespace StardewModdingAPI.Framework *********/ if (Context.IsWorldReady) { - // update player info - PlayerTracker curPlayer = this.CurrentPlayerTracker; + bool raiseWorldEvents = !this.SaveIdWatcher.IsChanged; // don't report changes from unloaded => loaded - // raise current location changed - if (curPlayer.TryGetNewLocation(out GameLocation newLocation)) + // raise location changes + if (this.LocationsWatcher.IsChanged) { - if (this.VerboseLogging) - this.Monitor.Log($"Context: set location to {newLocation.Name}.", LogLevel.Trace); - this.Events.Location_CurrentLocationChanged.Raise(new EventArgsCurrentLocationChanged(curPlayer.LocationWatcher.PreviousValue, newLocation)); + // location list changes + if (this.LocationsWatcher.IsLocationListChanged) + { + GameLocation[] added = this.LocationsWatcher.Added.ToArray(); + GameLocation[] removed = this.LocationsWatcher.Removed.ToArray(); + this.LocationsWatcher.ResetLocationList(); + + if (this.VerboseLogging) + { + string addedText = this.LocationsWatcher.Added.Any() ? string.Join(", ", added.Select(p => p.Name)) : "none"; + string removedText = this.LocationsWatcher.Removed.Any() ? string.Join(", ", removed.Select(p => p.Name)) : "none"; + this.Monitor.Log($"Context: location list changed (added {addedText}; removed {removedText}).", LogLevel.Trace); + } + + this.Events.Location_LocationsChanged.Raise(new EventArgsLocationsChanged(added, removed)); + } + + // raise location contents changed + if (raiseWorldEvents) + { + foreach (LocationTracker watcher in this.LocationsWatcher.Locations) + { + // objects changed + if (watcher.ObjectsWatcher.IsChanged) + { + GameLocation location = watcher.Location; + var added = watcher.ObjectsWatcher.Added; + var removed = watcher.ObjectsWatcher.Removed; + watcher.ObjectsWatcher.Reset(); + + this.Events.Location_ObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, added, removed)); + } + + // buildings changed + if (watcher.BuildingsWatcher.IsChanged) + { + GameLocation location = watcher.Location; + var added = watcher.BuildingsWatcher.Added; + var removed = watcher.BuildingsWatcher.Removed; + watcher.BuildingsWatcher.Reset(); + + this.Events.Location_BuildingsChanged.Raise(new EventArgsLocationBuildingsChanged(location, added, removed)); + } + } + } + else + this.LocationsWatcher.Reset(); } - // raise location list changed - if (this.LocationListWatcher.IsChanged) + // raise time changed + if (raiseWorldEvents && this.TimeWatcher.IsChanged) { + int was = this.TimeWatcher.PreviousValue; + int now = this.TimeWatcher.CurrentValue; + this.TimeWatcher.Reset(); + if (this.VerboseLogging) - { - string added = this.LocationListWatcher.Added.Any() ? string.Join(", ", this.LocationListWatcher.Added.Select(p => p.Name)) : "none"; - string removed = this.LocationListWatcher.Removed.Any() ? string.Join(", ", this.LocationListWatcher.Removed.Select(p => p.Name)) : "none"; - this.Monitor.Log($"Context: location list changed (added {added}; removed {removed}).", LogLevel.Trace); - } + this.Monitor.Log($"Context: time changed from {was} to {now}.", LogLevel.Trace); - this.Events.Location_LocationsChanged.Raise(new EventArgsGameLocationsChanged(Game1.locations)); + this.Events.Time_TimeOfDayChanged.Raise(new EventArgsIntChanged(was, now)); } + else + this.TimeWatcher.Reset(); - // raise events that shouldn't be triggered on initial load - if (!this.SaveIdWatcher.IsChanged) + // raise player events + if (raiseWorldEvents) { - // raise location objects changed - foreach (LocationTracker watcher in this.LocationWatchers.Values) - { - if (watcher.LocationObjectsWatcher.IsChanged) - { - GameLocation location = watcher.Location; - var added = watcher.LocationObjectsWatcher.Added; - var removed = watcher.LocationObjectsWatcher.Removed; - watcher.Reset(); + PlayerTracker curPlayer = this.CurrentPlayerTracker; - this.Events.Location_ObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, added, removed, watcher.Location.netObjects.FieldDict)); - } + // raise current location changed + if (curPlayer.TryGetNewLocation(out GameLocation newLocation)) + { + if (this.VerboseLogging) + this.Monitor.Log($"Context: set location to {newLocation.Name}.", LogLevel.Trace); + this.Events.Player_Warped.Raise(new EventArgsPlayerWarped(curPlayer.LocationWatcher.PreviousValue, newLocation)); } // raise player leveled up a skill @@ -574,27 +596,6 @@ namespace StardewModdingAPI.Framework this.Events.Player_InventoryChanged.Raise(new EventArgsInventoryChanged(Game1.player.Items, changedItems.ToList())); } - // raise current location's object list changed - { - if (curPlayer.TryGetLocationChanges(out IDictionaryWatcher watcher)) - { - if (this.VerboseLogging) - this.Monitor.Log("Context: current location objects changed.", LogLevel.Trace); - - GameLocation location = curPlayer.GetCurrentLocation(); - - this.Events.Location_LocationObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, watcher.Added, watcher.Removed, location.netObjects.FieldDict)); - } - } - - // raise time changed - if (this.TimeWatcher.IsChanged) - { - if (this.VerboseLogging) - this.Monitor.Log($"Context: time changed from {this.TimeWatcher.PreviousValue} to {this.TimeWatcher.CurrentValue}.", LogLevel.Trace); - this.Events.Time_TimeOfDayChanged.Raise(new EventArgsIntChanged(this.TimeWatcher.PreviousValue, this.TimeWatcher.CurrentValue)); - } - // raise mine level changed if (curPlayer.TryGetNewMineLevel(out int mineLevel)) { @@ -603,18 +604,11 @@ namespace StardewModdingAPI.Framework this.Events.Mine_LevelChanged.Raise(new EventArgsMineLevelChanged(curPlayer.MineLevelWatcher.PreviousValue, mineLevel)); } } + this.CurrentPlayerTracker?.Reset(); } - // update state - this.CurrentPlayerTracker?.Reset(); - this.LocationListWatcher.Reset(); + // update save ID watcher this.SaveIdWatcher.Reset(); - this.TimeWatcher.Reset(); - if (!Context.IsWorldReady) - { - foreach (LocationTracker watcher in this.LocationWatchers.Values) - watcher.Reset(); - } /********* ** Game update diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs new file mode 100644 index 00000000..f92edb90 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using Netcode; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// A watcher which detects changes to a Netcode collection. + internal class NetCollectionWatcher : BaseDisposableWatcher, ICollectionWatcher + where TValue : INetObject + { + /********* + ** Properties + *********/ + /// The field being watched. + private readonly NetCollection Field; + + /// The pairs added since the last reset. + private readonly List AddedImpl = new List(); + + /// The pairs demoved since the last reset. + private readonly List RemovedImpl = new List(); + + + /********* + ** Accessors + *********/ + /// Whether the collection changed since the last reset. + public bool IsChanged => this.AddedImpl.Count > 0 || this.RemovedImpl.Count > 0; + + /// The values added since the last reset. + public IEnumerable Added => this.AddedImpl; + + /// The values removed since the last reset. + public IEnumerable Removed => this.RemovedImpl; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The field to watch. + public NetCollectionWatcher(NetCollection field) + { + this.Field = field; + field.OnValueAdded += this.OnValueAdded; + field.OnValueRemoved += this.OnValueRemoved; + } + + /// Update the current value if needed. + public void Update() + { + this.AssertNotDisposed(); + } + + /// Set the current value as the baseline. + public void Reset() + { + this.AssertNotDisposed(); + + this.AddedImpl.Clear(); + this.RemovedImpl.Clear(); + } + + /// Stop watching the field and release all references. + public override void Dispose() + { + if (!this.IsDisposed) + { + this.Field.OnValueAdded -= this.OnValueAdded; + this.Field.OnValueRemoved -= this.OnValueRemoved; + } + + base.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// A callback invoked when an entry is added to the collection. + /// The added value. + private void OnValueAdded(TValue value) + { + this.AddedImpl.Add(value); + } + + /// A callback invoked when an entry is removed from the collection. + /// The added value. + private void OnValueRemoved(TValue value) + { + this.RemovedImpl.Add(value); + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs index bf261bb5..a4982faa 100644 --- a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs @@ -36,6 +36,14 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers return new ObservableCollectionWatcher(collection); } + /// Get a watcher for a net collection. + /// The value type. + /// The net collection. + public static NetCollectionWatcher ForNetCollection(NetCollection collection) where T : INetObject + { + return new NetCollectionWatcher(collection); + } + /// Get a watcher for a net dictionary. /// The dictionary key type. /// The dictionary value type. diff --git a/src/SMAPI/Framework/StateTracking/LocationTracker.cs b/src/SMAPI/Framework/StateTracking/LocationTracker.cs index 8cf4e7a2..07570401 100644 --- a/src/SMAPI/Framework/StateTracking/LocationTracker.cs +++ b/src/SMAPI/Framework/StateTracking/LocationTracker.cs @@ -1,8 +1,11 @@ using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using Microsoft.Xna.Framework; using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewValley; +using StardewValley.Buildings; +using StardewValley.Locations; using Object = StardewValley.Object; namespace StardewModdingAPI.Framework.StateTracking @@ -26,8 +29,11 @@ namespace StardewModdingAPI.Framework.StateTracking /// The tracked location. public GameLocation Location { get; } - /// Tracks changes to the current location's objects. - public IDictionaryWatcher LocationObjectsWatcher { get; } + /// Tracks changes to the location's buildings. + public ICollectionWatcher BuildingsWatcher { get; } + + /// Tracks changes to the location's objects. + public IDictionaryWatcher ObjectsWatcher { get; } /********* @@ -40,10 +46,15 @@ namespace StardewModdingAPI.Framework.StateTracking this.Location = location; // init watchers - this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(location.netObjects); - this.Watchers.AddRange(new[] + this.ObjectsWatcher = WatcherFactory.ForNetDictionary(location.netObjects); + this.BuildingsWatcher = location is BuildableGameLocation buildableLocation + ? WatcherFactory.ForNetCollection(buildableLocation.buildings) + : (ICollectionWatcher)WatcherFactory.ForObservableCollection(new ObservableCollection()); + + this.Watchers.AddRange(new IWatcher[] { - this.LocationObjectsWatcher + this.BuildingsWatcher, + this.ObjectsWatcher }); } diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs index 032705b7..dea2e30d 100644 --- a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs +++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs @@ -1,12 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; -using Microsoft.Xna.Framework; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewValley; using StardewValley.Locations; -using SObject = StardewValley.Object; namespace StardewModdingAPI.Framework.StateTracking { @@ -38,9 +36,6 @@ namespace StardewModdingAPI.Framework.StateTracking /// The player's current location. public IValueWatcher LocationWatcher { get; } - /// Tracks changes to the player's current location's objects. - public IDictionaryWatcher LocationObjectsWatcher { get; private set; } - /// The player's current mine level. public IValueWatcher MineLevelWatcher { get; } @@ -61,7 +56,6 @@ namespace StardewModdingAPI.Framework.StateTracking // init trackers this.LocationWatcher = WatcherFactory.ForReference(this.GetCurrentLocation); - this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(this.GetCurrentLocation().netObjects); this.MineLevelWatcher = WatcherFactory.ForEquatable(() => this.LastValidLocation is MineShaft mine ? mine.mineLevel : 0); this.SkillWatchers = new Dictionary> { @@ -77,7 +71,6 @@ namespace StardewModdingAPI.Framework.StateTracking this.Watchers.AddRange(new IWatcher[] { this.LocationWatcher, - this.LocationObjectsWatcher, this.MineLevelWatcher }); this.Watchers.AddRange(this.SkillWatchers.Values); @@ -93,16 +86,6 @@ namespace StardewModdingAPI.Framework.StateTracking foreach (IWatcher watcher in this.Watchers) watcher.Update(); - // replace location objects watcher - if (this.LocationWatcher.IsChanged) - { - this.Watchers.Remove(this.LocationObjectsWatcher); - this.LocationObjectsWatcher.Dispose(); - - this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(this.GetCurrentLocation().netObjects); - this.Watchers.Add(this.LocationObjectsWatcher); - } - // update inventory this.CurrentInventory = this.GetInventory(); } @@ -154,21 +137,6 @@ namespace StardewModdingAPI.Framework.StateTracking return this.LocationWatcher.IsChanged; } - /// Get object changes to the player's current location if they there as of the last reset. - /// The object change watcher. - /// Returns whether it changed. - public bool TryGetLocationChanges(out IDictionaryWatcher watcher) - { - if (this.LocationWatcher.IsChanged) - { - watcher = null; - return false; - } - - watcher = this.LocationObjectsWatcher; - return watcher.IsChanged; - } - /// Get the player's new mine level if it changed. /// The player's current mine level. /// Returns whether it changed. diff --git a/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs b/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs new file mode 100644 index 00000000..d9090c08 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs @@ -0,0 +1,221 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using StardewModdingAPI.Framework.StateTracking.FieldWatchers; +using StardewValley; +using StardewValley.Buildings; +using StardewValley.Locations; + +namespace StardewModdingAPI.Framework.StateTracking +{ + /// Detects changes to the game's locations. + internal class WorldLocationsTracker : IWatcher + { + /********* + ** Properties + *********/ + /// Tracks changes to the location list. + private readonly ICollectionWatcher LocationListWatcher; + + /// A lookup of the tracked locations. + private IDictionary LocationDict { get; } = new Dictionary(); + + /// A lookup of registered buildings and their indoor location. + private readonly IDictionary BuildingIndoors = new Dictionary(); + + + /********* + ** Accessors + *********/ + /// Whether locations were added or removed since the last reset. + public bool IsLocationListChanged => this.Added.Any() || this.Removed.Any(); + + /// Whether any tracked location data changed since the last reset. + public bool IsChanged => this.IsLocationListChanged || this.Locations.Any(p => p.IsChanged); + + /// The tracked locations. + public IEnumerable Locations => this.LocationDict.Values; + + /// The locations removed since the last update. + public ICollection Added { get; } = new HashSet(); + + /// The locations added since the last update. + public ICollection Removed { get; } = new HashSet(); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The game's list of locations. + public WorldLocationsTracker(ObservableCollection locations) + { + this.LocationListWatcher = WatcherFactory.ForObservableCollection(locations); + } + + /// Update the current value if needed. + public void Update() + { + // detect location changes + if (this.LocationListWatcher.IsChanged) + { + this.Remove(this.LocationListWatcher.Removed); + this.Add(this.LocationListWatcher.Added); + } + + // detect building changes + foreach (LocationTracker watcher in this.Locations.ToArray()) + { + if (watcher.BuildingsWatcher.IsChanged) + { + this.Remove(watcher.BuildingsWatcher.Removed); + this.Add(watcher.BuildingsWatcher.Added); + } + } + + // detect building interior changed (e.g. construction completed) + foreach (KeyValuePair pair in this.BuildingIndoors.Where(p => !object.Equals(p.Key.indoors.Value, p.Value))) + { + GameLocation oldIndoors = pair.Value; + GameLocation newIndoors = pair.Key.indoors.Value; + + if (oldIndoors != null) + this.Added.Add(oldIndoors); + if (newIndoors != null) + this.Removed.Add(newIndoors); + } + + // update watchers + foreach (IWatcher watcher in this.Locations) + watcher.Update(); + } + + /// Set the current location list as the baseline. + public void ResetLocationList() + { + this.Removed.Clear(); + this.Added.Clear(); + this.LocationListWatcher.Reset(); + } + + /// Set the current value as the baseline. + public void Reset() + { + this.ResetLocationList(); + foreach (IWatcher watcher in this.Locations) + watcher.Reset(); + } + + /// Stop watching the player fields and release all references. + public void Dispose() + { + this.LocationListWatcher.Dispose(); + foreach (IWatcher watcher in this.Locations) + watcher.Dispose(); + } + + + /********* + ** Private methods + *********/ + /**** + ** Enumerable wrappers + ****/ + /// Add the given buildings. + /// The buildings to add. + public void Add(IEnumerable buildings) + { + foreach (Building building in buildings) + this.Add(building); + } + + /// Add the given locations. + /// The locations to add. + public void Add(IEnumerable locations) + { + foreach (GameLocation location in locations) + this.Add(location); + } + + /// Remove the given buildings. + /// The buildings to remove. + public void Remove(IEnumerable buildings) + { + foreach (Building building in buildings) + this.Remove(building); + } + + /// Remove the given locations. + /// The locations to remove. + public void Remove(IEnumerable locations) + { + foreach (GameLocation location in locations) + this.Remove(location); + } + + /**** + ** Main add/remove logic + ****/ + /// Add the given building. + /// The building to add. + public void Add(Building building) + { + if (building == null) + return; + + GameLocation indoors = building.indoors.Value; + this.BuildingIndoors[building] = indoors; + this.Add(indoors); + } + + /// Add the given location. + /// The location to add. + public void Add(GameLocation location) + { + if (location == null) + return; + + // remove old location if needed + this.Remove(location); + + // track change + this.Added.Add(location); + + // add + this.LocationDict[location] = new LocationTracker(location); + if (location is BuildableGameLocation buildableLocation) + this.Add(buildableLocation.buildings); + } + + /// Remove the given building. + /// The building to remove. + public void Remove(Building building) + { + if (building == null) + return; + + this.BuildingIndoors.Remove(building); + this.Remove(building.indoors.Value); + } + + /// Remove the given location. + /// The location to remove. + public void Remove(GameLocation location) + { + if (location == null) + return; + + if (this.LocationDict.TryGetValue(location, out LocationTracker watcher)) + { + // track change + this.Removed.Add(location); + + // remove + this.LocationDict.Remove(location); + watcher.Dispose(); + if (location is BuildableGameLocation buildableLocation) + this.Remove(buildableLocation.buildings); + } + } + } +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 9f04887c..54fe9385 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -85,6 +85,7 @@ Properties\GlobalAssemblyInfo.cs + @@ -142,12 +143,14 @@ + + @@ -175,8 +178,8 @@ - - + + -- cgit From 61b023916eb92237b3b10b30b77792139de1097d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 9 May 2018 23:58:58 -0400 Subject: rewrite content logic to decentralise cache (#488) This is necessary due to changes in Stardew Valley 1.3, which now changes loaded assets and expects those changes to be persisted but not propagated to other content managers. --- src/SMAPI/Framework/ContentCoordinator.cs | 175 ++++++ src/SMAPI/Framework/ContentCore.cs | 797 ------------------------ src/SMAPI/Framework/ContentManagerShim.cs | 91 --- src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 38 +- src/SMAPI/Framework/SContentManager.cs | 617 ++++++++++++++++++ src/SMAPI/Framework/SGame.cs | 30 +- src/SMAPI/Program.cs | 10 +- src/SMAPI/StardewModdingAPI.csproj | 4 +- 8 files changed, 841 insertions(+), 921 deletions(-) create mode 100644 src/SMAPI/Framework/ContentCoordinator.cs delete mode 100644 src/SMAPI/Framework/ContentCore.cs delete mode 100644 src/SMAPI/Framework/ContentManagerShim.cs create mode 100644 src/SMAPI/Framework/SContentManager.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs new file mode 100644 index 00000000..86ebc5c3 --- /dev/null +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using Microsoft.Xna.Framework.Content; +using StardewModdingAPI.Framework.Content; +using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Metadata; +using StardewValley; + +namespace StardewModdingAPI.Framework +{ + /// The central logic for creating content managers, invalidating caches, and propagating asset changes. + internal class ContentCoordinator : IDisposable + { + /********* + ** Properties + *********/ + /// Encapsulates monitoring and logging. + private readonly IMonitor Monitor; + + /// Provides metadata for core game assets. + private readonly CoreAssetPropagator CoreAssets; + + /// Simplifies access to private code. + private readonly Reflector Reflection; + + /// The loaded content managers (including the ). + private readonly IList ContentManagers = new List(); + + + /********* + ** Accessors + *********/ + /// The primary content manager used for most assets. + public SContentManager MainContentManager { get; private set; } + + /// The current language as a constant. + public LocalizedContentManager.LanguageCode Language => this.MainContentManager.Language; + + /// Interceptors which provide the initial versions of matching assets. + public IDictionary> Loaders { get; } = new Dictionary>(); + + /// Interceptors which edit matching assets after they're loaded. + public IDictionary> Editors { get; } = new Dictionary>(); + + /// The absolute path to the . + public string FullRootDirectory { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The service provider to use to locate services. + /// The root directory to search for content. + /// The current culture for which to localise content. + /// Encapsulates monitoring and logging. + /// Simplifies access to private code. + public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection) + { + this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); + this.Reflection = reflection; + this.FullRootDirectory = Path.Combine(Constants.ExecutionPath, rootDirectory); + this.ContentManagers.Add( + this.MainContentManager = new SContentManager("Game1.content", serviceProvider, rootDirectory, currentCulture, this, monitor, reflection, isModFolder: false) + ); + this.CoreAssets = new CoreAssetPropagator(this.MainContentManager.NormaliseAssetName, reflection); + } + + /// Get a new content manager which defers loading to the content core. + /// A name for the mod manager. Not guaranteed to be unique. + /// Whether this content manager is wrapped around a mod folder. + /// The root directory to search for content (or null. for the default) + public SContentManager CreateContentManager(string name, bool isModFolder, string rootDirectory = null) + { + return new SContentManager(name, this.MainContentManager.ServiceProvider, rootDirectory ?? this.MainContentManager.RootDirectory, this.MainContentManager.CurrentCulture, this, this.Monitor, this.Reflection, isModFolder); + } + + /// Get the current content locale. + public string GetLocale() => this.MainContentManager.GetLocale(LocalizedContentManager.CurrentLanguageCode); + + /// Convert an absolute file path into a appropriate asset name. + /// The absolute path to the file. + public string GetAssetNameFromFilePath(string absolutePath) => this.MainContentManager.GetAssetNameFromFilePath(absolutePath, ContentSource.GameContent); + + /// Purge assets from the cache that match one of the interceptors. + /// The asset editors for which to purge matching assets. + /// The asset loaders for which to purge matching assets. + /// Returns the invalidated asset names. + public IEnumerable InvalidateCacheFor(IAssetEditor[] editors, IAssetLoader[] loaders) + { + if (!editors.Any() && !loaders.Any()) + return new string[0]; + + // get CanEdit/Load methods + MethodInfo canEdit = typeof(IAssetEditor).GetMethod(nameof(IAssetEditor.CanEdit)); + MethodInfo canLoad = typeof(IAssetLoader).GetMethod(nameof(IAssetLoader.CanLoad)); + if (canEdit == null || canLoad == null) + throw new InvalidOperationException("SMAPI could not access the interceptor methods."); // should never happen + + // invalidate matching keys + return this.InvalidateCache(asset => + { + // check loaders + MethodInfo canLoadGeneric = canLoad.MakeGenericMethod(asset.DataType); + if (loaders.Any(loader => (bool)canLoadGeneric.Invoke(loader, new object[] { asset }))) + return true; + + // check editors + MethodInfo canEditGeneric = canEdit.MakeGenericMethod(asset.DataType); + return editors.Any(editor => (bool)canEditGeneric.Invoke(editor, new object[] { asset })); + }); + } + + /// Purge matched assets from the cache. + /// Matches the asset keys to invalidate. + /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. + /// Returns the invalidated asset keys. + public IEnumerable InvalidateCache(Func predicate, bool dispose = false) + { + string locale = this.GetLocale(); + return this.InvalidateCache((assetName, type) => + { + IAssetInfo info = new AssetInfo(locale, assetName, type, this.MainContentManager.NormaliseAssetName); + return predicate(info); + }); + } + + /// Purge matched assets from the cache. + /// Matches the asset keys to invalidate. + /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. + /// Returns the invalidated asset names. + public IEnumerable InvalidateCache(Func predicate, bool dispose = false) + { + // invalidate cache + HashSet removedAssetNames = new HashSet(); + foreach (SContentManager contentManager in this.ContentManagers) + { + foreach (string name in contentManager.InvalidateCache(predicate, dispose)) + removedAssetNames.Add(name); + } + + // reload core game assets + int reloaded = 0; + foreach (string key in removedAssetNames) + { + if (this.CoreAssets.Propagate(this.MainContentManager, key)) // use an intercepted content manager + reloaded++; + } + + // report result + if (removedAssetNames.Any()) + this.Monitor.Log($"Invalidated {removedAssetNames.Count} asset names: {string.Join(", ", removedAssetNames.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace); + this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); + + return removedAssetNames; + } + + /// Dispose held resources. + public void Dispose() + { + if (this.MainContentManager == null) + return; // already disposed + + this.Monitor.Log("Disposing the content coordinator. Content managers will no longer be usable after this point.", LogLevel.Trace); + foreach (SContentManager contentManager in this.ContentManagers) + contentManager.Dispose(); + this.ContentManagers.Clear(); + this.MainContentManager = null; + } + } +} diff --git a/src/SMAPI/Framework/ContentCore.cs b/src/SMAPI/Framework/ContentCore.cs deleted file mode 100644 index 80fedd6c..00000000 --- a/src/SMAPI/Framework/ContentCore.cs +++ /dev/null @@ -1,797 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Content; -using Microsoft.Xna.Framework.Graphics; -using StardewModdingAPI.Framework.Content; -using StardewModdingAPI.Framework.Exceptions; -using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI.Framework.Utilities; -using StardewModdingAPI.Metadata; -using StardewValley; - -namespace StardewModdingAPI.Framework -{ - /// A thread-safe content handler which loads assets with support for mod injection and editing. - /// - /// This is the centralised content logic which manages all game assets. The game and mods don't use this class - /// directly; instead they use one of several instances, which proxy requests to - /// this class. That ensures that when the game disposes one content manager, the others can continue unaffected. - /// That notably requires this class to be thread-safe, since the content managers can be disposed asynchronously. - /// - /// Note that assets in the cache have two identifiers: the asset name (like "bundles") and key (like "bundles.pt-BR"). - /// For English and non-translatable assets, these have the same value. The underlying cache only knows about asset - /// keys, and the game and mods only know about asset names. The content manager handles resolving them. - /// - internal class ContentCore : IDisposable - { - /********* - ** Properties - *********/ - /// The underlying content manager. - private readonly LocalizedContentManager Content; - - /// Encapsulates monitoring and logging. - private readonly IMonitor Monitor; - - /// The underlying asset cache. - private readonly ContentCache Cache; - - /// A lookup which indicates whether the asset is localisable (i.e. the filename contains the locale), if previously loaded. - private readonly IDictionary IsLocalisableLookup; - - /// The language enum values indexed by locale code. - private readonly IDictionary LanguageCodes; - - /// Provides metadata for core game assets. - private readonly CoreAssetPropagator CoreAssets; - - /// The assets currently being intercepted by instances. This is used to prevent infinite loops when a loader loads a new asset. - private readonly ContextHash AssetsBeingLoaded = new ContextHash(); - - /// A lookup of the content managers which loaded each asset. - private readonly IDictionary> ContentManagersByAssetKey = new Dictionary>(); - - /// The path prefix for assets in mod folders. - private readonly string ModContentPrefix; - - /// A lock used to prevents concurrent changes to the cache while data is being read. - private readonly ReaderWriterLockSlim Lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); - - - /********* - ** Accessors - *********/ - /// The current language as a constant. - public LocalizedContentManager.LanguageCode Language => this.Content.GetCurrentLanguage(); - - /// Interceptors which provide the initial versions of matching assets. - public IDictionary> Loaders { get; } = new Dictionary>(); - - /// Interceptors which edit matching assets after they're loaded. - public IDictionary> Editors { get; } = new Dictionary>(); - - /// The absolute path to the . - public string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.Content.RootDirectory); - - /********* - ** Public methods - *********/ - /**** - ** Constructor - ****/ - /// Construct an instance. - /// The service provider to use to locate services. - /// The root directory to search for content. - /// The current culture for which to localise content. - /// Encapsulates monitoring and logging. - /// Simplifies access to private code. - public ContentCore(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection) - { - // init - this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); - this.Content = new LocalizedContentManager(serviceProvider, rootDirectory, currentCulture); - this.Cache = new ContentCache(this.Content, reflection); - this.ModContentPrefix = this.GetAssetNameFromFilePath(Constants.ModPath); - - // get asset data - this.CoreAssets = new CoreAssetPropagator(this.NormaliseAssetName, reflection); - this.LanguageCodes = this.GetKeyLocales().ToDictionary(p => p.Value, p => p.Key, StringComparer.InvariantCultureIgnoreCase); - this.IsLocalisableLookup = reflection.GetField>(this.Content, "_localizedAsset").GetValue(); - } - - /// Get a new content manager which defers loading to the content core. - /// The content manager's name for logs (if any). - /// The root directory to search for content (or null. for the default) - public ContentManagerShim CreateContentManager(string name, string rootDirectory = null) - { - return new ContentManagerShim(this, name, this.Content.ServiceProvider, rootDirectory ?? this.Content.RootDirectory, this.Content.CurrentCulture); - } - - /**** - ** Asset key/name handling - ****/ - /// Normalise path separators in a file path. For asset keys, see instead. - /// The file path to normalise. - [Pure] - public string NormalisePathSeparators(string path) - { - return this.Cache.NormalisePathSeparators(path); - } - - /// Normalise an asset name so it's consistent with the underlying cache. - /// The asset key. - [Pure] - public string NormaliseAssetName(string assetName) - { - return this.Cache.NormaliseKey(assetName); - } - - /// Assert that the given key has a valid format. - /// The asset key to check. - /// The asset key is empty or contains invalid characters. - [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")] - public void AssertValidAssetKeyFormat(string key) - { - if (string.IsNullOrWhiteSpace(key)) - throw new ArgumentException("The asset key or local path is empty."); - if (key.Intersect(Path.GetInvalidPathChars()).Any()) - throw new ArgumentException("The asset key or local path contains invalid characters."); - } - - /// Convert an absolute file path into a appropriate asset name. - /// The absolute path to the file. - public string GetAssetNameFromFilePath(string absolutePath) - { -#if SMAPI_FOR_WINDOWS - // XNA doesn't allow absolute asset paths, so get a path relative to the content folder - return this.GetRelativePath(absolutePath); -#else - // MonoGame is weird about relative paths on Mac, but allows absolute paths - return absolutePath; -#endif - } - - /**** - ** Content loading - ****/ - /// Get the current content locale. - public string GetLocale() - { - return this.GetLocale(this.Content.GetCurrentLanguage()); - } - - /// The locale for a language. - /// The language. - public string GetLocale(LocalizedContentManager.LanguageCode language) - { - return this.Content.LanguageCodeString(language); - } - - /// Get whether the content manager has already loaded and cached the given asset. - /// The asset path relative to the loader root directory, not including the .xnb extension. - public bool IsLoaded(string assetName) - { - assetName = this.Cache.NormaliseKey(assetName); - return this.WithReadLock(() => this.IsNormalisedKeyLoaded(assetName)); - } - - /// Get the cached asset keys. - public IEnumerable GetAssetKeys() - { - return this.WithReadLock(() => - this.Cache.Keys - .Select(this.GetAssetName) - .Distinct() - ); - } - - /// Load an asset through the content pipeline. When loading a .png file, this must be called outside the game's draw loop. - /// The expected asset type. - /// The asset path relative to the content directory. - /// The content manager instance for which to load the asset. - /// The language code for which to load content. - /// The is empty or contains invalid characters. - /// The content asset couldn't be loaded (e.g. because it doesn't exist). - public T Load(string assetName, ContentManager instance, LocalizedContentManager.LanguageCode language) - { - // normalise asset key - this.AssertValidAssetKeyFormat(assetName); - assetName = this.NormaliseAssetName(assetName); - - // load game content - if (!assetName.StartsWith(this.ModContentPrefix)) - return this.LoadImpl(assetName, instance, language); - - // load mod content - SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading content asset '{assetName}': {reasonPhrase}"); - try - { - return this.WithWriteLock(() => - { - // try cache - if (this.IsLoaded(assetName)) - return this.LoadImpl(assetName, instance, language); - - // get file - FileInfo file = this.GetModFile(assetName); - if (!file.Exists) - throw GetContentError("the specified path doesn't exist."); - - // load content - switch (file.Extension.ToLower()) - { - // XNB file - case ".xnb": - return this.LoadImpl(assetName, instance, language); - - // unpacked map - case ".tbin": - throw GetContentError($"can't read unpacked map file '{assetName}' directly from the underlying content manager. It must be loaded through the mod's {typeof(IModHelper)}.{nameof(IModHelper.Content)} helper."); - - // unpacked image - case ".png": - // validate - if (typeof(T) != typeof(Texture2D)) - throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'."); - - // fetch & cache - using (FileStream stream = File.OpenRead(file.FullName)) - { - Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); - texture = this.PremultiplyTransparency(texture); - this.InjectWithoutLock(assetName, texture, instance); - return (T)(object)texture; - } - - default: - throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.png', '.tbin', or '.xnb'."); - } - }); - } - catch (Exception ex) when (!(ex is SContentLoadException)) - { - if (ex.GetInnermostException() is DllNotFoundException dllEx && dllEx.Message == "libgdiplus.dylib") - throw GetContentError("couldn't find libgdiplus, which is needed to load mod images. Make sure Mono is installed and you're running the game through the normal launcher."); - throw new SContentLoadException($"The content manager failed loading content asset '{assetName}'.", ex); - } - } - - /// Inject an asset into the cache. - /// The type of asset to inject. - /// The asset path relative to the loader root directory, not including the .xnb extension. - /// The asset value. - /// The content manager instance for which to load the asset. - public void Inject(string assetName, T value, ContentManager instance) - { - this.WithWriteLock(() => this.InjectWithoutLock(assetName, value, instance)); - } - - /**** - ** Cache invalidation - ****/ - /// Purge assets from the cache that match one of the interceptors. - /// The asset editors for which to purge matching assets. - /// The asset loaders for which to purge matching assets. - /// Returns whether any cache entries were invalidated. - public bool InvalidateCacheFor(IAssetEditor[] editors, IAssetLoader[] loaders) - { - if (!editors.Any() && !loaders.Any()) - return false; - - // get CanEdit/Load methods - MethodInfo canEdit = typeof(IAssetEditor).GetMethod(nameof(IAssetEditor.CanEdit)); - MethodInfo canLoad = typeof(IAssetLoader).GetMethod(nameof(IAssetLoader.CanLoad)); - if (canEdit == null || canLoad == null) - throw new InvalidOperationException("SMAPI could not access the interceptor methods."); // should never happen - - // invalidate matching keys - return this.InvalidateCache(asset => - { - // check loaders - MethodInfo canLoadGeneric = canLoad.MakeGenericMethod(asset.DataType); - if (loaders.Any(loader => (bool)canLoadGeneric.Invoke(loader, new object[] { asset }))) - return true; - - // check editors - MethodInfo canEditGeneric = canEdit.MakeGenericMethod(asset.DataType); - return editors.Any(editor => (bool)canEditGeneric.Invoke(editor, new object[] { asset })); - }); - } - - /// Purge matched assets from the cache. - /// Matches the asset keys to invalidate. - /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. - /// Returns whether any cache entries were invalidated. - public bool InvalidateCache(Func predicate, bool dispose = false) - { - string locale = this.GetLocale(); - return this.InvalidateCache((assetName, type) => - { - IAssetInfo info = new AssetInfo(locale, assetName, type, this.NormaliseAssetName); - return predicate(info); - }); - } - - /// Purge matched assets from the cache. - /// Matches the asset keys to invalidate. - /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. - /// Returns whether any cache entries were invalidated. - public bool InvalidateCache(Func predicate, bool dispose = false) - { - return this.WithWriteLock(() => - { - // invalidate matching keys - HashSet removeKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); - HashSet removeAssetNames = new HashSet(StringComparer.InvariantCultureIgnoreCase); - this.Cache.Remove((key, type) => - { - this.ParseCacheKey(key, out string assetName, out _); - if (removeAssetNames.Contains(assetName) || predicate(assetName, type)) - { - removeAssetNames.Add(assetName); - removeKeys.Add(key); - return true; - } - return false; - }); - - // update reference tracking - foreach (string key in removeKeys) - this.ContentManagersByAssetKey.Remove(key); - - // reload core game assets - int reloaded = 0; - foreach (string key in removeAssetNames) - { - if (this.CoreAssets.Propagate(Game1.content, key)) // use an intercepted content manager - reloaded++; - } - - // report result - if (removeKeys.Any()) - { - this.Monitor.Log($"Invalidated {removeAssetNames.Count} asset names: {string.Join(", ", removeKeys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace); - return true; - } - this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); - return false; - }); - } - - /**** - ** Disposal - ****/ - /// Dispose assets for the given content manager shim. - /// The content manager whose assets to dispose. - internal void DisposeFor(ContentManagerShim shim) - { - this.Monitor.Log($"Content manager '{shim.Name}' disposed, disposing assets that aren't needed by any other asset loader.", LogLevel.Trace); - - this.WithWriteLock(() => - { - foreach (var entry in this.ContentManagersByAssetKey) - entry.Value.Remove(shim); - this.InvalidateCache((key, type) => !this.ContentManagersByAssetKey.TryGetValue(key, out var managers) || !managers.Any(), dispose: true); - }); - } - - - /********* - ** Private methods - *********/ - /**** - ** Disposal - ****/ - /// Dispose held resources. - public void Dispose() - { - this.Monitor.Log("Disposing SMAPI's main content manager. It will no longer be usable after this point.", LogLevel.Trace); - this.Content.Dispose(); - } - - /**** - ** Asset name/key handling - ****/ - /// Get a directory or file path relative to the content root. - /// The target file path. - private string GetRelativePath(string targetPath) - { - return PathUtilities.GetRelativePath(this.FullRootDirectory, targetPath); - } - - /// Get the locale codes (like ja-JP) used in asset keys. - private IDictionary GetKeyLocales() - { - // create locale => code map - IDictionary map = new Dictionary(); - foreach (LocalizedContentManager.LanguageCode code in Enum.GetValues(typeof(LocalizedContentManager.LanguageCode))) - map[code] = this.Content.LanguageCodeString(code); - - return map; - } - - /// Get the asset name from a cache key. - /// The input cache key. - private string GetAssetName(string cacheKey) - { - this.ParseCacheKey(cacheKey, out string assetName, out string _); - return assetName; - } - - /// Parse a cache key into its component parts. - /// The input cache key. - /// The original asset name. - /// The asset locale code (or null if not localised). - private void ParseCacheKey(string cacheKey, out string assetName, out string localeCode) - { - // handle localised key - if (!string.IsNullOrWhiteSpace(cacheKey)) - { - int lastSepIndex = cacheKey.LastIndexOf(".", StringComparison.InvariantCulture); - if (lastSepIndex >= 0) - { - string suffix = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); - if (this.LanguageCodes.ContainsKey(suffix)) - { - assetName = cacheKey.Substring(0, lastSepIndex); - localeCode = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); - return; - } - } - } - - // handle simple key - assetName = cacheKey; - localeCode = null; - } - - /**** - ** Cache handling - ****/ - /// Get whether an asset has already been loaded. - /// The normalised asset name. - private bool IsNormalisedKeyLoaded(string normalisedAssetName) - { - // default English - if (this.Language == LocalizedContentManager.LanguageCode.en) - return this.Cache.ContainsKey(normalisedAssetName); - - // translated - if (!this.IsLocalisableLookup.TryGetValue(normalisedAssetName, out bool localisable)) - return false; - return localisable - ? this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetLocale(this.Content.GetCurrentLanguage())}") - : this.Cache.ContainsKey(normalisedAssetName); - } - - /// Track that a content manager loaded an asset. - /// The asset key that was loaded. - /// The content manager that loaded the asset. - private void TrackAssetLoader(string key, ContentManager manager) - { - if (!this.ContentManagersByAssetKey.TryGetValue(key, out HashSet hash)) - hash = this.ContentManagersByAssetKey[key] = new HashSet(); - hash.Add(manager); - } - - /**** - ** Content loading - ****/ - /// Load an asset name without heuristics to support mod content. - /// The type of asset to load. - /// The asset path relative to the loader root directory, not including the .xnb extension. - /// The content manager instance for which to load the asset. - /// The language code for which to load content. - private T LoadImpl(string assetName, ContentManager instance, LocalizedContentManager.LanguageCode language) - { - return this.WithWriteLock(() => - { - // skip if already loaded - if (this.IsNormalisedKeyLoaded(assetName)) - { - this.TrackAssetLoader(assetName, instance); - return this.Content.Load(assetName, language); - } - - // load asset - T data; - if (this.AssetsBeingLoaded.Contains(assetName)) - { - this.Monitor.Log($"Broke loop while loading asset '{assetName}'.", LogLevel.Warn); - this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}", LogLevel.Trace); - data = this.Content.Load(assetName, language); - } - else - { - data = this.AssetsBeingLoaded.Track(assetName, () => - { - string locale = this.GetLocale(language); - IAssetInfo info = new AssetInfo(locale, assetName, typeof(T), this.NormaliseAssetName); - IAssetData asset = - this.ApplyLoader(info) - ?? new AssetDataForObject(info, this.Content.Load(assetName, language), this.NormaliseAssetName); - asset = this.ApplyEditors(info, asset); - return (T)asset.Data; - }); - } - - // update cache & return data - this.InjectWithoutLock(assetName, data, instance); - return data; - }); - } - - /// Inject an asset into the cache without acquiring a write lock. This should only be called from within a write lock. - /// The type of asset to inject. - /// The asset path relative to the loader root directory, not including the .xnb extension. - /// The asset value. - /// The content manager instance for which to load the asset. - private void InjectWithoutLock(string assetName, T value, ContentManager instance) - { - assetName = this.NormaliseAssetName(assetName); - this.Cache[assetName] = value; - this.TrackAssetLoader(assetName, instance); - } - - /// Get a file from the mod folder. - /// The asset path relative to the content folder. - private FileInfo GetModFile(string path) - { - // try exact match - FileInfo file = new FileInfo(Path.Combine(this.FullRootDirectory, path)); - - // try with default extension - if (!file.Exists && file.Extension.ToLower() != ".xnb") - { - FileInfo result = new FileInfo(file.FullName + ".xnb"); - if (result.Exists) - file = result; - } - - return file; - } - - /// Load the initial asset from the registered . - /// The basic asset metadata. - /// Returns the loaded asset metadata, or null if no loader matched. - private IAssetData ApplyLoader(IAssetInfo info) - { - // find matching loaders - var loaders = this.GetInterceptors(this.Loaders) - .Where(entry => - { - try - { - return entry.Value.CanLoad(info); - } - catch (Exception ex) - { - entry.Key.LogAsMod($"Mod failed when checking whether it could load asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - return false; - } - }) - .ToArray(); - - // validate loaders - if (!loaders.Any()) - return null; - if (loaders.Length > 1) - { - string[] loaderNames = loaders.Select(p => p.Key.DisplayName).ToArray(); - this.Monitor.Log($"Multiple mods want to provide the '{info.AssetName}' asset ({string.Join(", ", loaderNames)}), but an asset can't be loaded multiple times. SMAPI will use the default asset instead; uninstall one of the mods to fix this. (Message for modders: you should usually use {typeof(IAssetEditor)} instead to avoid conflicts.)", LogLevel.Warn); - return null; - } - - // fetch asset from loader - IModMetadata mod = loaders[0].Key; - IAssetLoader loader = loaders[0].Value; - T data; - try - { - data = loader.Load(info); - this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.AssetName}'.", LogLevel.Trace); - } - catch (Exception ex) - { - mod.LogAsMod($"Mod crashed when loading asset '{info.AssetName}'. SMAPI will use the default asset instead. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - return null; - } - - // validate asset - if (data == null) - { - mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Error); - return null; - } - - // return matched asset - return new AssetDataForObject(info, data, this.NormaliseAssetName); - } - - /// Apply any to a loaded asset. - /// The asset type. - /// The basic asset metadata. - /// The loaded asset. - private IAssetData ApplyEditors(IAssetInfo info, IAssetData asset) - { - IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.NormaliseAssetName); - - // edit asset - foreach (var entry in this.GetInterceptors(this.Editors)) - { - // check for match - IModMetadata mod = entry.Key; - IAssetEditor editor = entry.Value; - try - { - if (!editor.CanEdit(info)) - continue; - } - catch (Exception ex) - { - mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - continue; - } - - // try edit - object prevAsset = asset.Data; - try - { - editor.Edit(asset); - this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace); - } - catch (Exception ex) - { - mod.LogAsMod($"Mod crashed when editing asset '{info.AssetName}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - } - - // validate edit - if (asset.Data == null) - { - mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Warn); - asset = GetNewData(prevAsset); - } - else if (!(asset.Data is T)) - { - mod.LogAsMod($"Mod incorrectly set asset '{asset.AssetName}' to incompatible type '{asset.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn); - asset = GetNewData(prevAsset); - } - } - - // return result - return asset; - } - - /// Get all registered interceptors from a list. - private IEnumerable> GetInterceptors(IDictionary> entries) - { - foreach (var entry in entries) - { - IModMetadata mod = entry.Key; - IList interceptors = entry.Value; - - // registered editors - foreach (T interceptor in interceptors) - yield return new KeyValuePair(mod, interceptor); - } - } - - /// Premultiply a texture's alpha values to avoid transparency issues in the game. This is only possible if the game isn't currently drawing. - /// The texture to premultiply. - /// Returns a premultiplied texture. - /// Based on code by Layoric. - private Texture2D PremultiplyTransparency(Texture2D texture) - { - // validate - if (Context.IsInDrawLoop) - throw new NotSupportedException("Can't load a PNG file while the game is drawing to the screen. Make sure you load content outside the draw loop."); - - // process texture - SpriteBatch spriteBatch = Game1.spriteBatch; - GraphicsDevice gpu = Game1.graphics.GraphicsDevice; - using (RenderTarget2D renderTarget = new RenderTarget2D(Game1.graphics.GraphicsDevice, texture.Width, texture.Height)) - { - // create blank render target to premultiply - gpu.SetRenderTarget(renderTarget); - gpu.Clear(Color.Black); - - // multiply each color by the source alpha, and write just the color values into the final texture - spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState - { - ColorDestinationBlend = Blend.Zero, - ColorWriteChannels = ColorWriteChannels.Red | ColorWriteChannels.Green | ColorWriteChannels.Blue, - AlphaDestinationBlend = Blend.Zero, - AlphaSourceBlend = Blend.SourceAlpha, - ColorSourceBlend = Blend.SourceAlpha - }); - spriteBatch.Draw(texture, texture.Bounds, Color.White); - spriteBatch.End(); - - // copy the alpha values from the source texture into the final one without multiplying them - spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState - { - ColorWriteChannels = ColorWriteChannels.Alpha, - AlphaDestinationBlend = Blend.Zero, - ColorDestinationBlend = Blend.Zero, - AlphaSourceBlend = Blend.One, - ColorSourceBlend = Blend.One - }); - spriteBatch.Draw(texture, texture.Bounds, Color.White); - spriteBatch.End(); - - // release GPU - gpu.SetRenderTarget(null); - - // extract premultiplied data - Color[] data = new Color[texture.Width * texture.Height]; - renderTarget.GetData(data); - - // unset texture from GPU to regain control - gpu.Textures[0] = null; - - // update texture with premultiplied data - texture.SetData(data); - } - - return texture; - } - - /**** - ** Concurrency logic - ****/ - /// Acquire a read lock which prevents concurrent writes to the cache while it's open. - /// The action's return value. - /// The action to perform. - private T WithReadLock(Func action) - { - try - { - this.Lock.EnterReadLock(); - return action(); - } - finally - { - this.Lock.ExitReadLock(); - } - } - - /// Acquire a write lock which prevents concurrent reads or writes to the cache while it's open. - /// The action to perform. - private void WithWriteLock(Action action) - { - try - { - this.Lock.EnterWriteLock(); - action(); - } - finally - { - this.Lock.ExitWriteLock(); - } - } - - /// Acquire a write lock which prevents concurrent reads or writes to the cache while it's open. - /// The action's return value. - /// The action to perform. - private T WithWriteLock(Func action) - { - try - { - this.Lock.EnterWriteLock(); - return action(); - } - finally - { - this.Lock.ExitWriteLock(); - } - } - } -} diff --git a/src/SMAPI/Framework/ContentManagerShim.cs b/src/SMAPI/Framework/ContentManagerShim.cs deleted file mode 100644 index 66754fd7..00000000 --- a/src/SMAPI/Framework/ContentManagerShim.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Globalization; -using StardewValley; - -namespace StardewModdingAPI.Framework -{ - /// A minimal content manager which defers to SMAPI's core content logic. - internal class ContentManagerShim : LocalizedContentManager - { - /********* - ** Properties - *********/ - /// SMAPI's core content logic. - private readonly ContentCore ContentCore; - - - /********* - ** Accessors - *********/ - /// The content manager's name for logs (if any). - public string Name { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// SMAPI's core content logic. - /// The content manager's name for logs (if any). - /// The service provider to use to locate services. - /// The root directory to search for content. - /// The current culture for which to localise content. - public ContentManagerShim(ContentCore contentCore, string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture) - : base(serviceProvider, rootDirectory, currentCulture) - { - this.ContentCore = contentCore; - this.Name = name; - } - - /// Load an asset that has been processed by the content pipeline. - /// The type of asset to load. - /// The asset path relative to the loader root directory, not including the .xnb extension. - public override T Load(string assetName) - { - return this.Load(assetName, LocalizedContentManager.CurrentLanguageCode); - } - - /// Load an asset that has been processed by the content pipeline. - /// The type of asset to load. - /// The asset path relative to the loader root directory, not including the .xnb extension. - /// The language code for which to load content. - public override T Load(string assetName, LanguageCode language) - { - return this.ContentCore.Load(assetName, this, language); - } - - /// Load the base asset without localisation. - /// The type of asset to load. - /// The asset path relative to the loader root directory, not including the .xnb extension. - public override T LoadBase(string assetName) - { - return this.Load(assetName, LanguageCode.en); - } - - /// Inject an asset into the cache. - /// The type of asset to inject. - /// The asset path relative to the loader root directory, not including the .xnb extension. - /// The asset value. - public void Inject(string assetName, T value) - { - this.ContentCore.Inject(assetName, value, this); - } - - /// Create a new content manager for temporary use. - public override LocalizedContentManager CreateTemporary() - { - return this.ContentCore.CreateContentManager("(temporary)"); - } - - - /********* - ** Protected methods - *********/ - /// Dispose held resources. - /// Whether the content manager is disposing (rather than finalising). - protected override void Dispose(bool disposing) - { - this.ContentCore.DisposeFor(this); - } - } -} diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index c7d4c39e..4a71f7e7 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -23,10 +23,10 @@ namespace StardewModdingAPI.Framework.ModHelpers ** Properties *********/ /// SMAPI's core content logic. - private readonly ContentCore ContentCore; + private readonly ContentCoordinator ContentCore; /// The content manager for this mod. - private readonly ContentManagerShim ContentManager; + private readonly SContentManager ContentManager; /// The absolute path to the mod folder. private readonly string ModFolderPath; @@ -42,10 +42,10 @@ namespace StardewModdingAPI.Framework.ModHelpers ** Accessors *********/ /// The game's current locale code (like pt-BR). - public string CurrentLocale => this.ContentCore.GetLocale(); + public string CurrentLocale => this.ContentManager.GetLocale(); /// The game's current locale as an enum value. - public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.ContentCore.Language; + public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.ContentManager.Language; /// The observable implementation of . internal ObservableCollection ObservableAssetEditors { get; } = new ObservableCollection(); @@ -70,7 +70,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The unique ID of the relevant mod. /// The friendly mod name for use in errors. /// Encapsulates monitoring and logging. - public ContentHelper(ContentCore contentCore, ContentManagerShim contentManager, string modFolderPath, string modID, string modName, IMonitor monitor) + public ContentHelper(ContentCoordinator contentCore, SContentManager contentManager, string modFolderPath, string modID, string modName, IMonitor monitor) : base(modID) { this.ContentCore = contentCore; @@ -96,7 +96,7 @@ namespace StardewModdingAPI.Framework.ModHelpers switch (source) { case ContentSource.GameContent: - return this.ContentManager.Load(key); + return this.ContentCore.MainContentManager.Load(key); case ContentSource.ModFolder: // get file @@ -105,10 +105,10 @@ namespace StardewModdingAPI.Framework.ModHelpers throw GetContentError($"there's no matching file at path '{file.FullName}'."); // get asset path - string assetName = this.ContentCore.GetAssetNameFromFilePath(file.FullName); + string assetName = this.ContentManager.GetAssetNameFromFilePath(file.FullName, ContentSource.ModFolder); // try cache - if (this.ContentCore.IsLoaded(assetName)) + if (this.ContentManager.IsLoaded(assetName)) return this.ContentManager.Load(assetName); // fix map tilesheets @@ -146,7 +146,7 @@ namespace StardewModdingAPI.Framework.ModHelpers [Pure] public string NormaliseAssetName(string assetName) { - return this.ContentCore.NormaliseAssetName(assetName); + return this.ContentManager.NormaliseAssetName(assetName); } /// Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists. @@ -158,11 +158,11 @@ namespace StardewModdingAPI.Framework.ModHelpers switch (source) { case ContentSource.GameContent: - return this.ContentCore.NormaliseAssetName(key); + return this.ContentManager.NormaliseAssetName(key); case ContentSource.ModFolder: FileInfo file = this.GetModFile(key); - return this.ContentCore.NormaliseAssetName(this.ContentCore.GetAssetNameFromFilePath(file.FullName)); + return this.ContentManager.NormaliseAssetName(this.ContentManager.GetAssetNameFromFilePath(file.FullName, ContentSource.GameContent)); default: throw new NotSupportedException($"Unknown content source '{source}'."); @@ -177,7 +177,7 @@ namespace StardewModdingAPI.Framework.ModHelpers { string actualKey = this.GetActualAssetKey(key, ContentSource.GameContent); this.Monitor.Log($"Requested cache invalidation for '{actualKey}'.", LogLevel.Trace); - return this.ContentCore.InvalidateCache(asset => asset.AssetNameEquals(actualKey)); + return this.ContentCore.InvalidateCache(asset => asset.AssetNameEquals(actualKey)).Any(); } /// Remove all assets of the given type from the cache so they're reloaded on the next request. This can be a very expensive operation and should only be used in very specific cases. This will reload core game assets if needed, but references to the former assets will still show the previous content. @@ -186,7 +186,7 @@ namespace StardewModdingAPI.Framework.ModHelpers public bool InvalidateCache() { this.Monitor.Log($"Requested cache invalidation for all assets of type {typeof(T)}. This is an expensive operation and should be avoided if possible.", LogLevel.Trace); - return this.ContentCore.InvalidateCache((key, type) => typeof(T).IsAssignableFrom(type)); + return this.ContentCore.InvalidateCache((key, type) => typeof(T).IsAssignableFrom(type)).Any(); } /// Remove matching assets from the content cache so they're reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content. @@ -195,7 +195,7 @@ namespace StardewModdingAPI.Framework.ModHelpers public bool InvalidateCache(Func predicate) { this.Monitor.Log("Requested cache invalidation for all assets matching a predicate.", LogLevel.Trace); - return this.ContentCore.InvalidateCache(predicate); + return this.ContentCore.InvalidateCache(predicate).Any(); } /********* @@ -207,7 +207,7 @@ namespace StardewModdingAPI.Framework.ModHelpers [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")] private void AssertValidAssetKeyFormat(string key) { - this.ContentCore.AssertValidAssetKeyFormat(key); + this.ContentManager.AssertValidAssetKeyFormat(key); if (Path.IsPathRooted(key)) throw new ArgumentException("The asset key must not be an absolute path."); } @@ -235,7 +235,7 @@ namespace StardewModdingAPI.Framework.ModHelpers // get map info if (!map.TileSheets.Any()) return; - mapKey = this.ContentCore.NormaliseAssetName(mapKey); // Mono's Path.GetDirectoryName doesn't handle Windows dir separators + mapKey = this.ContentManager.NormaliseAssetName(mapKey); // Mono's Path.GetDirectoryName doesn't handle Windows dir separators string relativeMapFolder = Path.GetDirectoryName(mapKey) ?? ""; // folder path containing the map, relative to the mod folder // fix tilesheets @@ -251,7 +251,7 @@ namespace StardewModdingAPI.Framework.ModHelpers string seasonalImageSource = null; if (Game1.currentSeason != null) { - string filename = Path.GetFileName(imageSource); + string filename = Path.GetFileName(imageSource) ?? throw new InvalidOperationException($"The '{imageSource}' tilesheet couldn't be loaded: filename is unexpectedly null."); bool hasSeasonalPrefix = filename.StartsWith("spring_", StringComparison.CurrentCultureIgnoreCase) || filename.StartsWith("summer_", StringComparison.CurrentCultureIgnoreCase) @@ -341,7 +341,7 @@ namespace StardewModdingAPI.Framework.ModHelpers private FileInfo GetModFile(string path) { // try exact match - path = Path.Combine(this.ModFolderPath, this.ContentCore.NormalisePathSeparators(path)); + path = Path.Combine(this.ModFolderPath, this.ContentManager.NormalisePathSeparators(path)); FileInfo file = new FileInfo(path); // try with default extension @@ -360,7 +360,7 @@ namespace StardewModdingAPI.Framework.ModHelpers private FileInfo GetContentFolderFile(string key) { // get file path - string path = Path.Combine(this.ContentCore.FullRootDirectory, key); + string path = Path.Combine(this.ContentManager.FullRootDirectory, key); if (!path.EndsWith(".xnb")) path += ".xnb"; diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs new file mode 100644 index 00000000..8f008041 --- /dev/null +++ b/src/SMAPI/Framework/SContentManager.cs @@ -0,0 +1,617 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.IO; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.Content; +using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.Utilities; +using StardewValley; + +namespace StardewModdingAPI.Framework +{ + /// A minimal content manager which defers to SMAPI's core content logic. + internal class SContentManager : LocalizedContentManager + { + /********* + ** Properties + *********/ + /// The central coordinator which manages content managers. + private readonly ContentCoordinator Coordinator; + + /// The underlying asset cache. + private readonly ContentCache Cache; + + /// Encapsulates monitoring and logging. + private readonly IMonitor Monitor; + + /// A lookup which indicates whether the asset is localisable (i.e. the filename contains the locale), if previously loaded. + private readonly IDictionary IsLocalisableLookup; + + /// The language enum values indexed by locale code. + private readonly IDictionary LanguageCodes; + + /// The assets currently being intercepted by instances. This is used to prevent infinite loops when a loader loads a new asset. + private readonly ContextHash AssetsBeingLoaded = new ContextHash(); + + /// The path prefix for assets in mod folders. + private readonly string ModContentPrefix; + + /// Interceptors which provide the initial versions of matching assets. + private IDictionary> Loaders => this.Coordinator.Loaders; + + /// Interceptors which edit matching assets after they're loaded. + private IDictionary> Editors => this.Coordinator.Editors; + + + /********* + ** Accessors + *********/ + /// A name for the mod manager. Not guaranteed to be unique. + public string Name { get; } + + /// Whether this content manager is wrapped around a mod folder. + public bool IsModFolder { get; } + + /// The current language as a constant. + public LocalizedContentManager.LanguageCode Language => this.GetCurrentLanguage(); + + /// The absolute path to the . + public string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A name for the mod manager. Not guaranteed to be unique. + /// The service provider to use to locate services. + /// The root directory to search for content. + /// The current culture for which to localise content. + /// The central coordinator which manages content managers. + /// Encapsulates monitoring and logging. + /// Simplifies access to private code. + /// Whether this content manager is wrapped around a mod folder. + public SContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, bool isModFolder) + : base(serviceProvider, rootDirectory, currentCulture) + { + // init + this.Name = name; + this.IsModFolder = isModFolder; + this.Coordinator = coordinator ?? throw new ArgumentNullException(nameof(coordinator)); + this.Cache = new ContentCache(this, reflection); + this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); + this.ModContentPrefix = this.GetAssetNameFromFilePath(Constants.ModPath, ContentSource.GameContent); + + // get asset data + this.LanguageCodes = this.GetKeyLocales().ToDictionary(p => p.Value, p => p.Key, StringComparer.InvariantCultureIgnoreCase); + this.IsLocalisableLookup = reflection.GetField>(this, "_localizedAsset").GetValue(); + + } + + /// Load an asset that has been processed by the content pipeline. + /// The type of asset to load. + /// The asset path relative to the loader root directory, not including the .xnb extension. + public override T Load(string assetName) + { + return this.Load(assetName, LocalizedContentManager.CurrentLanguageCode); + } + + /// Load an asset that has been processed by the content pipeline. + /// The type of asset to load. + /// The asset path relative to the loader root directory, not including the .xnb extension. + /// The language code for which to load content. + public override T Load(string assetName, LanguageCode language) + { + // normalise asset key + this.AssertValidAssetKeyFormat(assetName); + assetName = this.NormaliseAssetName(assetName); + + // load game content + if (!this.IsModFolder && !assetName.StartsWith(this.ModContentPrefix)) + return this.LoadImpl(assetName, language); + + // load mod content + SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading content asset '{assetName}': {reasonPhrase}"); + try + { + // try cache + if (this.IsLoaded(assetName)) + return this.LoadImpl(assetName, language); + + // get file + FileInfo file = this.GetModFile(assetName); + if (!file.Exists) + throw GetContentError("the specified path doesn't exist."); + + // load content + switch (file.Extension.ToLower()) + { + // XNB file + case ".xnb": + return this.LoadImpl(assetName, language); + + // unpacked map + case ".tbin": + throw GetContentError($"can't read unpacked map file '{assetName}' directly from the underlying content manager. It must be loaded through the mod's {typeof(IModHelper)}.{nameof(IModHelper.Content)} helper."); + + // unpacked image + case ".png": + // validate + if (typeof(T) != typeof(Texture2D)) + throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'."); + + // fetch & cache + using (FileStream stream = File.OpenRead(file.FullName)) + { + Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); + texture = this.PremultiplyTransparency(texture); + this.Inject(assetName, texture); + return (T)(object)texture; + } + + default: + throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.png', '.tbin', or '.xnb'."); + } + } + catch (Exception ex) when (!(ex is SContentLoadException)) + { + if (ex.GetInnermostException() is DllNotFoundException dllEx && dllEx.Message == "libgdiplus.dylib") + throw GetContentError("couldn't find libgdiplus, which is needed to load mod images. Make sure Mono is installed and you're running the game through the normal launcher."); + throw new SContentLoadException($"The content manager failed loading content asset '{assetName}'.", ex); + } + } + + /// Load the base asset without localisation. + /// The type of asset to load. + /// The asset path relative to the loader root directory, not including the .xnb extension. + public override T LoadBase(string assetName) + { + return this.Load(assetName, LanguageCode.en); + } + + /// Inject an asset into the cache. + /// The type of asset to inject. + /// The asset path relative to the loader root directory, not including the .xnb extension. + /// The asset value. + public void Inject(string assetName, T value) + { + assetName = this.NormaliseAssetName(assetName); + this.Cache[assetName] = value; + } + + /// Create a new content manager for temporary use. + public override LocalizedContentManager CreateTemporary() + { + return this.Coordinator.CreateContentManager("(temporary)", isModFolder: false); + } + + /// Normalise path separators in a file path. For asset keys, see instead. + /// The file path to normalise. + [Pure] + public string NormalisePathSeparators(string path) + { + return this.Cache.NormalisePathSeparators(path); + } + + /// Normalise an asset name so it's consistent with the underlying cache. + /// The asset key. + [Pure] + public string NormaliseAssetName(string assetName) + { + return this.Cache.NormaliseKey(assetName); + } + + /// Assert that the given key has a valid format. + /// The asset key to check. + /// The asset key is empty or contains invalid characters. + [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")] + public void AssertValidAssetKeyFormat(string key) + { + if (string.IsNullOrWhiteSpace(key)) + throw new ArgumentException("The asset key or local path is empty."); + if (key.Intersect(Path.GetInvalidPathChars()).Any()) + throw new ArgumentException("The asset key or local path contains invalid characters."); + } + + /// Convert an absolute file path into an appropriate asset name. + /// The absolute path to the file. + /// The folder to which to get a relative path. + public string GetAssetNameFromFilePath(string absolutePath, ContentSource relativeTo) + { +#if SMAPI_FOR_WINDOWS + // XNA doesn't allow absolute asset paths, so get a path relative to the source folder + string sourcePath = relativeTo == ContentSource.GameContent ? this.Coordinator.FullRootDirectory : this.FullRootDirectory; + return this.GetRelativePath(sourcePath, absolutePath); +#else + // MonoGame is weird about relative paths on Mac, but allows absolute paths + return absolutePath; +#endif + } + + /**** + ** Content loading + ****/ + /// Get the current content locale. + public string GetLocale() + { + return this.GetLocale(this.GetCurrentLanguage()); + } + + /// The locale for a language. + /// The language. + public string GetLocale(LocalizedContentManager.LanguageCode language) + { + return this.LanguageCodeString(language); + } + + /// Get whether the content manager has already loaded and cached the given asset. + /// The asset path relative to the loader root directory, not including the .xnb extension. + public bool IsLoaded(string assetName) + { + assetName = this.Cache.NormaliseKey(assetName); + return this.IsNormalisedKeyLoaded(assetName); + } + + /// Get the cached asset keys. + public IEnumerable GetAssetKeys() + { + return this.Cache.Keys + .Select(this.GetAssetName) + .Distinct(); + } + + /**** + ** Cache invalidation + ****/ + /// Purge matched assets from the cache. + /// Matches the asset keys to invalidate. + /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. + /// Returns the number of invalidated assets. + public IEnumerable InvalidateCache(Func predicate, bool dispose = false) + { + HashSet removeAssetNames = new HashSet(StringComparer.InvariantCultureIgnoreCase); + this.Cache.Remove((key, type) => + { + this.ParseCacheKey(key, out string assetName, out _); + if (removeAssetNames.Contains(assetName) || predicate(assetName, type)) + { + removeAssetNames.Add(assetName); + return true; + } + return false; + }); + + return removeAssetNames; + } + + + /********* + ** Private methods + *********/ + /**** + ** Asset name/key handling + ****/ + /// Get a directory or file path relative to the content root. + /// The source file path. + /// The target file path. + private string GetRelativePath(string sourcePath, string targetPath) + { + return PathUtilities.GetRelativePath(sourcePath, targetPath); + } + + /// Get the locale codes (like ja-JP) used in asset keys. + private IDictionary GetKeyLocales() + { + // create locale => code map + IDictionary map = new Dictionary(); + foreach (LocalizedContentManager.LanguageCode code in Enum.GetValues(typeof(LocalizedContentManager.LanguageCode))) + map[code] = this.GetLocale(code); + + return map; + } + + /// Get the asset name from a cache key. + /// The input cache key. + private string GetAssetName(string cacheKey) + { + this.ParseCacheKey(cacheKey, out string assetName, out string _); + return assetName; + } + + /// Parse a cache key into its component parts. + /// The input cache key. + /// The original asset name. + /// The asset locale code (or null if not localised). + private void ParseCacheKey(string cacheKey, out string assetName, out string localeCode) + { + // handle localised key + if (!string.IsNullOrWhiteSpace(cacheKey)) + { + int lastSepIndex = cacheKey.LastIndexOf(".", StringComparison.InvariantCulture); + if (lastSepIndex >= 0) + { + string suffix = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); + if (this.LanguageCodes.ContainsKey(suffix)) + { + assetName = cacheKey.Substring(0, lastSepIndex); + localeCode = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); + return; + } + } + } + + // handle simple key + assetName = cacheKey; + localeCode = null; + } + + /**** + ** Cache handling + ****/ + /// Get whether an asset has already been loaded. + /// The normalised asset name. + private bool IsNormalisedKeyLoaded(string normalisedAssetName) + { + // default English + if (this.Language == LocalizedContentManager.LanguageCode.en) + return this.Cache.ContainsKey(normalisedAssetName); + + // translated + if (!this.IsLocalisableLookup.TryGetValue(normalisedAssetName, out bool localisable)) + return false; + return localisable + ? this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetLocale(this.GetCurrentLanguage())}") + : this.Cache.ContainsKey(normalisedAssetName); + } + + /**** + ** Content loading + ****/ + /// Load an asset name without heuristics to support mod content. + /// The type of asset to load. + /// The asset path relative to the loader root directory, not including the .xnb extension. + /// The language code for which to load content. + private T LoadImpl(string assetName, LocalizedContentManager.LanguageCode language) + { + // skip if already loaded + if (this.IsNormalisedKeyLoaded(assetName)) + return base.Load(assetName, language); + + // load asset + T data; + if (this.AssetsBeingLoaded.Contains(assetName)) + { + this.Monitor.Log($"Broke loop while loading asset '{assetName}'.", LogLevel.Warn); + this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}", LogLevel.Trace); + data = base.Load(assetName, language); + } + else + { + data = this.AssetsBeingLoaded.Track(assetName, () => + { + string locale = this.GetLocale(language); + IAssetInfo info = new AssetInfo(locale, assetName, typeof(T), this.NormaliseAssetName); + IAssetData asset = + this.ApplyLoader(info) + ?? new AssetDataForObject(info, base.Load(assetName, language), this.NormaliseAssetName); + asset = this.ApplyEditors(info, asset); + return (T)asset.Data; + }); + } + + // update cache & return data + this.Inject(assetName, data); + return data; + } + + /// Get a file from the mod folder. + /// The asset path relative to the content folder. + private FileInfo GetModFile(string path) + { + // try exact match + FileInfo file = new FileInfo(Path.Combine(this.FullRootDirectory, path)); + + // try with default extension + if (!file.Exists && file.Extension.ToLower() != ".xnb") + { + FileInfo result = new FileInfo(file.FullName + ".xnb"); + if (result.Exists) + file = result; + } + + return file; + } + + /// Load the initial asset from the registered . + /// The basic asset metadata. + /// Returns the loaded asset metadata, or null if no loader matched. + private IAssetData ApplyLoader(IAssetInfo info) + { + // find matching loaders + var loaders = this.GetInterceptors(this.Loaders) + .Where(entry => + { + try + { + return entry.Value.CanLoad(info); + } + catch (Exception ex) + { + entry.Key.LogAsMod($"Mod failed when checking whether it could load asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + return false; + } + }) + .ToArray(); + + // validate loaders + if (!loaders.Any()) + return null; + if (loaders.Length > 1) + { + string[] loaderNames = loaders.Select(p => p.Key.DisplayName).ToArray(); + this.Monitor.Log($"Multiple mods want to provide the '{info.AssetName}' asset ({string.Join(", ", loaderNames)}), but an asset can't be loaded multiple times. SMAPI will use the default asset instead; uninstall one of the mods to fix this. (Message for modders: you should usually use {typeof(IAssetEditor)} instead to avoid conflicts.)", LogLevel.Warn); + return null; + } + + // fetch asset from loader + IModMetadata mod = loaders[0].Key; + IAssetLoader loader = loaders[0].Value; + T data; + try + { + data = loader.Load(info); + this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.AssetName}'.", LogLevel.Trace); + } + catch (Exception ex) + { + mod.LogAsMod($"Mod crashed when loading asset '{info.AssetName}'. SMAPI will use the default asset instead. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + return null; + } + + // validate asset + if (data == null) + { + mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Error); + return null; + } + + // return matched asset + return new AssetDataForObject(info, data, this.NormaliseAssetName); + } + + /// Apply any to a loaded asset. + /// The asset type. + /// The basic asset metadata. + /// The loaded asset. + private IAssetData ApplyEditors(IAssetInfo info, IAssetData asset) + { + IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.NormaliseAssetName); + + // edit asset + foreach (var entry in this.GetInterceptors(this.Editors)) + { + // check for match + IModMetadata mod = entry.Key; + IAssetEditor editor = entry.Value; + try + { + if (!editor.CanEdit(info)) + continue; + } + catch (Exception ex) + { + mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + continue; + } + + // try edit + object prevAsset = asset.Data; + try + { + editor.Edit(asset); + this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace); + } + catch (Exception ex) + { + mod.LogAsMod($"Mod crashed when editing asset '{info.AssetName}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + } + + // validate edit + if (asset.Data == null) + { + mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Warn); + asset = GetNewData(prevAsset); + } + else if (!(asset.Data is T)) + { + mod.LogAsMod($"Mod incorrectly set asset '{asset.AssetName}' to incompatible type '{asset.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn); + asset = GetNewData(prevAsset); + } + } + + // return result + return asset; + } + + /// Get all registered interceptors from a list. + private IEnumerable> GetInterceptors(IDictionary> entries) + { + foreach (var entry in entries) + { + IModMetadata mod = entry.Key; + IList interceptors = entry.Value; + + // registered editors + foreach (T interceptor in interceptors) + yield return new KeyValuePair(mod, interceptor); + } + } + + /// Premultiply a texture's alpha values to avoid transparency issues in the game. This is only possible if the game isn't currently drawing. + /// The texture to premultiply. + /// Returns a premultiplied texture. + /// Based on code by Layoric. + private Texture2D PremultiplyTransparency(Texture2D texture) + { + // validate + if (Context.IsInDrawLoop) + throw new NotSupportedException("Can't load a PNG file while the game is drawing to the screen. Make sure you load content outside the draw loop."); + + // process texture + SpriteBatch spriteBatch = Game1.spriteBatch; + GraphicsDevice gpu = Game1.graphics.GraphicsDevice; + using (RenderTarget2D renderTarget = new RenderTarget2D(Game1.graphics.GraphicsDevice, texture.Width, texture.Height)) + { + // create blank render target to premultiply + gpu.SetRenderTarget(renderTarget); + gpu.Clear(Color.Black); + + // multiply each color by the source alpha, and write just the color values into the final texture + spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState + { + ColorDestinationBlend = Blend.Zero, + ColorWriteChannels = ColorWriteChannels.Red | ColorWriteChannels.Green | ColorWriteChannels.Blue, + AlphaDestinationBlend = Blend.Zero, + AlphaSourceBlend = Blend.SourceAlpha, + ColorSourceBlend = Blend.SourceAlpha + }); + spriteBatch.Draw(texture, texture.Bounds, Color.White); + spriteBatch.End(); + + // copy the alpha values from the source texture into the final one without multiplying them + spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState + { + ColorWriteChannels = ColorWriteChannels.Alpha, + AlphaDestinationBlend = Blend.Zero, + ColorDestinationBlend = Blend.Zero, + AlphaSourceBlend = Blend.One, + ColorSourceBlend = Blend.One + }); + spriteBatch.Draw(texture, texture.Bounds, Color.White); + spriteBatch.End(); + + // release GPU + gpu.SetRenderTarget(null); + + // extract premultiplied data + Color[] data = new Color[texture.Width * texture.Height]; + renderTarget.GetData(data); + + // unset texture from GPU to regain control + gpu.Textures[0] = null; + + // update texture with premultiplied data + texture.SetData(data); + } + + return texture; + } + } +} diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 70462559..48a70688 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -115,12 +115,15 @@ namespace StardewModdingAPI.Framework /// Simplifies access to private game code. private readonly Reflector Reflection; + /// Whether the next content manager requested by the game will be for . + private bool NextContentManagerIsMain; + /********* ** Accessors *********/ /// SMAPI's content manager. - public ContentCore ContentCore { get; private set; } + public ContentCoordinator ContentCore { get; private set; } /// The game's core multiplayer utility. public SMultiplayer Multiplayer => (SMultiplayer)Game1.multiplayer; @@ -140,6 +143,10 @@ namespace StardewModdingAPI.Framework /// A callback to invoke when the game exits. internal SGame(IMonitor monitor, Reflector reflection, EventManager eventManager, Action onGameInitialised, Action onGameExiting) { + // check expectations + if (this.ContentCore == null) + throw new InvalidOperationException($"The game didn't initialise its first content manager before SMAPI's {nameof(SGame)} constructor. This indicates an incompatible lifecycle change."); + // init XNA Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; @@ -150,8 +157,6 @@ namespace StardewModdingAPI.Framework this.Reflection = reflection; this.OnGameInitialised = onGameInitialised; this.OnGameExiting = onGameExiting; - if (this.ContentCore == null) // shouldn't happen since CreateContentManager is called first, but let's init here just in case - this.ContentCore = new ContentCore(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, this.Monitor, reflection); Game1.input = new SInputState(); Game1.multiplayer = new SMultiplayer(monitor, eventManager); @@ -190,14 +195,25 @@ namespace StardewModdingAPI.Framework /// The root directory to search for content. protected override LocalizedContentManager CreateContentManager(IServiceProvider serviceProvider, string rootDirectory) { - // NOTE: this method is called from the Game1 constructor, before the SGame constructor runs. - // Don't depend on anything being initialised at this point. + // Game1._temporaryContent initialising from SGame constructor + // NOTE: this method is called before the SGame constructor runs. Don't depend on anything being initialised at this point. if (this.ContentCore == null) { - this.ContentCore = new ContentCore(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, SGame.MonitorDuringInitialisation, SGame.ReflectorDuringInitialisation); + this.ContentCore = new ContentCoordinator(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, SGame.MonitorDuringInitialisation, SGame.ReflectorDuringInitialisation); SGame.MonitorDuringInitialisation = null; + this.NextContentManagerIsMain = true; + return this.ContentCore.CreateContentManager("Game1._temporaryContent", isModFolder: false); } - return this.ContentCore.CreateContentManager("(generated)", rootDirectory); + + // Game1.content initialising from LoadContent + if (this.NextContentManagerIsMain) + { + this.NextContentManagerIsMain = false; + return this.ContentCore.CreateContentManager("Game1.content", isModFolder: false, rootDirectory: rootDirectory); + } + + // any other content manager + return this.ContentCore.CreateContentManager("(generated)", isModFolder: false, rootDirectory: rootDirectory); } /// The method called when the game is updating its state. This happens roughly 60 times per second. diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index ebe44cf7..1612ff11 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -63,7 +63,7 @@ namespace StardewModdingAPI private SGame GameInstance; /// The underlying content manager. - private ContentCore ContentCore => this.GameInstance.ContentCore; + private ContentCoordinator ContentCore => this.GameInstance.ContentCore; /// Tracks the installed mods. /// This is initialised after the game starts. @@ -721,7 +721,7 @@ namespace StardewModdingAPI /// The JSON helper with which to read mods' JSON files. /// The content manager to use for mod content. /// Handles access to SMAPI's internal mod metadata list. - private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, ContentCore contentCore, ModDatabase modDatabase) + private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, ContentCoordinator contentCore, ModDatabase modDatabase) { this.Monitor.Log("Loading mods...", LogLevel.Trace); @@ -748,7 +748,7 @@ namespace StardewModdingAPI // load mod as content pack IManifest manifest = metadata.Manifest; IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); - ContentManagerShim contentManager = this.ContentCore.CreateContentManager($"Mods.{metadata.Manifest.UniqueID}", metadata.DirectoryPath); + SContentManager contentManager = this.ContentCore.CreateContentManager($"Mods.{metadata.Manifest.UniqueID}", isModFolder: true, rootDirectory: metadata.DirectoryPath); IContentHelper contentHelper = new ContentHelper(this.ContentCore, contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); IContentPack contentPack = new ContentPack(metadata.DirectoryPath, manifest, contentHelper, jsonHelper); metadata.SetMod(contentPack, monitor); @@ -833,7 +833,7 @@ namespace StardewModdingAPI IModHelper modHelper; { ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager); - ContentManagerShim contentManager = this.ContentCore.CreateContentManager($"Mods.{metadata.Manifest.UniqueID}", metadata.DirectoryPath); + SContentManager contentManager = this.ContentCore.CreateContentManager($"Mods.{metadata.Manifest.UniqueID}", isModFolder: true, rootDirectory: metadata.DirectoryPath); IContentHelper contentHelper = new ContentHelper(contentCore, contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor); @@ -843,7 +843,7 @@ namespace StardewModdingAPI IContentPack CreateTransitionalContentPack(string packDirPath, IManifest packManifest) { IMonitor packMonitor = this.GetSecondaryMonitor(packManifest.Name); - ContentManagerShim packContentManager = this.ContentCore.CreateContentManager($"Mods.{packManifest.UniqueID}", packDirPath); + SContentManager packContentManager = this.ContentCore.CreateContentManager($"Mods.{packManifest.UniqueID}", isModFolder: true, rootDirectory: packDirPath); IContentHelper packContentHelper = new ContentHelper(contentCore, packContentManager, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); return new ContentPack(packDirPath, packManifest, packContentHelper, this.JsonHelper); } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 54fe9385..320b97e7 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -122,7 +122,7 @@ - + @@ -220,7 +220,7 @@ - + -- cgit From 02c02a55eeeb744108d6a8335f6203a95ea20626 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 10 May 2018 00:47:20 -0400 Subject: generalise console color logic for reuse (#495) --- .../ConsoleWriting/ColorfulConsoleWriter.cs | 136 +++++++++++++++++++++ src/SMAPI.Internal/ConsoleWriting/LogLevel.cs | 27 ++++ .../ConsoleWriting/MonitorColorScheme.cs | 15 +++ src/SMAPI.Internal/SMAPI.Internal.projitems | 3 + .../Logging/ConsoleInterceptionManager.cs | 27 ---- src/SMAPI/Framework/Models/MonitorColorScheme.cs | 15 --- src/SMAPI/Framework/Models/SConfig.cs | 2 + src/SMAPI/Framework/Monitor.cs | 111 +++-------------- src/SMAPI/LogLevel.cs | 16 +-- src/SMAPI/StardewModdingAPI.csproj | 1 - 10 files changed, 210 insertions(+), 143 deletions(-) create mode 100644 src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs create mode 100644 src/SMAPI.Internal/ConsoleWriting/LogLevel.cs create mode 100644 src/SMAPI.Internal/ConsoleWriting/MonitorColorScheme.cs delete mode 100644 src/SMAPI/Framework/Models/MonitorColorScheme.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs b/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs new file mode 100644 index 00000000..5f8fe271 --- /dev/null +++ b/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Internal.ConsoleWriting +{ + /// Provides a wrapper for writing color-coded text to the console. + internal class ColorfulConsoleWriter + { + /********* + ** Properties + *********/ + /// The console text color for each log level. + private readonly IDictionary Colors; + + /// Whether the current console supports color formatting. + private readonly bool SupportsColor; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The target platform. + /// The console color scheme to use. + public ColorfulConsoleWriter(Platform platform, MonitorColorScheme colorScheme) + { + this.SupportsColor = this.TestColorSupport(); + this.Colors = this.GetConsoleColorScheme(platform, colorScheme); + } + + /// Write a message line to the log. + /// The message to log. + /// The log level. + public void WriteLine(string message, ConsoleLogLevel level) + { + if (this.SupportsColor) + { + if (level == ConsoleLogLevel.Critical) + { + Console.BackgroundColor = ConsoleColor.Red; + Console.ForegroundColor = ConsoleColor.White; + Console.WriteLine(message); + Console.ResetColor(); + } + else + { + Console.ForegroundColor = this.Colors[level]; + Console.WriteLine(message); + Console.ResetColor(); + } + } + else + Console.WriteLine(message); + } + + + /********* + ** Private methods + *********/ + /// Test whether the current console supports color formatting. + private bool TestColorSupport() + { + try + { + Console.ForegroundColor = Console.ForegroundColor; + return true; + } + catch (Exception) + { + return false; // Mono bug + } + } + + /// Get the color scheme to use for the current console. + /// The target platform. + /// The console color scheme to use. + private IDictionary GetConsoleColorScheme(Platform platform, MonitorColorScheme colorScheme) + { + // auto detect color scheme + if (colorScheme == MonitorColorScheme.AutoDetect) + { + colorScheme = platform == Platform.Mac + ? MonitorColorScheme.LightBackground // MacOS doesn't provide console background color info, but it's usually white. + : ColorfulConsoleWriter.IsDark(Console.BackgroundColor) ? MonitorColorScheme.DarkBackground : MonitorColorScheme.LightBackground; + } + + // get colors for scheme + switch (colorScheme) + { + case MonitorColorScheme.DarkBackground: + return new Dictionary + { + [ConsoleLogLevel.Trace] = ConsoleColor.DarkGray, + [ConsoleLogLevel.Debug] = ConsoleColor.DarkGray, + [ConsoleLogLevel.Info] = ConsoleColor.White, + [ConsoleLogLevel.Warn] = ConsoleColor.Yellow, + [ConsoleLogLevel.Error] = ConsoleColor.Red, + [ConsoleLogLevel.Alert] = ConsoleColor.Magenta + }; + + case MonitorColorScheme.LightBackground: + return new Dictionary + { + [ConsoleLogLevel.Trace] = ConsoleColor.DarkGray, + [ConsoleLogLevel.Debug] = ConsoleColor.DarkGray, + [ConsoleLogLevel.Info] = ConsoleColor.Black, + [ConsoleLogLevel.Warn] = ConsoleColor.DarkYellow, + [ConsoleLogLevel.Error] = ConsoleColor.Red, + [ConsoleLogLevel.Alert] = ConsoleColor.DarkMagenta + }; + + default: + throw new NotSupportedException($"Unknown color scheme '{colorScheme}'."); + } + } + + /// Get whether a console color should be considered dark, which is subjectively defined as 'white looks better than black on this text'. + /// The color to check. + private static bool IsDark(ConsoleColor color) + { + switch (color) + { + case ConsoleColor.Black: + case ConsoleColor.Blue: + case ConsoleColor.DarkBlue: + case ConsoleColor.DarkMagenta: // Powershell + case ConsoleColor.DarkRed: + case ConsoleColor.Red: + return true; + + default: + return false; + } + } + } +} diff --git a/src/SMAPI.Internal/ConsoleWriting/LogLevel.cs b/src/SMAPI.Internal/ConsoleWriting/LogLevel.cs new file mode 100644 index 00000000..85e69f51 --- /dev/null +++ b/src/SMAPI.Internal/ConsoleWriting/LogLevel.cs @@ -0,0 +1,27 @@ +namespace StardewModdingAPI.Internal.ConsoleWriting +{ + /// The log severity levels. + internal enum ConsoleLogLevel + { + /// Tracing info intended for developers. + Trace, + + /// Troubleshooting info that may be relevant to the player. + Debug, + + /// Info relevant to the player. This should be used judiciously. + Info, + + /// An issue the player should be aware of. This should be used rarely. + Warn, + + /// A message indicating something went wrong. + Error, + + /// Important information to highlight for the player when player action is needed (e.g. new version available). This should be used rarely to avoid alert fatigue. + Alert, + + /// A critical issue that generally signals an immediate end to the application. + Critical + } +} diff --git a/src/SMAPI.Internal/ConsoleWriting/MonitorColorScheme.cs b/src/SMAPI.Internal/ConsoleWriting/MonitorColorScheme.cs new file mode 100644 index 00000000..bccb56d7 --- /dev/null +++ b/src/SMAPI.Internal/ConsoleWriting/MonitorColorScheme.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Internal.ConsoleWriting +{ + /// A monitor color scheme to use. + internal enum MonitorColorScheme + { + /// Choose a color scheme automatically. + AutoDetect, + + /// Use lighter text colors that look better on a black or dark background. + DarkBackground, + + /// Use darker text colors that look better on a white or light background. + LightBackground + } +} diff --git a/src/SMAPI.Internal/SMAPI.Internal.projitems b/src/SMAPI.Internal/SMAPI.Internal.projitems index 764cb12e..dadae4b0 100644 --- a/src/SMAPI.Internal/SMAPI.Internal.projitems +++ b/src/SMAPI.Internal/SMAPI.Internal.projitems @@ -9,9 +9,12 @@ SMAPI.Internal + + + diff --git a/src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs b/src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs index b8f2c34e..c04bcd1a 100644 --- a/src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs +++ b/src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs @@ -15,9 +15,6 @@ namespace StardewModdingAPI.Framework.Logging /********* ** Accessors *********/ - /// Whether the current console supports color formatting. - public bool SupportsColor { get; } - /// The event raised when a message is written to the console directly. public event Action OnMessageIntercepted; @@ -32,9 +29,6 @@ namespace StardewModdingAPI.Framework.Logging this.Output = new InterceptingTextWriter(Console.Out); this.Output.OnMessageIntercepted += line => this.OnMessageIntercepted?.Invoke(line); Console.SetOut(this.Output); - - // test color support - this.SupportsColor = this.TestColorSupport(); } /// Get an exclusive lock and write to the console output without interception. @@ -61,26 +55,5 @@ namespace StardewModdingAPI.Framework.Logging Console.SetOut(this.Output.Out); this.Output.Dispose(); } - - - /********* - ** private methods - *********/ - /// Test whether the current console supports color formatting. - private bool TestColorSupport() - { - try - { - this.ExclusiveWriteWithoutInterception(() => - { - Console.ForegroundColor = Console.ForegroundColor; - }); - return true; - } - catch (Exception) - { - return false; // Mono bug - } - } } } diff --git a/src/SMAPI/Framework/Models/MonitorColorScheme.cs b/src/SMAPI/Framework/Models/MonitorColorScheme.cs deleted file mode 100644 index d8289d08..00000000 --- a/src/SMAPI/Framework/Models/MonitorColorScheme.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace StardewModdingAPI.Framework.Models -{ - /// A monitor color scheme to use. - internal enum MonitorColorScheme - { - /// Choose a color scheme automatically. - AutoDetect, - - /// Use lighter text colors that look better on a black or dark background. - DarkBackground, - - /// Use darker text colors that look better on a white or light background. - LightBackground - } -} diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index b732921f..e201e966 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -1,3 +1,5 @@ +using StardewModdingAPI.Internal.ConsoleWriting; + namespace StardewModdingAPI.Framework.Models { /// The SMAPI configuration settings. diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index 8df2e59b..2812a9cc 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -1,10 +1,8 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Threading; using StardewModdingAPI.Framework.Logging; -using StardewModdingAPI.Framework.Models; -using StardewModdingAPI.Internal; +using StardewModdingAPI.Internal.ConsoleWriting; namespace StardewModdingAPI.Framework { @@ -17,8 +15,11 @@ namespace StardewModdingAPI.Framework /// The name of the module which logs messages using this instance. private readonly string Source; + /// Handles writing color-coded text to the console. + private readonly ColorfulConsoleWriter ConsoleWriter; + /// Manages access to the console output. - private readonly ConsoleInterceptionManager ConsoleManager; + private readonly ConsoleInterceptionManager ConsoleInterceptor; /// The log file to which to write messages. private readonly LogFileManager LogFile; @@ -26,9 +27,6 @@ namespace StardewModdingAPI.Framework /// The maximum length of the values. private static readonly int MaxLevelLength = (from level in Enum.GetValues(typeof(LogLevel)).Cast() select level.ToString().Length).Max(); - /// The console text color for each log level. - private readonly IDictionary Colors; - /// Propagates notification that SMAPI should exit. private readonly CancellationTokenSource ExitTokenSource; @@ -54,21 +52,21 @@ namespace StardewModdingAPI.Framework *********/ /// Construct an instance. /// The name of the module which logs messages using this instance. - /// Manages access to the console output. + /// Intercepts access to the console output. /// The log file to which to write messages. /// Propagates notification that SMAPI should exit. /// The console color scheme to use. - public Monitor(string source, ConsoleInterceptionManager consoleManager, LogFileManager logFile, CancellationTokenSource exitTokenSource, MonitorColorScheme colorScheme) + public Monitor(string source, ConsoleInterceptionManager consoleInterceptor, LogFileManager logFile, CancellationTokenSource exitTokenSource, MonitorColorScheme colorScheme) { // validate if (string.IsNullOrWhiteSpace(source)) throw new ArgumentException("The log source cannot be empty."); // initialise - this.Colors = Monitor.GetConsoleColorScheme(colorScheme); this.Source = source; this.LogFile = logFile ?? throw new ArgumentNullException(nameof(logFile), "The log file manager cannot be null."); - this.ConsoleManager = consoleManager; + this.ConsoleWriter = new ColorfulConsoleWriter(Constants.Platform, colorScheme); + this.ConsoleInterceptor = consoleInterceptor; this.ExitTokenSource = exitTokenSource; } @@ -77,7 +75,7 @@ namespace StardewModdingAPI.Framework /// The log severity level. public void Log(string message, LogLevel level = LogLevel.Debug) { - this.LogImpl(this.Source, message, level, this.Colors[level]); + this.LogImpl(this.Source, message, (ConsoleLogLevel)level); } /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. @@ -92,7 +90,7 @@ namespace StardewModdingAPI.Framework internal void Newline() { if (this.WriteToConsole) - this.ConsoleManager.ExclusiveWriteWithoutInterception(Console.WriteLine); + this.ConsoleInterceptor.ExclusiveWriteWithoutInterception(Console.WriteLine); this.LogFile.WriteLine(""); } @@ -101,7 +99,7 @@ namespace StardewModdingAPI.Framework internal void LogUserInput(string input) { // user input already appears in the console, so just need to write to file - string prefix = this.GenerateMessagePrefix(this.Source, LogLevel.Info); + string prefix = this.GenerateMessagePrefix(this.Source, (ConsoleLogLevel)LogLevel.Info); this.LogFile.WriteLine($"{prefix} $>{input}"); } @@ -113,16 +111,14 @@ namespace StardewModdingAPI.Framework /// The message to log. private void LogFatal(string message) { - this.LogImpl(this.Source, message, LogLevel.Error, ConsoleColor.White, background: ConsoleColor.Red); + this.LogImpl(this.Source, message, ConsoleLogLevel.Critical); } /// Write a message line to the log. /// The name of the mod logging the message. /// The message to log. /// The log level. - /// The console foreground color. - /// The console background color (or null to leave it as-is). - private void LogImpl(string source, string message, LogLevel level, ConsoleColor color, ConsoleColor? background = null) + private void LogImpl(string source, string message, ConsoleLogLevel level) { // generate message string prefix = this.GenerateMessagePrefix(source, level); @@ -130,20 +126,11 @@ namespace StardewModdingAPI.Framework string consoleMessage = this.ShowFullStampInConsole ? fullMessage : $"[{source}] {message}"; // write to console - if (this.WriteToConsole && (this.ShowTraceInConsole || level != LogLevel.Trace)) + if (this.WriteToConsole && (this.ShowTraceInConsole || level != ConsoleLogLevel.Trace)) { - this.ConsoleManager.ExclusiveWriteWithoutInterception(() => + this.ConsoleInterceptor.ExclusiveWriteWithoutInterception(() => { - if (this.ConsoleManager.SupportsColor) - { - if (background.HasValue) - Console.BackgroundColor = background.Value; - Console.ForegroundColor = color; - Console.WriteLine(consoleMessage); - Console.ResetColor(); - } - else - Console.WriteLine(consoleMessage); + this.ConsoleWriter.WriteLine(consoleMessage, level); }); } @@ -154,72 +141,10 @@ namespace StardewModdingAPI.Framework /// Generate a message prefix for the current time. /// The name of the mod logging the message. /// The log level. - private string GenerateMessagePrefix(string source, LogLevel level) + private string GenerateMessagePrefix(string source, ConsoleLogLevel level) { string levelStr = level.ToString().ToUpper().PadRight(Monitor.MaxLevelLength); return $"[{DateTime.Now:HH:mm:ss} {levelStr} {source}]"; } - - /// Get the color scheme to use for the current console. - /// The console color scheme to use. - private static IDictionary GetConsoleColorScheme(MonitorColorScheme colorScheme) - { - // auto detect color scheme - if (colorScheme == MonitorColorScheme.AutoDetect) - { - if (Constants.Platform == Platform.Mac) - colorScheme = MonitorColorScheme.LightBackground; // MacOS doesn't provide console background color info, but it's usually white. - else - colorScheme = Monitor.IsDark(Console.BackgroundColor) ? MonitorColorScheme.DarkBackground : MonitorColorScheme.LightBackground; - } - - // get colors for scheme - switch (colorScheme) - { - case MonitorColorScheme.DarkBackground: - return new Dictionary - { - [LogLevel.Trace] = ConsoleColor.DarkGray, - [LogLevel.Debug] = ConsoleColor.DarkGray, - [LogLevel.Info] = ConsoleColor.White, - [LogLevel.Warn] = ConsoleColor.Yellow, - [LogLevel.Error] = ConsoleColor.Red, - [LogLevel.Alert] = ConsoleColor.Magenta - }; - - case MonitorColorScheme.LightBackground: - return new Dictionary - { - [LogLevel.Trace] = ConsoleColor.DarkGray, - [LogLevel.Debug] = ConsoleColor.DarkGray, - [LogLevel.Info] = ConsoleColor.Black, - [LogLevel.Warn] = ConsoleColor.DarkYellow, - [LogLevel.Error] = ConsoleColor.Red, - [LogLevel.Alert] = ConsoleColor.DarkMagenta - }; - - default: - throw new NotSupportedException($"Unknown color scheme '{colorScheme}'."); - } - } - - /// Get whether a console color should be considered dark, which is subjectively defined as 'white looks better than black on this text'. - /// The color to check. - private static bool IsDark(ConsoleColor color) - { - switch (color) - { - case ConsoleColor.Black: - case ConsoleColor.Blue: - case ConsoleColor.DarkBlue: - case ConsoleColor.DarkMagenta: // Powershell - case ConsoleColor.DarkRed: - case ConsoleColor.Red: - return true; - - default: - return false; - } - } } } diff --git a/src/SMAPI/LogLevel.cs b/src/SMAPI/LogLevel.cs index 89647876..7987f82a 100644 --- a/src/SMAPI/LogLevel.cs +++ b/src/SMAPI/LogLevel.cs @@ -1,24 +1,26 @@ +using StardewModdingAPI.Internal.ConsoleWriting; + namespace StardewModdingAPI { /// The log severity levels. public enum LogLevel { /// Tracing info intended for developers. - Trace, + Trace = ConsoleLogLevel.Trace, /// Troubleshooting info that may be relevant to the player. - Debug, + Debug = ConsoleLogLevel.Debug, /// Info relevant to the player. This should be used judiciously. - Info, + Info = ConsoleLogLevel.Info, /// An issue the player should be aware of. This should be used rarely. - Warn, + Warn = ConsoleLogLevel.Warn, /// A message indicating something went wrong. - Error, + Error = ConsoleLogLevel.Error, /// Important information to highlight for the player when player action is needed (e.g. new version available). This should be used rarely to avoid alert fatigue. - Alert + Alert = ConsoleLogLevel.Alert } -} \ No newline at end of file +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 320b97e7..01d5aaf1 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -125,7 +125,6 @@ - -- cgit From b9036f212e7898f9cd13006024d63aec61d50ed6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 12 May 2018 22:52:28 -0400 Subject: group mod warnings in console --- docs/release-notes.md | 1 + src/SMAPI/Framework/IModMetadata.cs | 7 ++++ src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 16 +++---- src/SMAPI/Framework/ModLoading/ModMetadata.cs | 11 +++++ src/SMAPI/Framework/ModLoading/ModWarning.cs | 31 ++++++++++++++ src/SMAPI/Program.cs | 53 +++++++++++++++++++++--- src/SMAPI/StardewModdingAPI.csproj | 1 + 7 files changed, 103 insertions(+), 17 deletions(-) create mode 100644 src/SMAPI/Framework/ModLoading/ModWarning.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/docs/release-notes.md b/docs/release-notes.md index b531d7ec..b380f11d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,7 @@ * Added prompt when in beta channel and a new version is found. * Added friendly error when game can't start audio. * Added console warning for mods which don't have update checks configured. + * Improved how mod warnings are shown in the console. * Fixed console color scheme on Mac or in PowerShell, configurable via `StardewModdingAPI.config.json`. * Fixed detection of GOG Galaxy install path in rare cases. * Fixed install error on Linux/Mac in some cases. diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index b7972fe1..c0d6408d 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -24,6 +24,9 @@ namespace StardewModdingAPI.Framework /// The metadata resolution status. ModMetadataStatus Status { get; } + /// Indicates non-error issues with the mod. + ModWarning Warnings { get; } + /// The reason the metadata is invalid, if any. string Error { get; } @@ -52,6 +55,10 @@ namespace StardewModdingAPI.Framework /// Return the instance for chaining. IModMetadata SetStatus(ModMetadataStatus status, string error = null); + /// Set a warning flag for the mod. + /// The warning to set. + IModMetadata SetWarning(ModWarning warning); + /// Set the mod instance. /// The mod instance to set. IModMetadata SetMod(IMod mod); diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 6c0e1c14..2fb2aba7 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -285,33 +285,27 @@ namespace StardewModdingAPI.Framework.ModLoading this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Broken code in {filename}: {handler.NounPhrase}."); if (!assumeCompatible) throw new IncompatibleInstructionException(handler.NounPhrase, $"Found an incompatible CIL instruction ({handler.NounPhrase}) while loading assembly {filename}."); - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Found broken code ({handler.NounPhrase}) while loading assembly {filename}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); + mod.SetWarning(ModWarning.BrokenCodeLoaded); break; case InstructionHandleResult.DetectedGamePatch: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected game patcher ({handler.NounPhrase}) in assembly {filename}."); - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}{mod.DisplayName} patches the game, which may impact game stability. If you encounter problems, try removing this mod first.", LogLevel.Warn); + mod.SetWarning(ModWarning.PatchesGame); break; case InstructionHandleResult.DetectedSaveSerialiser: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected possible save serialiser change ({handler.NounPhrase}) in assembly {filename}."); - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}{mod.DisplayName} seems to change the save serialiser. It may change your saves in such a way that they won't work without this mod in the future.", LogLevel.Warn); + mod.SetWarning(ModWarning.ChangesSaveSerialiser); break; case InstructionHandleResult.DetectedUnvalidatedUpdateTick: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected reference to {handler.NounPhrase} in assembly {filename}."); - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}{mod.DisplayName} uses a specialised SMAPI event that may crash the game or corrupt your save file. If you encounter problems, try removing this mod first.", LogLevel.Warn); + mod.SetWarning(ModWarning.UsesUnvalidatedUpdateTick); break; case InstructionHandleResult.DetectedDynamic: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected 'dynamic' keyword ({handler.NounPhrase}) in assembly {filename}."); - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}{mod.DisplayName} uses the 'dynamic' keyword, which isn't compatible with Stardew Valley on Linux or Mac.", -#if SMAPI_FOR_WINDOWS - this.IsDeveloperMode ? LogLevel.Warn : LogLevel.Debug -#else - LogLevel.Warn -#endif - ); + mod.SetWarning(ModWarning.UsesDynamic); break; case InstructionHandleResult.None: diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index d3a33e7a..e4ec7e3b 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -25,6 +25,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// The metadata resolution status. public ModMetadataStatus Status { get; private set; } + /// Indicates non-error issues with the mod. + public ModWarning Warnings { get; private set; } + /// The reason the metadata is invalid, if any. public string Error { get; private set; } @@ -71,6 +74,14 @@ namespace StardewModdingAPI.Framework.ModLoading return this; } + /// Set a warning flag for the mod. + /// The warning to set. + public IModMetadata SetWarning(ModWarning warning) + { + this.Warnings |= warning; + return this; + } + /// Set the mod instance. /// The mod instance to set. public IModMetadata SetMod(IMod mod) diff --git a/src/SMAPI/Framework/ModLoading/ModWarning.cs b/src/SMAPI/Framework/ModLoading/ModWarning.cs new file mode 100644 index 00000000..0e4b2570 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/ModWarning.cs @@ -0,0 +1,31 @@ +using System; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Indicates a detected non-error mod issue. + [Flags] + internal enum ModWarning + { + /// No issues detected. + None = 0, + + /// SMAPI detected incompatible code in the mod, but was configured to load it anyway. + BrokenCodeLoaded = 1, + + /// The mod affects the save serializer in a way that may make saves unloadable without the mod. + ChangesSaveSerialiser = 2, + + /// The mod patches the game in a way that may impact stability. + PatchesGame = 4, + + /// The mod uses the dynamic keyword which won't work on Linux/Mac. + UsesDynamic = 8, + + /// The mod references which may impact stability. + UsesUnvalidatedUpdateTick = 16, + + /// The mod has no update keys set. + NoUpdateKeys = 32 + } +} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index f410abc1..e50c4ec9 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -742,7 +742,7 @@ namespace StardewModdingAPI // show warning for missing update key if (metadata.HasManifest() && !metadata.HasUpdateKeys()) - this.Monitor.Log($" {metadata.DisplayName} has no {nameof(IManifest.UpdateKeys)} in its manifest. You may not see update alerts for this mod.", LogLevel.Warn); + metadata.SetWarning(ModWarning.NoUpdateKeys); // validate status if (metadata.Status == ModMetadataStatus.Failed) @@ -791,7 +791,7 @@ namespace StardewModdingAPI // show warnings if (metadata.HasManifest() && !metadata.HasUpdateKeys() && !this.AllowMissingUpdateKeys.Contains(metadata.Manifest.UniqueID)) - this.Monitor.Log($" {metadata.DisplayName} has no {nameof(IManifest.UpdateKeys)} in its manifest. You may not see update alerts for this mod.", LogLevel.Warn); + metadata.SetWarning(ModWarning.NoUpdateKeys); // validate status if (metadata.Status == ModMetadataStatus.Failed) @@ -831,6 +831,10 @@ namespace StardewModdingAPI // initialise mod try { + // get mod instance + if (!this.TryLoadModEntry(modAssembly, error => TrackSkip(metadata, error), out Mod mod)) + continue; + // get content packs if (!contentPacksByModID.TryGetValue(manifest.UniqueID, out IContentPack[] contentPacks)) contentPacks = new IContentPack[0]; @@ -858,10 +862,6 @@ namespace StardewModdingAPI modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); } - // get mod instance - if (!this.TryLoadModEntry(modAssembly, error => TrackSkip(metadata, error), out Mod mod)) - continue; - // init mod mod.ModManifest = manifest; mod.Helper = modHelper; @@ -931,6 +931,28 @@ namespace StardewModdingAPI this.Monitor.Newline(); } + // log warnings + { + IModMetadata[] modsWithWarnings = this.ModRegistry.GetAll().Where(p => p.Warnings != ModWarning.None).ToArray(); + if (modsWithWarnings.Any()) + { + this.Monitor.Log($"Found issues with {modsWithWarnings.Length} mods:", LogLevel.Warn); + foreach (IModMetadata metadata in modsWithWarnings) + { + string[] warnings = this.GetWarningText(metadata.Warnings).ToArray(); + if (warnings.Length == 1) + this.Monitor.Log($" {metadata.DisplayName}: {warnings[0]}", LogLevel.Warn); + else + { + this.Monitor.Log($" {metadata.DisplayName}:", LogLevel.Warn); + foreach (string warning in warnings) + this.Monitor.Log(" - " + warning, LogLevel.Warn); + } + } + this.Monitor.Newline(); + } + } + // initialise translations this.ReloadTranslations(loadedMods); @@ -1020,6 +1042,25 @@ namespace StardewModdingAPI this.ModRegistry.AreAllModsInitialised = true; } + /// Get the warning text for a mod warning bit mask. + /// The mod warning bit mask. + private IEnumerable GetWarningText(ModWarning mask) + { + if (mask.HasFlag(ModWarning.BrokenCodeLoaded)) + yield return "has broken code, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly."; + if (mask.HasFlag(ModWarning.ChangesSaveSerialiser)) + yield return "accesses the save serialiser and may break your saves."; + if (mask.HasFlag(ModWarning.PatchesGame)) + yield return "patches the game. This may cause errors or bugs in-game. If you have issues, try removing this mod first."; + if (mask.HasFlag(ModWarning.UsesUnvalidatedUpdateTick)) + yield return "bypasses normal SMAPI event protections. This may cause errors or save corruption. If you have issues, try removing this mod first."; + if (mask.HasFlag(ModWarning.UsesDynamic)) + yield return "uses the 'dynamic' keyword. This won't work on Linux/Mac."; + if (mask.HasFlag(ModWarning.NoUpdateKeys)) + yield return "has no update keys in its manifest. SMAPI won't show update alerts for this mod."; + + } + /// Load a mod's entry class. /// The mod assembly. /// A callback invoked when loading fails. diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 01d5aaf1..19c1e6fe 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -113,6 +113,7 @@ + -- cgit From bd04d46dd1d66b30d4f21575bbbd2e541eabcef3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 22 May 2018 22:53:44 -0400 Subject: refactor content API to fix load errors with decentralised cache (#524) --- docs/release-notes.md | 2 + src/SMAPI/Framework/ContentCoordinator.cs | 123 +++- .../ContentManagers/BaseContentManager.cs | 268 +++++++++ .../ContentManagers/GameContentManager.cs | 252 ++++++++ .../Framework/ContentManagers/IContentManager.cs | 81 +++ .../Framework/ContentManagers/ModContentManager.cs | 207 +++++++ src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 66 ++- src/SMAPI/Framework/SContentManager.cs | 653 --------------------- src/SMAPI/Framework/SGame.cs | 4 +- src/SMAPI/Framework/Utilities/PathUtilities.cs | 7 +- src/SMAPI/Program.cs | 9 +- src/SMAPI/StardewModdingAPI.csproj | 5 +- 12 files changed, 968 insertions(+), 709 deletions(-) create mode 100644 src/SMAPI/Framework/ContentManagers/BaseContentManager.cs create mode 100644 src/SMAPI/Framework/ContentManagers/GameContentManager.cs create mode 100644 src/SMAPI/Framework/ContentManagers/IContentManager.cs create mode 100644 src/SMAPI/Framework/ContentManagers/ModContentManager.cs delete mode 100644 src/SMAPI/Framework/SContentManager.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/docs/release-notes.md b/docs/release-notes.md index 118cc441..b053789d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -32,6 +32,8 @@ * Fixed input suppression not working consistently for clicks. * Fixed console command input not saved to the log. * Fixed `helper.ModRegistry.GetApi` interface validation errors not mentioning which interface caused the issue. + * Fixed mods able to intercept other mods' assets via the internal asset keys. + * Fixed mods able to indirectly change other mods' data through shared content caches. * **Breaking changes** (see [migration guide](https://stardewvalleywiki.com/Modding:Migrate_to_Stardew_Valley_1.3)): * Dropped some deprecated APIs. * `LocationEvents` have been rewritten (see above). diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 397a9d90..c2614001 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -5,10 +5,14 @@ using System.IO; using System.Linq; using System.Reflection; using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Content; +using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Metadata; using StardewValley; +using xTile; namespace StardewModdingAPI.Framework { @@ -18,6 +22,9 @@ namespace StardewModdingAPI.Framework /********* ** Properties *********/ + /// An asset key prefix for assets from SMAPI mod folders. + private readonly string ManagedPrefix = "SMAPI"; + /// Encapsulates monitoring and logging. private readonly IMonitor Monitor; @@ -28,7 +35,7 @@ namespace StardewModdingAPI.Framework private readonly Reflector Reflection; /// The loaded content managers (including the ). - private readonly IList ContentManagers = new List(); + private readonly IList ContentManagers = new List(); /// Whether the content coordinator has been disposed. private bool IsDisposed; @@ -38,7 +45,7 @@ namespace StardewModdingAPI.Framework ** Accessors *********/ /// The primary content manager used for most assets. - public SContentManager MainContentManager { get; private set; } + public GameContentManager MainContentManager { get; private set; } /// The current language as a constant. public LocalizedContentManager.LanguageCode Language => this.MainContentManager.Language; @@ -68,28 +75,110 @@ namespace StardewModdingAPI.Framework this.Reflection = reflection; this.FullRootDirectory = Path.Combine(Constants.ExecutionPath, rootDirectory); this.ContentManagers.Add( - this.MainContentManager = new SContentManager("Game1.content", serviceProvider, rootDirectory, currentCulture, this, monitor, reflection, this.OnDisposing, isModFolder: false) + this.MainContentManager = new GameContentManager("Game1.content", serviceProvider, rootDirectory, currentCulture, this, monitor, reflection, this.OnDisposing) ); - this.CoreAssets = new CoreAssetPropagator(this.MainContentManager.NormaliseAssetName, reflection); + this.CoreAssets = new CoreAssetPropagator(this.MainContentManager.AssertAndNormaliseAssetName, reflection); + } + + /// Get a new content manager which handles reading files from the game content folder with support for interception. + /// A name for the mod manager. Not guaranteed to be unique. + public GameContentManager CreateGameContentManager(string name) + { + GameContentManager manager = new GameContentManager(name, this.MainContentManager.ServiceProvider, this.MainContentManager.RootDirectory, this.MainContentManager.CurrentCulture, this, this.Monitor, this.Reflection, this.OnDisposing); + this.ContentManagers.Add(manager); + return manager; } - /// Get a new content manager which defers loading to the content core. + /// Get a new content manager which handles reading files from a SMAPI mod folder with support for unpacked files. /// A name for the mod manager. Not guaranteed to be unique. - /// Whether this content manager is wrapped around a mod folder. - /// The root directory to search for content (or null. for the default) - public SContentManager CreateContentManager(string name, bool isModFolder, string rootDirectory = null) + /// The root directory to search for content (or null for the default). + public ModContentManager CreateModContentManager(string name, string rootDirectory) { - SContentManager manager = new SContentManager(name, this.MainContentManager.ServiceProvider, rootDirectory ?? this.MainContentManager.RootDirectory, this.MainContentManager.CurrentCulture, this, this.Monitor, this.Reflection, this.OnDisposing, isModFolder); + ModContentManager manager = new ModContentManager(name, this.MainContentManager.ServiceProvider, rootDirectory, this.MainContentManager.CurrentCulture, this, this.Monitor, this.Reflection, this.OnDisposing); this.ContentManagers.Add(manager); return manager; } /// Get the current content locale. - public string GetLocale() => this.MainContentManager.GetLocale(LocalizedContentManager.CurrentLanguageCode); + public string GetLocale() + { + return this.MainContentManager.GetLocale(LocalizedContentManager.CurrentLanguageCode); + } + + /// Get whether this asset is mapped to a mod folder. + /// The asset key. + public bool IsManagedAssetKey(string key) + { + return key.StartsWith(this.ManagedPrefix); + } + + /// Parse a managed SMAPI asset key which maps to a mod folder. + /// The asset key. + /// The unique name for the content manager which should load this asset. + /// The relative path within the mod folder. + /// Returns whether the asset was parsed successfully. + public bool TryParseManagedAssetKey(string key, out string contentManagerID, out string relativePath) + { + contentManagerID = null; + relativePath = null; + + // not a managed asset + if (!key.StartsWith(this.ManagedPrefix)) + return false; - /// Convert an absolute file path into a appropriate asset name. - /// The absolute path to the file. - public string GetAssetNameFromFilePath(string absolutePath) => this.MainContentManager.GetAssetNameFromFilePath(absolutePath, ContentSource.GameContent); + // parse + string[] parts = PathUtilities.GetSegments(key, 3); + if (parts.Length != 3) // managed key prefix, mod id, relative path + return false; + contentManagerID = Path.Combine(parts[0], parts[1]); + relativePath = parts[2]; + return true; + } + + /// Get the managed asset key prefix for a mod. + /// The mod's unique ID. + public string GetManagedAssetPrefix(string modID) + { + return Path.Combine(this.ManagedPrefix, modID.ToLower()); + } + + /// Get a copy of an asset from a mod folder. + /// The asset type. + /// The internal asset key. + /// The unique name for the content manager which should load this asset. + /// The internal SMAPI asset key. + /// The language code for which to load content. + public T LoadAndCloneManagedAsset(string internalKey, string contentManagerID, string relativePath, LocalizedContentManager.LanguageCode language) + { + // get content manager + IContentManager contentManager = this.ContentManagers.FirstOrDefault(p => p.Name == contentManagerID); + if (contentManager == null) + throw new InvalidOperationException($"The '{contentManagerID}' prefix isn't handled by any mod."); + + // get cloned asset + T data = contentManager.Load(internalKey, language); + switch (data as object) + { + case Texture2D source: + { + int[] pixels = new int[source.Width * source.Height]; + source.GetData(pixels); + + Texture2D clone = new Texture2D(source.GraphicsDevice, source.Width, source.Height); + clone.SetData(pixels); + return (T)(object)clone; + } + + case Dictionary source: + return (T)(object)new Dictionary(source); + + case Dictionary source: + return (T)(object)new Dictionary(source); + + default: + return data; + } + } /// Purge assets from the cache that match one of the interceptors. /// The asset editors for which to purge matching assets. @@ -129,7 +218,7 @@ namespace StardewModdingAPI.Framework string locale = this.GetLocale(); return this.InvalidateCache((assetName, type) => { - IAssetInfo info = new AssetInfo(locale, assetName, type, this.MainContentManager.NormaliseAssetName); + IAssetInfo info = new AssetInfo(locale, assetName, type, this.MainContentManager.AssertAndNormaliseAssetName); return predicate(info); }); } @@ -142,7 +231,7 @@ namespace StardewModdingAPI.Framework { // invalidate cache HashSet removedAssetNames = new HashSet(); - foreach (SContentManager contentManager in this.ContentManagers) + foreach (IContentManager contentManager in this.ContentManagers) { foreach (string name in contentManager.InvalidateCache(predicate, dispose)) removedAssetNames.Add(name); @@ -172,7 +261,7 @@ namespace StardewModdingAPI.Framework this.IsDisposed = true; this.Monitor.Log("Disposing the content coordinator. Content managers will no longer be usable after this point.", LogLevel.Trace); - foreach (SContentManager contentManager in this.ContentManagers) + foreach (IContentManager contentManager in this.ContentManagers) contentManager.Dispose(); this.ContentManagers.Clear(); this.MainContentManager = null; @@ -184,7 +273,7 @@ namespace StardewModdingAPI.Framework *********/ /// A callback invoked when a content manager is disposed. /// The content manager being disposed. - private void OnDisposing(SContentManager contentManager) + private void OnDisposing(IContentManager contentManager) { if (this.IsDisposed) return; diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs new file mode 100644 index 00000000..ff0e2de4 --- /dev/null +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -0,0 +1,268 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.IO; +using System.Linq; +using Microsoft.Xna.Framework.Content; +using StardewModdingAPI.Framework.Content; +using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Framework.Reflection; +using StardewValley; + +namespace StardewModdingAPI.Framework.ContentManagers +{ + /// A content manager which handles reading files from a SMAPI mod folder with support for unpacked files. + internal abstract class BaseContentManager : LocalizedContentManager, IContentManager + { + /********* + ** Properties + *********/ + /// The central coordinator which manages content managers. + protected readonly ContentCoordinator Coordinator; + + /// The underlying asset cache. + protected readonly ContentCache Cache; + + /// Encapsulates monitoring and logging. + protected readonly IMonitor Monitor; + + /// Whether the content coordinator has been disposed. + private bool IsDisposed; + + /// The language enum values indexed by locale code. + private readonly IDictionary LanguageCodes; + + /// A callback to invoke when the content manager is being disposed. + private readonly Action OnDisposing; + + + /********* + ** Accessors + *********/ + /// A name for the mod manager. Not guaranteed to be unique. + public string Name { get; } + + /// The current language as a constant. + public LanguageCode Language => this.GetCurrentLanguage(); + + /// The absolute path to the . + public string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory); + + /// Whether this content manager is for a mod folder. + public bool IsModContentManager { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A name for the mod manager. Not guaranteed to be unique. + /// The service provider to use to locate services. + /// The root directory to search for content. + /// The current culture for which to localise content. + /// The central coordinator which manages content managers. + /// Encapsulates monitoring and logging. + /// Simplifies access to private code. + /// A callback to invoke when the content manager is being disposed. + /// Whether this content manager is for a mod folder. + protected BaseContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action onDisposing, bool isModFolder) + : base(serviceProvider, rootDirectory, currentCulture) + { + // init + this.Name = name; + this.Coordinator = coordinator ?? throw new ArgumentNullException(nameof(coordinator)); + this.Cache = new ContentCache(this, reflection); + this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); + this.OnDisposing = onDisposing; + this.IsModContentManager = isModFolder; + + // get asset data + this.LanguageCodes = this.GetKeyLocales().ToDictionary(p => p.Value, p => p.Key, StringComparer.InvariantCultureIgnoreCase); + } + + /// Load an asset that has been processed by the content pipeline. + /// The type of asset to load. + /// The asset path relative to the loader root directory, not including the .xnb extension. + public override T Load(string assetName) + { + return this.Load(assetName, LocalizedContentManager.CurrentLanguageCode); + } + + /// Load the base asset without localisation. + /// The type of asset to load. + /// The asset path relative to the loader root directory, not including the .xnb extension. + public override T LoadBase(string assetName) + { + return this.Load(assetName, LanguageCode.en); + } + + /// Inject an asset into the cache. + /// The type of asset to inject. + /// The asset path relative to the loader root directory, not including the .xnb extension. + /// The asset value. + public void Inject(string assetName, T value) + { + assetName = this.AssertAndNormaliseAssetName(assetName); + this.Cache[assetName] = value; + + } + + /// Normalise path separators in a file path. For asset keys, see instead. + /// The file path to normalise. + [Pure] + public string NormalisePathSeparators(string path) + { + return this.Cache.NormalisePathSeparators(path); + } + + /// Assert that the given key has a valid format and return a normalised form consistent with the underlying cache. + /// The asset key to check. + /// The asset key is empty or contains invalid characters. + [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")] + public string AssertAndNormaliseAssetName(string assetName) + { + // NOTE: the game checks for ContentLoadException to handle invalid keys, so avoid + // throwing other types like ArgumentException here. + if (string.IsNullOrWhiteSpace(assetName)) + throw new SContentLoadException("The asset key or local path is empty."); + if (assetName.Intersect(Path.GetInvalidPathChars()).Any()) + throw new SContentLoadException("The asset key or local path contains invalid characters."); + + return this.Cache.NormaliseKey(assetName); + } + + /**** + ** Content loading + ****/ + /// Get the current content locale. + public string GetLocale() + { + return this.GetLocale(this.GetCurrentLanguage()); + } + + /// The locale for a language. + /// The language. + public string GetLocale(LanguageCode language) + { + return this.LanguageCodeString(language); + } + + /// Get whether the content manager has already loaded and cached the given asset. + /// The asset path relative to the loader root directory, not including the .xnb extension. + public bool IsLoaded(string assetName) + { + assetName = this.Cache.NormaliseKey(assetName); + return this.IsNormalisedKeyLoaded(assetName); + } + + /// Get the cached asset keys. + public IEnumerable GetAssetKeys() + { + return this.Cache.Keys + .Select(this.GetAssetName) + .Distinct(); + } + + /**** + ** Cache invalidation + ****/ + /// Purge matched assets from the cache. + /// Matches the asset keys to invalidate. + /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. + /// Returns the number of invalidated assets. + public IEnumerable InvalidateCache(Func predicate, bool dispose = false) + { + HashSet removeAssetNames = new HashSet(StringComparer.InvariantCultureIgnoreCase); + this.Cache.Remove((key, type) => + { + this.ParseCacheKey(key, out string assetName, out _); + + if (removeAssetNames.Contains(assetName) || predicate(assetName, type)) + { + removeAssetNames.Add(assetName); + return true; + } + return false; + }); + + return removeAssetNames; + } + + /// Dispose held resources. + /// Whether the content manager is being disposed (rather than finalized). + protected override void Dispose(bool isDisposing) + { + if (this.IsDisposed) + return; + this.IsDisposed = true; + + this.OnDisposing(this); + base.Dispose(isDisposing); + } + + /// + public override void Unload() + { + if (this.IsDisposed) + return; // base logic doesn't allow unloading twice, which happens due to SMAPI and the game both unloading + + base.Unload(); + } + + + /********* + ** Private methods + *********/ + /// Get the locale codes (like ja-JP) used in asset keys. + private IDictionary GetKeyLocales() + { + // create locale => code map + IDictionary map = new Dictionary(); + foreach (LanguageCode code in Enum.GetValues(typeof(LanguageCode))) + map[code] = this.GetLocale(code); + + return map; + } + + /// Get the asset name from a cache key. + /// The input cache key. + private string GetAssetName(string cacheKey) + { + this.ParseCacheKey(cacheKey, out string assetName, out string _); + return assetName; + } + + /// Parse a cache key into its component parts. + /// The input cache key. + /// The original asset name. + /// The asset locale code (or null if not localised). + protected void ParseCacheKey(string cacheKey, out string assetName, out string localeCode) + { + // handle localised key + if (!string.IsNullOrWhiteSpace(cacheKey)) + { + int lastSepIndex = cacheKey.LastIndexOf(".", StringComparison.InvariantCulture); + if (lastSepIndex >= 0) + { + string suffix = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); + if (this.LanguageCodes.ContainsKey(suffix)) + { + assetName = cacheKey.Substring(0, lastSepIndex); + localeCode = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); + return; + } + } + } + + // handle simple key + assetName = cacheKey; + localeCode = null; + } + + /// Get whether an asset has already been loaded. + /// The normalised asset name. + protected abstract bool IsNormalisedKeyLoaded(string normalisedAssetName); + } +} diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs new file mode 100644 index 00000000..cfedb5af --- /dev/null +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using StardewModdingAPI.Framework.Content; +using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.Utilities; +using StardewValley; + +namespace StardewModdingAPI.Framework.ContentManagers +{ + /// A content manager which handles reading files from the game content folder with support for interception. + internal class GameContentManager : BaseContentManager + { + /********* + ** Properties + *********/ + /// The assets currently being intercepted by instances. This is used to prevent infinite loops when a loader loads a new asset. + private readonly ContextHash AssetsBeingLoaded = new ContextHash(); + + /// Interceptors which provide the initial versions of matching assets. + private IDictionary> Loaders => this.Coordinator.Loaders; + + /// Interceptors which edit matching assets after they're loaded. + private IDictionary> Editors => this.Coordinator.Editors; + + /// A lookup which indicates whether the asset is localisable (i.e. the filename contains the locale), if previously loaded. + private readonly IDictionary IsLocalisableLookup; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A name for the mod manager. Not guaranteed to be unique. + /// The service provider to use to locate services. + /// The root directory to search for content. + /// The current culture for which to localise content. + /// The central coordinator which manages content managers. + /// Encapsulates monitoring and logging. + /// Simplifies access to private code. + /// A callback to invoke when the content manager is being disposed. + public GameContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action onDisposing) + : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isModFolder: false) + { + this.IsLocalisableLookup = reflection.GetField>(this, "_localizedAsset").GetValue(); + } + + /// Load an asset that has been processed by the content pipeline. + /// The type of asset to load. + /// The asset path relative to the loader root directory, not including the .xnb extension. + /// The language code for which to load content. + public override T Load(string assetName, LanguageCode language) + { + assetName = this.AssertAndNormaliseAssetName(assetName); + + // get from cache + if (this.IsLoaded(assetName)) + return base.Load(assetName, language); + + // get managed asset + if (this.Coordinator.TryParseManagedAssetKey(assetName, out string contentManagerID, out string relativePath)) + { + T managedAsset = this.Coordinator.LoadAndCloneManagedAsset(assetName, contentManagerID, relativePath, language); + this.Inject(assetName, managedAsset); + return managedAsset; + } + + // load asset + T data; + if (this.AssetsBeingLoaded.Contains(assetName)) + { + this.Monitor.Log($"Broke loop while loading asset '{assetName}'.", LogLevel.Warn); + this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}", LogLevel.Trace); + data = base.Load(assetName, language); + } + else + { + data = this.AssetsBeingLoaded.Track(assetName, () => + { + string locale = this.GetLocale(language); + IAssetInfo info = new AssetInfo(locale, assetName, typeof(T), this.AssertAndNormaliseAssetName); + IAssetData asset = + this.ApplyLoader(info) + ?? new AssetDataForObject(info, base.Load(assetName, language), this.AssertAndNormaliseAssetName); + asset = this.ApplyEditors(info, asset); + return (T)asset.Data; + }); + } + + // update cache & return data + this.Inject(assetName, data); + return data; + } + + /// Create a new content manager for temporary use. + public override LocalizedContentManager CreateTemporary() + { + return this.Coordinator.CreateGameContentManager("(temporary)"); + } + + + /********* + ** Private methods + *********/ + /// Get whether an asset has already been loaded. + /// The normalised asset name. + protected override bool IsNormalisedKeyLoaded(string normalisedAssetName) + { + // default English + if (this.Language == LocalizedContentManager.LanguageCode.en || this.Coordinator.IsManagedAssetKey(normalisedAssetName)) + return this.Cache.ContainsKey(normalisedAssetName); + + // translated + string localeKey = $"{normalisedAssetName}.{this.GetLocale(this.GetCurrentLanguage())}"; + if (this.IsLocalisableLookup.TryGetValue(localeKey, out bool localisable)) + { + return localisable + ? this.Cache.ContainsKey(localeKey) + : this.Cache.ContainsKey(normalisedAssetName); + } + + // not loaded yet + return false; + } + + /// Load the initial asset from the registered . + /// The basic asset metadata. + /// Returns the loaded asset metadata, or null if no loader matched. + private IAssetData ApplyLoader(IAssetInfo info) + { + // find matching loaders + var loaders = this.GetInterceptors(this.Loaders) + .Where(entry => + { + try + { + return entry.Value.CanLoad(info); + } + catch (Exception ex) + { + entry.Key.LogAsMod($"Mod failed when checking whether it could load asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + return false; + } + }) + .ToArray(); + + // validate loaders + if (!loaders.Any()) + return null; + if (loaders.Length > 1) + { + string[] loaderNames = loaders.Select(p => p.Key.DisplayName).ToArray(); + this.Monitor.Log($"Multiple mods want to provide the '{info.AssetName}' asset ({string.Join(", ", loaderNames)}), but an asset can't be loaded multiple times. SMAPI will use the default asset instead; uninstall one of the mods to fix this. (Message for modders: you should usually use {typeof(IAssetEditor)} instead to avoid conflicts.)", LogLevel.Warn); + return null; + } + + // fetch asset from loader + IModMetadata mod = loaders[0].Key; + IAssetLoader loader = loaders[0].Value; + T data; + try + { + data = loader.Load(info); + this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.AssetName}'.", LogLevel.Trace); + } + catch (Exception ex) + { + mod.LogAsMod($"Mod crashed when loading asset '{info.AssetName}'. SMAPI will use the default asset instead. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + return null; + } + + // validate asset + if (data == null) + { + mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Error); + return null; + } + + // return matched asset + return new AssetDataForObject(info, data, this.AssertAndNormaliseAssetName); + } + + /// Apply any to a loaded asset. + /// The asset type. + /// The basic asset metadata. + /// The loaded asset. + private IAssetData ApplyEditors(IAssetInfo info, IAssetData asset) + { + IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.AssertAndNormaliseAssetName); + + // edit asset + foreach (var entry in this.GetInterceptors(this.Editors)) + { + // check for match + IModMetadata mod = entry.Key; + IAssetEditor editor = entry.Value; + try + { + if (!editor.CanEdit(info)) + continue; + } + catch (Exception ex) + { + mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + continue; + } + + // try edit + object prevAsset = asset.Data; + try + { + editor.Edit(asset); + this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace); + } + catch (Exception ex) + { + mod.LogAsMod($"Mod crashed when editing asset '{info.AssetName}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + } + + // validate edit + if (asset.Data == null) + { + mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Warn); + asset = GetNewData(prevAsset); + } + else if (!(asset.Data is T)) + { + mod.LogAsMod($"Mod incorrectly set asset '{asset.AssetName}' to incompatible type '{asset.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn); + asset = GetNewData(prevAsset); + } + } + + // return result + return asset; + } + + /// Get all registered interceptors from a list. + private IEnumerable> GetInterceptors(IDictionary> entries) + { + foreach (var entry in entries) + { + IModMetadata mod = entry.Key; + IList interceptors = entry.Value; + + // registered editors + foreach (T interceptor in interceptors) + yield return new KeyValuePair(mod, interceptor); + } + } + } +} diff --git a/src/SMAPI/Framework/ContentManagers/IContentManager.cs b/src/SMAPI/Framework/ContentManagers/IContentManager.cs new file mode 100644 index 00000000..aa5be9b6 --- /dev/null +++ b/src/SMAPI/Framework/ContentManagers/IContentManager.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using Microsoft.Xna.Framework.Content; +using StardewModdingAPI.Framework.Exceptions; +using StardewValley; + +namespace StardewModdingAPI.Framework.ContentManagers +{ + /// A content manager which handles reading files. + internal interface IContentManager : IDisposable + { + /********* + ** Accessors + *********/ + /// A name for the mod manager. Not guaranteed to be unique. + string Name { get; } + + /// The current language as a constant. + LocalizedContentManager.LanguageCode Language { get; } + + /// The absolute path to the . + string FullRootDirectory { get; } + + /// Whether this content manager is for a mod folder. + bool IsModContentManager { get; } + + + /********* + ** Methods + *********/ + /// Load an asset that has been processed by the content pipeline. + /// The type of asset to load. + /// The asset path relative to the loader root directory, not including the .xnb extension. + T Load(string assetName); + + /// Load an asset that has been processed by the content pipeline. + /// The type of asset to load. + /// The asset path relative to the loader root directory, not including the .xnb extension. + /// The language code for which to load content. + T Load(string assetName, LocalizedContentManager.LanguageCode language); + + /// Inject an asset into the cache. + /// The type of asset to inject. + /// The asset path relative to the loader root directory, not including the .xnb extension. + /// The asset value. + void Inject(string assetName, T value); + + /// Normalise path separators in a file path. For asset keys, see instead. + /// The file path to normalise. + [Pure] + string NormalisePathSeparators(string path); + + /// Assert that the given key has a valid format and return a normalised form consistent with the underlying cache. + /// The asset key to check. + /// The asset key is empty or contains invalid characters. + [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")] + string AssertAndNormaliseAssetName(string assetName); + + /// Get the current content locale. + string GetLocale(); + + /// The locale for a language. + /// The language. + string GetLocale(LocalizedContentManager.LanguageCode language); + + /// Get whether the content manager has already loaded and cached the given asset. + /// The asset path relative to the loader root directory, not including the .xnb extension. + bool IsLoaded(string assetName); + + /// Get the cached asset keys. + IEnumerable GetAssetKeys(); + + /// Purge matched assets from the cache. + /// Matches the asset keys to invalidate. + /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. + /// Returns the number of invalidated assets. + IEnumerable InvalidateCache(Func predicate, bool dispose = false); + } +} diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs new file mode 100644 index 00000000..80bf37e9 --- /dev/null +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -0,0 +1,207 @@ +using System; +using System.Globalization; +using System.IO; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Framework.Reflection; +using StardewValley; + +namespace StardewModdingAPI.Framework.ContentManagers +{ + /// A content manager which handles reading files from a SMAPI mod folder with support for unpacked files. + internal class ModContentManager : BaseContentManager + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A name for the mod manager. Not guaranteed to be unique. + /// The service provider to use to locate services. + /// The root directory to search for content. + /// The current culture for which to localise content. + /// The central coordinator which manages content managers. + /// Encapsulates monitoring and logging. + /// Simplifies access to private code. + /// A callback to invoke when the content manager is being disposed. + public ModContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action onDisposing) + : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isModFolder: true) { } + + /// Load an asset that has been processed by the content pipeline. + /// The type of asset to load. + /// The asset path relative to the loader root directory, not including the .xnb extension. + /// The language code for which to load content. + public override T Load(string assetName, LanguageCode language) + { + assetName = this.AssertAndNormaliseAssetName(assetName); + + // get from cache + if (this.IsLoaded(assetName)) + return base.Load(assetName, language); + + // get managed asset + if (this.Coordinator.TryParseManagedAssetKey(assetName, out string contentManagerID, out string relativePath)) + { + if (contentManagerID != this.Name) + { + T data = this.Coordinator.LoadAndCloneManagedAsset(assetName, contentManagerID, relativePath, language); + this.Inject(assetName, data); + return data; + } + + return this.LoadManagedAsset(assetName, contentManagerID, relativePath, language); + } + + throw new NotSupportedException("Can't load content folder asset from a mod content manager."); + } + + /// Create a new content manager for temporary use. + public override LocalizedContentManager CreateTemporary() + { + throw new NotSupportedException("Can't create a temporary mod content manager."); + } + + + /********* + ** Private methods + *********/ + /// Get whether an asset has already been loaded. + /// The normalised asset name. + protected override bool IsNormalisedKeyLoaded(string normalisedAssetName) + { + return this.Cache.ContainsKey(normalisedAssetName); + } + + /// Load a managed mod asset. + /// The type of asset to load. + /// The internal asset key. + /// The unique name for the content manager which should load this asset. + /// The relative path within the mod folder. + /// The language code for which to load content. + private T LoadManagedAsset(string internalKey, string contentManagerID, string relativePath, LanguageCode language) + { + SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading asset '{relativePath}' from {contentManagerID}: {reasonPhrase}"); + try + { + // get file + FileInfo file = this.GetModFile(relativePath); + if (!file.Exists) + throw GetContentError("the specified path doesn't exist."); + + // load content + switch (file.Extension.ToLower()) + { + // XNB file + case ".xnb": + return base.Load(relativePath, language); + + // unpacked map + case ".tbin": + throw GetContentError($"can't read unpacked map file directly from the underlying content manager. It must be loaded through the mod's {typeof(IModHelper)}.{nameof(IModHelper.Content)} helper."); + + // unpacked image + case ".png": + // validate + if (typeof(T) != typeof(Texture2D)) + throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'."); + + // fetch & cache + using (FileStream stream = File.OpenRead(file.FullName)) + { + Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); + texture = this.PremultiplyTransparency(texture); + this.Inject(internalKey, texture); + return (T)(object)texture; + } + + default: + throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.png', '.tbin', or '.xnb'."); + } + } + catch (Exception ex) when (!(ex is SContentLoadException)) + { + if (ex.GetInnermostException() is DllNotFoundException dllEx && dllEx.Message == "libgdiplus.dylib") + throw GetContentError("couldn't find libgdiplus, which is needed to load mod images. Make sure Mono is installed and you're running the game through the normal launcher."); + throw new SContentLoadException($"The content manager failed loading content asset '{relativePath}' from {contentManagerID}.", ex); + } + } + + /// Get a file from the mod folder. + /// The asset path relative to the content folder. + private FileInfo GetModFile(string path) + { + // try exact match + FileInfo file = new FileInfo(Path.Combine(this.FullRootDirectory, path)); + + // try with default extension + if (!file.Exists && file.Extension.ToLower() != ".xnb") + { + FileInfo result = new FileInfo(file.FullName + ".xnb"); + if (result.Exists) + file = result; + } + + return file; + } + + /// Premultiply a texture's alpha values to avoid transparency issues in the game. This is only possible if the game isn't currently drawing. + /// The texture to premultiply. + /// Returns a premultiplied texture. + /// Based on code by Layoric. + private Texture2D PremultiplyTransparency(Texture2D texture) + { + // validate + if (Context.IsInDrawLoop) + throw new NotSupportedException("Can't load a PNG file while the game is drawing to the screen. Make sure you load content outside the draw loop."); + + // process texture + SpriteBatch spriteBatch = Game1.spriteBatch; + GraphicsDevice gpu = Game1.graphics.GraphicsDevice; + using (RenderTarget2D renderTarget = new RenderTarget2D(Game1.graphics.GraphicsDevice, texture.Width, texture.Height)) + { + // create blank render target to premultiply + gpu.SetRenderTarget(renderTarget); + gpu.Clear(Color.Black); + + // multiply each color by the source alpha, and write just the color values into the final texture + spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState + { + ColorDestinationBlend = Blend.Zero, + ColorWriteChannels = ColorWriteChannels.Red | ColorWriteChannels.Green | ColorWriteChannels.Blue, + AlphaDestinationBlend = Blend.Zero, + AlphaSourceBlend = Blend.SourceAlpha, + ColorSourceBlend = Blend.SourceAlpha + }); + spriteBatch.Draw(texture, texture.Bounds, Color.White); + spriteBatch.End(); + + // copy the alpha values from the source texture into the final one without multiplying them + spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState + { + ColorWriteChannels = ColorWriteChannels.Alpha, + AlphaDestinationBlend = Blend.Zero, + ColorDestinationBlend = Blend.Zero, + AlphaSourceBlend = Blend.One, + ColorSourceBlend = Blend.One + }); + spriteBatch.Draw(texture, texture.Bounds, Color.White); + spriteBatch.End(); + + // release GPU + gpu.SetRenderTarget(null); + + // extract premultiplied data + Color[] data = new Color[texture.Width * texture.Height]; + renderTarget.GetData(data); + + // unset texture from GPU to regain control + gpu.Textures[0] = null; + + // update texture with premultiplied data + texture.SetData(data); + } + + return texture; + } + } +} diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index 4a71f7e7..ce26c980 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Utilities; using StardewValley; @@ -25,8 +26,11 @@ namespace StardewModdingAPI.Framework.ModHelpers /// SMAPI's core content logic. private readonly ContentCoordinator ContentCore; - /// The content manager for this mod. - private readonly SContentManager ContentManager; + /// A content manager for this mod which manages files from the game's Content folder. + private readonly IContentManager GameContentManager; + + /// A content manager for this mod which manages files from the mod's folder. + private readonly IContentManager ModContentManager; /// The absolute path to the mod folder. private readonly string ModFolderPath; @@ -42,10 +46,10 @@ namespace StardewModdingAPI.Framework.ModHelpers ** Accessors *********/ /// The game's current locale code (like pt-BR). - public string CurrentLocale => this.ContentManager.GetLocale(); + public string CurrentLocale => this.GameContentManager.GetLocale(); /// The game's current locale as an enum value. - public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.ContentManager.Language; + public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.GameContentManager.Language; /// The observable implementation of . internal ObservableCollection ObservableAssetEditors { get; } = new ObservableCollection(); @@ -65,16 +69,16 @@ namespace StardewModdingAPI.Framework.ModHelpers *********/ /// Construct an instance. /// SMAPI's core content logic. - /// The content manager for this mod. /// The absolute path to the mod folder. /// The unique ID of the relevant mod. /// The friendly mod name for use in errors. /// Encapsulates monitoring and logging. - public ContentHelper(ContentCoordinator contentCore, SContentManager contentManager, string modFolderPath, string modID, string modName, IMonitor monitor) + public ContentHelper(ContentCoordinator contentCore, string modFolderPath, string modID, string modName, IMonitor monitor) : base(modID) { this.ContentCore = contentCore; - this.ContentManager = contentManager; + this.GameContentManager = contentCore.CreateGameContentManager(this.ContentCore.GetManagedAssetPrefix(modID) + ".content"); + this.ModContentManager = contentCore.CreateModContentManager(this.ContentCore.GetManagedAssetPrefix(modID), rootDirectory: modFolderPath); this.ModFolderPath = modFolderPath; this.ModName = modName; this.Monitor = monitor; @@ -92,24 +96,22 @@ namespace StardewModdingAPI.Framework.ModHelpers try { - this.AssertValidAssetKeyFormat(key); + this.AssertAndNormaliseAssetName(key); switch (source) { case ContentSource.GameContent: - return this.ContentCore.MainContentManager.Load(key); + return this.GameContentManager.Load(key); case ContentSource.ModFolder: // get file FileInfo file = this.GetModFile(key); if (!file.Exists) throw GetContentError($"there's no matching file at path '{file.FullName}'."); - - // get asset path - string assetName = this.ContentManager.GetAssetNameFromFilePath(file.FullName, ContentSource.ModFolder); + string internalKey = this.GetInternalModAssetKey(file); // try cache - if (this.ContentManager.IsLoaded(assetName)) - return this.ContentManager.Load(assetName); + if (this.ModContentManager.IsLoaded(internalKey)) + return this.ModContentManager.Load(internalKey); // fix map tilesheets if (file.Extension.ToLower() == ".tbin") @@ -121,15 +123,15 @@ namespace StardewModdingAPI.Framework.ModHelpers // fetch & cache FormatManager formatManager = FormatManager.Instance; Map map = formatManager.LoadMap(file.FullName); - this.FixCustomTilesheetPaths(map, key); + this.FixCustomTilesheetPaths(map, relativeMapPath: key); // inject map - this.ContentManager.Inject(assetName, map); + this.ModContentManager.Inject(internalKey, map); return (T)(object)map; } // load through content manager - return this.ContentManager.Load(assetName); + return this.ModContentManager.Load(internalKey); default: throw GetContentError($"unknown content source '{source}'."); @@ -146,7 +148,7 @@ namespace StardewModdingAPI.Framework.ModHelpers [Pure] public string NormaliseAssetName(string assetName) { - return this.ContentManager.NormaliseAssetName(assetName); + return this.ModContentManager.AssertAndNormaliseAssetName(assetName); } /// Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists. @@ -158,11 +160,11 @@ namespace StardewModdingAPI.Framework.ModHelpers switch (source) { case ContentSource.GameContent: - return this.ContentManager.NormaliseAssetName(key); + return this.GameContentManager.AssertAndNormaliseAssetName(key); case ContentSource.ModFolder: FileInfo file = this.GetModFile(key); - return this.ContentManager.NormaliseAssetName(this.ContentManager.GetAssetNameFromFilePath(file.FullName, ContentSource.GameContent)); + return this.GetInternalModAssetKey(file); default: throw new NotSupportedException($"Unknown content source '{source}'."); @@ -205,16 +207,24 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The asset key to check. /// The asset key is empty or contains invalid characters. [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")] - private void AssertValidAssetKeyFormat(string key) + private void AssertAndNormaliseAssetName(string key) { - this.ContentManager.AssertValidAssetKeyFormat(key); + this.ModContentManager.AssertAndNormaliseAssetName(key); if (Path.IsPathRooted(key)) throw new ArgumentException("The asset key must not be an absolute path."); } + /// Get the internal key in the content cache for a mod asset. + /// The asset file. + private string GetInternalModAssetKey(FileInfo modFile) + { + string relativePath = PathUtilities.GetRelativePath(this.ModFolderPath, modFile.FullName); + return Path.Combine(this.ModContentManager.Name, relativePath); + } + /// Fix custom map tilesheet paths so they can be found by the content manager. /// The map whose tilesheets to fix. - /// The map asset key within the mod folder. + /// The relative map path within the mod folder. /// A map tilesheet couldn't be resolved. /// /// The game's logic for tilesheets in is a bit specialised. It boils @@ -230,13 +240,13 @@ namespace StardewModdingAPI.Framework.ModHelpers /// /// While that doesn't exactly match the game logic, it's close enough that it's unlikely to make a difference. /// - private void FixCustomTilesheetPaths(Map map, string mapKey) + private void FixCustomTilesheetPaths(Map map, string relativeMapPath) { // get map info if (!map.TileSheets.Any()) return; - mapKey = this.ContentManager.NormaliseAssetName(mapKey); // Mono's Path.GetDirectoryName doesn't handle Windows dir separators - string relativeMapFolder = Path.GetDirectoryName(mapKey) ?? ""; // folder path containing the map, relative to the mod folder + relativeMapPath = this.ModContentManager.AssertAndNormaliseAssetName(relativeMapPath); // Mono's Path.GetDirectoryName doesn't handle Windows dir separators + string relativeMapFolder = Path.GetDirectoryName(relativeMapPath) ?? ""; // folder path containing the map, relative to the mod folder // fix tilesheets foreach (TileSheet tilesheet in map.TileSheets) @@ -341,7 +351,7 @@ namespace StardewModdingAPI.Framework.ModHelpers private FileInfo GetModFile(string path) { // try exact match - path = Path.Combine(this.ModFolderPath, this.ContentManager.NormalisePathSeparators(path)); + path = Path.Combine(this.ModFolderPath, this.ModContentManager.NormalisePathSeparators(path)); FileInfo file = new FileInfo(path); // try with default extension @@ -360,7 +370,7 @@ namespace StardewModdingAPI.Framework.ModHelpers private FileInfo GetContentFolderFile(string key) { // get file path - string path = Path.Combine(this.ContentManager.FullRootDirectory, key); + string path = Path.Combine(this.GameContentManager.FullRootDirectory, key); if (!path.EndsWith(".xnb")) path += ".xnb"; diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs deleted file mode 100644 index e97e655d..00000000 --- a/src/SMAPI/Framework/SContentManager.cs +++ /dev/null @@ -1,653 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; -using System.Globalization; -using System.IO; -using System.Linq; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Content; -using Microsoft.Xna.Framework.Graphics; -using StardewModdingAPI.Framework.Content; -using StardewModdingAPI.Framework.Exceptions; -using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI.Framework.Utilities; -using StardewValley; - -namespace StardewModdingAPI.Framework -{ - /// A minimal content manager which defers to SMAPI's core content logic. - internal class SContentManager : LocalizedContentManager - { - /********* - ** Properties - *********/ - /// The central coordinator which manages content managers. - private readonly ContentCoordinator Coordinator; - - /// The underlying asset cache. - private readonly ContentCache Cache; - - /// Encapsulates monitoring and logging. - private readonly IMonitor Monitor; - - /// A lookup which indicates whether the asset is localisable (i.e. the filename contains the locale), if previously loaded. - private readonly IDictionary IsLocalisableLookup; - - /// The language enum values indexed by locale code. - private readonly IDictionary LanguageCodes; - - /// The assets currently being intercepted by instances. This is used to prevent infinite loops when a loader loads a new asset. - private readonly ContextHash AssetsBeingLoaded = new ContextHash(); - - /// The path prefix for assets in mod folders. - private readonly string ModContentPrefix; - - /// A callback to invoke when the content manager is being disposed. - private readonly Action OnDisposing; - - /// Interceptors which provide the initial versions of matching assets. - private IDictionary> Loaders => this.Coordinator.Loaders; - - /// Interceptors which edit matching assets after they're loaded. - private IDictionary> Editors => this.Coordinator.Editors; - - /// Whether the content coordinator has been disposed. - private bool IsDisposed; - - - /********* - ** Accessors - *********/ - /// A name for the mod manager. Not guaranteed to be unique. - public string Name { get; } - - /// Whether this content manager is wrapped around a mod folder. - public bool IsModFolder { get; } - - /// The current language as a constant. - public LocalizedContentManager.LanguageCode Language => this.GetCurrentLanguage(); - - /// The absolute path to the . - public string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory); - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// A name for the mod manager. Not guaranteed to be unique. - /// The service provider to use to locate services. - /// The root directory to search for content. - /// The current culture for which to localise content. - /// The central coordinator which manages content managers. - /// Encapsulates monitoring and logging. - /// Simplifies access to private code. - /// Whether this content manager is wrapped around a mod folder. - /// A callback to invoke when the content manager is being disposed. - public SContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action onDisposing, bool isModFolder) - : base(serviceProvider, rootDirectory, currentCulture) - { - // init - this.Name = name; - this.IsModFolder = isModFolder; - this.Coordinator = coordinator ?? throw new ArgumentNullException(nameof(coordinator)); - this.Cache = new ContentCache(this, reflection); - this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); - this.ModContentPrefix = this.GetAssetNameFromFilePath(Constants.ModPath, ContentSource.GameContent); - this.OnDisposing = onDisposing; - - // get asset data - this.LanguageCodes = this.GetKeyLocales().ToDictionary(p => p.Value, p => p.Key, StringComparer.InvariantCultureIgnoreCase); - this.IsLocalisableLookup = reflection.GetField>(this, "_localizedAsset").GetValue(); - - } - - /// Load an asset that has been processed by the content pipeline. - /// The type of asset to load. - /// The asset path relative to the loader root directory, not including the .xnb extension. - public override T Load(string assetName) - { - return this.Load(assetName, LocalizedContentManager.CurrentLanguageCode); - } - - /// Load an asset that has been processed by the content pipeline. - /// The type of asset to load. - /// The asset path relative to the loader root directory, not including the .xnb extension. - /// The language code for which to load content. - public override T Load(string assetName, LanguageCode language) - { - // normalise asset key - this.AssertValidAssetKeyFormat(assetName); - assetName = this.NormaliseAssetName(assetName); - - // load game content - if (!this.IsModFolder && !assetName.StartsWith(this.ModContentPrefix)) - return this.LoadImpl(assetName, language); - - // load mod content - SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading content asset '{assetName}': {reasonPhrase}"); - try - { - // try cache - if (this.IsLoaded(assetName)) - return this.LoadImpl(assetName, language); - - // get file - FileInfo file = this.GetModFile(assetName); - if (!file.Exists) - throw GetContentError("the specified path doesn't exist."); - - // load content - switch (file.Extension.ToLower()) - { - // XNB file - case ".xnb": - return this.LoadImpl(assetName, language); - - // unpacked map - case ".tbin": - throw GetContentError($"can't read unpacked map file '{assetName}' directly from the underlying content manager. It must be loaded through the mod's {typeof(IModHelper)}.{nameof(IModHelper.Content)} helper."); - - // unpacked image - case ".png": - // validate - if (typeof(T) != typeof(Texture2D)) - throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'."); - - // fetch & cache - using (FileStream stream = File.OpenRead(file.FullName)) - { - Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); - texture = this.PremultiplyTransparency(texture); - this.Inject(assetName, texture); - return (T)(object)texture; - } - - default: - throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.png', '.tbin', or '.xnb'."); - } - } - catch (Exception ex) when (!(ex is SContentLoadException)) - { - if (ex.GetInnermostException() is DllNotFoundException dllEx && dllEx.Message == "libgdiplus.dylib") - throw GetContentError("couldn't find libgdiplus, which is needed to load mod images. Make sure Mono is installed and you're running the game through the normal launcher."); - throw new SContentLoadException($"The content manager failed loading content asset '{assetName}'.", ex); - } - } - - /// Load the base asset without localisation. - /// The type of asset to load. - /// The asset path relative to the loader root directory, not including the .xnb extension. - public override T LoadBase(string assetName) - { - return this.Load(assetName, LanguageCode.en); - } - - /// Inject an asset into the cache. - /// The type of asset to inject. - /// The asset path relative to the loader root directory, not including the .xnb extension. - /// The asset value. - public void Inject(string assetName, T value) - { - assetName = this.NormaliseAssetName(assetName); - this.Cache[assetName] = value; - } - - /// Create a new content manager for temporary use. - public override LocalizedContentManager CreateTemporary() - { - return this.Coordinator.CreateContentManager("(temporary)", isModFolder: false); - } - - /// Normalise path separators in a file path. For asset keys, see instead. - /// The file path to normalise. - [Pure] - public string NormalisePathSeparators(string path) - { - return this.Cache.NormalisePathSeparators(path); - } - - /// Normalise an asset name so it's consistent with the underlying cache. - /// The asset key. - [Pure] - public string NormaliseAssetName(string assetName) - { - return this.Cache.NormaliseKey(assetName); - } - - /// Assert that the given key has a valid format. - /// The asset key to check. - /// The asset key is empty or contains invalid characters. - [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")] - public void AssertValidAssetKeyFormat(string key) - { - // NOTE: the game checks for ContentLoadException to handle invalid keys, so avoid - // throwing other types like ArgumentException here. - if (string.IsNullOrWhiteSpace(key)) - throw new SContentLoadException("The asset key or local path is empty."); - if (key.Intersect(Path.GetInvalidPathChars()).Any()) - throw new SContentLoadException("The asset key or local path contains invalid characters."); - } - - /// Convert an absolute file path into an appropriate asset name. - /// The absolute path to the file. - /// The folder to which to get a relative path. - public string GetAssetNameFromFilePath(string absolutePath, ContentSource relativeTo) - { -#if SMAPI_FOR_WINDOWS - // XNA doesn't allow absolute asset paths, so get a path relative to the source folder - string sourcePath = relativeTo == ContentSource.GameContent ? this.Coordinator.FullRootDirectory : this.FullRootDirectory; - return this.GetRelativePath(sourcePath, absolutePath); -#else - // MonoGame is weird about relative paths on Mac, but allows absolute paths - return absolutePath; -#endif - } - - /**** - ** Content loading - ****/ - /// Get the current content locale. - public string GetLocale() - { - return this.GetLocale(this.GetCurrentLanguage()); - } - - /// The locale for a language. - /// The language. - public string GetLocale(LocalizedContentManager.LanguageCode language) - { - return this.LanguageCodeString(language); - } - - /// Get whether the content manager has already loaded and cached the given asset. - /// The asset path relative to the loader root directory, not including the .xnb extension. - public bool IsLoaded(string assetName) - { - assetName = this.Cache.NormaliseKey(assetName); - return this.IsNormalisedKeyLoaded(assetName); - } - - /// Get the cached asset keys. - public IEnumerable GetAssetKeys() - { - return this.Cache.Keys - .Select(this.GetAssetName) - .Distinct(); - } - - /**** - ** Cache invalidation - ****/ - /// Purge matched assets from the cache. - /// Matches the asset keys to invalidate. - /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. - /// Returns the number of invalidated assets. - public IEnumerable InvalidateCache(Func predicate, bool dispose = false) - { - HashSet removeAssetNames = new HashSet(StringComparer.InvariantCultureIgnoreCase); - this.Cache.Remove((key, type) => - { - this.ParseCacheKey(key, out string assetName, out _); - if (removeAssetNames.Contains(assetName) || predicate(assetName, type)) - { - removeAssetNames.Add(assetName); - return true; - } - return false; - }); - - return removeAssetNames; - } - - /// Dispose held resources. - /// Whether the content manager is being disposed (rather than finalized). - protected override void Dispose(bool isDisposing) - { - if (this.IsDisposed) - return; - this.IsDisposed = true; - - this.OnDisposing(this); - base.Dispose(isDisposing); - } - - /// - public override void Unload() - { - if (this.IsDisposed) - return; // base logic doesn't allow unloading twice, which happens due to SMAPI and the game both unloading - - base.Unload(); - } - - - /********* - ** Private methods - *********/ - /**** - ** Asset name/key handling - ****/ - /// Get a directory or file path relative to the content root. - /// The source file path. - /// The target file path. - private string GetRelativePath(string sourcePath, string targetPath) - { - return PathUtilities.GetRelativePath(sourcePath, targetPath); - } - - /// Get the locale codes (like ja-JP) used in asset keys. - private IDictionary GetKeyLocales() - { - // create locale => code map - IDictionary map = new Dictionary(); - foreach (LocalizedContentManager.LanguageCode code in Enum.GetValues(typeof(LocalizedContentManager.LanguageCode))) - map[code] = this.GetLocale(code); - - return map; - } - - /// Get the asset name from a cache key. - /// The input cache key. - private string GetAssetName(string cacheKey) - { - this.ParseCacheKey(cacheKey, out string assetName, out string _); - return assetName; - } - - /// Parse a cache key into its component parts. - /// The input cache key. - /// The original asset name. - /// The asset locale code (or null if not localised). - private void ParseCacheKey(string cacheKey, out string assetName, out string localeCode) - { - // handle localised key - if (!string.IsNullOrWhiteSpace(cacheKey)) - { - int lastSepIndex = cacheKey.LastIndexOf(".", StringComparison.InvariantCulture); - if (lastSepIndex >= 0) - { - string suffix = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); - if (this.LanguageCodes.ContainsKey(suffix)) - { - assetName = cacheKey.Substring(0, lastSepIndex); - localeCode = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); - return; - } - } - } - - // handle simple key - assetName = cacheKey; - localeCode = null; - } - - /**** - ** Cache handling - ****/ - /// Get whether an asset has already been loaded. - /// The normalised asset name. - private bool IsNormalisedKeyLoaded(string normalisedAssetName) - { - // default English - if (this.Language == LocalizedContentManager.LanguageCode.en) - return this.Cache.ContainsKey(normalisedAssetName); - - // translated - string localeKey = $"{normalisedAssetName}.{this.GetLocale(this.GetCurrentLanguage())}"; - if (this.IsLocalisableLookup.TryGetValue(localeKey, out bool localisable)) - { - return localisable - ? this.Cache.ContainsKey(localeKey) - : this.Cache.ContainsKey(normalisedAssetName); - } - - // not loaded yet - return false; - } - - /**** - ** Content loading - ****/ - /// Load an asset name without heuristics to support mod content. - /// The type of asset to load. - /// The asset path relative to the loader root directory, not including the .xnb extension. - /// The language code for which to load content. - private T LoadImpl(string assetName, LocalizedContentManager.LanguageCode language) - { - // skip if already loaded - if (this.IsNormalisedKeyLoaded(assetName)) - return base.Load(assetName, language); - - // load asset - T data; - if (this.AssetsBeingLoaded.Contains(assetName)) - { - this.Monitor.Log($"Broke loop while loading asset '{assetName}'.", LogLevel.Warn); - this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}", LogLevel.Trace); - data = base.Load(assetName, language); - } - else - { - data = this.AssetsBeingLoaded.Track(assetName, () => - { - string locale = this.GetLocale(language); - IAssetInfo info = new AssetInfo(locale, assetName, typeof(T), this.NormaliseAssetName); - IAssetData asset = - this.ApplyLoader(info) - ?? new AssetDataForObject(info, base.Load(assetName, language), this.NormaliseAssetName); - asset = this.ApplyEditors(info, asset); - return (T)asset.Data; - }); - } - - // update cache & return data - this.Inject(assetName, data); - return data; - } - - /// Get a file from the mod folder. - /// The asset path relative to the content folder. - private FileInfo GetModFile(string path) - { - // try exact match - FileInfo file = new FileInfo(Path.Combine(this.FullRootDirectory, path)); - - // try with default extension - if (!file.Exists && file.Extension.ToLower() != ".xnb") - { - FileInfo result = new FileInfo(file.FullName + ".xnb"); - if (result.Exists) - file = result; - } - - return file; - } - - /// Load the initial asset from the registered . - /// The basic asset metadata. - /// Returns the loaded asset metadata, or null if no loader matched. - private IAssetData ApplyLoader(IAssetInfo info) - { - // find matching loaders - var loaders = this.GetInterceptors(this.Loaders) - .Where(entry => - { - try - { - return entry.Value.CanLoad(info); - } - catch (Exception ex) - { - entry.Key.LogAsMod($"Mod failed when checking whether it could load asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - return false; - } - }) - .ToArray(); - - // validate loaders - if (!loaders.Any()) - return null; - if (loaders.Length > 1) - { - string[] loaderNames = loaders.Select(p => p.Key.DisplayName).ToArray(); - this.Monitor.Log($"Multiple mods want to provide the '{info.AssetName}' asset ({string.Join(", ", loaderNames)}), but an asset can't be loaded multiple times. SMAPI will use the default asset instead; uninstall one of the mods to fix this. (Message for modders: you should usually use {typeof(IAssetEditor)} instead to avoid conflicts.)", LogLevel.Warn); - return null; - } - - // fetch asset from loader - IModMetadata mod = loaders[0].Key; - IAssetLoader loader = loaders[0].Value; - T data; - try - { - data = loader.Load(info); - this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.AssetName}'.", LogLevel.Trace); - } - catch (Exception ex) - { - mod.LogAsMod($"Mod crashed when loading asset '{info.AssetName}'. SMAPI will use the default asset instead. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - return null; - } - - // validate asset - if (data == null) - { - mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Error); - return null; - } - - // return matched asset - return new AssetDataForObject(info, data, this.NormaliseAssetName); - } - - /// Apply any to a loaded asset. - /// The asset type. - /// The basic asset metadata. - /// The loaded asset. - private IAssetData ApplyEditors(IAssetInfo info, IAssetData asset) - { - IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.NormaliseAssetName); - - // edit asset - foreach (var entry in this.GetInterceptors(this.Editors)) - { - // check for match - IModMetadata mod = entry.Key; - IAssetEditor editor = entry.Value; - try - { - if (!editor.CanEdit(info)) - continue; - } - catch (Exception ex) - { - mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - continue; - } - - // try edit - object prevAsset = asset.Data; - try - { - editor.Edit(asset); - this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace); - } - catch (Exception ex) - { - mod.LogAsMod($"Mod crashed when editing asset '{info.AssetName}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - } - - // validate edit - if (asset.Data == null) - { - mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Warn); - asset = GetNewData(prevAsset); - } - else if (!(asset.Data is T)) - { - mod.LogAsMod($"Mod incorrectly set asset '{asset.AssetName}' to incompatible type '{asset.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn); - asset = GetNewData(prevAsset); - } - } - - // return result - return asset; - } - - /// Get all registered interceptors from a list. - private IEnumerable> GetInterceptors(IDictionary> entries) - { - foreach (var entry in entries) - { - IModMetadata mod = entry.Key; - IList interceptors = entry.Value; - - // registered editors - foreach (T interceptor in interceptors) - yield return new KeyValuePair(mod, interceptor); - } - } - - /// Premultiply a texture's alpha values to avoid transparency issues in the game. This is only possible if the game isn't currently drawing. - /// The texture to premultiply. - /// Returns a premultiplied texture. - /// Based on code by Layoric. - private Texture2D PremultiplyTransparency(Texture2D texture) - { - // validate - if (Context.IsInDrawLoop) - throw new NotSupportedException("Can't load a PNG file while the game is drawing to the screen. Make sure you load content outside the draw loop."); - - // process texture - SpriteBatch spriteBatch = Game1.spriteBatch; - GraphicsDevice gpu = Game1.graphics.GraphicsDevice; - using (RenderTarget2D renderTarget = new RenderTarget2D(Game1.graphics.GraphicsDevice, texture.Width, texture.Height)) - { - // create blank render target to premultiply - gpu.SetRenderTarget(renderTarget); - gpu.Clear(Color.Black); - - // multiply each color by the source alpha, and write just the color values into the final texture - spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState - { - ColorDestinationBlend = Blend.Zero, - ColorWriteChannels = ColorWriteChannels.Red | ColorWriteChannels.Green | ColorWriteChannels.Blue, - AlphaDestinationBlend = Blend.Zero, - AlphaSourceBlend = Blend.SourceAlpha, - ColorSourceBlend = Blend.SourceAlpha - }); - spriteBatch.Draw(texture, texture.Bounds, Color.White); - spriteBatch.End(); - - // copy the alpha values from the source texture into the final one without multiplying them - spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState - { - ColorWriteChannels = ColorWriteChannels.Alpha, - AlphaDestinationBlend = Blend.Zero, - ColorDestinationBlend = Blend.Zero, - AlphaSourceBlend = Blend.One, - ColorSourceBlend = Blend.One - }); - spriteBatch.Draw(texture, texture.Bounds, Color.White); - spriteBatch.End(); - - // release GPU - gpu.SetRenderTarget(null); - - // extract premultiplied data - Color[] data = new Color[texture.Width * texture.Height]; - renderTarget.GetData(data); - - // unset texture from GPU to regain control - gpu.Textures[0] = null; - - // update texture with premultiplied data - texture.SetData(data); - } - - return texture; - } - } -} diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 8a4f987b..91612fb0 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -202,7 +202,7 @@ namespace StardewModdingAPI.Framework this.ContentCore = new ContentCoordinator(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, SGame.MonitorDuringInitialisation, SGame.ReflectorDuringInitialisation); SGame.MonitorDuringInitialisation = null; this.NextContentManagerIsMain = true; - return this.ContentCore.CreateContentManager("Game1._temporaryContent", isModFolder: false); + return this.ContentCore.CreateGameContentManager("Game1._temporaryContent"); } // Game1.content initialising from LoadContent @@ -213,7 +213,7 @@ namespace StardewModdingAPI.Framework } // any other content manager - return this.ContentCore.CreateContentManager("(generated)", isModFolder: false, rootDirectory: rootDirectory); + return this.ContentCore.CreateGameContentManager("(generated)"); } /// The method called when the game is updating its state. This happens roughly 60 times per second. diff --git a/src/SMAPI/Framework/Utilities/PathUtilities.cs b/src/SMAPI/Framework/Utilities/PathUtilities.cs index 0233d796..51d45ebd 100644 --- a/src/SMAPI/Framework/Utilities/PathUtilities.cs +++ b/src/SMAPI/Framework/Utilities/PathUtilities.cs @@ -23,9 +23,12 @@ namespace StardewModdingAPI.Framework.Utilities *********/ /// Get the segments from a path (e.g. /usr/bin/boop => usr, bin, and boop). /// The path to split. - public static string[] GetSegments(string path) + /// The number of segments to match. Any additional segments will be merged into the last returned part. + public static string[] GetSegments(string path, int? limit = null) { - return path.Split(PathUtilities.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries); + return limit.HasValue + ? path.Split(PathUtilities.PossiblePathSeparators, limit.Value, StringSplitOptions.RemoveEmptyEntries) + : path.Split(PathUtilities.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries); } /// Normalise path separators in a file path. diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 6aff6dc6..340d2ddb 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -755,8 +755,7 @@ namespace StardewModdingAPI // load mod as content pack IManifest manifest = metadata.Manifest; IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); - SContentManager contentManager = this.ContentCore.CreateContentManager($"Mods.{metadata.Manifest.UniqueID}", isModFolder: true, rootDirectory: metadata.DirectoryPath); - IContentHelper contentHelper = new ContentHelper(this.ContentCore, contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); + IContentHelper contentHelper = new ContentHelper(this.ContentCore, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); IContentPack contentPack = new ContentPack(metadata.DirectoryPath, manifest, contentHelper, jsonHelper); metadata.SetMod(contentPack, monitor); this.ModRegistry.Add(metadata); @@ -844,8 +843,7 @@ namespace StardewModdingAPI IModHelper modHelper; { ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager); - SContentManager contentManager = this.ContentCore.CreateContentManager($"Mods.{metadata.Manifest.UniqueID}", isModFolder: true, rootDirectory: metadata.DirectoryPath); - IContentHelper contentHelper = new ContentHelper(contentCore, contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); + IContentHelper contentHelper = new ContentHelper(contentCore, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor); IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(manifest.UniqueID, this.GameInstance.Multiplayer); @@ -854,8 +852,7 @@ namespace StardewModdingAPI IContentPack CreateTransitionalContentPack(string packDirPath, IManifest packManifest) { IMonitor packMonitor = this.GetSecondaryMonitor(packManifest.Name); - SContentManager packContentManager = this.ContentCore.CreateContentManager($"Mods.{packManifest.UniqueID}", isModFolder: true, rootDirectory: packDirPath); - IContentHelper packContentHelper = new ContentHelper(contentCore, packContentManager, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); + IContentHelper packContentHelper = new ContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); return new ContentPack(packDirPath, packManifest, packContentHelper, this.JsonHelper); } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 19c1e6fe..e9e0ea54 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -87,6 +87,10 @@ + + + + @@ -123,7 +127,6 @@ - -- cgit From 2a7bcb28f6a390e165c2cb8817f333aad750a032 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 25 May 2018 01:13:28 -0400 Subject: add empty toolkit project & fix misleading build configuration name (#532) --- build/common.targets | 8 ++- build/prepare-install-package.targets | 8 ++- src/SMAPI.sln | 82 ++++++++++++---------- src/SMAPI/StardewModdingAPI.csproj | 6 ++ .../Properties/AssemblyInfo.cs | 6 ++ .../StardewModdingAPI.Toolkit.csproj | 22 ++++++ 6 files changed, 91 insertions(+), 41 deletions(-) create mode 100644 src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs create mode 100644 src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/build/common.targets b/build/common.targets index b382ea54..2ee24e52 100644 --- a/build/common.targets +++ b/build/common.targets @@ -25,7 +25,7 @@ - + @@ -95,7 +95,7 @@ - + @@ -106,6 +106,10 @@ + + + + diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index 433078d7..9a5cde3f 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -7,7 +7,9 @@ --> - $(SolutionDir)\..\bin\$(Configuration)\SMAPI + $(SolutionDir)\..\bin\$(Configuration) + $(CompiledRootPath)\SMAPI + $(CompiledRootPath)\SMAPI.Toolkit\net4.5 $(SolutionDir)\..\bin\Packaged $(PackagePath)\internal @@ -35,6 +37,8 @@ + + @@ -46,6 +50,8 @@ + + diff --git a/src/SMAPI.sln b/src/SMAPI.sln index dec26694..0eb42cce 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -60,6 +60,8 @@ Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "StardewModdingAPI.Internal" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Mods.SaveBackup", "SMAPI.Mods.SaveBackup\StardewModdingAPI.Mods.SaveBackup.csproj", "{E272EB5D-8C57-417E-8E60-C1079D3F53C4}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Toolkit", "StardewModdingAPI.Toolkit\StardewModdingAPI.Toolkit.csproj", "{EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution SMAPI.Internal\SMAPI.Internal.projitems*{443ddf81-6aaf-420a-a610-3459f37e5575}*SharedItemsImports = 4 @@ -68,46 +70,50 @@ Global SMAPI.Internal\SMAPI.Internal.projitems*{f1a573b0-f436-472c-ae29-0b91ea6b9f8f}*SharedItemsImports = 4 EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|x86 = Debug|x86 - Release|x86 = Release|x86 + Debug|Default = Debug|Default + Release|Default = Release|Default EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {28480467-1A48-46A7-99F8-236D95225359}.Debug|x86.ActiveCfg = Debug|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Debug|x86.Build.0 = Debug|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Release|x86.ActiveCfg = Release|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Release|x86.Build.0 = Release|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|x86.ActiveCfg = Debug|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|x86.Build.0 = Debug|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|x86.ActiveCfg = Release|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|x86.Build.0 = Release|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|x86.ActiveCfg = Debug|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|x86.Build.0 = Debug|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|x86.ActiveCfg = Release|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|x86.Build.0 = Release|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|x86.ActiveCfg = Debug|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|x86.Build.0 = Debug|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|x86.ActiveCfg = Release|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|x86.Build.0 = Release|x86 - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|x86.ActiveCfg = Debug|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|x86.Build.0 = Debug|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|x86.ActiveCfg = Release|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|x86.Build.0 = Release|Any CPU - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|x86.ActiveCfg = Debug|x86 - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|x86.Build.0 = Debug|x86 - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|x86.ActiveCfg = Release|x86 - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|x86.Build.0 = Release|x86 - {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|x86.ActiveCfg = Debug|Any CPU - {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|x86.Build.0 = Debug|Any CPU - {80AD8528-AA49-4731-B4A6-C691845815A1}.Release|x86.ActiveCfg = Release|Any CPU - {80AD8528-AA49-4731-B4A6-C691845815A1}.Release|x86.Build.0 = Release|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|x86.ActiveCfg = Debug|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|x86.Build.0 = Debug|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|x86.ActiveCfg = Release|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|x86.Build.0 = Release|Any CPU - {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Debug|x86.ActiveCfg = Debug|x86 - {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Debug|x86.Build.0 = Debug|x86 - {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Release|x86.ActiveCfg = Release|x86 - {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Release|x86.Build.0 = Release|x86 + {28480467-1A48-46A7-99F8-236D95225359}.Debug|Default.ActiveCfg = Debug|x86 + {28480467-1A48-46A7-99F8-236D95225359}.Debug|Default.Build.0 = Debug|x86 + {28480467-1A48-46A7-99F8-236D95225359}.Release|Default.ActiveCfg = Release|x86 + {28480467-1A48-46A7-99F8-236D95225359}.Release|Default.Build.0 = Release|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|Default.ActiveCfg = Debug|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|Default.Build.0 = Debug|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|Default.ActiveCfg = Release|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|Default.Build.0 = Release|x86 + {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|Default.ActiveCfg = Debug|x86 + {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|Default.Build.0 = Debug|x86 + {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Default.ActiveCfg = Release|x86 + {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Default.Build.0 = Release|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Default.ActiveCfg = Debug|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Default.Build.0 = Debug|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Default.ActiveCfg = Release|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Default.Build.0 = Release|x86 + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Default.ActiveCfg = Debug|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Default.Build.0 = Debug|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Default.ActiveCfg = Release|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Default.Build.0 = Release|Any CPU + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Default.ActiveCfg = Debug|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Default.Build.0 = Debug|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Default.ActiveCfg = Release|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Default.Build.0 = Release|x86 + {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|Default.ActiveCfg = Debug|Any CPU + {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|Default.Build.0 = Debug|Any CPU + {80AD8528-AA49-4731-B4A6-C691845815A1}.Release|Default.ActiveCfg = Release|Any CPU + {80AD8528-AA49-4731-B4A6-C691845815A1}.Release|Default.Build.0 = Release|Any CPU + {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|Default.ActiveCfg = Debug|Any CPU + {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|Default.Build.0 = Debug|Any CPU + {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|Default.ActiveCfg = Release|Any CPU + {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|Default.Build.0 = Release|Any CPU + {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Debug|Default.ActiveCfg = Debug|x86 + {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Debug|Default.Build.0 = Debug|x86 + {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Release|Default.ActiveCfg = Release|x86 + {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Release|Default.Build.0 = Release|x86 + {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Debug|Default.ActiveCfg = Debug|Any CPU + {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Debug|Default.Build.0 = Debug|Any CPU + {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Release|Default.ActiveCfg = Release|Any CPU + {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Release|Default.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index e9e0ea54..926bcbbc 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -314,6 +314,12 @@ + + + {ea5cfd2e-9453-4d29-b80f-8e0ea23f4ac6} + StardewModdingAPI.Toolkit + + diff --git a/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs b/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..20118fce --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +[assembly: AssemblyTitle("SMAPI.Toolkit")] +[assembly: AssemblyDescription("")] +[assembly: InternalsVisibleTo("StardewModdingAPI")] diff --git a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj new file mode 100644 index 00000000..ffda899a --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj @@ -0,0 +1,22 @@ + + + + net4.5;netstandard2.0 + false + + + + ..\..\bin\$(Configuration)\SMAPI.Toolkit + + + + + + + + + + + + + -- cgit From 69b17f1db87d9aeb5dd6d6f9c81ac9ac62f2a6d3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 25 May 2018 02:06:28 -0400 Subject: move PathUtilities into toolkit (#532) --- src/SMAPI.Tests/Core/PathUtilitiesTests.cs | 70 ---------------------- src/SMAPI.Tests/StardewModdingAPI.Tests.csproj | 8 ++- src/SMAPI.Tests/Toolkit/PathUtilitiesTests.cs | 70 ++++++++++++++++++++++ src/SMAPI/Framework/Content/ContentCache.cs | 2 +- src/SMAPI/Framework/ContentCoordinator.cs | 3 +- src/SMAPI/Framework/ContentPack.cs | 2 +- src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 2 +- src/SMAPI/Framework/ModHelpers/ModHelper.cs | 10 ++-- src/SMAPI/Framework/ModLoading/ModResolver.cs | 2 +- src/SMAPI/Framework/Utilities/PathUtilities.cs | 65 -------------------- src/SMAPI/Program.cs | 2 +- src/SMAPI/StardewModdingAPI.csproj | 1 - .../Utilities/PathUtilities.cs | 65 ++++++++++++++++++++ 13 files changed, 152 insertions(+), 150 deletions(-) delete mode 100644 src/SMAPI.Tests/Core/PathUtilitiesTests.cs create mode 100644 src/SMAPI.Tests/Toolkit/PathUtilitiesTests.cs delete mode 100644 src/SMAPI/Framework/Utilities/PathUtilities.cs create mode 100644 src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI.Tests/Core/PathUtilitiesTests.cs b/src/SMAPI.Tests/Core/PathUtilitiesTests.cs deleted file mode 100644 index 268ba504..00000000 --- a/src/SMAPI.Tests/Core/PathUtilitiesTests.cs +++ /dev/null @@ -1,70 +0,0 @@ -using NUnit.Framework; -using StardewModdingAPI.Framework.Utilities; - -namespace StardewModdingAPI.Tests.Core -{ - /// Unit tests for . - [TestFixture] - public class PathUtilitiesTests - { - /********* - ** Unit tests - *********/ - [Test(Description = "Assert that GetSegments returns the expected values.")] - [TestCase("", ExpectedResult = "")] - [TestCase("/", ExpectedResult = "")] - [TestCase("///", ExpectedResult = "")] - [TestCase("/usr/bin", ExpectedResult = "usr|bin")] - [TestCase("/usr//bin//", ExpectedResult = "usr|bin")] - [TestCase("/usr//bin//.././boop.exe", ExpectedResult = "usr|bin|..|.|boop.exe")] - [TestCase(@"C:", ExpectedResult = "C:")] - [TestCase(@"C:/boop", ExpectedResult = "C:|boop")] - [TestCase(@"C:\boop\/usr//bin//.././boop.exe", ExpectedResult = "C:|boop|usr|bin|..|.|boop.exe")] - public string GetSegments(string path) - { - return string.Join("|", PathUtilities.GetSegments(path)); - } - - [Test(Description = "Assert that NormalisePathSeparators returns the expected values.")] -#if SMAPI_FOR_WINDOWS - [TestCase("", ExpectedResult = "")] - [TestCase("/", ExpectedResult = "")] - [TestCase("///", ExpectedResult = "")] - [TestCase("/usr/bin", ExpectedResult = @"usr\bin")] - [TestCase("/usr//bin//", ExpectedResult = @"usr\bin")] - [TestCase("/usr//bin//.././boop.exe", ExpectedResult = @"usr\bin\..\.\boop.exe")] - [TestCase("C:", ExpectedResult = "C:")] - [TestCase("C:/boop", ExpectedResult = @"C:\boop")] - [TestCase(@"C:\usr\bin//.././boop.exe", ExpectedResult = @"C:\usr\bin\..\.\boop.exe")] -#else - [TestCase("", ExpectedResult = "")] - [TestCase("/", ExpectedResult = "/")] - [TestCase("///", ExpectedResult = "/")] - [TestCase("/usr/bin", ExpectedResult = "/usr/bin")] - [TestCase("/usr//bin//", ExpectedResult = "/usr/bin")] - [TestCase("/usr//bin//.././boop.exe", ExpectedResult = "/usr/bin/.././boop.exe")] - [TestCase("C:", ExpectedResult = "C:")] - [TestCase("C:/boop", ExpectedResult = "C:/boop")] - [TestCase(@"C:\usr\bin//.././boop.exe", ExpectedResult = "C:/usr/bin/.././boop.exe")] -#endif - public string NormalisePathSeparators(string path) - { - return PathUtilities.NormalisePathSeparators(path); - } - - [Test(Description = "Assert that GetRelativePath returns the expected values.")] -#if SMAPI_FOR_WINDOWS - [TestCase(@"C:\", @"C:\", ExpectedResult = "./")] - [TestCase(@"C:\grandparent\parent\child", @"C:\grandparent\parent\sibling", ExpectedResult = @"..\sibling")] - [TestCase(@"C:\grandparent\parent\child", @"C:\cousin\file.exe", ExpectedResult = @"..\..\..\cousin\file.exe")] -#else - [TestCase("/", "/", ExpectedResult = "./")] - [TestCase("/grandparent/parent/child", "/grandparent/parent/sibling", ExpectedResult = "../sibling")] - [TestCase("/grandparent/parent/child", "/cousin/file.exe", ExpectedResult = "../../../cousin/file.exe")] -#endif - public string GetRelativePath(string sourceDir, string targetPath) - { - return PathUtilities.GetRelativePath(sourceDir, targetPath); - } - } -} diff --git a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj index f4d7b3e3..04c8d12f 100644 --- a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj +++ b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj @@ -1,4 +1,4 @@ - + @@ -56,7 +56,7 @@ Properties\GlobalAssemblyInfo.cs - + @@ -73,6 +73,10 @@ {f1a573b0-f436-472c-ae29-0b91ea6b9f8f} StardewModdingAPI + + {ea5cfd2e-9453-4d29-b80f-8e0ea23f4ac6} + StardewModdingAPI.Toolkit + diff --git a/src/SMAPI.Tests/Toolkit/PathUtilitiesTests.cs b/src/SMAPI.Tests/Toolkit/PathUtilitiesTests.cs new file mode 100644 index 00000000..229b9a14 --- /dev/null +++ b/src/SMAPI.Tests/Toolkit/PathUtilitiesTests.cs @@ -0,0 +1,70 @@ +using NUnit.Framework; +using StardewModdingAPI.Toolkit.Utilities; + +namespace StardewModdingAPI.Tests.Toolkit +{ + /// Unit tests for . + [TestFixture] + public class PathUtilitiesTests + { + /********* + ** Unit tests + *********/ + [Test(Description = "Assert that GetSegments returns the expected values.")] + [TestCase("", ExpectedResult = "")] + [TestCase("/", ExpectedResult = "")] + [TestCase("///", ExpectedResult = "")] + [TestCase("/usr/bin", ExpectedResult = "usr|bin")] + [TestCase("/usr//bin//", ExpectedResult = "usr|bin")] + [TestCase("/usr//bin//.././boop.exe", ExpectedResult = "usr|bin|..|.|boop.exe")] + [TestCase(@"C:", ExpectedResult = "C:")] + [TestCase(@"C:/boop", ExpectedResult = "C:|boop")] + [TestCase(@"C:\boop\/usr//bin//.././boop.exe", ExpectedResult = "C:|boop|usr|bin|..|.|boop.exe")] + public string GetSegments(string path) + { + return string.Join("|", PathUtilities.GetSegments(path)); + } + + [Test(Description = "Assert that NormalisePathSeparators returns the expected values.")] +#if SMAPI_FOR_WINDOWS + [TestCase("", ExpectedResult = "")] + [TestCase("/", ExpectedResult = "")] + [TestCase("///", ExpectedResult = "")] + [TestCase("/usr/bin", ExpectedResult = @"usr\bin")] + [TestCase("/usr//bin//", ExpectedResult = @"usr\bin")] + [TestCase("/usr//bin//.././boop.exe", ExpectedResult = @"usr\bin\..\.\boop.exe")] + [TestCase("C:", ExpectedResult = "C:")] + [TestCase("C:/boop", ExpectedResult = @"C:\boop")] + [TestCase(@"C:\usr\bin//.././boop.exe", ExpectedResult = @"C:\usr\bin\..\.\boop.exe")] +#else + [TestCase("", ExpectedResult = "")] + [TestCase("/", ExpectedResult = "/")] + [TestCase("///", ExpectedResult = "/")] + [TestCase("/usr/bin", ExpectedResult = "/usr/bin")] + [TestCase("/usr//bin//", ExpectedResult = "/usr/bin")] + [TestCase("/usr//bin//.././boop.exe", ExpectedResult = "/usr/bin/.././boop.exe")] + [TestCase("C:", ExpectedResult = "C:")] + [TestCase("C:/boop", ExpectedResult = "C:/boop")] + [TestCase(@"C:\usr\bin//.././boop.exe", ExpectedResult = "C:/usr/bin/.././boop.exe")] +#endif + public string NormalisePathSeparators(string path) + { + return PathUtilities.NormalisePathSeparators(path); + } + + [Test(Description = "Assert that GetRelativePath returns the expected values.")] +#if SMAPI_FOR_WINDOWS + [TestCase(@"C:\", @"C:\", ExpectedResult = "./")] + [TestCase(@"C:\grandparent\parent\child", @"C:\grandparent\parent\sibling", ExpectedResult = @"..\sibling")] + [TestCase(@"C:\grandparent\parent\child", @"C:\cousin\file.exe", ExpectedResult = @"..\..\..\cousin\file.exe")] +#else + [TestCase("/", "/", ExpectedResult = "./")] + [TestCase("/grandparent/parent/child", "/grandparent/parent/sibling", ExpectedResult = "../sibling")] + [TestCase("/grandparent/parent/child", "/cousin/file.exe", ExpectedResult = "../../../cousin/file.exe")] +#endif + public string GetRelativePath(string sourceDir, string targetPath) + { + return PathUtilities.GetRelativePath(sourceDir, targetPath); + } + } +} diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs index c2818cdd..a5dfac9d 100644 --- a/src/SMAPI/Framework/Content/ContentCache.cs +++ b/src/SMAPI/Framework/Content/ContentCache.cs @@ -4,8 +4,8 @@ using System.Diagnostics.Contracts; using System.Linq; using Microsoft.Xna.Framework; using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Internal; +using StardewModdingAPI.Toolkit.Utilities; using StardewValley; namespace StardewModdingAPI.Framework.Content diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index c2614001..1a57dd22 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -9,10 +9,9 @@ using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Metadata; +using StardewModdingAPI.Toolkit.Utilities; using StardewValley; -using xTile; namespace StardewModdingAPI.Framework { diff --git a/src/SMAPI/Framework/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs index 071fb872..ee6df1ec 100644 --- a/src/SMAPI/Framework/ContentPack.cs +++ b/src/SMAPI/Framework/ContentPack.cs @@ -3,7 +3,7 @@ using System.IO; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Serialisation; -using StardewModdingAPI.Framework.Utilities; +using StardewModdingAPI.Toolkit.Utilities; using xTile; namespace StardewModdingAPI.Framework diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index ce26c980..671dc21e 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -9,7 +9,7 @@ using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.Exceptions; -using StardewModdingAPI.Framework.Utilities; +using StardewModdingAPI.Toolkit.Utilities; using StardewValley; using xTile; using xTile.Format; diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index 26fe7198..61d2075c 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -4,7 +4,7 @@ using System.IO; using System.Linq; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.Serialisation; -using StardewModdingAPI.Framework.Utilities; +using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Framework.ModHelpers { @@ -157,13 +157,13 @@ namespace StardewModdingAPI.Framework.ModHelpers this.DeprecationManager.Warn($"{nameof(IModHelper)}.{nameof(IModHelper.CreateTransitionalContentPack)}", "2.5", DeprecationLevel.Notice); // validate - if(string.IsNullOrWhiteSpace(directoryPath)) + if (string.IsNullOrWhiteSpace(directoryPath)) throw new ArgumentNullException(nameof(directoryPath)); - if(string.IsNullOrWhiteSpace(id)) + if (string.IsNullOrWhiteSpace(id)) throw new ArgumentNullException(nameof(id)); - if(string.IsNullOrWhiteSpace(name)) + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name)); - if(!Directory.Exists(directoryPath)) + if (!Directory.Exists(directoryPath)) throw new ArgumentException($"Can't create content pack for directory path '{directoryPath}' because no such directory exists."); // create manifest diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index b5339183..d46caa55 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -7,7 +7,7 @@ using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.Serialisation; -using StardewModdingAPI.Framework.Utilities; +using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Framework.ModLoading { diff --git a/src/SMAPI/Framework/Utilities/PathUtilities.cs b/src/SMAPI/Framework/Utilities/PathUtilities.cs deleted file mode 100644 index 51d45ebd..00000000 --- a/src/SMAPI/Framework/Utilities/PathUtilities.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Diagnostics.Contracts; -using System.IO; -using System.Linq; - -namespace StardewModdingAPI.Framework.Utilities -{ - /// Provides utilities for normalising file paths. - internal static class PathUtilities - { - /********* - ** Properties - *********/ - /// The possible directory separator characters in a file path. - private static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray(); - - /// The preferred directory separator chaeacter in an asset key. - private static readonly string PreferredPathSeparator = Path.DirectorySeparatorChar.ToString(); - - - /********* - ** Public methods - *********/ - /// Get the segments from a path (e.g. /usr/bin/boop => usr, bin, and boop). - /// The path to split. - /// The number of segments to match. Any additional segments will be merged into the last returned part. - public static string[] GetSegments(string path, int? limit = null) - { - return limit.HasValue - ? path.Split(PathUtilities.PossiblePathSeparators, limit.Value, StringSplitOptions.RemoveEmptyEntries) - : path.Split(PathUtilities.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries); - } - - /// Normalise path separators in a file path. - /// The file path to normalise. - [Pure] - public static string NormalisePathSeparators(string path) - { - string[] parts = PathUtilities.GetSegments(path); - string normalised = string.Join(PathUtilities.PreferredPathSeparator, parts); - if (path.StartsWith(PathUtilities.PreferredPathSeparator)) - normalised = PathUtilities.PreferredPathSeparator + normalised; // keep root slash - return normalised; - } - - /// Get a directory or file path relative to a given source path. - /// The source folder path. - /// The target folder or file path. - [Pure] - public static string GetRelativePath(string sourceDir, string targetPath) - { - // convert to URIs - Uri from = new Uri(sourceDir.TrimEnd(PathUtilities.PossiblePathSeparators) + "/"); - Uri to = new Uri(targetPath.TrimEnd(PathUtilities.PossiblePathSeparators) + "/"); - if (from.Scheme != to.Scheme) - throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{sourceDir}'."); - - // get relative path - string relative = PathUtilities.NormalisePathSeparators(Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString())); - if (relative == "") - relative = "./"; - return relative; - } - } -} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index aeb9b9fb..a1cfcf0c 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -25,9 +25,9 @@ using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; -using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Internal; using StardewModdingAPI.Internal.Models; +using StardewModdingAPI.Toolkit.Utilities; using StardewValley; using Monitor = StardewModdingAPI.Framework.Monitor; using SObject = StardewValley.Object; diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 926bcbbc..3e6fdb24 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -157,7 +157,6 @@ - diff --git a/src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs b/src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs new file mode 100644 index 00000000..2e74e7d9 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs @@ -0,0 +1,65 @@ +using System; +using System.Diagnostics.Contracts; +using System.IO; +using System.Linq; + +namespace StardewModdingAPI.Toolkit.Utilities +{ + /// Provides utilities for normalising file paths. + public static class PathUtilities + { + /********* + ** Properties + *********/ + /// The possible directory separator characters in a file path. + private static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray(); + + /// The preferred directory separator chaeacter in an asset key. + private static readonly string PreferredPathSeparator = Path.DirectorySeparatorChar.ToString(); + + + /********* + ** Public methods + *********/ + /// Get the segments from a path (e.g. /usr/bin/boop => usr, bin, and boop). + /// The path to split. + /// The number of segments to match. Any additional segments will be merged into the last returned part. + public static string[] GetSegments(string path, int? limit = null) + { + return limit.HasValue + ? path.Split(PathUtilities.PossiblePathSeparators, limit.Value, StringSplitOptions.RemoveEmptyEntries) + : path.Split(PathUtilities.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries); + } + + /// Normalise path separators in a file path. + /// The file path to normalise. + [Pure] + public static string NormalisePathSeparators(string path) + { + string[] parts = PathUtilities.GetSegments(path); + string normalised = string.Join(PathUtilities.PreferredPathSeparator, parts); + if (path.StartsWith(PathUtilities.PreferredPathSeparator)) + normalised = PathUtilities.PreferredPathSeparator + normalised; // keep root slash + return normalised; + } + + /// Get a directory or file path relative to a given source path. + /// The source folder path. + /// The target folder or file path. + [Pure] + public static string GetRelativePath(string sourceDir, string targetPath) + { + // convert to URIs + Uri from = new Uri(sourceDir.TrimEnd(PathUtilities.PossiblePathSeparators) + "/"); + Uri to = new Uri(targetPath.TrimEnd(PathUtilities.PossiblePathSeparators) + "/"); + if (from.Scheme != to.Scheme) + throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{sourceDir}'."); + + // get relative path + string relative = PathUtilities.NormalisePathSeparators(Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString())); + if (relative == "") + relative = "./"; + return relative; + } + } +} -- cgit From 558fb8a865b638cf5536856e7dcab44823feeaf3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 31 May 2018 22:47:56 -0400 Subject: move location events into new event system (#310) --- .../Events/EventArgsLocationObjectsChanged.cs | 1 - src/SMAPI/Events/IModEvents.cs | 9 ++++ src/SMAPI/Events/IWorldEvents.cs | 20 ++++++++ src/SMAPI/Events/WorldBuildingsChangedEventArgs.cs | 39 +++++++++++++++ src/SMAPI/Events/WorldLocationsChangedEventArgs.cs | 33 +++++++++++++ src/SMAPI/Events/WorldObjectsChangedEventArgs.cs | 40 ++++++++++++++++ src/SMAPI/Framework/Events/EventManager.cs | 26 ++++++++-- src/SMAPI/Framework/Events/ManagedEvent.cs | 20 +++++++- src/SMAPI/Framework/Events/ManagedEventBase.cs | 9 ++-- src/SMAPI/Framework/Events/ModEvents.cs | 26 ++++++++++ src/SMAPI/Framework/Events/ModWorldEvents.cs | 56 ++++++++++++++++++++++ src/SMAPI/Framework/ModHelpers/ModHelper.cs | 16 +++++-- src/SMAPI/Framework/SGame.cs | 3 ++ src/SMAPI/IModHelper.cs | 5 ++ src/SMAPI/Program.cs | 3 +- src/SMAPI/StardewModdingAPI.csproj | 7 +++ 16 files changed, 297 insertions(+), 16 deletions(-) create mode 100644 src/SMAPI/Events/IModEvents.cs create mode 100644 src/SMAPI/Events/IWorldEvents.cs create mode 100644 src/SMAPI/Events/WorldBuildingsChangedEventArgs.cs create mode 100644 src/SMAPI/Events/WorldLocationsChangedEventArgs.cs create mode 100644 src/SMAPI/Events/WorldObjectsChangedEventArgs.cs create mode 100644 src/SMAPI/Framework/Events/ModEvents.cs create mode 100644 src/SMAPI/Framework/Events/ModWorldEvents.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs b/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs index 410ef6e6..3bb387d5 100644 --- a/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs +++ b/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework; -using Netcode; using StardewValley; using SObject = StardewValley.Object; diff --git a/src/SMAPI/Events/IModEvents.cs b/src/SMAPI/Events/IModEvents.cs new file mode 100644 index 00000000..99e5523f --- /dev/null +++ b/src/SMAPI/Events/IModEvents.cs @@ -0,0 +1,9 @@ +namespace StardewModdingAPI.Events +{ + /// Manages access to events raised by SMAPI. + public interface IModEvents + { + /// Events raised when something changes in the world. + IWorldEvents World { get; } + } +} diff --git a/src/SMAPI/Events/IWorldEvents.cs b/src/SMAPI/Events/IWorldEvents.cs new file mode 100644 index 00000000..5c713250 --- /dev/null +++ b/src/SMAPI/Events/IWorldEvents.cs @@ -0,0 +1,20 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Provides events raised when something changes in the world. + public interface IWorldEvents + { + /********* + ** Events + *********/ + /// Raised after a game location is added or removed. + event EventHandler LocationsChanged; + + /// Raised after buildings are added or removed in a location. + event EventHandler BuildingsChanged; + + /// Raised after objects are added or removed in a location. + event EventHandler ObjectsChanged; + } +} diff --git a/src/SMAPI/Events/WorldBuildingsChangedEventArgs.cs b/src/SMAPI/Events/WorldBuildingsChangedEventArgs.cs new file mode 100644 index 00000000..1f68fd02 --- /dev/null +++ b/src/SMAPI/Events/WorldBuildingsChangedEventArgs.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; +using StardewValley.Buildings; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class WorldBuildingsChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The buildings added to the location. + public IEnumerable Added { get; } + + /// The buildings removed from the location. + public IEnumerable Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The buildings added to the location. + /// The buildings removed from the location. + public WorldBuildingsChangedEventArgs(GameLocation location, IEnumerable added, IEnumerable removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Events/WorldLocationsChangedEventArgs.cs b/src/SMAPI/Events/WorldLocationsChangedEventArgs.cs new file mode 100644 index 00000000..5cf77959 --- /dev/null +++ b/src/SMAPI/Events/WorldLocationsChangedEventArgs.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class WorldLocationsChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The added locations. + public IEnumerable Added { get; } + + /// The removed locations. + public IEnumerable Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The added locations. + /// The removed locations. + public WorldLocationsChangedEventArgs(IEnumerable added, IEnumerable removed) + { + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Events/WorldObjectsChangedEventArgs.cs b/src/SMAPI/Events/WorldObjectsChangedEventArgs.cs new file mode 100644 index 00000000..fb20acd4 --- /dev/null +++ b/src/SMAPI/Events/WorldObjectsChangedEventArgs.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using StardewValley; +using Object = StardewValley.Object; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class WorldObjectsChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The objects added to the location. + public IEnumerable> Added { get; } + + /// The objects removed from the location. + public IEnumerable> Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The objects added to the location. + /// The objects removed from the location. + public WorldObjectsChangedEventArgs(GameLocation location, IEnumerable> added, IEnumerable> removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 84036127..53ea699a 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -1,4 +1,3 @@ -using System; using System.Diagnostics.CodeAnalysis; using Microsoft.Xna.Framework.Input; using StardewModdingAPI.Events; @@ -10,7 +9,23 @@ namespace StardewModdingAPI.Framework.Events internal class EventManager { /********* - ** Properties + ** Events (new) + *********/ + /**** + ** World + ****/ + /// Raised after a game location is added or removed. + public readonly ManagedEvent World_LocationsChanged; + + /// Raised after buildings are added or removed in a location. + public readonly ManagedEvent World_BuildingsChanged; + + /// Raised after objects are added or removed in a location. + public readonly ManagedEvent World_ObjectsChanged; + + + /********* + ** Events (old) *********/ /**** ** ContentEvents @@ -209,7 +224,12 @@ namespace StardewModdingAPI.Framework.Events ManagedEvent ManageEventOf(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); ManagedEvent ManageEvent(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); - // init events + // init events (new) + this.World_BuildingsChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LocationsChanged)); + this.World_LocationsChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.BuildingsChanged)); + this.World_ObjectsChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.ObjectsChanged)); + + // init events (old) this.Content_LocaleChanged = ManageEventOf>(nameof(ContentEvents), nameof(ContentEvents.AfterLocaleChanged)); this.Control_ControllerButtonPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonPressed)); diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index e54a4fd3..c1ebf6c7 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -27,9 +27,17 @@ namespace StardewModdingAPI.Framework.Events /// Add an event handler. /// The event handler. public void Add(EventHandler handler) + { + this.Add(handler, this.ModRegistry.GetFromStack()); + } + + /// Add an event handler. + /// The event handler. + /// The mod which added the event handler. + public void Add(EventHandler handler, IModMetadata mod) { this.Event += handler; - this.AddTracking(handler, this.Event?.GetInvocationList().Cast>()); + this.AddTracking(mod, handler, this.Event?.GetInvocationList().Cast>()); } /// Remove an event handler. @@ -84,9 +92,17 @@ namespace StardewModdingAPI.Framework.Events /// Add an event handler. /// The event handler. public void Add(EventHandler handler) + { + this.Add(handler, this.ModRegistry.GetFromStack()); + } + + /// Add an event handler. + /// The event handler. + /// The mod which added the event handler. + public void Add(EventHandler handler, IModMetadata mod) { this.Event += handler; - this.AddTracking(handler, this.Event?.GetInvocationList().Cast()); + this.AddTracking(mod, handler, this.Event?.GetInvocationList().Cast()); } /// Remove an event handler. diff --git a/src/SMAPI/Framework/Events/ManagedEventBase.cs b/src/SMAPI/Framework/Events/ManagedEventBase.cs index 7e42d613..f3a278dc 100644 --- a/src/SMAPI/Framework/Events/ManagedEventBase.cs +++ b/src/SMAPI/Framework/Events/ManagedEventBase.cs @@ -17,7 +17,7 @@ namespace StardewModdingAPI.Framework.Events private readonly IMonitor Monitor; /// The mod registry with which to identify mods. - private readonly ModRegistry ModRegistry; + protected readonly ModRegistry ModRegistry; /// The display names for the mods which added each delegate. private readonly IDictionary SourceMods = new Dictionary(); @@ -50,11 +50,12 @@ namespace StardewModdingAPI.Framework.Events } /// Track an event handler. + /// The mod which added the handler. /// The event handler. /// The updated event invocation list. - protected void AddTracking(TEventHandler handler, IEnumerable invocationList) + protected void AddTracking(IModMetadata mod, TEventHandler handler, IEnumerable invocationList) { - this.SourceMods[handler] = this.ModRegistry.GetFromStack(); + this.SourceMods[handler] = mod; this.CachedInvocationList = invocationList?.ToArray() ?? new TEventHandler[0]; } @@ -64,7 +65,7 @@ namespace StardewModdingAPI.Framework.Events protected void RemoveTracking(TEventHandler handler, IEnumerable invocationList) { this.CachedInvocationList = invocationList?.ToArray() ?? new TEventHandler[0]; - if(!this.CachedInvocationList.Contains(handler)) // don't remove if there's still a reference to the removed handler (e.g. it was added twice and removed once) + if (!this.CachedInvocationList.Contains(handler)) // don't remove if there's still a reference to the removed handler (e.g. it was added twice and removed once) this.SourceMods.Remove(handler); } diff --git a/src/SMAPI/Framework/Events/ModEvents.cs b/src/SMAPI/Framework/Events/ModEvents.cs new file mode 100644 index 00000000..cc4cf8d7 --- /dev/null +++ b/src/SMAPI/Framework/Events/ModEvents.cs @@ -0,0 +1,26 @@ +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// Manages access to events raised by SMAPI. + internal class ModEvents : IModEvents + { + /********* + ** Accessors + *********/ + /// Events raised when something changes in the world. + public IWorldEvents World { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + public ModEvents(IModMetadata mod, EventManager eventManager) + { + this.World = new ModWorldEvents(mod, eventManager); + } + } +} diff --git a/src/SMAPI/Framework/Events/ModWorldEvents.cs b/src/SMAPI/Framework/Events/ModWorldEvents.cs new file mode 100644 index 00000000..a76a7eb5 --- /dev/null +++ b/src/SMAPI/Framework/Events/ModWorldEvents.cs @@ -0,0 +1,56 @@ +using System; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// Events raised when something changes in the world. + public class ModWorldEvents : IWorldEvents + { + /********* + ** Properties + *********/ + /// The underlying event manager. + private readonly EventManager EventManager; + + /// The mod which uses this instance. + private readonly IModMetadata Mod; + + + /********* + ** Accessors + *********/ + /// Raised after a game location is added or removed. + public event EventHandler LocationsChanged + { + add => this.EventManager.World_LocationsChanged.Add(value, this.Mod); + remove => this.EventManager.World_LocationsChanged.Remove(value); + } + + /// Raised after buildings are added or removed in a location. + public event EventHandler BuildingsChanged + { + add => this.EventManager.World_BuildingsChanged.Add(value, this.Mod); + remove => this.EventManager.World_BuildingsChanged.Remove(value); + } + + /// Raised after objects are added or removed in a location. + public event EventHandler ObjectsChanged + { + add => this.EventManager.World_ObjectsChanged.Add(value); + remove => this.EventManager.World_ObjectsChanged.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + internal ModWorldEvents(IModMetadata mod, EventManager eventManager) + { + this.Mod = mod; + this.EventManager = eventManager; + } + } +} diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index 26fe7198..92cb9d94 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.Serialisation; using StardewModdingAPI.Framework.Utilities; @@ -33,6 +34,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The full path to the mod's folder. public string DirectoryPath { get; } + /// Manages access to events raised by SMAPI, which let your mod react when something happens in the game. + public IModEvents Events { get; } + /// An API for loading content assets. public IContentHelper Content { get; } @@ -59,6 +63,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The mod's unique ID. /// The full path to the mod's folder. /// Encapsulate SMAPI's JSON parsing. + /// Manages access to events raised by SMAPI. /// An API for loading content assets. /// An API for managing console commands. /// an API for fetching metadata about loaded mods. @@ -70,7 +75,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Manages deprecation warnings. /// An argument is null or empty. /// The path does not exist on disk. - public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, IContentHelper contentHelper, ICommandHelper commandHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper, IEnumerable contentPacks, Func createContentPack, DeprecationManager deprecationManager) + public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, IModEvents events, IContentHelper contentHelper, ICommandHelper commandHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper, IEnumerable contentPacks, Func createContentPack, DeprecationManager deprecationManager) : base(modID) { // validate directory @@ -91,6 +96,7 @@ namespace StardewModdingAPI.Framework.ModHelpers this.ContentPacks = contentPacks.ToArray(); this.CreateContentPack = createContentPack; this.DeprecationManager = deprecationManager; + this.Events = events; } /**** @@ -157,13 +163,13 @@ namespace StardewModdingAPI.Framework.ModHelpers this.DeprecationManager.Warn($"{nameof(IModHelper)}.{nameof(IModHelper.CreateTransitionalContentPack)}", "2.5", DeprecationLevel.Notice); // validate - if(string.IsNullOrWhiteSpace(directoryPath)) + if (string.IsNullOrWhiteSpace(directoryPath)) throw new ArgumentNullException(nameof(directoryPath)); - if(string.IsNullOrWhiteSpace(id)) + if (string.IsNullOrWhiteSpace(id)) throw new ArgumentNullException(nameof(id)); - if(string.IsNullOrWhiteSpace(name)) + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name)); - if(!Directory.Exists(directoryPath)) + if (!Directory.Exists(directoryPath)) throw new ArgumentException($"Can't create content pack for directory path '{directoryPath}' because no such directory exists."); // create manifest diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 369f1f40..e7e9f74f 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -543,6 +543,7 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Context: location list changed (added {addedText}; removed {removedText}).", LogLevel.Trace); } + this.Events.World_LocationsChanged.Raise(new WorldLocationsChangedEventArgs(added, removed)); this.Events.Location_LocationsChanged.Raise(new EventArgsLocationsChanged(added, removed)); } @@ -559,6 +560,7 @@ namespace StardewModdingAPI.Framework var removed = watcher.ObjectsWatcher.Removed.ToArray(); watcher.ObjectsWatcher.Reset(); + this.Events.World_ObjectsChanged.Raise(new WorldObjectsChangedEventArgs(location, added, removed)); this.Events.Location_ObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, added, removed)); } @@ -570,6 +572,7 @@ namespace StardewModdingAPI.Framework var removed = watcher.BuildingsWatcher.Removed.ToArray(); watcher.BuildingsWatcher.Reset(); + this.Events.World_BuildingsChanged.Raise(new WorldBuildingsChangedEventArgs(location, added, removed)); this.Events.Location_BuildingsChanged.Raise(new EventArgsLocationBuildingsChanged(location, added, removed)); } } diff --git a/src/SMAPI/IModHelper.cs b/src/SMAPI/IModHelper.cs index 5e39161d..68c2f1c4 100644 --- a/src/SMAPI/IModHelper.cs +++ b/src/SMAPI/IModHelper.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using StardewModdingAPI.Events; namespace StardewModdingAPI { @@ -12,6 +13,10 @@ namespace StardewModdingAPI /// The full path to the mod's folder. string DirectoryPath { get; } + /// Manages access to events raised by SMAPI, which let your mod react when something happens in the game. + [Obsolete("This is an experimental interface which may change at any time. Don't depend on this for released mods.")] + IModEvents Events { get; } + /// An API for loading content assets. IContentHelper Content { get; } diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 844dc5d8..48ad922b 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -840,6 +840,7 @@ namespace StardewModdingAPI IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); IModHelper modHelper; { + IModEvents events = new ModEvents(metadata, this.EventManager); ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager); IContentHelper contentHelper = new ContentHelper(contentCore, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager); @@ -854,7 +855,7 @@ namespace StardewModdingAPI return new ContentPack(packDirPath, packManifest, packContentHelper, this.JsonHelper); } - modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); + modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, events, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); } // init mod diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index e9e0ea54..6a062930 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -86,17 +86,23 @@ Properties\GlobalAssemblyInfo.cs + + + + + + @@ -129,6 +135,7 @@ + -- cgit From cca7bf197079975bf310ca90c03b78d0f77fdf02 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 1 Jun 2018 01:15:26 -0400 Subject: rename new events for clarity (#310) --- src/SMAPI/Events/IWorldEvents.cs | 6 ++-- .../Events/WorldBuildingListChangedEventArgs.cs | 39 +++++++++++++++++++++ src/SMAPI/Events/WorldBuildingsChangedEventArgs.cs | 39 --------------------- .../Events/WorldLocationListChangedEventArgs.cs | 33 ++++++++++++++++++ src/SMAPI/Events/WorldLocationsChangedEventArgs.cs | 33 ------------------ .../Events/WorldObjectListChangedEventArgs.cs | 40 ++++++++++++++++++++++ src/SMAPI/Events/WorldObjectsChangedEventArgs.cs | 40 ---------------------- src/SMAPI/Framework/Events/EventManager.cs | 12 +++---- src/SMAPI/Framework/Events/ModWorldEvents.cs | 18 +++++----- src/SMAPI/Framework/SGame.cs | 6 ++-- src/SMAPI/StardewModdingAPI.csproj | 6 ++-- 11 files changed, 136 insertions(+), 136 deletions(-) create mode 100644 src/SMAPI/Events/WorldBuildingListChangedEventArgs.cs delete mode 100644 src/SMAPI/Events/WorldBuildingsChangedEventArgs.cs create mode 100644 src/SMAPI/Events/WorldLocationListChangedEventArgs.cs delete mode 100644 src/SMAPI/Events/WorldLocationsChangedEventArgs.cs create mode 100644 src/SMAPI/Events/WorldObjectListChangedEventArgs.cs delete mode 100644 src/SMAPI/Events/WorldObjectsChangedEventArgs.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI/Events/IWorldEvents.cs b/src/SMAPI/Events/IWorldEvents.cs index 5c713250..a9a5fe6b 100644 --- a/src/SMAPI/Events/IWorldEvents.cs +++ b/src/SMAPI/Events/IWorldEvents.cs @@ -9,12 +9,12 @@ namespace StardewModdingAPI.Events ** Events *********/ /// Raised after a game location is added or removed. - event EventHandler LocationsChanged; + event EventHandler LocationListChanged; /// Raised after buildings are added or removed in a location. - event EventHandler BuildingsChanged; + event EventHandler BuildingListChanged; /// Raised after objects are added or removed in a location. - event EventHandler ObjectsChanged; + event EventHandler ObjectListChanged; } } diff --git a/src/SMAPI/Events/WorldBuildingListChangedEventArgs.cs b/src/SMAPI/Events/WorldBuildingListChangedEventArgs.cs new file mode 100644 index 00000000..e73b9396 --- /dev/null +++ b/src/SMAPI/Events/WorldBuildingListChangedEventArgs.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; +using StardewValley.Buildings; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class WorldBuildingListChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The buildings added to the location. + public IEnumerable Added { get; } + + /// The buildings removed from the location. + public IEnumerable Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The buildings added to the location. + /// The buildings removed from the location. + public WorldBuildingListChangedEventArgs(GameLocation location, IEnumerable added, IEnumerable removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Events/WorldBuildingsChangedEventArgs.cs b/src/SMAPI/Events/WorldBuildingsChangedEventArgs.cs deleted file mode 100644 index 1f68fd02..00000000 --- a/src/SMAPI/Events/WorldBuildingsChangedEventArgs.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using StardewValley; -using StardewValley.Buildings; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class WorldBuildingsChangedEventArgs : EventArgs - { - /********* - ** Accessors - *********/ - /// The location which changed. - public GameLocation Location { get; } - - /// The buildings added to the location. - public IEnumerable Added { get; } - - /// The buildings removed from the location. - public IEnumerable Removed { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The location which changed. - /// The buildings added to the location. - /// The buildings removed from the location. - public WorldBuildingsChangedEventArgs(GameLocation location, IEnumerable added, IEnumerable removed) - { - this.Location = location; - this.Added = added.ToArray(); - this.Removed = removed.ToArray(); - } - } -} diff --git a/src/SMAPI/Events/WorldLocationListChangedEventArgs.cs b/src/SMAPI/Events/WorldLocationListChangedEventArgs.cs new file mode 100644 index 00000000..8bc26a43 --- /dev/null +++ b/src/SMAPI/Events/WorldLocationListChangedEventArgs.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class WorldLocationListChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The added locations. + public IEnumerable Added { get; } + + /// The removed locations. + public IEnumerable Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The added locations. + /// The removed locations. + public WorldLocationListChangedEventArgs(IEnumerable added, IEnumerable removed) + { + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Events/WorldLocationsChangedEventArgs.cs b/src/SMAPI/Events/WorldLocationsChangedEventArgs.cs deleted file mode 100644 index 5cf77959..00000000 --- a/src/SMAPI/Events/WorldLocationsChangedEventArgs.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using StardewValley; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class WorldLocationsChangedEventArgs : EventArgs - { - /********* - ** Accessors - *********/ - /// The added locations. - public IEnumerable Added { get; } - - /// The removed locations. - public IEnumerable Removed { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The added locations. - /// The removed locations. - public WorldLocationsChangedEventArgs(IEnumerable added, IEnumerable removed) - { - this.Added = added.ToArray(); - this.Removed = removed.ToArray(); - } - } -} diff --git a/src/SMAPI/Events/WorldObjectListChangedEventArgs.cs b/src/SMAPI/Events/WorldObjectListChangedEventArgs.cs new file mode 100644 index 00000000..5623a49b --- /dev/null +++ b/src/SMAPI/Events/WorldObjectListChangedEventArgs.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using StardewValley; +using Object = StardewValley.Object; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class WorldObjectListChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The objects added to the location. + public IEnumerable> Added { get; } + + /// The objects removed from the location. + public IEnumerable> Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The objects added to the location. + /// The objects removed from the location. + public WorldObjectListChangedEventArgs(GameLocation location, IEnumerable> added, IEnumerable> removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Events/WorldObjectsChangedEventArgs.cs b/src/SMAPI/Events/WorldObjectsChangedEventArgs.cs deleted file mode 100644 index fb20acd4..00000000 --- a/src/SMAPI/Events/WorldObjectsChangedEventArgs.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Xna.Framework; -using StardewValley; -using Object = StardewValley.Object; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class WorldObjectsChangedEventArgs : EventArgs - { - /********* - ** Accessors - *********/ - /// The location which changed. - public GameLocation Location { get; } - - /// The objects added to the location. - public IEnumerable> Added { get; } - - /// The objects removed from the location. - public IEnumerable> Removed { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The location which changed. - /// The objects added to the location. - /// The objects removed from the location. - public WorldObjectsChangedEventArgs(GameLocation location, IEnumerable> added, IEnumerable> removed) - { - this.Location = location; - this.Added = added.ToArray(); - this.Removed = removed.ToArray(); - } - } -} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 53ea699a..0e1e6241 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -15,13 +15,13 @@ namespace StardewModdingAPI.Framework.Events ** World ****/ /// Raised after a game location is added or removed. - public readonly ManagedEvent World_LocationsChanged; + public readonly ManagedEvent World_LocationListChanged; /// Raised after buildings are added or removed in a location. - public readonly ManagedEvent World_BuildingsChanged; + public readonly ManagedEvent World_BuildingListChanged; /// Raised after objects are added or removed in a location. - public readonly ManagedEvent World_ObjectsChanged; + public readonly ManagedEvent World_ObjectListChanged; /********* @@ -225,9 +225,9 @@ namespace StardewModdingAPI.Framework.Events ManagedEvent ManageEvent(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); // init events (new) - this.World_BuildingsChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LocationsChanged)); - this.World_LocationsChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.BuildingsChanged)); - this.World_ObjectsChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.ObjectsChanged)); + this.World_BuildingListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LocationListChanged)); + this.World_LocationListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.BuildingListChanged)); + this.World_ObjectListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.ObjectListChanged)); // init events (old) this.Content_LocaleChanged = ManageEventOf>(nameof(ContentEvents), nameof(ContentEvents.AfterLocaleChanged)); diff --git a/src/SMAPI/Framework/Events/ModWorldEvents.cs b/src/SMAPI/Framework/Events/ModWorldEvents.cs index a76a7eb5..7036b765 100644 --- a/src/SMAPI/Framework/Events/ModWorldEvents.cs +++ b/src/SMAPI/Framework/Events/ModWorldEvents.cs @@ -20,24 +20,24 @@ namespace StardewModdingAPI.Framework.Events ** Accessors *********/ /// Raised after a game location is added or removed. - public event EventHandler LocationsChanged + public event EventHandler LocationListChanged { - add => this.EventManager.World_LocationsChanged.Add(value, this.Mod); - remove => this.EventManager.World_LocationsChanged.Remove(value); + add => this.EventManager.World_LocationListChanged.Add(value, this.Mod); + remove => this.EventManager.World_LocationListChanged.Remove(value); } /// Raised after buildings are added or removed in a location. - public event EventHandler BuildingsChanged + public event EventHandler BuildingListChanged { - add => this.EventManager.World_BuildingsChanged.Add(value, this.Mod); - remove => this.EventManager.World_BuildingsChanged.Remove(value); + add => this.EventManager.World_BuildingListChanged.Add(value, this.Mod); + remove => this.EventManager.World_BuildingListChanged.Remove(value); } /// Raised after objects are added or removed in a location. - public event EventHandler ObjectsChanged + public event EventHandler ObjectListChanged { - add => this.EventManager.World_ObjectsChanged.Add(value); - remove => this.EventManager.World_ObjectsChanged.Remove(value); + add => this.EventManager.World_ObjectListChanged.Add(value); + remove => this.EventManager.World_ObjectListChanged.Remove(value); } diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index e7e9f74f..9442c749 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -543,7 +543,7 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Context: location list changed (added {addedText}; removed {removedText}).", LogLevel.Trace); } - this.Events.World_LocationsChanged.Raise(new WorldLocationsChangedEventArgs(added, removed)); + this.Events.World_LocationListChanged.Raise(new WorldLocationListChangedEventArgs(added, removed)); this.Events.Location_LocationsChanged.Raise(new EventArgsLocationsChanged(added, removed)); } @@ -560,7 +560,7 @@ namespace StardewModdingAPI.Framework var removed = watcher.ObjectsWatcher.Removed.ToArray(); watcher.ObjectsWatcher.Reset(); - this.Events.World_ObjectsChanged.Raise(new WorldObjectsChangedEventArgs(location, added, removed)); + this.Events.World_ObjectListChanged.Raise(new WorldObjectListChangedEventArgs(location, added, removed)); this.Events.Location_ObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, added, removed)); } @@ -572,7 +572,7 @@ namespace StardewModdingAPI.Framework var removed = watcher.BuildingsWatcher.Removed.ToArray(); watcher.BuildingsWatcher.Reset(); - this.Events.World_BuildingsChanged.Raise(new WorldBuildingsChangedEventArgs(location, added, removed)); + this.Events.World_BuildingListChanged.Raise(new WorldBuildingListChangedEventArgs(location, added, removed)); this.Events.Location_BuildingsChanged.Raise(new EventArgsLocationBuildingsChanged(location, added, removed)); } } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 6a062930..951a9e6b 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -88,9 +88,9 @@ - - - + + + -- cgit From b3f116a8f123387b1f2f9ce1a099d1fd8081708d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 1 Jun 2018 01:58:31 -0400 Subject: add terrain feature list changed event (#310) --- src/SMAPI/Events/IWorldEvents.cs | 3 ++ .../WorldTerrainFeatureListChangedEventArgs.cs | 40 ++++++++++++++++++++++ src/SMAPI/Framework/Events/EventManager.cs | 4 +++ src/SMAPI/Framework/Events/ModWorldEvents.cs | 7 ++++ src/SMAPI/Framework/SGame.cs | 22 +++++++++--- .../Framework/StateTracking/LocationTracker.cs | 12 +++++-- src/SMAPI/StardewModdingAPI.csproj | 1 + 7 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 src/SMAPI/Events/WorldTerrainFeatureListChangedEventArgs.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI/Events/IWorldEvents.cs b/src/SMAPI/Events/IWorldEvents.cs index a9a5fe6b..7ec26bae 100644 --- a/src/SMAPI/Events/IWorldEvents.cs +++ b/src/SMAPI/Events/IWorldEvents.cs @@ -16,5 +16,8 @@ namespace StardewModdingAPI.Events /// Raised after objects are added or removed in a location. event EventHandler ObjectListChanged; + + /// Raised after terrain features are added or removed in a location. + event EventHandler TerrainFeatureListChanged; } } diff --git a/src/SMAPI/Events/WorldTerrainFeatureListChangedEventArgs.cs b/src/SMAPI/Events/WorldTerrainFeatureListChangedEventArgs.cs new file mode 100644 index 00000000..cb089811 --- /dev/null +++ b/src/SMAPI/Events/WorldTerrainFeatureListChangedEventArgs.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using StardewValley; +using StardewValley.TerrainFeatures; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class WorldTerrainFeatureListChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The terrain features added to the location. + public IEnumerable> Added { get; } + + /// The terrain features removed from the location. + public IEnumerable> Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The terrain features added to the location. + /// The terrain features removed from the location. + public WorldTerrainFeatureListChangedEventArgs(GameLocation location, IEnumerable> added, IEnumerable> removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 0e1e6241..c909c1bd 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -23,6 +23,9 @@ namespace StardewModdingAPI.Framework.Events /// Raised after objects are added or removed in a location. public readonly ManagedEvent World_ObjectListChanged; + /// Raised after terrain features are added or removed in a location. + public readonly ManagedEvent World_TerrainFeatureListChanged; + /********* ** Events (old) @@ -228,6 +231,7 @@ namespace StardewModdingAPI.Framework.Events this.World_BuildingListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LocationListChanged)); this.World_LocationListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.BuildingListChanged)); this.World_ObjectListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.ObjectListChanged)); + this.World_TerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.TerrainFeatureListChanged)); // init events (old) this.Content_LocaleChanged = ManageEventOf>(nameof(ContentEvents), nameof(ContentEvents.AfterLocaleChanged)); diff --git a/src/SMAPI/Framework/Events/ModWorldEvents.cs b/src/SMAPI/Framework/Events/ModWorldEvents.cs index 7036b765..14646c5d 100644 --- a/src/SMAPI/Framework/Events/ModWorldEvents.cs +++ b/src/SMAPI/Framework/Events/ModWorldEvents.cs @@ -40,6 +40,13 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.World_ObjectListChanged.Remove(value); } + /// Raised after terrain features are added or removed in a location. + public event EventHandler TerrainFeatureListChanged + { + add => this.EventManager.World_TerrainFeatureListChanged.Add(value); + remove => this.EventManager.World_TerrainFeatureListChanged.Remove(value); + } + /********* ** Public methods diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 9442c749..02d6dd2c 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -18,11 +18,14 @@ using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewModdingAPI.Framework.Utilities; using StardewValley; using StardewValley.BellsAndWhistles; +using StardewValley.Buildings; using StardewValley.Locations; using StardewValley.Menus; +using StardewValley.TerrainFeatures; using StardewValley.Tools; using xTile.Dimensions; using xTile.Layers; +using Object = StardewValley.Object; namespace StardewModdingAPI.Framework { @@ -556,8 +559,8 @@ namespace StardewModdingAPI.Framework if (watcher.ObjectsWatcher.IsChanged) { GameLocation location = watcher.Location; - var added = watcher.ObjectsWatcher.Added.ToArray(); - var removed = watcher.ObjectsWatcher.Removed.ToArray(); + KeyValuePair[] added = watcher.ObjectsWatcher.Added.ToArray(); + KeyValuePair[] removed = watcher.ObjectsWatcher.Removed.ToArray(); watcher.ObjectsWatcher.Reset(); this.Events.World_ObjectListChanged.Raise(new WorldObjectListChangedEventArgs(location, added, removed)); @@ -568,13 +571,24 @@ namespace StardewModdingAPI.Framework if (watcher.BuildingsWatcher.IsChanged) { GameLocation location = watcher.Location; - var added = watcher.BuildingsWatcher.Added.ToArray(); - var removed = watcher.BuildingsWatcher.Removed.ToArray(); + Building[] added = watcher.BuildingsWatcher.Added.ToArray(); + Building[] removed = watcher.BuildingsWatcher.Removed.ToArray(); watcher.BuildingsWatcher.Reset(); this.Events.World_BuildingListChanged.Raise(new WorldBuildingListChangedEventArgs(location, added, removed)); this.Events.Location_BuildingsChanged.Raise(new EventArgsLocationBuildingsChanged(location, added, removed)); } + + // terrain features changed + if (watcher.TerrainFeaturesWatcher.IsChanged) + { + GameLocation location = watcher.Location; + KeyValuePair[] added = watcher.TerrainFeaturesWatcher.Added.ToArray(); + KeyValuePair[] removed = watcher.TerrainFeaturesWatcher.Removed.ToArray(); + watcher.TerrainFeaturesWatcher.Reset(); + + this.Events.World_TerrainFeatureListChanged.Raise(new WorldTerrainFeatureListChangedEventArgs(location, added, removed)); + } } } else diff --git a/src/SMAPI/Framework/StateTracking/LocationTracker.cs b/src/SMAPI/Framework/StateTracking/LocationTracker.cs index 07570401..d31b1128 100644 --- a/src/SMAPI/Framework/StateTracking/LocationTracker.cs +++ b/src/SMAPI/Framework/StateTracking/LocationTracker.cs @@ -6,6 +6,7 @@ using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewValley; using StardewValley.Buildings; using StardewValley.Locations; +using StardewValley.TerrainFeatures; using Object = StardewValley.Object; namespace StardewModdingAPI.Framework.StateTracking @@ -29,12 +30,15 @@ namespace StardewModdingAPI.Framework.StateTracking /// The tracked location. public GameLocation Location { get; } - /// Tracks changes to the location's buildings. + /// Tracks added or removed buildings. public ICollectionWatcher BuildingsWatcher { get; } - /// Tracks changes to the location's objects. + /// Tracks added or removed objects. public IDictionaryWatcher ObjectsWatcher { get; } + /// Tracks added or removed terrain features. + public IDictionaryWatcher TerrainFeaturesWatcher { get; } + /********* ** Public methods @@ -50,11 +54,13 @@ namespace StardewModdingAPI.Framework.StateTracking this.BuildingsWatcher = location is BuildableGameLocation buildableLocation ? WatcherFactory.ForNetCollection(buildableLocation.buildings) : (ICollectionWatcher)WatcherFactory.ForObservableCollection(new ObservableCollection()); + this.TerrainFeaturesWatcher = WatcherFactory.ForNetDictionary(location.terrainFeatures); this.Watchers.AddRange(new IWatcher[] { this.BuildingsWatcher, - this.ObjectsWatcher + this.ObjectsWatcher, + this.TerrainFeaturesWatcher }); } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 951a9e6b..50d8c533 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -88,6 +88,7 @@ + -- cgit From 07bbfea7dd9dac5bbacf8bfc3c35d3f65ec71b75 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 1 Jun 2018 02:14:01 -0400 Subject: add NPC list changed event (#310) --- src/SMAPI/Events/IWorldEvents.cs | 3 ++ src/SMAPI/Events/WorldNpcListChangedEventArgs.cs | 38 ++++++++++++++++++++++ src/SMAPI/Framework/Events/EventManager.cs | 4 +++ src/SMAPI/Framework/Events/ModWorldEvents.cs | 7 ++++ src/SMAPI/Framework/SGame.cs | 35 +++++++++++++------- .../Framework/StateTracking/LocationTracker.cs | 7 +++- src/SMAPI/StardewModdingAPI.csproj | 1 + 7 files changed, 82 insertions(+), 13 deletions(-) create mode 100644 src/SMAPI/Events/WorldNpcListChangedEventArgs.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI/Events/IWorldEvents.cs b/src/SMAPI/Events/IWorldEvents.cs index 7ec26bae..ce288ae1 100644 --- a/src/SMAPI/Events/IWorldEvents.cs +++ b/src/SMAPI/Events/IWorldEvents.cs @@ -14,6 +14,9 @@ namespace StardewModdingAPI.Events /// Raised after buildings are added or removed in a location. event EventHandler BuildingListChanged; + /// Raised after NPCs are added or removed in a location. + event EventHandler NpcListChanged; + /// Raised after objects are added or removed in a location. event EventHandler ObjectListChanged; diff --git a/src/SMAPI/Events/WorldNpcListChangedEventArgs.cs b/src/SMAPI/Events/WorldNpcListChangedEventArgs.cs new file mode 100644 index 00000000..e251f894 --- /dev/null +++ b/src/SMAPI/Events/WorldNpcListChangedEventArgs.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class WorldNpcListChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The NPCs added to the location. + public IEnumerable Added { get; } + + /// The NPCs removed from the location. + public IEnumerable Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The NPCs added to the location. + /// The NPCs removed from the location. + public WorldNpcListChangedEventArgs(GameLocation location, IEnumerable added, IEnumerable removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index c909c1bd..cb331e7a 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -20,6 +20,9 @@ namespace StardewModdingAPI.Framework.Events /// Raised after buildings are added or removed in a location. public readonly ManagedEvent World_BuildingListChanged; + /// Raised after NPCs are added or removed in a location. + public readonly ManagedEvent World_NpcListChanged; + /// Raised after objects are added or removed in a location. public readonly ManagedEvent World_ObjectListChanged; @@ -230,6 +233,7 @@ namespace StardewModdingAPI.Framework.Events // init events (new) this.World_BuildingListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LocationListChanged)); this.World_LocationListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.BuildingListChanged)); + this.World_NpcListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.NpcListChanged)); this.World_ObjectListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.ObjectListChanged)); this.World_TerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.TerrainFeatureListChanged)); diff --git a/src/SMAPI/Framework/Events/ModWorldEvents.cs b/src/SMAPI/Framework/Events/ModWorldEvents.cs index 14646c5d..ca851550 100644 --- a/src/SMAPI/Framework/Events/ModWorldEvents.cs +++ b/src/SMAPI/Framework/Events/ModWorldEvents.cs @@ -33,6 +33,13 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.World_BuildingListChanged.Remove(value); } + /// Raised after NPCs are added or removed in a location. + public event EventHandler NpcListChanged + { + add => this.EventManager.World_NpcListChanged.Add(value); + remove => this.EventManager.World_NpcListChanged.Remove(value); + } + /// Raised after objects are added or removed in a location. public event EventHandler ObjectListChanged { diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 02d6dd2c..38f96566 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -555,18 +555,6 @@ namespace StardewModdingAPI.Framework { foreach (LocationTracker watcher in this.LocationsWatcher.Locations) { - // objects changed - if (watcher.ObjectsWatcher.IsChanged) - { - GameLocation location = watcher.Location; - KeyValuePair[] added = watcher.ObjectsWatcher.Added.ToArray(); - KeyValuePair[] removed = watcher.ObjectsWatcher.Removed.ToArray(); - watcher.ObjectsWatcher.Reset(); - - this.Events.World_ObjectListChanged.Raise(new WorldObjectListChangedEventArgs(location, added, removed)); - this.Events.Location_ObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, added, removed)); - } - // buildings changed if (watcher.BuildingsWatcher.IsChanged) { @@ -579,6 +567,29 @@ namespace StardewModdingAPI.Framework this.Events.Location_BuildingsChanged.Raise(new EventArgsLocationBuildingsChanged(location, added, removed)); } + // NPCs changed + if (watcher.NpcsWatcher.IsChanged) + { + GameLocation location = watcher.Location; + NPC[] added = watcher.NpcsWatcher.Added.ToArray(); + NPC[] removed = watcher.NpcsWatcher.Removed.ToArray(); + watcher.NpcsWatcher.Reset(); + + this.Events.World_NpcListChanged.Raise(new WorldNpcListChangedEventArgs(location, added, removed)); + } + + // objects changed + if (watcher.ObjectsWatcher.IsChanged) + { + GameLocation location = watcher.Location; + KeyValuePair[] added = watcher.ObjectsWatcher.Added.ToArray(); + KeyValuePair[] removed = watcher.ObjectsWatcher.Removed.ToArray(); + watcher.ObjectsWatcher.Reset(); + + this.Events.World_ObjectListChanged.Raise(new WorldObjectListChangedEventArgs(location, added, removed)); + this.Events.Location_ObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, added, removed)); + } + // terrain features changed if (watcher.TerrainFeaturesWatcher.IsChanged) { diff --git a/src/SMAPI/Framework/StateTracking/LocationTracker.cs b/src/SMAPI/Framework/StateTracking/LocationTracker.cs index d31b1128..4ac455ac 100644 --- a/src/SMAPI/Framework/StateTracking/LocationTracker.cs +++ b/src/SMAPI/Framework/StateTracking/LocationTracker.cs @@ -33,6 +33,9 @@ namespace StardewModdingAPI.Framework.StateTracking /// Tracks added or removed buildings. public ICollectionWatcher BuildingsWatcher { get; } + /// Tracks added or removed NPCs. + public ICollectionWatcher NpcsWatcher { get; } + /// Tracks added or removed objects. public IDictionaryWatcher ObjectsWatcher { get; } @@ -50,15 +53,17 @@ namespace StardewModdingAPI.Framework.StateTracking this.Location = location; // init watchers - this.ObjectsWatcher = WatcherFactory.ForNetDictionary(location.netObjects); this.BuildingsWatcher = location is BuildableGameLocation buildableLocation ? WatcherFactory.ForNetCollection(buildableLocation.buildings) : (ICollectionWatcher)WatcherFactory.ForObservableCollection(new ObservableCollection()); + this.NpcsWatcher = WatcherFactory.ForNetCollection(location.characters); + this.ObjectsWatcher = WatcherFactory.ForNetDictionary(location.netObjects); this.TerrainFeaturesWatcher = WatcherFactory.ForNetDictionary(location.terrainFeatures); this.Watchers.AddRange(new IWatcher[] { this.BuildingsWatcher, + this.NpcsWatcher, this.ObjectsWatcher, this.TerrainFeaturesWatcher }); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 50d8c533..37b624fb 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -88,6 +88,7 @@ + -- cgit From 92006bd6ed20365f1ded4bd07387b0ba9ed0ff92 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 1 Jun 2018 23:16:42 -0400 Subject: add large terrain feature list changed event (#310) --- src/SMAPI/Events/IWorldEvents.cs | 5 ++- ...WorldLargeTerrainFeatureListChangedEventArgs.cs | 39 ++++++++++++++++++++++ src/SMAPI/Framework/Events/EventManager.cs | 6 +++- src/SMAPI/Framework/Events/ModWorldEvents.cs | 9 ++++- src/SMAPI/Framework/SGame.cs | 11 ++++++ .../Framework/StateTracking/LocationTracker.cs | 5 +++ src/SMAPI/StardewModdingAPI.csproj | 1 + 7 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 src/SMAPI/Events/WorldLargeTerrainFeatureListChangedEventArgs.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI/Events/IWorldEvents.cs b/src/SMAPI/Events/IWorldEvents.cs index ce288ae1..6a43baf2 100644 --- a/src/SMAPI/Events/IWorldEvents.cs +++ b/src/SMAPI/Events/IWorldEvents.cs @@ -14,13 +14,16 @@ namespace StardewModdingAPI.Events /// Raised after buildings are added or removed in a location. event EventHandler BuildingListChanged; + /// Raised after large terrain features (like bushes) are added or removed in a location. + event EventHandler LargeTerrainFeatureListChanged; + /// Raised after NPCs are added or removed in a location. event EventHandler NpcListChanged; /// Raised after objects are added or removed in a location. event EventHandler ObjectListChanged; - /// Raised after terrain features are added or removed in a location. + /// Raised after terrain features (like floors and trees) are added or removed in a location. event EventHandler TerrainFeatureListChanged; } } diff --git a/src/SMAPI/Events/WorldLargeTerrainFeatureListChangedEventArgs.cs b/src/SMAPI/Events/WorldLargeTerrainFeatureListChangedEventArgs.cs new file mode 100644 index 00000000..053a0e41 --- /dev/null +++ b/src/SMAPI/Events/WorldLargeTerrainFeatureListChangedEventArgs.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; +using StardewValley.TerrainFeatures; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class WorldLargeTerrainFeatureListChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The large terrain features added to the location. + public IEnumerable Added { get; } + + /// The large terrain features removed from the location. + public IEnumerable Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The large terrain features added to the location. + /// The large terrain features removed from the location. + public WorldLargeTerrainFeatureListChangedEventArgs(GameLocation location, IEnumerable added, IEnumerable removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index cb331e7a..7ce8c640 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -20,13 +20,16 @@ namespace StardewModdingAPI.Framework.Events /// Raised after buildings are added or removed in a location. public readonly ManagedEvent World_BuildingListChanged; + /// Raised after large terrain features (like bushes) are added or removed in a location. + public readonly ManagedEvent World_LargeTerrainFeatureListChanged; + /// Raised after NPCs are added or removed in a location. public readonly ManagedEvent World_NpcListChanged; /// Raised after objects are added or removed in a location. public readonly ManagedEvent World_ObjectListChanged; - /// Raised after terrain features are added or removed in a location. + /// Raised after terrain features (like floors and trees) are added or removed in a location. public readonly ManagedEvent World_TerrainFeatureListChanged; @@ -232,6 +235,7 @@ namespace StardewModdingAPI.Framework.Events // init events (new) this.World_BuildingListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LocationListChanged)); + this.World_LargeTerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LargeTerrainFeatureListChanged)); this.World_LocationListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.BuildingListChanged)); this.World_NpcListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.NpcListChanged)); this.World_ObjectListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.ObjectListChanged)); diff --git a/src/SMAPI/Framework/Events/ModWorldEvents.cs b/src/SMAPI/Framework/Events/ModWorldEvents.cs index ca851550..db03e447 100644 --- a/src/SMAPI/Framework/Events/ModWorldEvents.cs +++ b/src/SMAPI/Framework/Events/ModWorldEvents.cs @@ -33,6 +33,13 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.World_BuildingListChanged.Remove(value); } + /// Raised after large terrain features (like bushes) are added or removed in a location. + public event EventHandler LargeTerrainFeatureListChanged + { + add => this.EventManager.World_LargeTerrainFeatureListChanged.Add(value, this.Mod); + remove => this.EventManager.World_LargeTerrainFeatureListChanged.Remove(value); + } + /// Raised after NPCs are added or removed in a location. public event EventHandler NpcListChanged { @@ -47,7 +54,7 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.World_ObjectListChanged.Remove(value); } - /// Raised after terrain features are added or removed in a location. + /// Raised after terrain features (like floors and trees) are added or removed in a location. public event EventHandler TerrainFeatureListChanged { add => this.EventManager.World_TerrainFeatureListChanged.Add(value); diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 38f96566..90dbacfe 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -567,6 +567,17 @@ namespace StardewModdingAPI.Framework this.Events.Location_BuildingsChanged.Raise(new EventArgsLocationBuildingsChanged(location, added, removed)); } + // large terrain features changed + if (watcher.LargeTerrainFeaturesWatcher.IsChanged) + { + GameLocation location = watcher.Location; + LargeTerrainFeature[] added = watcher.LargeTerrainFeaturesWatcher.Added.ToArray(); + LargeTerrainFeature[] removed = watcher.LargeTerrainFeaturesWatcher.Removed.ToArray(); + watcher.LargeTerrainFeaturesWatcher.Reset(); + + this.Events.World_LargeTerrainFeatureListChanged.Raise(new WorldLargeTerrainFeatureListChangedEventArgs(location, added, removed)); + } + // NPCs changed if (watcher.NpcsWatcher.IsChanged) { diff --git a/src/SMAPI/Framework/StateTracking/LocationTracker.cs b/src/SMAPI/Framework/StateTracking/LocationTracker.cs index 4ac455ac..1b4c0b19 100644 --- a/src/SMAPI/Framework/StateTracking/LocationTracker.cs +++ b/src/SMAPI/Framework/StateTracking/LocationTracker.cs @@ -33,6 +33,9 @@ namespace StardewModdingAPI.Framework.StateTracking /// Tracks added or removed buildings. public ICollectionWatcher BuildingsWatcher { get; } + /// Tracks added or removed large terrain features. + public ICollectionWatcher LargeTerrainFeaturesWatcher { get; } + /// Tracks added or removed NPCs. public ICollectionWatcher NpcsWatcher { get; } @@ -56,6 +59,7 @@ namespace StardewModdingAPI.Framework.StateTracking this.BuildingsWatcher = location is BuildableGameLocation buildableLocation ? WatcherFactory.ForNetCollection(buildableLocation.buildings) : (ICollectionWatcher)WatcherFactory.ForObservableCollection(new ObservableCollection()); + this.LargeTerrainFeaturesWatcher = WatcherFactory.ForNetCollection(location.largeTerrainFeatures); this.NpcsWatcher = WatcherFactory.ForNetCollection(location.characters); this.ObjectsWatcher = WatcherFactory.ForNetDictionary(location.netObjects); this.TerrainFeaturesWatcher = WatcherFactory.ForNetDictionary(location.terrainFeatures); @@ -63,6 +67,7 @@ namespace StardewModdingAPI.Framework.StateTracking this.Watchers.AddRange(new IWatcher[] { this.BuildingsWatcher, + this.LargeTerrainFeaturesWatcher, this.NpcsWatcher, this.ObjectsWatcher, this.TerrainFeaturesWatcher diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 37b624fb..f9c93671 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -89,6 +89,7 @@ + -- cgit From 97a2bdfdd443b3d5b79f48eb1d718ebf255f5e0f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 2 Jun 2018 01:47:33 -0400 Subject: add base implementation for mod event classes (#310) --- src/SMAPI/Framework/Events/ModEventsBase.cs | 28 ++++++++++++++++++++++++++++ src/SMAPI/Framework/Events/ModWorldEvents.cs | 17 ++--------------- src/SMAPI/StardewModdingAPI.csproj | 1 + 3 files changed, 31 insertions(+), 15 deletions(-) create mode 100644 src/SMAPI/Framework/Events/ModEventsBase.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI/Framework/Events/ModEventsBase.cs b/src/SMAPI/Framework/Events/ModEventsBase.cs new file mode 100644 index 00000000..545c58a8 --- /dev/null +++ b/src/SMAPI/Framework/Events/ModEventsBase.cs @@ -0,0 +1,28 @@ +namespace StardewModdingAPI.Framework.Events +{ + /// An internal base class for event API classes. + internal abstract class ModEventsBase + { + /********* + ** Properties + *********/ + /// The underlying event manager. + protected readonly EventManager EventManager; + + /// The mod which uses this instance. + protected readonly IModMetadata Mod; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + internal ModEventsBase(IModMetadata mod, EventManager eventManager) + { + this.Mod = mod; + this.EventManager = eventManager; + } + } +} diff --git a/src/SMAPI/Framework/Events/ModWorldEvents.cs b/src/SMAPI/Framework/Events/ModWorldEvents.cs index db03e447..e1a53e0c 100644 --- a/src/SMAPI/Framework/Events/ModWorldEvents.cs +++ b/src/SMAPI/Framework/Events/ModWorldEvents.cs @@ -4,18 +4,8 @@ using StardewModdingAPI.Events; namespace StardewModdingAPI.Framework.Events { /// Events raised when something changes in the world. - public class ModWorldEvents : IWorldEvents + internal class ModWorldEvents : ModEventsBase, IWorldEvents { - /********* - ** Properties - *********/ - /// The underlying event manager. - private readonly EventManager EventManager; - - /// The mod which uses this instance. - private readonly IModMetadata Mod; - - /********* ** Accessors *********/ @@ -69,9 +59,6 @@ namespace StardewModdingAPI.Framework.Events /// The mod which uses this instance. /// The underlying event manager. internal ModWorldEvents(IModMetadata mod, EventManager eventManager) - { - this.Mod = mod; - this.EventManager = eventManager; - } + : base(mod, eventManager) { } } } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index f9c93671..7b9629e2 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -98,6 +98,7 @@ + -- cgit From 0df7a967a6980db7f4da8d393feae97c968e3375 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 2 Jun 2018 01:48:35 -0400 Subject: add new-style input events (#310) --- src/SMAPI/Events/ControlEvents.cs | 28 ++++++------ src/SMAPI/Events/EventArgsInput.cs | 10 ++--- src/SMAPI/Events/IInputEvents.cs | 14 ++++++ src/SMAPI/Events/IModEvents.cs | 3 ++ src/SMAPI/Events/InputButtonPressedEventArgs.cs | 56 ++++++++++++++++++++++++ src/SMAPI/Events/InputButtonReleasedEventArgs.cs | 56 ++++++++++++++++++++++++ src/SMAPI/Events/InputEvents.cs | 8 ++-- src/SMAPI/Framework/Events/EventManager.cs | 48 ++++++++++++-------- src/SMAPI/Framework/Events/ModEvents.cs | 4 ++ src/SMAPI/Framework/Events/ModInputEvents.cs | 36 +++++++++++++++ src/SMAPI/Framework/SGame.cs | 20 +++++---- src/SMAPI/StardewModdingAPI.csproj | 4 ++ 12 files changed, 235 insertions(+), 52 deletions(-) create mode 100644 src/SMAPI/Events/IInputEvents.cs create mode 100644 src/SMAPI/Events/InputButtonPressedEventArgs.cs create mode 100644 src/SMAPI/Events/InputButtonReleasedEventArgs.cs create mode 100644 src/SMAPI/Framework/Events/ModInputEvents.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI/Events/ControlEvents.cs b/src/SMAPI/Events/ControlEvents.cs index 973bb245..77bbf3ab 100644 --- a/src/SMAPI/Events/ControlEvents.cs +++ b/src/SMAPI/Events/ControlEvents.cs @@ -20,22 +20,22 @@ namespace StardewModdingAPI.Events /// Raised when the changes. That happens when the player presses or releases a key. public static event EventHandler KeyboardChanged { - add => ControlEvents.EventManager.Control_KeyboardChanged.Add(value); - remove => ControlEvents.EventManager.Control_KeyboardChanged.Remove(value); + add => ControlEvents.EventManager.Legacy_Control_KeyboardChanged.Add(value); + remove => ControlEvents.EventManager.Legacy_Control_KeyboardChanged.Remove(value); } /// Raised when the player presses a keyboard key. public static event EventHandler KeyPressed { - add => ControlEvents.EventManager.Control_KeyPressed.Add(value); - remove => ControlEvents.EventManager.Control_KeyPressed.Remove(value); + add => ControlEvents.EventManager.Legacy_Control_KeyPressed.Add(value); + remove => ControlEvents.EventManager.Legacy_Control_KeyPressed.Remove(value); } /// Raised when the player releases a keyboard key. public static event EventHandler KeyReleased { - add => ControlEvents.EventManager.Control_KeyReleased.Add(value); - remove => ControlEvents.EventManager.Control_KeyReleased.Remove(value); + add => ControlEvents.EventManager.Legacy_Control_KeyReleased.Add(value); + remove => ControlEvents.EventManager.Legacy_Control_KeyReleased.Remove(value); } /// Raised when the changes. That happens when the player moves the mouse, scrolls the mouse wheel, or presses/releases a button. @@ -48,29 +48,29 @@ namespace StardewModdingAPI.Events /// The player pressed a controller button. This event isn't raised for trigger buttons. public static event EventHandler ControllerButtonPressed { - add => ControlEvents.EventManager.Control_ControllerButtonPressed.Add(value); - remove => ControlEvents.EventManager.Control_ControllerButtonPressed.Remove(value); + add => ControlEvents.EventManager.Legacy_Control_ControllerButtonPressed.Add(value); + remove => ControlEvents.EventManager.Legacy_Control_ControllerButtonPressed.Remove(value); } /// The player released a controller button. This event isn't raised for trigger buttons. public static event EventHandler ControllerButtonReleased { - add => ControlEvents.EventManager.Control_ControllerButtonReleased.Add(value); - remove => ControlEvents.EventManager.Control_ControllerButtonReleased.Remove(value); + add => ControlEvents.EventManager.Legacy_Control_ControllerButtonReleased.Add(value); + remove => ControlEvents.EventManager.Legacy_Control_ControllerButtonReleased.Remove(value); } /// The player pressed a controller trigger button. public static event EventHandler ControllerTriggerPressed { - add => ControlEvents.EventManager.Control_ControllerTriggerPressed.Add(value); - remove => ControlEvents.EventManager.Control_ControllerTriggerPressed.Remove(value); + add => ControlEvents.EventManager.Legacy_Control_ControllerTriggerPressed.Add(value); + remove => ControlEvents.EventManager.Legacy_Control_ControllerTriggerPressed.Remove(value); } /// The player released a controller trigger button. public static event EventHandler ControllerTriggerReleased { - add => ControlEvents.EventManager.Control_ControllerTriggerReleased.Add(value); - remove => ControlEvents.EventManager.Control_ControllerTriggerReleased.Remove(value); + add => ControlEvents.EventManager.Legacy_Control_ControllerTriggerReleased.Add(value); + remove => ControlEvents.EventManager.Legacy_Control_ControllerTriggerReleased.Remove(value); } diff --git a/src/SMAPI/Events/EventArgsInput.cs b/src/SMAPI/Events/EventArgsInput.cs index d60f4017..0cafdba5 100644 --- a/src/SMAPI/Events/EventArgsInput.cs +++ b/src/SMAPI/Events/EventArgsInput.cs @@ -23,10 +23,10 @@ namespace StardewModdingAPI.Events public ICursorPosition Cursor { get; } /// Whether the input should trigger actions on the affected tile. - public bool IsActionButton { get; } + public bool IsActionButton => this.Button.IsActionButton(); /// Whether the input should use tools on the affected tile. - public bool IsUseToolButton { get; } + public bool IsUseToolButton => this.Button.IsUseToolButton(); /// Whether a mod has indicated the key was already handled. public bool IsSuppressed => this.SuppressButtons.Contains(this.Button); @@ -38,15 +38,11 @@ namespace StardewModdingAPI.Events /// Construct an instance. /// The button on the controller, keyboard, or mouse. /// The cursor position. - /// Whether the input should trigger actions on the affected tile. - /// Whether the input should use tools on the affected tile. /// The buttons to suppress. - public EventArgsInput(SButton button, ICursorPosition cursor, bool isActionButton, bool isUseToolButton, HashSet suppressButtons) + public EventArgsInput(SButton button, ICursorPosition cursor, HashSet suppressButtons) { this.Button = button; this.Cursor = cursor; - this.IsActionButton = isActionButton; - this.IsUseToolButton = isUseToolButton; this.SuppressButtons = suppressButtons; } diff --git a/src/SMAPI/Events/IInputEvents.cs b/src/SMAPI/Events/IInputEvents.cs new file mode 100644 index 00000000..92802fda --- /dev/null +++ b/src/SMAPI/Events/IInputEvents.cs @@ -0,0 +1,14 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Events raised when the player provides input using a controller, keyboard, or mouse. + public interface IInputEvents + { + /// Raised when the player presses a button on the keyboard, controller, or mouse. + event EventHandler ButtonPressed; + + /// Raised when the player releases a button on the keyboard, controller, or mouse. + event EventHandler ButtonReleased; + } +} diff --git a/src/SMAPI/Events/IModEvents.cs b/src/SMAPI/Events/IModEvents.cs index 99e5523f..16ec6557 100644 --- a/src/SMAPI/Events/IModEvents.cs +++ b/src/SMAPI/Events/IModEvents.cs @@ -3,6 +3,9 @@ namespace StardewModdingAPI.Events /// Manages access to events raised by SMAPI. public interface IModEvents { + /// Events raised when the player provides input using a controller, keyboard, or mouse. + IInputEvents Input { get; } + /// Events raised when something changes in the world. IWorldEvents World { get; } } diff --git a/src/SMAPI/Events/InputButtonPressedEventArgs.cs b/src/SMAPI/Events/InputButtonPressedEventArgs.cs new file mode 100644 index 00000000..c8d55cd4 --- /dev/null +++ b/src/SMAPI/Events/InputButtonPressedEventArgs.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Events +{ + /// Event arguments when a button is pressed. + public class InputButtonPressedArgsInput : EventArgs + { + /********* + ** Properties + *********/ + /// The buttons to suppress. + private readonly HashSet SuppressButtons; + + + /********* + ** Accessors + *********/ + /// The button on the controller, keyboard, or mouse. + public SButton Button { get; } + + /// The current cursor position. + public ICursorPosition Cursor { get; } + + /// Whether a mod has indicated the key was already handled. + public bool IsSuppressed => this.SuppressButtons.Contains(this.Button); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The button on the controller, keyboard, or mouse. + /// The cursor position. + /// The buttons to suppress. + public InputButtonPressedArgsInput(SButton button, ICursorPosition cursor, HashSet suppressButtons) + { + this.Button = button; + this.Cursor = cursor; + this.SuppressButtons = suppressButtons; + } + + /// Prevent the game from handling the current button press. This doesn't prevent other mods from receiving the event. + public void SuppressButton() + { + this.SuppressButton(this.Button); + } + + /// Prevent the game from handling a button press. This doesn't prevent other mods from receiving the event. + /// The button to suppress. + public void SuppressButton(SButton button) + { + this.SuppressButtons.Add(button); + } + } +} diff --git a/src/SMAPI/Events/InputButtonReleasedEventArgs.cs b/src/SMAPI/Events/InputButtonReleasedEventArgs.cs new file mode 100644 index 00000000..863fab6a --- /dev/null +++ b/src/SMAPI/Events/InputButtonReleasedEventArgs.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Events +{ + /// Event arguments when a button is released. + public class InputButtonReleasedArgsInput : EventArgs + { + /********* + ** Properties + *********/ + /// The buttons to suppress. + private readonly HashSet SuppressButtons; + + + /********* + ** Accessors + *********/ + /// The button on the controller, keyboard, or mouse. + public SButton Button { get; } + + /// The current cursor position. + public ICursorPosition Cursor { get; } + + /// Whether a mod has indicated the key was already handled. + public bool IsSuppressed => this.SuppressButtons.Contains(this.Button); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The button on the controller, keyboard, or mouse. + /// The cursor position. + /// The buttons to suppress. + public InputButtonReleasedArgsInput(SButton button, ICursorPosition cursor, HashSet suppressButtons) + { + this.Button = button; + this.Cursor = cursor; + this.SuppressButtons = suppressButtons; + } + + /// Prevent the game from handling the current button press. This doesn't prevent other mods from receiving the event. + public void SuppressButton() + { + this.SuppressButton(this.Button); + } + + /// Prevent the game from handling a button press. This doesn't prevent other mods from receiving the event. + /// The button to suppress. + public void SuppressButton(SButton button) + { + this.SuppressButtons.Add(button); + } + } +} diff --git a/src/SMAPI/Events/InputEvents.cs b/src/SMAPI/Events/InputEvents.cs index 84d7ce5d..e62d6ee6 100644 --- a/src/SMAPI/Events/InputEvents.cs +++ b/src/SMAPI/Events/InputEvents.cs @@ -19,15 +19,15 @@ namespace StardewModdingAPI.Events /// Raised when the player presses a button on the keyboard, controller, or mouse. public static event EventHandler ButtonPressed { - add => InputEvents.EventManager.Input_ButtonPressed.Add(value); - remove => InputEvents.EventManager.Input_ButtonPressed.Remove(value); + add => InputEvents.EventManager.Legacy_Input_ButtonPressed.Add(value); + remove => InputEvents.EventManager.Legacy_Input_ButtonPressed.Remove(value); } /// Raised when the player releases a keyboard key on the keyboard, controller, or mouse. public static event EventHandler ButtonReleased { - add => InputEvents.EventManager.Input_ButtonReleased.Add(value); - remove => InputEvents.EventManager.Input_ButtonReleased.Remove(value); + add => InputEvents.EventManager.Legacy_Input_ButtonReleased.Add(value); + remove => InputEvents.EventManager.Legacy_Input_ButtonReleased.Remove(value); } diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 100e4e43..62d9582e 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -32,6 +32,15 @@ namespace StardewModdingAPI.Framework.Events /// Raised after terrain features (like floors and trees) are added or removed in a location. public readonly ManagedEvent World_TerrainFeatureListChanged; + /**** + ** Input + ****/ + /// Raised when the player presses a button on the keyboard, controller, or mouse. + public readonly ManagedEvent Input_ButtonPressed; + + /// Raised when the player released a button on the keyboard, controller, or mouse. + public readonly ManagedEvent Input_ButtonReleased; + /********* ** Events (old) @@ -46,28 +55,28 @@ namespace StardewModdingAPI.Framework.Events ** ControlEvents ****/ /// Raised when the changes. That happens when the player presses or releases a key. - public readonly ManagedEvent Control_KeyboardChanged; + public readonly ManagedEvent Legacy_Control_KeyboardChanged; /// Raised when the player presses a keyboard key. - public readonly ManagedEvent Control_KeyPressed; + public readonly ManagedEvent Legacy_Control_KeyPressed; /// Raised when the player releases a keyboard key. - public readonly ManagedEvent Control_KeyReleased; + public readonly ManagedEvent Legacy_Control_KeyReleased; /// Raised when the changes. That happens when the player moves the mouse, scrolls the mouse wheel, or presses/releases a button. public readonly ManagedEvent Control_MouseChanged; /// The player pressed a controller button. This event isn't raised for trigger buttons. - public readonly ManagedEvent Control_ControllerButtonPressed; + public readonly ManagedEvent Legacy_Control_ControllerButtonPressed; /// The player released a controller button. This event isn't raised for trigger buttons. - public readonly ManagedEvent Control_ControllerButtonReleased; + public readonly ManagedEvent Legacy_Control_ControllerButtonReleased; /// The player pressed a controller trigger button. - public readonly ManagedEvent Control_ControllerTriggerPressed; + public readonly ManagedEvent Legacy_Control_ControllerTriggerPressed; /// The player released a controller trigger button. - public readonly ManagedEvent Control_ControllerTriggerReleased; + public readonly ManagedEvent Legacy_Control_ControllerTriggerReleased; /**** ** GameEvents @@ -124,10 +133,10 @@ namespace StardewModdingAPI.Framework.Events ** InputEvents ****/ /// Raised when the player presses a button on the keyboard, controller, or mouse. - public readonly ManagedEvent Input_ButtonPressed; + public readonly ManagedEvent Legacy_Input_ButtonPressed; /// Raised when the player releases a keyboard key on the keyboard, controller, or mouse. - public readonly ManagedEvent Input_ButtonReleased; + public readonly ManagedEvent Legacy_Input_ButtonReleased; /**** ** LocationEvents @@ -234,6 +243,9 @@ namespace StardewModdingAPI.Framework.Events ManagedEvent ManageEvent(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); // init events (new) + this.Input_ButtonPressed = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed)); + this.Input_ButtonReleased = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased)); + this.World_BuildingListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LocationListChanged)); this.World_LargeTerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LargeTerrainFeatureListChanged)); this.World_LocationListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.BuildingListChanged)); @@ -244,13 +256,13 @@ namespace StardewModdingAPI.Framework.Events // init events (old) this.Content_LocaleChanged = ManageEventOf>(nameof(ContentEvents), nameof(ContentEvents.AfterLocaleChanged)); - this.Control_ControllerButtonPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonPressed)); - this.Control_ControllerButtonReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonReleased)); - this.Control_ControllerTriggerPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerPressed)); - this.Control_ControllerTriggerReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerReleased)); - this.Control_KeyboardChanged = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyboardChanged)); - this.Control_KeyPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyPressed)); - this.Control_KeyReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyReleased)); + this.Legacy_Control_ControllerButtonPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonPressed)); + this.Legacy_Control_ControllerButtonReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonReleased)); + this.Legacy_Control_ControllerTriggerPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerPressed)); + this.Legacy_Control_ControllerTriggerReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerReleased)); + this.Legacy_Control_KeyboardChanged = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyboardChanged)); + this.Legacy_Control_KeyPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyPressed)); + this.Legacy_Control_KeyReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyReleased)); this.Control_MouseChanged = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.MouseChanged)); this.Game_FirstUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.FirstUpdateTick)); @@ -270,8 +282,8 @@ namespace StardewModdingAPI.Framework.Events this.Graphics_OnPreRenderGuiEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPreRenderGuiEvent)); this.Graphics_OnPostRenderGuiEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPostRenderGuiEvent)); - this.Input_ButtonPressed = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonPressed)); - this.Input_ButtonReleased = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonReleased)); + this.Legacy_Input_ButtonPressed = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonPressed)); + this.Legacy_Input_ButtonReleased = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonReleased)); this.Legacy_Location_LocationsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.LocationsChanged)); this.Legacy_Location_BuildingsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.BuildingsChanged)); diff --git a/src/SMAPI/Framework/Events/ModEvents.cs b/src/SMAPI/Framework/Events/ModEvents.cs index cc4cf8d7..90853141 100644 --- a/src/SMAPI/Framework/Events/ModEvents.cs +++ b/src/SMAPI/Framework/Events/ModEvents.cs @@ -8,6 +8,9 @@ namespace StardewModdingAPI.Framework.Events /********* ** Accessors *********/ + /// Events raised when the player provides input using a controller, keyboard, or mouse. + public IInputEvents Input { get; } + /// Events raised when something changes in the world. public IWorldEvents World { get; } @@ -20,6 +23,7 @@ namespace StardewModdingAPI.Framework.Events /// The underlying event manager. public ModEvents(IModMetadata mod, EventManager eventManager) { + this.Input = new ModInputEvents(mod, eventManager); this.World = new ModWorldEvents(mod, eventManager); } } diff --git a/src/SMAPI/Framework/Events/ModInputEvents.cs b/src/SMAPI/Framework/Events/ModInputEvents.cs new file mode 100644 index 00000000..18baec16 --- /dev/null +++ b/src/SMAPI/Framework/Events/ModInputEvents.cs @@ -0,0 +1,36 @@ +using System; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// Events raised when the player provides input using a controller, keyboard, or mouse. + internal class ModInputEvents : ModEventsBase, IInputEvents + { + /********* + ** Accessors + *********/ + /// Raised when the player presses a button on the keyboard, controller, or mouse. + public event EventHandler ButtonPressed + { + add => this.EventManager.Input_ButtonPressed.Add(value); + remove => this.EventManager.Input_ButtonPressed.Remove(value); + } + + /// Raised when the player releases a button on the keyboard, controller, or mouse. + public event EventHandler ButtonReleased + { + add => this.EventManager.Input_ButtonReleased.Add(value); + remove => this.EventManager.Input_ButtonReleased.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + internal ModInputEvents(IModMetadata mod, EventManager eventManager) + : base(mod, eventManager) { } + } +} diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 1611861f..f87293c2 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -459,45 +459,47 @@ namespace StardewModdingAPI.Framework if (status == InputStatus.Pressed) { - this.Events.Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton(), inputState.SuppressButtons)); + this.Events.Input_ButtonPressed.Raise(new InputButtonPressedArgsInput(button, cursor, inputState.SuppressButtons)); + this.Events.Legacy_Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, inputState.SuppressButtons)); // legacy events if (button.TryGetKeyboard(out Keys key)) { if (key != Keys.None) - this.Events.Control_KeyPressed.Raise(new EventArgsKeyPressed(key)); + this.Events.Legacy_Control_KeyPressed.Raise(new EventArgsKeyPressed(key)); } else if (button.TryGetController(out Buttons controllerButton)) { if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) - this.Events.Control_ControllerTriggerPressed.Raise(new EventArgsControllerTriggerPressed(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.RealController.Triggers.Left : inputState.RealController.Triggers.Right)); + this.Events.Legacy_Control_ControllerTriggerPressed.Raise(new EventArgsControllerTriggerPressed(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.RealController.Triggers.Left : inputState.RealController.Triggers.Right)); else - this.Events.Control_ControllerButtonPressed.Raise(new EventArgsControllerButtonPressed(PlayerIndex.One, controllerButton)); + this.Events.Legacy_Control_ControllerButtonPressed.Raise(new EventArgsControllerButtonPressed(PlayerIndex.One, controllerButton)); } } else if (status == InputStatus.Released) { - this.Events.Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton(), inputState.SuppressButtons)); + this.Events.Input_ButtonReleased.Raise(new InputButtonReleasedArgsInput(button, cursor, inputState.SuppressButtons)); + this.Events.Legacy_Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, inputState.SuppressButtons)); // legacy events if (button.TryGetKeyboard(out Keys key)) { if (key != Keys.None) - this.Events.Control_KeyReleased.Raise(new EventArgsKeyPressed(key)); + this.Events.Legacy_Control_KeyReleased.Raise(new EventArgsKeyPressed(key)); } else if (button.TryGetController(out Buttons controllerButton)) { if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) - this.Events.Control_ControllerTriggerReleased.Raise(new EventArgsControllerTriggerReleased(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.RealController.Triggers.Left : inputState.RealController.Triggers.Right)); + this.Events.Legacy_Control_ControllerTriggerReleased.Raise(new EventArgsControllerTriggerReleased(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.RealController.Triggers.Left : inputState.RealController.Triggers.Right)); else - this.Events.Control_ControllerButtonReleased.Raise(new EventArgsControllerButtonReleased(PlayerIndex.One, controllerButton)); + this.Events.Legacy_Control_ControllerButtonReleased.Raise(new EventArgsControllerButtonReleased(PlayerIndex.One, controllerButton)); } } } // raise legacy state-changed events if (inputState.RealKeyboard != previousInputState.RealKeyboard) - this.Events.Control_KeyboardChanged.Raise(new EventArgsKeyboardStateChanged(previousInputState.RealKeyboard, inputState.RealKeyboard)); + this.Events.Legacy_Control_KeyboardChanged.Raise(new EventArgsKeyboardStateChanged(previousInputState.RealKeyboard, inputState.RealKeyboard)); if (inputState.RealMouse != previousInputState.RealMouse) this.Events.Control_MouseChanged.Raise(new EventArgsMouseStateChanged(previousInputState.RealMouse, inputState.RealMouse, previousInputState.MousePosition, inputState.MousePosition)); } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 7b9629e2..1bdad1b5 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -85,7 +85,10 @@ Properties\GlobalAssemblyInfo.cs + + + @@ -107,6 +110,7 @@ + -- cgit From 6f931aa576b2b2f6a64e7e0522e01f6a37c92c8a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 2 Jun 2018 02:35:26 -0400 Subject: add Input.CursorMoved event (#310) --- src/SMAPI/Events/ControlEvents.cs | 8 +++---- src/SMAPI/Events/IInputEvents.cs | 7 +++++-- src/SMAPI/Events/InputCursorMovedEventArgs.cs | 30 +++++++++++++++++++++++++++ src/SMAPI/Framework/Events/EventManager.cs | 20 +++++++++++------- src/SMAPI/Framework/Events/ModInputEvents.cs | 11 ++++++++-- src/SMAPI/Framework/SGame.cs | 27 +++++++++++++++++------- src/SMAPI/StardewModdingAPI.csproj | 1 + 7 files changed, 81 insertions(+), 23 deletions(-) create mode 100644 src/SMAPI/Events/InputCursorMovedEventArgs.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI/Events/ControlEvents.cs b/src/SMAPI/Events/ControlEvents.cs index 77bbf3ab..a3994d1d 100644 --- a/src/SMAPI/Events/ControlEvents.cs +++ b/src/SMAPI/Events/ControlEvents.cs @@ -24,14 +24,14 @@ namespace StardewModdingAPI.Events remove => ControlEvents.EventManager.Legacy_Control_KeyboardChanged.Remove(value); } - /// Raised when the player presses a keyboard key. + /// Raised after the player presses a keyboard key. public static event EventHandler KeyPressed { add => ControlEvents.EventManager.Legacy_Control_KeyPressed.Add(value); remove => ControlEvents.EventManager.Legacy_Control_KeyPressed.Remove(value); } - /// Raised when the player releases a keyboard key. + /// Raised after the player releases a keyboard key. public static event EventHandler KeyReleased { add => ControlEvents.EventManager.Legacy_Control_KeyReleased.Add(value); @@ -41,8 +41,8 @@ namespace StardewModdingAPI.Events /// Raised when the changes. That happens when the player moves the mouse, scrolls the mouse wheel, or presses/releases a button. public static event EventHandler MouseChanged { - add => ControlEvents.EventManager.Control_MouseChanged.Add(value); - remove => ControlEvents.EventManager.Control_MouseChanged.Remove(value); + add => ControlEvents.EventManager.Legacy_Control_MouseChanged.Add(value); + remove => ControlEvents.EventManager.Legacy_Control_MouseChanged.Remove(value); } /// The player pressed a controller button. This event isn't raised for trigger buttons. diff --git a/src/SMAPI/Events/IInputEvents.cs b/src/SMAPI/Events/IInputEvents.cs index 92802fda..938c772b 100644 --- a/src/SMAPI/Events/IInputEvents.cs +++ b/src/SMAPI/Events/IInputEvents.cs @@ -5,10 +5,13 @@ namespace StardewModdingAPI.Events /// Events raised when the player provides input using a controller, keyboard, or mouse. public interface IInputEvents { - /// Raised when the player presses a button on the keyboard, controller, or mouse. + /// Raised after the player presses a button on the keyboard, controller, or mouse. event EventHandler ButtonPressed; - /// Raised when the player releases a button on the keyboard, controller, or mouse. + /// Raised after the player releases a button on the keyboard, controller, or mouse. event EventHandler ButtonReleased; + + /// Raised after the player moves the in-game cursor. + event EventHandler CursorMoved; } } diff --git a/src/SMAPI/Events/InputCursorMovedEventArgs.cs b/src/SMAPI/Events/InputCursorMovedEventArgs.cs new file mode 100644 index 00000000..02e1ee2c --- /dev/null +++ b/src/SMAPI/Events/InputCursorMovedEventArgs.cs @@ -0,0 +1,30 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments when the in-game cursor is moved. + public class InputCursorMovedArgsInput : EventArgs + { + /********* + ** Accessors + *********/ + /// The previous cursor position. + public ICursorPosition OldPosition { get; } + + /// The current cursor position. + public ICursorPosition NewPosition { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous cursor position. + /// The new cursor position. + public InputCursorMovedArgsInput(ICursorPosition oldPosition, ICursorPosition newPosition) + { + this.OldPosition = oldPosition; + this.NewPosition = newPosition; + } + } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 62d9582e..eea74587 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -35,12 +35,15 @@ namespace StardewModdingAPI.Framework.Events /**** ** Input ****/ - /// Raised when the player presses a button on the keyboard, controller, or mouse. + /// Raised after the player presses a button on the keyboard, controller, or mouse. public readonly ManagedEvent Input_ButtonPressed; - /// Raised when the player released a button on the keyboard, controller, or mouse. + /// Raised after the player released a button on the keyboard, controller, or mouse. public readonly ManagedEvent Input_ButtonReleased; + /// Raised after the player moves the in-game cursor. + public readonly ManagedEvent Input_CursorMoved; + /********* ** Events (old) @@ -57,14 +60,14 @@ namespace StardewModdingAPI.Framework.Events /// Raised when the changes. That happens when the player presses or releases a key. public readonly ManagedEvent Legacy_Control_KeyboardChanged; - /// Raised when the player presses a keyboard key. + /// Raised after the player presses a keyboard key. public readonly ManagedEvent Legacy_Control_KeyPressed; - /// Raised when the player releases a keyboard key. + /// Raised after the player releases a keyboard key. public readonly ManagedEvent Legacy_Control_KeyReleased; /// Raised when the changes. That happens when the player moves the mouse, scrolls the mouse wheel, or presses/releases a button. - public readonly ManagedEvent Control_MouseChanged; + public readonly ManagedEvent Legacy_Control_MouseChanged; /// The player pressed a controller button. This event isn't raised for trigger buttons. public readonly ManagedEvent Legacy_Control_ControllerButtonPressed; @@ -132,10 +135,10 @@ namespace StardewModdingAPI.Framework.Events /**** ** InputEvents ****/ - /// Raised when the player presses a button on the keyboard, controller, or mouse. + /// Raised after the player presses a button on the keyboard, controller, or mouse. public readonly ManagedEvent Legacy_Input_ButtonPressed; - /// Raised when the player releases a keyboard key on the keyboard, controller, or mouse. + /// Raised after the player releases a keyboard key on the keyboard, controller, or mouse. public readonly ManagedEvent Legacy_Input_ButtonReleased; /**** @@ -245,6 +248,7 @@ namespace StardewModdingAPI.Framework.Events // init events (new) this.Input_ButtonPressed = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed)); this.Input_ButtonReleased = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased)); + this.Input_CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); this.World_BuildingListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LocationListChanged)); this.World_LargeTerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LargeTerrainFeatureListChanged)); @@ -263,7 +267,7 @@ namespace StardewModdingAPI.Framework.Events this.Legacy_Control_KeyboardChanged = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyboardChanged)); this.Legacy_Control_KeyPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyPressed)); this.Legacy_Control_KeyReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyReleased)); - this.Control_MouseChanged = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.MouseChanged)); + this.Legacy_Control_MouseChanged = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.MouseChanged)); this.Game_FirstUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.FirstUpdateTick)); this.Game_UpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.UpdateTick)); diff --git a/src/SMAPI/Framework/Events/ModInputEvents.cs b/src/SMAPI/Framework/Events/ModInputEvents.cs index 18baec16..48dd0369 100644 --- a/src/SMAPI/Framework/Events/ModInputEvents.cs +++ b/src/SMAPI/Framework/Events/ModInputEvents.cs @@ -9,20 +9,27 @@ namespace StardewModdingAPI.Framework.Events /********* ** Accessors *********/ - /// Raised when the player presses a button on the keyboard, controller, or mouse. + /// Raised after the player presses a button on the keyboard, controller, or mouse. public event EventHandler ButtonPressed { add => this.EventManager.Input_ButtonPressed.Add(value); remove => this.EventManager.Input_ButtonPressed.Remove(value); } - /// Raised when the player releases a button on the keyboard, controller, or mouse. + /// Raised after the player releases a button on the keyboard, controller, or mouse. public event EventHandler ButtonReleased { add => this.EventManager.Input_ButtonReleased.Add(value); remove => this.EventManager.Input_ButtonReleased.Remove(value); } + /// Raised after the player moves the in-game cursor. + public event EventHandler CursorMoved + { + add => this.EventManager.Input_CursorMoved.Add(value); + remove => this.EventManager.Input_CursorMoved.Remove(value); + } + /********* ** Public methods diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index f87293c2..f4e0d3c5 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -103,6 +103,9 @@ namespace StardewModdingAPI.Framework /// The previous content locale. private LocalizedContentManager.LanguageCode? PreviousLocale; + /// The previous cursor position. + private ICursorPosition PreviousCursorPosition; + /// An index incremented on every tick and reset every 60th tick (0–59). private int CurrentUpdateTick; @@ -444,14 +447,24 @@ namespace StardewModdingAPI.Framework { // cursor position Vector2 screenPixels = new Vector2(Game1.getMouseX(), Game1.getMouseY()); - Vector2 tile = new Vector2((int)((Game1.viewport.X + screenPixels.X) / Game1.tileSize), (int)((Game1.viewport.Y + screenPixels.Y) / Game1.tileSize)); - Vector2 grabTile = (Game1.mouseCursorTransparency > 0 && Utility.tileWithinRadiusOfPlayer((int)tile.X, (int)tile.Y, 1, Game1.player)) // derived from Game1.pressActionButton - ? tile - : Game1.player.GetGrabTile(); - cursor = new CursorPosition(screenPixels, tile, grabTile); + if (this.PreviousCursorPosition == null || screenPixels != this.PreviousCursorPosition.ScreenPixels) + { + Vector2 tile = new Vector2((int)((Game1.viewport.X + screenPixels.X) / Game1.tileSize), (int)((Game1.viewport.Y + screenPixels.Y) / Game1.tileSize)); + Vector2 grabTile = (Game1.mouseCursorTransparency > 0 && Utility.tileWithinRadiusOfPlayer((int)tile.X, (int)tile.Y, 1, Game1.player)) // derived from Game1.pressActionButton + ? tile + : Game1.player.GetGrabTile(); + cursor = new CursorPosition(screenPixels, tile, grabTile); + } + else + cursor = this.PreviousCursorPosition; } - // raise input events + // raise cursor moved event + if (this.PreviousCursorPosition != null && cursor.ScreenPixels != this.PreviousCursorPosition.ScreenPixels) + this.Events.Input_CursorMoved.Raise(new InputCursorMovedArgsInput(this.PreviousCursorPosition, cursor)); + this.PreviousCursorPosition = cursor; + + // raise input button events foreach (var pair in inputState.ActiveButtons) { SButton button = pair.Key; @@ -501,7 +514,7 @@ namespace StardewModdingAPI.Framework if (inputState.RealKeyboard != previousInputState.RealKeyboard) this.Events.Legacy_Control_KeyboardChanged.Raise(new EventArgsKeyboardStateChanged(previousInputState.RealKeyboard, inputState.RealKeyboard)); if (inputState.RealMouse != previousInputState.RealMouse) - this.Events.Control_MouseChanged.Raise(new EventArgsMouseStateChanged(previousInputState.RealMouse, inputState.RealMouse, previousInputState.MousePosition, inputState.MousePosition)); + this.Events.Legacy_Control_MouseChanged.Raise(new EventArgsMouseStateChanged(previousInputState.RealMouse, inputState.RealMouse, previousInputState.MousePosition, inputState.MousePosition)); } } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 1bdad1b5..604ad64f 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -85,6 +85,7 @@ Properties\GlobalAssemblyInfo.cs + -- cgit From 90f55a6921ac798e03d6f81240d3a9899544c031 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 2 Jun 2018 12:14:15 -0400 Subject: add mouse scroll event (#310) --- src/SMAPI/Events/IInputEvents.cs | 3 ++ .../Events/InputMouseWheelScrolledEventArgs.cs | 38 ++++++++++++++++++++++ src/SMAPI/Framework/Events/EventManager.cs | 4 +++ src/SMAPI/Framework/Events/ModInputEvents.cs | 7 ++++ src/SMAPI/Framework/SGame.cs | 15 +++++++++ src/SMAPI/StardewModdingAPI.csproj | 1 + 6 files changed, 68 insertions(+) create mode 100644 src/SMAPI/Events/InputMouseWheelScrolledEventArgs.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI/Events/IInputEvents.cs b/src/SMAPI/Events/IInputEvents.cs index 938c772b..64d82c57 100644 --- a/src/SMAPI/Events/IInputEvents.cs +++ b/src/SMAPI/Events/IInputEvents.cs @@ -13,5 +13,8 @@ namespace StardewModdingAPI.Events /// Raised after the player moves the in-game cursor. event EventHandler CursorMoved; + + /// Raised after the player scrolls the mouse wheel. + event EventHandler MouseWheelScrolled; } } diff --git a/src/SMAPI/Events/InputMouseWheelScrolledEventArgs.cs b/src/SMAPI/Events/InputMouseWheelScrolledEventArgs.cs new file mode 100644 index 00000000..9afab9cc --- /dev/null +++ b/src/SMAPI/Events/InputMouseWheelScrolledEventArgs.cs @@ -0,0 +1,38 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments when the player scrolls the mouse wheel. + public class InputMouseWheelScrolledEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The cursor position. + public ICursorPosition Position { get; } + + /// The old scroll value. + public int OldValue { get; } + + /// The new scroll value. + public int NewValue { get; } + + /// The amount by which the scroll value changed. + public int Delta => this.NewValue - this.OldValue; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The cursor position. + /// The old scroll value. + /// The new scroll value. + public InputMouseWheelScrolledEventArgs(ICursorPosition position, int oldValue, int newValue) + { + this.Position = position; + this.OldValue = oldValue; + this.NewValue = newValue; + } + } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index eea74587..9f67244a 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -44,6 +44,9 @@ namespace StardewModdingAPI.Framework.Events /// Raised after the player moves the in-game cursor. public readonly ManagedEvent Input_CursorMoved; + /// Raised after the player scrolls the mouse wheel. + public readonly ManagedEvent Input_MouseWheelScrolled; + /********* ** Events (old) @@ -249,6 +252,7 @@ namespace StardewModdingAPI.Framework.Events this.Input_ButtonPressed = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed)); this.Input_ButtonReleased = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased)); this.Input_CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); + this.Input_MouseWheelScrolled = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled)); this.World_BuildingListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LocationListChanged)); this.World_LargeTerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LargeTerrainFeatureListChanged)); diff --git a/src/SMAPI/Framework/Events/ModInputEvents.cs b/src/SMAPI/Framework/Events/ModInputEvents.cs index 48dd0369..387ea87a 100644 --- a/src/SMAPI/Framework/Events/ModInputEvents.cs +++ b/src/SMAPI/Framework/Events/ModInputEvents.cs @@ -30,6 +30,13 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.Input_CursorMoved.Remove(value); } + /// Raised after the player scrolls the mouse wheel. + public event EventHandler MouseWheelScrolled + { + add => this.EventManager.Input_MouseWheelScrolled.Add(value); + remove => this.EventManager.Input_MouseWheelScrolled.Remove(value); + } + /********* ** Public methods diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 894a771f..18529728 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -103,6 +103,9 @@ namespace StardewModdingAPI.Framework /// Tracks changes to the cursor position. private readonly IValueWatcher CursorWatcher; + /// Tracks changes to the mouse wheel scroll. + private readonly IValueWatcher MouseWheelScrollWatcher; + /// The previous content locale. private LocalizedContentManager.LanguageCode? PreviousLocale; @@ -172,6 +175,7 @@ namespace StardewModdingAPI.Framework // init watchers Game1.locations = new ObservableCollection(); this.CursorWatcher = WatcherFactory.ForEquatable(() => this.Input.MousePosition); + this.MouseWheelScrollWatcher = WatcherFactory.ForEquatable(() => this.Input.RealMouse.ScrollWheelValue); this.SaveIdWatcher = WatcherFactory.ForEquatable(() => Game1.hasLoadedGame ? Game1.uniqueIDForThisGame : 0); this.WindowSizeWatcher = WatcherFactory.ForEquatable(() => new Point(Game1.viewport.Width, Game1.viewport.Height)); this.TimeWatcher = WatcherFactory.ForEquatable(() => Game1.timeOfDay); @@ -180,6 +184,7 @@ namespace StardewModdingAPI.Framework this.Watchers.AddRange(new IWatcher[] { this.CursorWatcher, + this.MouseWheelScrollWatcher, this.SaveIdWatcher, this.WindowSizeWatcher, this.TimeWatcher, @@ -468,6 +473,16 @@ namespace StardewModdingAPI.Framework } this.PreviousCursorPosition = cursor; + // raise mouse wheel scrolled + if (this.MouseWheelScrollWatcher.IsChanged) + { + int oldValue = this.MouseWheelScrollWatcher.PreviousValue; + int newValue = this.MouseWheelScrollWatcher.CurrentValue; + this.MouseWheelScrollWatcher.Reset(); + + this.Events.Input_MouseWheelScrolled.Raise(new InputMouseWheelScrolledEventArgs(cursor, oldValue, newValue)); + } + // raise input button events foreach (var pair in inputState.ActiveButtons) { diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 604ad64f..b81f1359 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -85,6 +85,7 @@ Properties\GlobalAssemblyInfo.cs + -- cgit From d41fe6ff88b569f991f219c5f348d3688fba956f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 2 Jun 2018 16:00:16 -0400 Subject: add input API --- docs/release-notes.md | 1 + src/SMAPI/Framework/CursorPosition.cs | 7 +++- src/SMAPI/Framework/Input/SInputState.cs | 28 +++++++++++--- src/SMAPI/Framework/ModHelpers/InputHelper.cs | 54 +++++++++++++++++++++++++++ src/SMAPI/Framework/ModHelpers/ModHelper.cs | 8 +++- src/SMAPI/Framework/SGame.cs | 25 ++++--------- src/SMAPI/IInputHelper.cs | 21 +++++++++++ src/SMAPI/Program.cs | 2 +- src/SMAPI/StardewModdingAPI.csproj | 2 + 9 files changed, 121 insertions(+), 27 deletions(-) create mode 100644 src/SMAPI/Framework/ModHelpers/InputHelper.cs create mode 100644 src/SMAPI/IInputHelper.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/docs/release-notes.md b/docs/release-notes.md index 750fa37f..8824c0fb 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ * Renamed `install.exe` to `install on Windows.exe` to avoid confusion. * For modders: + * Added [input API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Input) for reading and suppressing keyboard, controller, and mouse input. * Added code analysis to mod build config package to flag common issues as warnings. * Replaced `LocationEvents` with a more powerful set of events for multiplayer: * now raised for all locations; diff --git a/src/SMAPI/Framework/CursorPosition.cs b/src/SMAPI/Framework/CursorPosition.cs index db02b3d1..6f716746 100644 --- a/src/SMAPI/Framework/CursorPosition.cs +++ b/src/SMAPI/Framework/CursorPosition.cs @@ -8,6 +8,9 @@ namespace StardewModdingAPI.Framework /********* ** Accessors *********/ + /// The raw pixel position, not adjusted for the game zoom. + public Vector2 RawPixels { get; } + /// The pixel position relative to the top-left corner of the visible screen. public Vector2 ScreenPixels { get; } @@ -22,11 +25,13 @@ namespace StardewModdingAPI.Framework ** Public methods *********/ /// Construct an instance. + /// The raw pixel position, not adjusted for the game zoom. /// The pixel position relative to the top-left corner of the visible screen. /// The tile position relative to the top-left corner of the map. /// The tile position that the game considers under the cursor for purposes of clicking actions. - public CursorPosition(Vector2 screenPixels, Vector2 tile, Vector2 grabTile) + public CursorPosition(Vector2 rawPixels, Vector2 screenPixels, Vector2 tile, Vector2 grabTile) { + this.RawPixels = rawPixels; this.ScreenPixels = screenPixels; this.Tile = tile; this.GrabTile = grabTile; diff --git a/src/SMAPI/Framework/Input/SInputState.cs b/src/SMAPI/Framework/Input/SInputState.cs index 27e40ab4..44fd0618 100644 --- a/src/SMAPI/Framework/Input/SInputState.cs +++ b/src/SMAPI/Framework/Input/SInputState.cs @@ -8,7 +8,7 @@ using StardewValley; #pragma warning disable 809 // obsolete override of non-obsolete method (this is deliberate) namespace StardewModdingAPI.Framework.Input { - /// A summary of input changes during an update frame. + /// Manages the game's input state. internal sealed class SInputState : InputState { /********* @@ -17,6 +17,9 @@ namespace StardewModdingAPI.Framework.Input /// The maximum amount of direction to ignore for the left thumbstick. private const float LeftThumbstickDeadZone = 0.2f; + /// The cursor position on the screen adjusted for the zoom level. + private CursorPosition CursorPositionImpl; + /********* ** Accessors @@ -39,8 +42,8 @@ namespace StardewModdingAPI.Framework.Input /// A derivative of which suppresses the buttons in . public MouseState SuppressedMouse { get; private set; } - /// The mouse position on the screen adjusted for the zoom level. - public Point MousePosition { get; private set; } + /// The cursor position on the screen adjusted for the zoom level. + public ICursorPosition CursorPosition => this.CursorPositionImpl; /// The buttons which were pressed, held, or released. public IDictionary ActiveButtons { get; private set; } = new Dictionary(); @@ -61,7 +64,7 @@ namespace StardewModdingAPI.Framework.Input RealController = this.RealController, RealKeyboard = this.RealKeyboard, RealMouse = this.RealMouse, - MousePosition = this.MousePosition + CursorPositionImpl = this.CursorPositionImpl }; } @@ -78,15 +81,16 @@ namespace StardewModdingAPI.Framework.Input GamePadState realController = GamePad.GetState(PlayerIndex.One); KeyboardState realKeyboard = Keyboard.GetState(); MouseState realMouse = Mouse.GetState(); - Point mousePosition = new Point((int)(this.RealMouse.X * (1.0 / Game1.options.zoomLevel)), (int)(this.RealMouse.Y * (1.0 / Game1.options.zoomLevel))); // derived from Game1::getMouseX var activeButtons = this.DeriveStatuses(this.ActiveButtons, realKeyboard, realMouse, realController); + Vector2 cursorRawPixelPos = new Vector2(this.RealMouse.X, this.RealMouse.Y); // update real states this.ActiveButtons = activeButtons; this.RealController = realController; this.RealKeyboard = realKeyboard; this.RealMouse = realMouse; - this.MousePosition = mousePosition; + if (this.CursorPositionImpl?.RawPixels != cursorRawPixelPos) + this.CursorPositionImpl = this.GetCursorPosition(cursorRawPixelPos); // update suppressed states this.SuppressButtons.RemoveWhere(p => !this.GetStatus(activeButtons, p).IsDown()); @@ -157,6 +161,18 @@ namespace StardewModdingAPI.Framework.Input /********* ** Private methods *********/ + /// Get the current cursor position. + /// The raw pixel position from the mouse state. + private CursorPosition GetCursorPosition(Vector2 rawPixelPos) + { + Vector2 screenPixels = new Vector2((int)(rawPixelPos.X * (1.0 / Game1.options.zoomLevel)), (int)(rawPixelPos.Y * (1.0 / Game1.options.zoomLevel))); // derived from Game1::getMouseX + Vector2 tile = new Vector2((int)((Game1.viewport.X + screenPixels.X) / Game1.tileSize), (int)((Game1.viewport.Y + screenPixels.Y) / Game1.tileSize)); + Vector2 grabTile = (Game1.mouseCursorTransparency > 0 && Utility.tileWithinRadiusOfPlayer((int)tile.X, (int)tile.Y, 1, Game1.player)) // derived from Game1.pressActionButton + ? tile + : Game1.player.GetGrabTile(); + return new CursorPosition(rawPixelPos, screenPixels, tile, grabTile); + } + /// Whether input should be suppressed in the current context. private bool ShouldSuppressNow() { diff --git a/src/SMAPI/Framework/ModHelpers/InputHelper.cs b/src/SMAPI/Framework/ModHelpers/InputHelper.cs new file mode 100644 index 00000000..f4cd12b6 --- /dev/null +++ b/src/SMAPI/Framework/ModHelpers/InputHelper.cs @@ -0,0 +1,54 @@ +using StardewModdingAPI.Framework.Input; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// Provides an API for checking and changing input state. + internal class InputHelper : BaseHelper, IInputHelper + { + /********* + ** Accessors + *********/ + /// Manages the game's input state. + private readonly SInputState InputState; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique ID of the relevant mod. + /// Manages the game's input state. + public InputHelper(string modID, SInputState inputState) + : base(modID) + { + this.InputState = inputState; + } + + /// Get the current cursor position. + public ICursorPosition GetCursorPosition() + { + return this.InputState.CursorPosition; + } + + /// Get whether a button is currently pressed. + /// The button. + public bool IsDown(SButton button) + { + return this.InputState.IsDown(button); + } + + /// Get whether a button is currently suppressed, so the game won't see it. + /// The button. + public bool IsSuppressed(SButton button) + { + return this.InputState.SuppressButtons.Contains(button); + } + + /// Prevent the game from handling a button press. This doesn't prevent other mods from receiving the event. + /// The button to suppress. + public void Suppress(SButton button) + { + this.InputState.SuppressButtons.Add(button); + } + } +} diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index 92cb9d94..1e07dafa 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.Serialisation; using StardewModdingAPI.Framework.Utilities; @@ -40,6 +41,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// An API for loading content assets. public IContentHelper Content { get; } + /// An API for checking and changing input state. + public IInputHelper Input { get; } + /// An API for accessing private game code. public IReflectionHelper Reflection { get; } @@ -63,6 +67,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The mod's unique ID. /// The full path to the mod's folder. /// Encapsulate SMAPI's JSON parsing. + /// Manages the game's input state. /// Manages access to events raised by SMAPI. /// An API for loading content assets. /// An API for managing console commands. @@ -75,7 +80,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Manages deprecation warnings. /// An argument is null or empty. /// The path does not exist on disk. - public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, IModEvents events, IContentHelper contentHelper, ICommandHelper commandHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper, IEnumerable contentPacks, Func createContentPack, DeprecationManager deprecationManager) + public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, SInputState inputState, IModEvents events, IContentHelper contentHelper, ICommandHelper commandHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper, IEnumerable contentPacks, Func createContentPack, DeprecationManager deprecationManager) : base(modID) { // validate directory @@ -88,6 +93,7 @@ namespace StardewModdingAPI.Framework.ModHelpers this.DirectoryPath = modDirectory; this.JsonHelper = jsonHelper ?? throw new ArgumentNullException(nameof(jsonHelper)); this.Content = contentHelper ?? throw new ArgumentNullException(nameof(contentHelper)); + this.Input = new InputHelper(modID, inputState); this.ModRegistry = modRegistry ?? throw new ArgumentNullException(nameof(modRegistry)); this.ConsoleCommands = commandHelper ?? throw new ArgumentNullException(nameof(commandHelper)); this.Reflection = reflectionHelper ?? throw new ArgumentNullException(nameof(reflectionHelper)); diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 18529728..560b54a4 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -54,9 +54,6 @@ namespace StardewModdingAPI.Framework /// Manages SMAPI events for mods. private readonly EventManager Events; - /// Manages input visible to the game. - private SInputState Input => (SInputState)Game1.input; - /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second @@ -101,7 +98,7 @@ namespace StardewModdingAPI.Framework private readonly IValueWatcher ActiveMenuWatcher; /// Tracks changes to the cursor position. - private readonly IValueWatcher CursorWatcher; + private readonly IValueWatcher CursorWatcher; /// Tracks changes to the mouse wheel scroll. private readonly IValueWatcher MouseWheelScrollWatcher; @@ -137,6 +134,9 @@ namespace StardewModdingAPI.Framework /// SMAPI's content manager. public ContentCoordinator ContentCore { get; private set; } + /// Manages input visible to the game. + public SInputState Input => (SInputState)Game1.input; + /// The game's core multiplayer utility. public SMultiplayer Multiplayer => (SMultiplayer)Game1.multiplayer; @@ -174,7 +174,7 @@ namespace StardewModdingAPI.Framework // init watchers Game1.locations = new ObservableCollection(); - this.CursorWatcher = WatcherFactory.ForEquatable(() => this.Input.MousePosition); + this.CursorWatcher = WatcherFactory.ForEquatable(() => this.Input.CursorPosition.ScreenPixels); this.MouseWheelScrollWatcher = WatcherFactory.ForEquatable(() => this.Input.RealMouse.ScrollWheelValue); this.SaveIdWatcher = WatcherFactory.ForEquatable(() => Game1.hasLoadedGame ? Game1.uniqueIDForThisGame : 0); this.WindowSizeWatcher = WatcherFactory.ForEquatable(() => new Point(Game1.viewport.Width, Game1.viewport.Height)); @@ -452,18 +452,7 @@ namespace StardewModdingAPI.Framework bool isChatInput = Game1.IsChatting || (Context.IsMultiplayer && Context.IsWorldReady && Game1.activeClickableMenu == null && Game1.currentMinigame == null && inputState.IsAnyDown(Game1.options.chatButton)); if (!isChatInput) { - // get cursor position - ICursorPosition cursor = this.PreviousCursorPosition; - if (this.CursorWatcher.IsChanged) - { - // cursor position - Vector2 screenPixels = new Vector2(this.CursorWatcher.CurrentValue.X, this.CursorWatcher.CurrentValue.Y); - Vector2 tile = new Vector2((int)((Game1.viewport.X + screenPixels.X) / Game1.tileSize), (int)((Game1.viewport.Y + screenPixels.Y) / Game1.tileSize)); - Vector2 grabTile = (Game1.mouseCursorTransparency > 0 && Utility.tileWithinRadiusOfPlayer((int)tile.X, (int)tile.Y, 1, Game1.player)) // derived from Game1.pressActionButton - ? tile - : Game1.player.GetGrabTile(); - cursor = new CursorPosition(screenPixels, tile, grabTile); - } + ICursorPosition cursor = this.Input.CursorPosition; // raise cursor moved event if (this.CursorWatcher.IsChanged && this.PreviousCursorPosition != null) @@ -533,7 +522,7 @@ namespace StardewModdingAPI.Framework if (inputState.RealKeyboard != previousInputState.RealKeyboard) this.Events.Legacy_Control_KeyboardChanged.Raise(new EventArgsKeyboardStateChanged(previousInputState.RealKeyboard, inputState.RealKeyboard)); if (inputState.RealMouse != previousInputState.RealMouse) - this.Events.Legacy_Control_MouseChanged.Raise(new EventArgsMouseStateChanged(previousInputState.RealMouse, inputState.RealMouse, previousInputState.MousePosition, inputState.MousePosition)); + this.Events.Legacy_Control_MouseChanged.Raise(new EventArgsMouseStateChanged(previousInputState.RealMouse, inputState.RealMouse, new Point((int)previousInputState.CursorPosition.ScreenPixels.X, (int)previousInputState.CursorPosition.ScreenPixels.Y), new Point((int)inputState.CursorPosition.ScreenPixels.X, (int)inputState.CursorPosition.ScreenPixels.Y))); } } diff --git a/src/SMAPI/IInputHelper.cs b/src/SMAPI/IInputHelper.cs new file mode 100644 index 00000000..328f504b --- /dev/null +++ b/src/SMAPI/IInputHelper.cs @@ -0,0 +1,21 @@ +namespace StardewModdingAPI +{ + /// Provides an API for checking and changing input state. + public interface IInputHelper : IModLinked + { + /// Get the current cursor position. + ICursorPosition GetCursorPosition(); + + /// Get whether a button is currently pressed. + /// The button. + bool IsDown(SButton button); + + /// Get whether a button is currently suppressed, so the game won't see it. + /// The button. + bool IsSuppressed(SButton button); + + /// Prevent the game from handling a button press. This doesn't prevent other mods from receiving the event. + /// The button to suppress. + void Suppress(SButton button); + } +} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 48ad922b..76c12351 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -855,7 +855,7 @@ namespace StardewModdingAPI return new ContentPack(packDirPath, packManifest, packContentHelper, this.JsonHelper); } - modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, events, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); + modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, this.GameInstance.Input, events, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); } // init mod diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index b81f1359..f4aee551 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -114,6 +114,8 @@ + + -- cgit From de74b038e4c15d393de8828c89814ef7eaefe507 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 2 Jun 2018 18:22:04 -0400 Subject: move web API client into toolkit (#532) --- src/SMAPI.Internal/Models/ModInfoModel.cs | 56 ----------------- src/SMAPI.Internal/Models/ModSeachModel.cs | 37 ----------- src/SMAPI.Internal/SMAPI.Internal.projitems | 2 - src/SMAPI.Web/Controllers/ModsApiController.cs | 2 +- .../Framework/ModRepositories/BaseRepository.cs | 2 +- .../ModRepositories/ChucklefishRepository.cs | 2 +- .../Framework/ModRepositories/GitHubRepository.cs | 2 +- .../Framework/ModRepositories/IModRepository.cs | 2 +- .../Framework/ModRepositories/NexusRepository.cs | 2 +- src/SMAPI/Constants.cs | 16 ++++- src/SMAPI/Framework/WebApiClient.cs | 73 ---------------------- src/SMAPI/Program.cs | 4 +- src/SMAPI/SemanticVersion.cs | 19 +++--- src/SMAPI/StardewModdingAPI.csproj | 1 - .../Framework/Clients/WebApi/ModInfoModel.cs | 56 +++++++++++++++++ .../Framework/Clients/WebApi/ModSeachModel.cs | 37 +++++++++++ .../Framework/Clients/WebApi/WebApiClient.cs | 70 +++++++++++++++++++++ 17 files changed, 192 insertions(+), 191 deletions(-) delete mode 100644 src/SMAPI.Internal/Models/ModInfoModel.cs delete mode 100644 src/SMAPI.Internal/Models/ModSeachModel.cs delete mode 100644 src/SMAPI/Framework/WebApiClient.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI.Internal/Models/ModInfoModel.cs b/src/SMAPI.Internal/Models/ModInfoModel.cs deleted file mode 100644 index 725c88bb..00000000 --- a/src/SMAPI.Internal/Models/ModInfoModel.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace StardewModdingAPI.Internal.Models -{ - /// Generic metadata about a mod. - internal class ModInfoModel - { - /********* - ** Accessors - *********/ - /// The mod name. - public string Name { get; set; } - - /// The semantic version for the mod's latest release. - public string Version { get; set; } - - /// The semantic version for the mod's latest preview release, if available and different from . - public string PreviewVersion { get; set; } - - /// The mod's web URL. - public string Url { get; set; } - - /// The error message indicating why the mod is invalid (if applicable). - public string Error { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an empty instance. - public ModInfoModel() - { - // needed for JSON deserialising - } - - /// Construct an instance. - /// The mod name. - /// The semantic version for the mod's latest release. - /// The semantic version for the mod's latest preview release, if available and different from . - /// The mod's web URL. - /// The error message indicating why the mod is invalid (if applicable). - public ModInfoModel(string name, string version, string url, string previewVersion = null, string error = null) - { - this.Name = name; - this.Version = version; - this.PreviewVersion = previewVersion; - this.Url = url; - this.Error = error; // mainly initialised here for the JSON deserialiser - } - - /// Construct an instance. - /// The error message indicating why the mod is invalid. - public ModInfoModel(string error) - { - this.Error = error; - } - } -} diff --git a/src/SMAPI.Internal/Models/ModSeachModel.cs b/src/SMAPI.Internal/Models/ModSeachModel.cs deleted file mode 100644 index fac72135..00000000 --- a/src/SMAPI.Internal/Models/ModSeachModel.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace StardewModdingAPI.Internal.Models -{ - /// Specifies mods whose update-check info to fetch. - internal class ModSearchModel - { - /********* - ** Accessors - *********/ - /// The namespaced mod keys to search. - public string[] ModKeys { get; set; } - - /// Whether to allow non-semantic versions, instead of returning an error for those. - public bool AllowInvalidVersions { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an empty instance. - public ModSearchModel() - { - // needed for JSON deserialising - } - - /// Construct an instance. - /// The namespaced mod keys to search. - /// Whether to allow non-semantic versions, instead of returning an error for those. - public ModSearchModel(IEnumerable modKeys, bool allowInvalidVersions) - { - this.ModKeys = modKeys.ToArray(); - this.AllowInvalidVersions = allowInvalidVersions; - } - } -} diff --git a/src/SMAPI.Internal/SMAPI.Internal.projitems b/src/SMAPI.Internal/SMAPI.Internal.projitems index 33b8cbfa..54b12003 100644 --- a/src/SMAPI.Internal/SMAPI.Internal.projitems +++ b/src/SMAPI.Internal/SMAPI.Internal.projitems @@ -12,8 +12,6 @@ - - diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index fc90d067..1ec855d5 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; -using StardewModdingAPI.Internal.Models; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.Framework.Clients.Nexus; diff --git a/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs index bd27d624..4a4a40cd 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs @@ -1,6 +1,6 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; -using StardewModdingAPI.Internal.Models; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; namespace StardewModdingAPI.Web.Framework.ModRepositories { diff --git a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs index 2782e2b9..e6074a60 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using StardewModdingAPI.Internal.Models; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; namespace StardewModdingAPI.Web.Framework.ModRepositories diff --git a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs index f4abd379..1d7e4fff 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using StardewModdingAPI.Internal.Models; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Web.Framework.Clients.GitHub; namespace StardewModdingAPI.Web.Framework.ModRepositories diff --git a/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs index 79fe8f87..4c879c8d 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using StardewModdingAPI.Internal.Models; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; namespace StardewModdingAPI.Web.Framework.ModRepositories { diff --git a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs index 87a87ab7..6cac6b8f 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using StardewModdingAPI.Internal.Models; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Web.Framework.Clients.Nexus; namespace StardewModdingAPI.Web.Framework.ModRepositories diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index bdbd5fbb..786c1a39 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -38,10 +38,10 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta.15"); + public static ISemanticVersion ApiVersion { get; } /// The minimum supported version of Stardew Valley. - public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.13"); + public static ISemanticVersion MinimumGameVersion { get; } /// The maximum supported version of Stardew Valley. public static ISemanticVersion MaximumGameVersion { get; } = null; @@ -70,6 +70,9 @@ namespace StardewModdingAPI /**** ** Internal ****/ + /// SMAPI's current semantic version as a mod toolkit version. + internal static Toolkit.ISemanticVersion ApiVersionForToolkit { get; } + /// The URL of the SMAPI home page. internal const string HomePageUrl = "https://smapi.io"; @@ -104,6 +107,15 @@ namespace StardewModdingAPI /********* ** Internal methods *********/ + /// Initialise the static values. + static Constants() + { + Constants.ApiVersionForToolkit = new Toolkit.SemanticVersion("2.6-beta.15"); + Constants.MinimumGameVersion = new GameVersion("1.3.13"); + + Constants.ApiVersion = new SemanticVersion(Constants.ApiVersionForToolkit); + } + /// Get metadata for mapping assemblies to the current platform. /// The target game platform. internal static PlatformAssemblyMap GetAssemblyMap(Platform targetPlatform) diff --git a/src/SMAPI/Framework/WebApiClient.cs b/src/SMAPI/Framework/WebApiClient.cs deleted file mode 100644 index e33b2681..00000000 --- a/src/SMAPI/Framework/WebApiClient.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using Newtonsoft.Json; -using StardewModdingAPI.Internal.Models; - -namespace StardewModdingAPI.Framework -{ - /// Provides methods for interacting with the SMAPI web API. - internal class WebApiClient - { - /********* - ** Properties - *********/ - /// The base URL for the web API. - private readonly Uri BaseUrl; - - /// The API version number. - private readonly ISemanticVersion Version; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The base URL for the web API. - /// The web API version. - public WebApiClient(string baseUrl, ISemanticVersion version) - { -#if !SMAPI_FOR_WINDOWS - baseUrl = baseUrl.Replace("https://", "http://"); // workaround for OpenSSL issues with the game's bundled Mono on Linux/Mac -#endif - this.BaseUrl = new Uri(baseUrl); - this.Version = version; - } - - /// Get the latest SMAPI version. - /// The mod keys for which to fetch the latest version. - public IDictionary GetModInfo(params string[] modKeys) - { - return this.Post>( - $"v{this.Version}/mods", - new ModSearchModel(modKeys, allowInvalidVersions: true) - ); - } - - - /********* - ** Private methods - *********/ - /// Fetch the response from the backend API. - /// The body content type. - /// The expected response type. - /// The request URL, optionally excluding the base URL. - /// The body content to post. - private TResult Post(string url, TBody content) - { - /*** - ** Note: avoid HttpClient for Mac compatibility. - ***/ - using (WebClient client = new WebClient()) - { - Uri fullUrl = new Uri(this.BaseUrl, url); - string data = JsonConvert.SerializeObject(content); - - client.Headers["Content-Type"] = "application/json"; - client.Headers["User-Agent"] = $"SMAPI/{this.Version}"; - string response = client.UploadString(fullUrl, data); - return JsonConvert.DeserializeObject(response); - } - } - } -} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index d5f5fdcd..b7b4dfc7 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -26,7 +26,7 @@ using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; using StardewModdingAPI.Internal; -using StardewModdingAPI.Internal.Models; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Toolkit.Utilities; using StardewValley; using Monitor = StardewModdingAPI.Framework.Monitor; @@ -561,7 +561,7 @@ namespace StardewModdingAPI new Thread(() => { // create client - WebApiClient client = new WebApiClient(this.Settings.WebApiBaseUrl, Constants.ApiVersion); + WebApiClient client = new WebApiClient(this.Settings.WebApiBaseUrl, Constants.ApiVersionForToolkit); this.Monitor.Log("Checking for updates...", LogLevel.Trace); // check SMAPI version diff --git a/src/SMAPI/SemanticVersion.cs b/src/SMAPI/SemanticVersion.cs index 3ee3ccf3..c4dd1912 100644 --- a/src/SMAPI/SemanticVersion.cs +++ b/src/SMAPI/SemanticVersion.cs @@ -54,6 +54,13 @@ namespace StardewModdingAPI public SemanticVersion(Version version) : this(new Toolkit.SemanticVersion(version)) { } + /// Construct an instance. + /// The underlying semantic version implementation. + internal SemanticVersion(Toolkit.ISemanticVersion version) + { + this.Version = version; + } + /// Whether this is a pre-release version. public bool IsPrerelease() { @@ -146,17 +153,5 @@ namespace StardewModdingAPI parsed = null; return false; } - - - /********* - ** Private methods - *********/ - /// Construct an instance. - /// The underlying semantic version implementation. - private SemanticVersion(Toolkit.ISemanticVersion version) - { - this.Version = version; - } - } } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 96b3aa5b..a5ccc62d 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -279,7 +279,6 @@ - diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs new file mode 100644 index 00000000..e1a204c6 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs @@ -0,0 +1,56 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +{ + /// Generic metadata about a mod. + public class ModInfoModel + { + /********* + ** Accessors + *********/ + /// The mod name. + public string Name { get; set; } + + /// The semantic version for the mod's latest release. + public string Version { get; set; } + + /// The semantic version for the mod's latest preview release, if available and different from . + public string PreviewVersion { get; set; } + + /// The mod's web URL. + public string Url { get; set; } + + /// The error message indicating why the mod is invalid (if applicable). + public string Error { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public ModInfoModel() + { + // needed for JSON deserialising + } + + /// Construct an instance. + /// The mod name. + /// The semantic version for the mod's latest release. + /// The semantic version for the mod's latest preview release, if available and different from . + /// The mod's web URL. + /// The error message indicating why the mod is invalid (if applicable). + public ModInfoModel(string name, string version, string url, string previewVersion = null, string error = null) + { + this.Name = name; + this.Version = version; + this.PreviewVersion = previewVersion; + this.Url = url; + this.Error = error; // mainly initialised here for the JSON deserialiser + } + + /// Construct an instance. + /// The error message indicating why the mod is invalid. + public ModInfoModel(string error) + { + this.Error = error; + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs new file mode 100644 index 00000000..c0ee34ea --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +{ + /// Specifies mods whose update-check info to fetch. + public class ModSearchModel + { + /********* + ** Accessors + *********/ + /// The namespaced mod keys to search. + public string[] ModKeys { get; set; } + + /// Whether to allow non-semantic versions, instead of returning an error for those. + public bool AllowInvalidVersions { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public ModSearchModel() + { + // needed for JSON deserialising + } + + /// Construct an instance. + /// The namespaced mod keys to search. + /// Whether to allow non-semantic versions, instead of returning an error for those. + public ModSearchModel(IEnumerable modKeys, bool allowInvalidVersions) + { + this.ModKeys = modKeys.ToArray(); + this.AllowInvalidVersions = allowInvalidVersions; + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs new file mode 100644 index 00000000..277fbeeb --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Net; +using Newtonsoft.Json; + +namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +{ + /// Provides methods for interacting with the SMAPI web API. + internal class WebApiClient + { + /********* + ** Properties + *********/ + /// The base URL for the web API. + private readonly Uri BaseUrl; + + /// The API version number. + private readonly ISemanticVersion Version; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The base URL for the web API. + /// The web API version. + public WebApiClient(string baseUrl, ISemanticVersion version) + { +#if !SMAPI_FOR_WINDOWS + baseUrl = baseUrl.Replace("https://", "http://"); // workaround for OpenSSL issues with the game's bundled Mono on Linux/Mac +#endif + this.BaseUrl = new Uri(baseUrl); + this.Version = version; + } + + /// Get the latest SMAPI version. + /// The mod keys for which to fetch the latest version. + public IDictionary GetModInfo(params string[] modKeys) + { + return this.Post>( + $"v{this.Version}/mods", + new ModSearchModel(modKeys, allowInvalidVersions: true) + ); + } + + + /********* + ** Private methods + *********/ + /// Fetch the response from the backend API. + /// The body content type. + /// The expected response type. + /// The request URL, optionally excluding the base URL. + /// The body content to post. + private TResult Post(string url, TBody content) + { + // note: avoid HttpClient for Mac compatibility + using (WebClient client = new WebClient()) + { + Uri fullUrl = new Uri(this.BaseUrl, url); + string data = JsonConvert.SerializeObject(content); + + client.Headers["Content-Type"] = "application/json"; + client.Headers["User-Agent"] = $"SMAPI/{this.Version}"; + string response = client.UploadString(fullUrl, data); + return JsonConvert.DeserializeObject(response); + } + } + } +} -- cgit From 625c538f244519700f3942b2b2969845db9a99b0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 5 Jun 2018 20:22:46 -0400 Subject: move manifest parsing into toolkit (#532) --- src/SMAPI.Tests/Core/ModResolverTests.cs | 84 +++++-------- src/SMAPI.Tests/Utilities/SemanticVersionTests.cs | 1 + src/SMAPI/Framework/ContentPack.cs | 2 +- src/SMAPI/Framework/Exceptions/SParseException.cs | 17 --- src/SMAPI/Framework/InternalExtensions.cs | 16 --- src/SMAPI/Framework/LegacyManifestVersion.cs | 26 ---- src/SMAPI/Framework/ModHelpers/ModHelper.cs | 20 ++- src/SMAPI/Framework/ModLoading/ModResolver.cs | 37 +++--- src/SMAPI/Framework/Models/Manifest.cs | 74 +++++++++-- .../Framework/Models/ManifestContentPackFor.cs | 25 +++- src/SMAPI/Framework/Models/ManifestDependency.cs | 11 +- .../Framework/Serialisation/ColorConverter.cs | 47 +++++++ .../CrossplatformConverters/ColorConverter.cs | 46 ------- .../CrossplatformConverters/PointConverter.cs | 42 ------- .../CrossplatformConverters/RectangleConverter.cs | 51 -------- src/SMAPI/Framework/Serialisation/JsonHelper.cs | 139 --------------------- .../Framework/Serialisation/PointConverter.cs | 43 +++++++ .../Framework/Serialisation/RectangleConverter.cs | 52 ++++++++ .../Serialisation/SimpleReadOnlyConverter.cs | 77 ------------ .../ManifestContentPackForConverter.cs | 50 -------- .../ManifestDependencyArrayConverter.cs | 60 --------- .../SmapiConverters/SemanticVersionConverter.cs | 36 ------ .../SmapiConverters/StringEnumConverter.cs | 22 ---- src/SMAPI/IManifest.cs | 2 +- src/SMAPI/Program.cs | 18 ++- src/SMAPI/StardewModdingAPI.csproj | 20 +-- .../Converters/ManifestContentPackForConverter.cs | 50 ++++++++ .../Converters/ManifestDependencyArrayConverter.cs | 60 +++++++++ .../Converters/SemanticVersionConverter.cs | 36 ++++++ .../Converters/SimpleReadOnlyConverter.cs | 76 +++++++++++ .../Converters/StringEnumConverter.cs | 22 ++++ .../Serialisation/InternalExtensions.cs | 21 ++++ .../Serialisation/JsonHelper.cs | 123 ++++++++++++++++++ .../Serialisation/Models/LegacyManifestVersion.cs | 26 ++++ .../Serialisation/Models/Manifest.cs | 49 ++++++++ .../Serialisation/Models/ManifestContentPackFor.cs | 15 +++ .../Serialisation/Models/ManifestDependency.cs | 35 ++++++ .../Serialisation/SParseException.cs | 17 +++ 38 files changed, 851 insertions(+), 697 deletions(-) delete mode 100644 src/SMAPI/Framework/Exceptions/SParseException.cs delete mode 100644 src/SMAPI/Framework/LegacyManifestVersion.cs create mode 100644 src/SMAPI/Framework/Serialisation/ColorConverter.cs delete mode 100644 src/SMAPI/Framework/Serialisation/CrossplatformConverters/ColorConverter.cs delete mode 100644 src/SMAPI/Framework/Serialisation/CrossplatformConverters/PointConverter.cs delete mode 100644 src/SMAPI/Framework/Serialisation/CrossplatformConverters/RectangleConverter.cs delete mode 100644 src/SMAPI/Framework/Serialisation/JsonHelper.cs create mode 100644 src/SMAPI/Framework/Serialisation/PointConverter.cs create mode 100644 src/SMAPI/Framework/Serialisation/RectangleConverter.cs delete mode 100644 src/SMAPI/Framework/Serialisation/SimpleReadOnlyConverter.cs delete mode 100644 src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestContentPackForConverter.cs delete mode 100644 src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestDependencyArrayConverter.cs delete mode 100644 src/SMAPI/Framework/Serialisation/SmapiConverters/SemanticVersionConverter.cs delete mode 100644 src/SMAPI/Framework/Serialisation/SmapiConverters/StringEnumConverter.cs create mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/Converters/ManifestContentPackForConverter.cs create mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/Converters/ManifestDependencyArrayConverter.cs create mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs create mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/Converters/SimpleReadOnlyConverter.cs create mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/Converters/StringEnumConverter.cs create mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/InternalExtensions.cs create mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/JsonHelper.cs create mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/Models/LegacyManifestVersion.cs create mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs create mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs create mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs create mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/SParseException.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index d63eb1a2..2fbeb9b6 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -9,7 +9,7 @@ using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModLoading; -using StardewModdingAPI.Framework.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation; namespace StardewModdingAPI.Tests.Core { @@ -93,8 +93,8 @@ namespace StardewModdingAPI.Tests.Core Assert.IsNotNull(mod, "The loaded manifest shouldn't be null."); Assert.AreEqual(null, mod.DataRecord, "The data record should be null since we didn't provide one."); Assert.AreEqual(modFolder, mod.DirectoryPath, "The directory path doesn't match."); - Assert.AreEqual(ModMetadataStatus.Found, mod.Status, "The status doesn't match."); Assert.AreEqual(null, mod.Error, "The error should be null since parsing should have succeeded."); + Assert.AreEqual(ModMetadataStatus.Found, mod.Status, "The status doesn't match."); Assert.AreEqual(original[nameof(IManifest.Name)], mod.DisplayName, "The display name should use the manifest name."); Assert.AreEqual(original[nameof(IManifest.Name)], mod.Manifest.Name, "The manifest's name doesn't match."); @@ -160,7 +160,7 @@ namespace StardewModdingAPI.Tests.Core { // arrange Mock mock = this.GetMetadata("Mod A", new string[0], allowStatusChange: true); - mock.Setup(p => p.Manifest).Returns(this.GetManifest(m => m.MinimumApiVersion = new SemanticVersion("1.1"))); + mock.Setup(p => p.Manifest).Returns(this.GetManifest(minimumApiVersion: "1.1")); this.SetupMetadataForValidation(mock); // act @@ -174,7 +174,7 @@ namespace StardewModdingAPI.Tests.Core public void ValidateManifests_MissingEntryDLL_Fails() { // arrange - Mock mock = this.GetMetadata(this.GetManifest("Mod A", "1.0", manifest => manifest.EntryDll = "Missing.dll"), allowStatusChange: true); + Mock mock = this.GetMetadata(this.GetManifest(id: "Mod A", version: "1.0", entryDll: "Missing.dll"), allowStatusChange: true); this.SetupMetadataForValidation(mock); // act @@ -189,7 +189,7 @@ namespace StardewModdingAPI.Tests.Core { // arrange Mock modA = this.GetMetadata("Mod A", new string[0], allowStatusChange: true); - Mock modB = this.GetMetadata(this.GetManifest("Mod A", "1.0", manifest => manifest.Name = "Mod B"), allowStatusChange: true); + Mock modB = this.GetMetadata(this.GetManifest(id: "Mod A", name: "Mod B", version: "1.0"), allowStatusChange: true); Mock modC = this.GetMetadata("Mod C", new string[0], allowStatusChange: false); foreach (Mock mod in new[] { modA, modB, modC }) this.SetupMetadataForValidation(mod); @@ -398,8 +398,8 @@ namespace StardewModdingAPI.Tests.Core { // arrange // A 1.0 ◀── B (need A 1.1) - Mock modA = this.GetMetadata(this.GetManifest("Mod A", "1.0")); - Mock modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.1")), allowStatusChange: true); + Mock modA = this.GetMetadata(this.GetManifest(id: "Mod A", version: "1.0")); + Mock modB = this.GetMetadata(this.GetManifest(id: "Mod B", version: "1.0", dependencies: new IManifestDependency[] { new ManifestDependency("Mod A", "1.1") }), allowStatusChange: true); // act IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modA.Object, modB.Object }, new ModDatabase()).ToArray(); @@ -414,8 +414,8 @@ namespace StardewModdingAPI.Tests.Core { // arrange // A 1.0 ◀── B (need A 1.0-beta) - Mock modA = this.GetMetadata(this.GetManifest("Mod A", "1.0")); - Mock modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.0-beta")), allowStatusChange: false); + Mock modA = this.GetMetadata(this.GetManifest(id: "Mod A", version: "1.0")); + Mock modB = this.GetMetadata(this.GetManifest(id: "Mod B", version: "1.0", dependencies: new IManifestDependency[] { new ManifestDependency("Mod A", "1.0-beta") }), allowStatusChange: false); // act IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modA.Object, modB.Object }, new ModDatabase()).ToArray(); @@ -431,8 +431,8 @@ namespace StardewModdingAPI.Tests.Core { // arrange // A ◀── B - Mock modA = this.GetMetadata(this.GetManifest("Mod A", "1.0")); - Mock modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.0", required: false)), allowStatusChange: false); + Mock modA = this.GetMetadata(this.GetManifest(id: "Mod A", version: "1.0")); + Mock modB = this.GetMetadata(this.GetManifest(id: "Mod B", version: "1.0", dependencies: new IManifestDependency[] { new ManifestDependency("Mod A", "1.0", required: false) }), allowStatusChange: false); // act IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modB.Object, modA.Object }, new ModDatabase()).ToArray(); @@ -448,7 +448,7 @@ namespace StardewModdingAPI.Tests.Core { // arrange // A ◀── B where A doesn't exist - Mock modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.0", required: false)), allowStatusChange: false); + Mock modB = this.GetMetadata(this.GetManifest(id: "Mod B", version: "1.0", dependencies: new IManifestDependency[] { new ManifestDependency("Mod A", "1.0", required: false) }), allowStatusChange: false); // act IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modB.Object }, new ModDatabase()).ToArray(); @@ -463,46 +463,26 @@ namespace StardewModdingAPI.Tests.Core ** Private methods *********/ /// Get a randomised basic manifest. - /// Adjust the generated manifest. - private Manifest GetManifest(Action adjust = null) - { - Manifest manifest = new Manifest - { - Name = Sample.String(), - Author = Sample.String(), - Version = new SemanticVersion(Sample.Int(), Sample.Int(), Sample.Int(), Sample.String()), - Description = Sample.String(), - UniqueID = $"{Sample.String()}.{Sample.String()}", - EntryDll = $"{Sample.String()}.dll" - }; - adjust?.Invoke(manifest); - return manifest; - } - - /// Get a randomised basic manifest. - /// The mod's name and unique ID. - /// The mod version. - /// Adjust the generated manifest. - /// The dependencies this mod requires. - private IManifest GetManifest(string uniqueID, string version, Action adjust, params IManifestDependency[] dependencies) - { - return this.GetManifest(manifest => - { - manifest.Name = uniqueID; - manifest.UniqueID = uniqueID; - manifest.Version = new SemanticVersion(version); - manifest.Dependencies = dependencies; - adjust?.Invoke(manifest); - }); - } - - /// Get a randomised basic manifest. - /// The mod's name and unique ID. - /// The mod version. - /// The dependencies this mod requires. - private IManifest GetManifest(string uniqueID, string version, params IManifestDependency[] dependencies) + /// The value, or null for a generated value. + /// The value, or null for a generated value. + /// The value, or null for a generated value. + /// The value, or null for a generated value. + /// The value. + /// The value. + /// The value. + private Manifest GetManifest(string id = null, string name = null, string version = null, string entryDll = null, string contentPackForID = null, string minimumApiVersion = null, IManifestDependency[] dependencies = null) { - return this.GetManifest(uniqueID, version, null, dependencies); + return new Manifest( + uniqueID: id ?? $"{Sample.String()}.{Sample.String()}", + name: name ?? id ?? Sample.String(), + author: Sample.String(), + description: Sample.String(), + version: version != null ? new SemanticVersion(version) : new SemanticVersion(Sample.Int(), Sample.Int(), Sample.Int(), Sample.String()), + entryDll: entryDll ?? $"{Sample.String()}.dll", + contentPackFor: contentPackForID != null ? new ManifestContentPackFor(contentPackForID) : null, + minimumApiVersion: minimumApiVersion != null ? new SemanticVersion(minimumApiVersion) : null, + dependencies: dependencies + ); } /// Get a randomised basic manifest. @@ -518,7 +498,7 @@ namespace StardewModdingAPI.Tests.Core /// Whether the code being tested is allowed to change the mod status. private Mock GetMetadata(string uniqueID, string[] dependencies, bool allowStatusChange = false) { - IManifest manifest = this.GetManifest(uniqueID, "1.0", dependencies?.Select(dependencyID => (IManifestDependency)new ManifestDependency(dependencyID, null)).ToArray()); + IManifest manifest = this.GetManifest(id: uniqueID, version: "1.0", dependencies: dependencies?.Select(dependencyID => (IManifestDependency)new ManifestDependency(dependencyID, null)).ToArray()); return this.GetMetadata(manifest, allowStatusChange); } diff --git a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs index f1a72012..b797393b 100644 --- a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs +++ b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using Newtonsoft.Json; using NUnit.Framework; using StardewModdingAPI.Framework; +using StardewModdingAPI.Toolkit.Serialisation.Models; namespace StardewModdingAPI.Tests.Utilities { diff --git a/src/SMAPI/Framework/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs index ee6df1ec..4a4adb90 100644 --- a/src/SMAPI/Framework/ContentPack.cs +++ b/src/SMAPI/Framework/ContentPack.cs @@ -2,7 +2,7 @@ using System; using System.IO; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; -using StardewModdingAPI.Framework.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation; using StardewModdingAPI.Toolkit.Utilities; using xTile; diff --git a/src/SMAPI/Framework/Exceptions/SParseException.cs b/src/SMAPI/Framework/Exceptions/SParseException.cs deleted file mode 100644 index f7133ee7..00000000 --- a/src/SMAPI/Framework/Exceptions/SParseException.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace StardewModdingAPI.Framework.Exceptions -{ - /// A format exception which provides a user-facing error message. - internal class SParseException : FormatException - { - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The error message. - /// The underlying exception, if any. - public SParseException(string message, Exception ex = null) - : base(message, ex) { } - } -} diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index bff4807c..ff3925fb 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Reflection; using Microsoft.Xna.Framework.Graphics; -using Newtonsoft.Json.Linq; using StardewModdingAPI.Framework.Reflection; using StardewValley; @@ -91,20 +90,5 @@ namespace StardewModdingAPI.Framework // get result return reflection.GetField(Game1.spriteBatch, fieldName).GetValue(); } - - /**** - ** Json.NET - ****/ - /// Get a JSON field value from a case-insensitive field name. This will check for an exact match first, then search without case sensitivity. - /// The value type. - /// The JSON object to search. - /// The field name. - public static T ValueIgnoreCase(this JObject obj, string fieldName) - { - JToken token = obj.GetValue(fieldName, StringComparison.InvariantCultureIgnoreCase); - return token != null - ? token.Value() - : default(T); - } } } diff --git a/src/SMAPI/Framework/LegacyManifestVersion.cs b/src/SMAPI/Framework/LegacyManifestVersion.cs deleted file mode 100644 index 454b9137..00000000 --- a/src/SMAPI/Framework/LegacyManifestVersion.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Newtonsoft.Json; - -namespace StardewModdingAPI.Framework -{ - /// An implementation of that hamdles the legacy version format. - internal class LegacyManifestVersion : SemanticVersion - { - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The major version incremented for major API changes. - /// The minor version incremented for backwards-compatible changes. - /// The patch version for backwards-compatible bug fixes. - /// An optional build tag. - [JsonConstructor] - public LegacyManifestVersion(int majorVersion, int minorVersion, int patchVersion, string build = null) - : base( - majorVersion, - minorVersion, - patchVersion, - build != "0" ? build : null // '0' from incorrect examples in old SMAPI documentation - ) - { } - } -} diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index e8726938..18904857 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -5,7 +5,7 @@ using System.Linq; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Models; -using StardewModdingAPI.Framework.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation; using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Framework.ModHelpers @@ -179,16 +179,14 @@ namespace StardewModdingAPI.Framework.ModHelpers throw new ArgumentException($"Can't create content pack for directory path '{directoryPath}' because no such directory exists."); // create manifest - IManifest manifest = new Manifest - { - Name = name, - Author = author, - Description = description, - Version = version, - UniqueID = id, - UpdateKeys = new string[0], - ContentPackFor = new ManifestContentPackFor { UniqueID = this.ModID } - }; + IManifest manifest = new Manifest( + uniqueID: id, + name: name, + author: author, + description: description, + version: version, + contentPackFor: new ManifestContentPackFor(this.ModID) + ); // create content pack return this.CreateContentPack(directoryPath, manifest); diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index d46caa55..ddc8650c 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -3,11 +3,11 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; -using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.Models; -using StardewModdingAPI.Framework.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation; using StardewModdingAPI.Toolkit.Utilities; +using ToolkitManifest = StardewModdingAPI.Toolkit.Serialisation.Models.Manifest; namespace StardewModdingAPI.Framework.ModLoading { @@ -28,25 +28,28 @@ namespace StardewModdingAPI.Framework.ModLoading { // read file Manifest manifest = null; - string path = Path.Combine(modDir.FullName, "manifest.json"); string error = null; - try { - manifest = jsonHelper.ReadJsonFile(path); - if (manifest == null) + string path = Path.Combine(modDir.FullName, "manifest.json"); + try { - error = File.Exists(path) - ? "its manifest is invalid." - : "it doesn't have a manifest."; + ToolkitManifest rawManifest = jsonHelper.ReadJsonFile(path); + if (rawManifest == null) + { + error = File.Exists(path) + ? "its manifest is invalid." + : "it doesn't have a manifest."; + } + manifest = new Manifest(rawManifest); + } + catch (SParseException ex) + { + error = $"parsing its manifest failed: {ex.Message}"; + } + catch (Exception ex) + { + error = $"parsing its manifest failed:\n{ex.GetLogSummary()}"; } - } - catch (SParseException ex) - { - error = $"parsing its manifest failed: {ex.Message}"; - } - catch (Exception ex) - { - error = $"parsing its manifest failed:\n{ex.GetLogSummary()}"; } // parse internal data record (if any) diff --git a/src/SMAPI/Framework/Models/Manifest.cs b/src/SMAPI/Framework/Models/Manifest.cs index f5867cf3..92ffe0dc 100644 --- a/src/SMAPI/Framework/Models/Manifest.cs +++ b/src/SMAPI/Framework/Models/Manifest.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; +using System.Linq; using Newtonsoft.Json; -using StardewModdingAPI.Framework.Serialisation.SmapiConverters; namespace StardewModdingAPI.Framework.Models { @@ -11,39 +11,87 @@ namespace StardewModdingAPI.Framework.Models ** Accessors *********/ /// The mod name. - public string Name { get; set; } + public string Name { get; } /// A brief description of the mod. - public string Description { get; set; } + public string Description { get; } /// The mod author's name. - public string Author { get; set; } + public string Author { get; } /// The mod version. - public ISemanticVersion Version { get; set; } + public ISemanticVersion Version { get; } /// The minimum SMAPI version required by this mod, if any. - public ISemanticVersion MinimumApiVersion { get; set; } + public ISemanticVersion MinimumApiVersion { get; } /// The name of the DLL in the directory that has the method. Mutually exclusive with . - public string EntryDll { get; set; } + public string EntryDll { get; } /// The mod which will read this as a content pack. Mutually exclusive with . - [JsonConverter(typeof(ManifestContentPackForConverter))] - public IManifestContentPackFor ContentPackFor { get; set; } + public IManifestContentPackFor ContentPackFor { get; } /// The other mods that must be loaded before this mod. - [JsonConverter(typeof(ManifestDependencyArrayConverter))] - public IManifestDependency[] Dependencies { get; set; } + public IManifestDependency[] Dependencies { get; } /// The namespaced mod IDs to query for updates (like Nexus:541). public string[] UpdateKeys { get; set; } /// The unique mod ID. - public string UniqueID { get; set; } + public string UniqueID { get; } /// Any manifest fields which didn't match a valid field. [JsonExtensionData] - public IDictionary ExtraFields { get; set; } + public IDictionary ExtraFields { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The toolkit manifest. + public Manifest(Toolkit.Serialisation.Models.Manifest manifest) + : this( + uniqueID: manifest.UniqueID, + name: manifest.Name, + author: manifest.Author, + description: manifest.Description, + version: manifest.Version != null ? new SemanticVersion(manifest.Version) : null, + entryDll: manifest.EntryDll, + minimumApiVersion: manifest.MinimumApiVersion != null ? new SemanticVersion(manifest.MinimumApiVersion) : null, + contentPackFor: manifest.ContentPackFor != null ? new ManifestContentPackFor(manifest.ContentPackFor) : null, + dependencies: manifest.Dependencies?.Select(p => p != null ? (IManifestDependency)new ManifestDependency(p) : null).ToArray(), + updateKeys: manifest.UpdateKeys, + extraFields: manifest.ExtraFields + ) + { } + + /// Construct an instance for a transitional content pack. + /// The unique mod ID. + /// The mod name. + /// The mod author's name. + /// A brief description of the mod. + /// The mod version. + /// The name of the DLL in the directory that has the method. Mutually exclusive with . + /// The minimum SMAPI version required by this mod, if any. + /// The modID which will read this as a content pack. Mutually exclusive with . + /// The other mods that must be loaded before this mod. + /// The namespaced mod IDs to query for updates (like Nexus:541). + /// Any manifest fields which didn't match a valid field. + public Manifest(string uniqueID, string name, string author, string description, ISemanticVersion version, string entryDll = null, ISemanticVersion minimumApiVersion = null, IManifestContentPackFor contentPackFor = null, IManifestDependency[] dependencies = null, string[] updateKeys = null, IDictionary extraFields = null) + { + this.Name = name; + this.Author = author; + this.Description = description; + this.Version = version; + this.UniqueID = uniqueID; + this.UpdateKeys = new string[0]; + this.EntryDll = entryDll; + this.ContentPackFor = contentPackFor; + this.MinimumApiVersion = minimumApiVersion; + this.Dependencies = dependencies ?? new IManifestDependency[0]; + this.UpdateKeys = updateKeys ?? new string[0]; + this.ExtraFields = extraFields; + } } } diff --git a/src/SMAPI/Framework/Models/ManifestContentPackFor.cs b/src/SMAPI/Framework/Models/ManifestContentPackFor.cs index 7836bbcc..cdad8893 100644 --- a/src/SMAPI/Framework/Models/ManifestContentPackFor.cs +++ b/src/SMAPI/Framework/Models/ManifestContentPackFor.cs @@ -7,9 +7,30 @@ namespace StardewModdingAPI.Framework.Models ** Accessors *********/ /// The unique ID of the mod which can read this content pack. - public string UniqueID { get; set; } + public string UniqueID { get; } /// The minimum required version (if any). - public ISemanticVersion MinimumVersion { get; set; } + public ISemanticVersion MinimumVersion { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The toolkit instance. + public ManifestContentPackFor(Toolkit.Serialisation.Models.ManifestContentPackFor contentPackFor) + { + this.UniqueID = contentPackFor.UniqueID; + this.MinimumVersion = new SemanticVersion(contentPackFor.MinimumVersion); + } + + /// Construct an instance. + /// The unique ID of the mod which can read this content pack. + /// The minimum required version (if any). + public ManifestContentPackFor(string uniqueID, ISemanticVersion minimumVersion = null) + { + this.UniqueID = uniqueID; + this.MinimumVersion = minimumVersion; + } } } diff --git a/src/SMAPI/Framework/Models/ManifestDependency.cs b/src/SMAPI/Framework/Models/ManifestDependency.cs index 97f0775a..e92597f3 100644 --- a/src/SMAPI/Framework/Models/ManifestDependency.cs +++ b/src/SMAPI/Framework/Models/ManifestDependency.cs @@ -7,18 +7,23 @@ namespace StardewModdingAPI.Framework.Models ** Accessors *********/ /// The unique mod ID to require. - public string UniqueID { get; set; } + public string UniqueID { get; } /// The minimum required version (if any). - public ISemanticVersion MinimumVersion { get; set; } + public ISemanticVersion MinimumVersion { get; } /// Whether the dependency must be installed to use the mod. - public bool IsRequired { get; set; } + public bool IsRequired { get; } /********* ** Public methods *********/ + /// Construct an instance. + /// The toolkit instance. + public ManifestDependency(Toolkit.Serialisation.Models.ManifestDependency dependency) + : this(dependency.UniqueID, dependency.MinimumVersion?.ToString(), dependency.IsRequired) { } + /// Construct an instance. /// The unique mod ID to require. /// The minimum required version (if any). diff --git a/src/SMAPI/Framework/Serialisation/ColorConverter.cs b/src/SMAPI/Framework/Serialisation/ColorConverter.cs new file mode 100644 index 00000000..c27065bf --- /dev/null +++ b/src/SMAPI/Framework/Serialisation/ColorConverter.cs @@ -0,0 +1,47 @@ +using System; +using Microsoft.Xna.Framework; +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation.Converters; + +namespace StardewModdingAPI.Framework.Serialisation +{ + /// Handles deserialisation of for crossplatform compatibility. + /// + /// - Linux/Mac format: { "B": 76, "G": 51, "R": 25, "A": 102 } + /// - Windows format: "26, 51, 76, 102" + /// + internal class ColorConverter : SimpleReadOnlyConverter + { + /********* + ** Protected methods + *********/ + /// Read a JSON object. + /// The JSON object to read. + /// The path to the current JSON node. + protected override Color ReadObject(JObject obj, string path) + { + int r = obj.ValueIgnoreCase(nameof(Color.R)); + int g = obj.ValueIgnoreCase(nameof(Color.G)); + int b = obj.ValueIgnoreCase(nameof(Color.B)); + int a = obj.ValueIgnoreCase(nameof(Color.A)); + return new Color(r, g, b, a); + } + + /// Read a JSON string. + /// The JSON string value. + /// The path to the current JSON node. + protected override Color ReadString(string str, string path) + { + string[] parts = str.Split(','); + if (parts.Length != 4) + throw new SParseException($"Can't parse {typeof(Color).Name} from invalid value '{str}' (path: {path})."); + + int r = Convert.ToInt32(parts[0]); + int g = Convert.ToInt32(parts[1]); + int b = Convert.ToInt32(parts[2]); + int a = Convert.ToInt32(parts[3]); + return new Color(r, g, b, a); + } + } +} diff --git a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/ColorConverter.cs b/src/SMAPI/Framework/Serialisation/CrossplatformConverters/ColorConverter.cs deleted file mode 100644 index f1b2f04f..00000000 --- a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/ColorConverter.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using Microsoft.Xna.Framework; -using Newtonsoft.Json.Linq; -using StardewModdingAPI.Framework.Exceptions; - -namespace StardewModdingAPI.Framework.Serialisation.CrossplatformConverters -{ - /// Handles deserialisation of for crossplatform compatibility. - /// - /// - Linux/Mac format: { "B": 76, "G": 51, "R": 25, "A": 102 } - /// - Windows format: "26, 51, 76, 102" - /// - internal class ColorConverter : SimpleReadOnlyConverter - { - /********* - ** Protected methods - *********/ - /// Read a JSON object. - /// The JSON object to read. - /// The path to the current JSON node. - protected override Color ReadObject(JObject obj, string path) - { - int r = obj.ValueIgnoreCase(nameof(Color.R)); - int g = obj.ValueIgnoreCase(nameof(Color.G)); - int b = obj.ValueIgnoreCase(nameof(Color.B)); - int a = obj.ValueIgnoreCase(nameof(Color.A)); - return new Color(r, g, b, a); - } - - /// Read a JSON string. - /// The JSON string value. - /// The path to the current JSON node. - protected override Color ReadString(string str, string path) - { - string[] parts = str.Split(','); - if (parts.Length != 4) - throw new SParseException($"Can't parse {typeof(Color).Name} from invalid value '{str}' (path: {path})."); - - int r = Convert.ToInt32(parts[0]); - int g = Convert.ToInt32(parts[1]); - int b = Convert.ToInt32(parts[2]); - int a = Convert.ToInt32(parts[3]); - return new Color(r, g, b, a); - } - } -} diff --git a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/PointConverter.cs b/src/SMAPI/Framework/Serialisation/CrossplatformConverters/PointConverter.cs deleted file mode 100644 index 434b7ea5..00000000 --- a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/PointConverter.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using Microsoft.Xna.Framework; -using Newtonsoft.Json.Linq; -using StardewModdingAPI.Framework.Exceptions; - -namespace StardewModdingAPI.Framework.Serialisation.CrossplatformConverters -{ - /// Handles deserialisation of for crossplatform compatibility. - /// - /// - Linux/Mac format: { "X": 1, "Y": 2 } - /// - Windows format: "1, 2" - /// - internal class PointConverter : SimpleReadOnlyConverter - { - /********* - ** Protected methods - *********/ - /// Read a JSON object. - /// The JSON object to read. - /// The path to the current JSON node. - protected override Point ReadObject(JObject obj, string path) - { - int x = obj.ValueIgnoreCase(nameof(Point.X)); - int y = obj.ValueIgnoreCase(nameof(Point.Y)); - return new Point(x, y); - } - - /// Read a JSON string. - /// The JSON string value. - /// The path to the current JSON node. - protected override Point ReadString(string str, string path) - { - string[] parts = str.Split(','); - if (parts.Length != 2) - throw new SParseException($"Can't parse {typeof(Point).Name} from invalid value '{str}' (path: {path})."); - - int x = Convert.ToInt32(parts[0]); - int y = Convert.ToInt32(parts[1]); - return new Point(x, y); - } - } -} diff --git a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/RectangleConverter.cs b/src/SMAPI/Framework/Serialisation/CrossplatformConverters/RectangleConverter.cs deleted file mode 100644 index 62bc8637..00000000 --- a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/RectangleConverter.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Text.RegularExpressions; -using Microsoft.Xna.Framework; -using Newtonsoft.Json.Linq; -using StardewModdingAPI.Framework.Exceptions; - -namespace StardewModdingAPI.Framework.Serialisation.CrossplatformConverters -{ - /// Handles deserialisation of for crossplatform compatibility. - /// - /// - Linux/Mac format: { "X": 1, "Y": 2, "Width": 3, "Height": 4 } - /// - Windows format: "{X:1 Y:2 Width:3 Height:4}" - /// - internal class RectangleConverter : SimpleReadOnlyConverter - { - /********* - ** Protected methods - *********/ - /// Read a JSON object. - /// The JSON object to read. - /// The path to the current JSON node. - protected override Rectangle ReadObject(JObject obj, string path) - { - int x = obj.ValueIgnoreCase(nameof(Rectangle.X)); - int y = obj.ValueIgnoreCase(nameof(Rectangle.Y)); - int width = obj.ValueIgnoreCase(nameof(Rectangle.Width)); - int height = obj.ValueIgnoreCase(nameof(Rectangle.Height)); - return new Rectangle(x, y, width, height); - } - - /// Read a JSON string. - /// The JSON string value. - /// The path to the current JSON node. - protected override Rectangle ReadString(string str, string path) - { - if (string.IsNullOrWhiteSpace(str)) - return Rectangle.Empty; - - var match = Regex.Match(str, @"^\{X:(?\d+) Y:(?\d+) Width:(?\d+) Height:(?\d+)\}$", RegexOptions.IgnoreCase); - if (!match.Success) - throw new SParseException($"Can't parse {typeof(Rectangle).Name} from invalid value '{str}' (path: {path})."); - - int x = Convert.ToInt32(match.Groups["x"].Value); - int y = Convert.ToInt32(match.Groups["y"].Value); - int width = Convert.ToInt32(match.Groups["width"].Value); - int height = Convert.ToInt32(match.Groups["height"].Value); - - return new Rectangle(x, y, width, height); - } - } -} diff --git a/src/SMAPI/Framework/Serialisation/JsonHelper.cs b/src/SMAPI/Framework/Serialisation/JsonHelper.cs deleted file mode 100644 index 6cba343e..00000000 --- a/src/SMAPI/Framework/Serialisation/JsonHelper.cs +++ /dev/null @@ -1,139 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Microsoft.Xna.Framework.Input; -using Newtonsoft.Json; -using StardewModdingAPI.Framework.Serialisation.CrossplatformConverters; -using StardewModdingAPI.Framework.Serialisation.SmapiConverters; - -namespace StardewModdingAPI.Framework.Serialisation -{ - /// Encapsulates SMAPI's JSON file parsing. - internal class JsonHelper - { - /********* - ** Accessors - *********/ - /// The JSON settings to use when serialising and deserialising files. - private readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings - { - Formatting = Formatting.Indented, - ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection values are duplicated each time the config is loaded - Converters = new List - { - // SMAPI types - new SemanticVersionConverter(), - - // enums - new StringEnumConverter(), - new StringEnumConverter(), - new StringEnumConverter(), - - // crossplatform compatibility - new ColorConverter(), - new PointConverter(), - new RectangleConverter() - } - }; - - - /********* - ** Public methods - *********/ - /// Read a JSON file. - /// The model type. - /// The absolete file path. - /// Returns the deserialised model, or null if the file doesn't exist or is empty. - /// The given path is empty or invalid. - public TModel ReadJsonFile(string fullPath) - where TModel : class - { - // validate - if (string.IsNullOrWhiteSpace(fullPath)) - throw new ArgumentException("The file path is empty or invalid.", nameof(fullPath)); - - // read file - string json; - try - { - json = File.ReadAllText(fullPath); - } - catch (Exception ex) when (ex is DirectoryNotFoundException || ex is FileNotFoundException) - { - return null; - } - - // deserialise model - try - { - return this.Deserialise(json); - } - catch (Exception ex) - { - string error = $"Can't parse JSON file at {fullPath}."; - - if (ex is JsonReaderException) - { - error += " This doesn't seem to be valid JSON."; - if (json.Contains("“") || json.Contains("”")) - error += " Found curly quotes in the text; note that only straight quotes are allowed in JSON."; - } - error += $"\nTechnical details: {ex.Message}"; - throw new JsonReaderException(error); - } - } - - /// Save to a JSON file. - /// The model type. - /// The absolete file path. - /// The model to save. - /// The given path is empty or invalid. - public void WriteJsonFile(string fullPath, TModel model) - where TModel : class - { - // validate - if (string.IsNullOrWhiteSpace(fullPath)) - throw new ArgumentException("The file path is empty or invalid.", nameof(fullPath)); - - // create directory if needed - string dir = Path.GetDirectoryName(fullPath); - if (dir == null) - throw new ArgumentException("The file path is invalid.", nameof(fullPath)); - if (!Directory.Exists(dir)) - Directory.CreateDirectory(dir); - - // write file - string json = JsonConvert.SerializeObject(model, this.JsonSettings); - File.WriteAllText(fullPath, json); - } - - - /********* - ** Private methods - *********/ - /// Deserialize JSON text if possible. - /// The model type. - /// The raw JSON text. - private TModel Deserialise(string json) - { - try - { - return JsonConvert.DeserializeObject(json, this.JsonSettings); - } - catch (JsonReaderException) - { - // try replacing curly quotes - if (json.Contains("“") || json.Contains("”")) - { - try - { - return JsonConvert.DeserializeObject(json.Replace('“', '"').Replace('”', '"'), this.JsonSettings); - } - catch { /* rethrow original error */ } - } - - throw; - } - } - } -} diff --git a/src/SMAPI/Framework/Serialisation/PointConverter.cs b/src/SMAPI/Framework/Serialisation/PointConverter.cs new file mode 100644 index 00000000..fbc857d2 --- /dev/null +++ b/src/SMAPI/Framework/Serialisation/PointConverter.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.Xna.Framework; +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation.Converters; + +namespace StardewModdingAPI.Framework.Serialisation +{ + /// Handles deserialisation of for crossplatform compatibility. + /// + /// - Linux/Mac format: { "X": 1, "Y": 2 } + /// - Windows format: "1, 2" + /// + internal class PointConverter : SimpleReadOnlyConverter + { + /********* + ** Protected methods + *********/ + /// Read a JSON object. + /// The JSON object to read. + /// The path to the current JSON node. + protected override Point ReadObject(JObject obj, string path) + { + int x = obj.ValueIgnoreCase(nameof(Point.X)); + int y = obj.ValueIgnoreCase(nameof(Point.Y)); + return new Point(x, y); + } + + /// Read a JSON string. + /// The JSON string value. + /// The path to the current JSON node. + protected override Point ReadString(string str, string path) + { + string[] parts = str.Split(','); + if (parts.Length != 2) + throw new SParseException($"Can't parse {typeof(Point).Name} from invalid value '{str}' (path: {path})."); + + int x = Convert.ToInt32(parts[0]); + int y = Convert.ToInt32(parts[1]); + return new Point(x, y); + } + } +} diff --git a/src/SMAPI/Framework/Serialisation/RectangleConverter.cs b/src/SMAPI/Framework/Serialisation/RectangleConverter.cs new file mode 100644 index 00000000..4f55cc32 --- /dev/null +++ b/src/SMAPI/Framework/Serialisation/RectangleConverter.cs @@ -0,0 +1,52 @@ +using System; +using System.Text.RegularExpressions; +using Microsoft.Xna.Framework; +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation.Converters; + +namespace StardewModdingAPI.Framework.Serialisation +{ + /// Handles deserialisation of for crossplatform compatibility. + /// + /// - Linux/Mac format: { "X": 1, "Y": 2, "Width": 3, "Height": 4 } + /// - Windows format: "{X:1 Y:2 Width:3 Height:4}" + /// + internal class RectangleConverter : SimpleReadOnlyConverter + { + /********* + ** Protected methods + *********/ + /// Read a JSON object. + /// The JSON object to read. + /// The path to the current JSON node. + protected override Rectangle ReadObject(JObject obj, string path) + { + int x = obj.ValueIgnoreCase(nameof(Rectangle.X)); + int y = obj.ValueIgnoreCase(nameof(Rectangle.Y)); + int width = obj.ValueIgnoreCase(nameof(Rectangle.Width)); + int height = obj.ValueIgnoreCase(nameof(Rectangle.Height)); + return new Rectangle(x, y, width, height); + } + + /// Read a JSON string. + /// The JSON string value. + /// The path to the current JSON node. + protected override Rectangle ReadString(string str, string path) + { + if (string.IsNullOrWhiteSpace(str)) + return Rectangle.Empty; + + var match = Regex.Match(str, @"^\{X:(?\d+) Y:(?\d+) Width:(?\d+) Height:(?\d+)\}$", RegexOptions.IgnoreCase); + if (!match.Success) + throw new SParseException($"Can't parse {typeof(Rectangle).Name} from invalid value '{str}' (path: {path})."); + + int x = Convert.ToInt32(match.Groups["x"].Value); + int y = Convert.ToInt32(match.Groups["y"].Value); + int width = Convert.ToInt32(match.Groups["width"].Value); + int height = Convert.ToInt32(match.Groups["height"].Value); + + return new Rectangle(x, y, width, height); + } + } +} diff --git a/src/SMAPI/Framework/Serialisation/SimpleReadOnlyConverter.cs b/src/SMAPI/Framework/Serialisation/SimpleReadOnlyConverter.cs deleted file mode 100644 index 5765ad96..00000000 --- a/src/SMAPI/Framework/Serialisation/SimpleReadOnlyConverter.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using StardewModdingAPI.Framework.Exceptions; - -namespace StardewModdingAPI.Framework.Serialisation -{ - /// The base implementation for simplified converters which deserialise without overriding serialisation. - /// The type to deserialise. - internal abstract class SimpleReadOnlyConverter : JsonConverter - { - /********* - ** Accessors - *********/ - /// Whether this converter can write JSON. - public override bool CanWrite => false; - - - /********* - ** Public methods - *********/ - /// Get whether this instance can convert the specified object type. - /// The object type. - public override bool CanConvert(Type objectType) - { - return objectType == typeof(T); - } - - /// Writes the JSON representation of the object. - /// The JSON writer. - /// The value. - /// The calling serializer. - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - throw new InvalidOperationException("This converter does not write JSON."); - } - - /// Reads the JSON representation of the object. - /// The JSON reader. - /// The object type. - /// The object being read. - /// The calling serializer. - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - string path = reader.Path; - switch (reader.TokenType) - { - case JsonToken.StartObject: - return this.ReadObject(JObject.Load(reader), path); - case JsonToken.String: - return this.ReadString(JToken.Load(reader).Value(), path); - default: - throw new SParseException($"Can't parse {typeof(T).Name} from {reader.TokenType} node (path: {reader.Path})."); - } - } - - - /********* - ** Protected methods - *********/ - /// Read a JSON object. - /// The JSON object to read. - /// The path to the current JSON node. - protected virtual T ReadObject(JObject obj, string path) - { - throw new SParseException($"Can't parse {typeof(T).Name} from object node (path: {path})."); - } - - /// Read a JSON string. - /// The JSON string value. - /// The path to the current JSON node. - protected virtual T ReadString(string str, string path) - { - throw new SParseException($"Can't parse {typeof(T).Name} from string node (path: {path})."); - } - } -} diff --git a/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestContentPackForConverter.cs b/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestContentPackForConverter.cs deleted file mode 100644 index af7558f6..00000000 --- a/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestContentPackForConverter.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using Newtonsoft.Json; -using StardewModdingAPI.Framework.Models; - -namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters -{ - /// Handles deserialisation of arrays. - internal class ManifestContentPackForConverter : JsonConverter - { - /********* - ** Accessors - *********/ - /// Whether this converter can write JSON. - public override bool CanWrite => false; - - - /********* - ** Public methods - *********/ - /// Get whether this instance can convert the specified object type. - /// The object type. - public override bool CanConvert(Type objectType) - { - return objectType == typeof(IManifestContentPackFor[]); - } - - - /********* - ** Protected methods - *********/ - /// Read the JSON representation of the object. - /// The JSON reader. - /// The object type. - /// The object being read. - /// The calling serializer. - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - return serializer.Deserialize(reader); - } - - /// Writes the JSON representation of the object. - /// The JSON writer. - /// The value. - /// The calling serializer. - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - throw new InvalidOperationException("This converter does not write JSON."); - } - } -} diff --git a/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestDependencyArrayConverter.cs b/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestDependencyArrayConverter.cs deleted file mode 100644 index 4150d5fb..00000000 --- a/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestDependencyArrayConverter.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using StardewModdingAPI.Framework.Models; - -namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters -{ - /// Handles deserialisation of arrays. - internal class ManifestDependencyArrayConverter : JsonConverter - { - /********* - ** Accessors - *********/ - /// Whether this converter can write JSON. - public override bool CanWrite => false; - - - /********* - ** Public methods - *********/ - /// Get whether this instance can convert the specified object type. - /// The object type. - public override bool CanConvert(Type objectType) - { - return objectType == typeof(IManifestDependency[]); - } - - - /********* - ** Protected methods - *********/ - /// Read the JSON representation of the object. - /// The JSON reader. - /// The object type. - /// The object being read. - /// The calling serializer. - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - List result = new List(); - foreach (JObject obj in JArray.Load(reader).Children()) - { - string uniqueID = obj.ValueIgnoreCase(nameof(IManifestDependency.UniqueID)); - string minVersion = obj.ValueIgnoreCase(nameof(IManifestDependency.MinimumVersion)); - bool required = obj.ValueIgnoreCase(nameof(IManifestDependency.IsRequired)) ?? true; - result.Add(new ManifestDependency(uniqueID, minVersion, required)); - } - return result.ToArray(); - } - - /// Writes the JSON representation of the object. - /// The JSON writer. - /// The value. - /// The calling serializer. - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - throw new InvalidOperationException("This converter does not write JSON."); - } - } -} diff --git a/src/SMAPI/Framework/Serialisation/SmapiConverters/SemanticVersionConverter.cs b/src/SMAPI/Framework/Serialisation/SmapiConverters/SemanticVersionConverter.cs deleted file mode 100644 index 7ee7e29b..00000000 --- a/src/SMAPI/Framework/Serialisation/SmapiConverters/SemanticVersionConverter.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Newtonsoft.Json.Linq; -using StardewModdingAPI.Framework.Exceptions; - -namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters -{ - /// Handles deserialisation of . - internal class SemanticVersionConverter : SimpleReadOnlyConverter - { - /********* - ** Protected methods - *********/ - /// Read a JSON object. - /// The JSON object to read. - /// The path to the current JSON node. - protected override ISemanticVersion ReadObject(JObject obj, string path) - { - int major = obj.ValueIgnoreCase(nameof(ISemanticVersion.MajorVersion)); - int minor = obj.ValueIgnoreCase(nameof(ISemanticVersion.MinorVersion)); - int patch = obj.ValueIgnoreCase(nameof(ISemanticVersion.PatchVersion)); - string build = obj.ValueIgnoreCase(nameof(ISemanticVersion.Build)); - return new LegacyManifestVersion(major, minor, patch, build); - } - - /// Read a JSON string. - /// The JSON string value. - /// The path to the current JSON node. - protected override ISemanticVersion ReadString(string str, string path) - { - if (string.IsNullOrWhiteSpace(str)) - return null; - if (!SemanticVersion.TryParse(str, out ISemanticVersion version)) - throw new SParseException($"Can't parse semantic version from invalid value '{str}', should be formatted like 1.2, 1.2.30, or 1.2.30-beta (path: {path})."); - return version; - } - } -} diff --git a/src/SMAPI/Framework/Serialisation/SmapiConverters/StringEnumConverter.cs b/src/SMAPI/Framework/Serialisation/SmapiConverters/StringEnumConverter.cs deleted file mode 100644 index c88ac834..00000000 --- a/src/SMAPI/Framework/Serialisation/SmapiConverters/StringEnumConverter.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using Newtonsoft.Json.Converters; - -namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters -{ - /// A variant of which only converts a specified enum. - /// The enum type. - internal class StringEnumConverter : StringEnumConverter - { - /********* - ** Public methods - *********/ - /// Get whether this instance can convert the specified object type. - /// The object type. - public override bool CanConvert(Type type) - { - return - base.CanConvert(type) - && (Nullable.GetUnderlyingType(type) ?? type) == typeof(T); - } - } -} diff --git a/src/SMAPI/IManifest.cs b/src/SMAPI/IManifest.cs index 183ac105..6c07d374 100644 --- a/src/SMAPI/IManifest.cs +++ b/src/SMAPI/IManifest.cs @@ -36,7 +36,7 @@ namespace StardewModdingAPI IManifestDependency[] Dependencies { get; } /// The namespaced mod IDs to query for updates (like Nexus:541). - string[] UpdateKeys { get; set; } + string[] UpdateKeys { get; } /// Any manifest fields which didn't match a valid field. IDictionary ExtraFields { get; } diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 5c5137ef..e55a96b2 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -10,6 +10,7 @@ using System.Runtime.ExceptionServices; using System.Security; using System.Text.RegularExpressions; using System.Threading; +using Microsoft.Xna.Framework.Input; #if SMAPI_FOR_WINDOWS using System.Windows.Forms; #endif @@ -27,8 +28,11 @@ using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; using StardewModdingAPI.Internal; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; +using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation.Converters; using StardewModdingAPI.Toolkit.Utilities; using StardewValley; +using Keys = System.Windows.Forms.Keys; using Monitor = StardewModdingAPI.Framework.Monitor; using SObject = StardewValley.Object; using ThreadState = System.Threading.ThreadState; @@ -148,6 +152,18 @@ namespace StardewModdingAPI }; this.EventManager = new EventManager(this.Monitor, this.ModRegistry); + // init JSON parser + JsonConverter[] converters = { + new StringEnumConverter(), + new StringEnumConverter(), + new StringEnumConverter(), + new ColorConverter(), + new PointConverter(), + new RectangleConverter() + }; + foreach (JsonConverter converter in converters) + this.JsonHelper.JsonSettings.Converters.Add(converter); + // hook up events ContentEvents.Init(this.EventManager); ControlEvents.Init(this.EventManager); @@ -1093,7 +1109,7 @@ namespace StardewModdingAPI /// The mods for which to reload translations. private void ReloadTranslations(IEnumerable mods) { - JsonHelper jsonHelper = new JsonHelper(); + JsonHelper jsonHelper = this.JsonHelper; foreach (IModMetadata metadata in mods) { if (metadata.IsContentPack) diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index a5ccc62d..3c953ec5 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -103,6 +103,9 @@ + + + @@ -114,16 +117,17 @@ + + + - - @@ -151,13 +155,6 @@ - - - - - - - @@ -235,16 +232,12 @@ - - - - @@ -283,7 +276,6 @@ - diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/ManifestContentPackForConverter.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/ManifestContentPackForConverter.cs new file mode 100644 index 00000000..232c22a7 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/ManifestContentPackForConverter.cs @@ -0,0 +1,50 @@ +using System; +using Newtonsoft.Json; +using StardewModdingAPI.Toolkit.Serialisation.Models; + +namespace StardewModdingAPI.Toolkit.Serialisation.Converters +{ + /// Handles deserialisation of arrays. + public class ManifestContentPackForConverter : JsonConverter + { + /********* + ** Accessors + *********/ + /// Whether this converter can write JSON. + public override bool CanWrite => false; + + + /********* + ** Public methods + *********/ + /// Get whether this instance can convert the specified object type. + /// The object type. + public override bool CanConvert(Type objectType) + { + return objectType == typeof(ManifestContentPackFor[]); + } + + + /********* + ** Protected methods + *********/ + /// Read the JSON representation of the object. + /// The JSON reader. + /// The object type. + /// The object being read. + /// The calling serializer. + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return serializer.Deserialize(reader); + } + + /// Writes the JSON representation of the object. + /// The JSON writer. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new InvalidOperationException("This converter does not write JSON."); + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/ManifestDependencyArrayConverter.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/ManifestDependencyArrayConverter.cs new file mode 100644 index 00000000..0a304ee3 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/ManifestDependencyArrayConverter.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Toolkit.Serialisation.Models; + +namespace StardewModdingAPI.Toolkit.Serialisation.Converters +{ + /// Handles deserialisation of arrays. + internal class ManifestDependencyArrayConverter : JsonConverter + { + /********* + ** Accessors + *********/ + /// Whether this converter can write JSON. + public override bool CanWrite => false; + + + /********* + ** Public methods + *********/ + /// Get whether this instance can convert the specified object type. + /// The object type. + public override bool CanConvert(Type objectType) + { + return objectType == typeof(ManifestDependency[]); + } + + + /********* + ** Protected methods + *********/ + /// Read the JSON representation of the object. + /// The JSON reader. + /// The object type. + /// The object being read. + /// The calling serializer. + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + List result = new List(); + foreach (JObject obj in JArray.Load(reader).Children()) + { + string uniqueID = obj.ValueIgnoreCase(nameof(ManifestDependency.UniqueID)); + string minVersion = obj.ValueIgnoreCase(nameof(ManifestDependency.MinimumVersion)); + bool required = obj.ValueIgnoreCase(nameof(ManifestDependency.IsRequired)) ?? true; + result.Add(new ManifestDependency(uniqueID, minVersion, required)); + } + return result.ToArray(); + } + + /// Writes the JSON representation of the object. + /// The JSON writer. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new InvalidOperationException("This converter does not write JSON."); + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs new file mode 100644 index 00000000..2075d27e --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs @@ -0,0 +1,36 @@ +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Toolkit.Serialisation.Models; + +namespace StardewModdingAPI.Toolkit.Serialisation.Converters +{ + /// Handles deserialisation of . + internal class SemanticVersionConverter : SimpleReadOnlyConverter + { + /********* + ** Protected methods + *********/ + /// Read a JSON object. + /// The JSON object to read. + /// The path to the current JSON node. + protected override SemanticVersion ReadObject(JObject obj, string path) + { + int major = obj.ValueIgnoreCase("MajorVersion"); + int minor = obj.ValueIgnoreCase("MinorVersion"); + int patch = obj.ValueIgnoreCase("PatchVersion"); + string build = obj.ValueIgnoreCase("Build"); + return new LegacyManifestVersion(major, minor, patch, build); + } + + /// Read a JSON string. + /// The JSON string value. + /// The path to the current JSON node. + protected override SemanticVersion ReadString(string str, string path) + { + if (string.IsNullOrWhiteSpace(str)) + return null; + if (!SemanticVersion.TryParse(str, out ISemanticVersion version)) + throw new SParseException($"Can't parse semantic version from invalid value '{str}', should be formatted like 1.2, 1.2.30, or 1.2.30-beta (path: {path})."); + return (SemanticVersion)version; + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SimpleReadOnlyConverter.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SimpleReadOnlyConverter.cs new file mode 100644 index 00000000..5e0b0f4a --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SimpleReadOnlyConverter.cs @@ -0,0 +1,76 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace StardewModdingAPI.Toolkit.Serialisation.Converters +{ + /// The base implementation for simplified converters which deserialise without overriding serialisation. + /// The type to deserialise. + internal abstract class SimpleReadOnlyConverter : JsonConverter + { + /********* + ** Accessors + *********/ + /// Whether this converter can write JSON. + public override bool CanWrite => false; + + + /********* + ** Public methods + *********/ + /// Get whether this instance can convert the specified object type. + /// The object type. + public override bool CanConvert(Type objectType) + { + return objectType == typeof(T); + } + + /// Writes the JSON representation of the object. + /// The JSON writer. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new InvalidOperationException("This converter does not write JSON."); + } + + /// Reads the JSON representation of the object. + /// The JSON reader. + /// The object type. + /// The object being read. + /// The calling serializer. + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + string path = reader.Path; + switch (reader.TokenType) + { + case JsonToken.StartObject: + return this.ReadObject(JObject.Load(reader), path); + case JsonToken.String: + return this.ReadString(JToken.Load(reader).Value(), path); + default: + throw new SParseException($"Can't parse {typeof(T).Name} from {reader.TokenType} node (path: {reader.Path})."); + } + } + + + /********* + ** Protected methods + *********/ + /// Read a JSON object. + /// The JSON object to read. + /// The path to the current JSON node. + protected virtual T ReadObject(JObject obj, string path) + { + throw new SParseException($"Can't parse {typeof(T).Name} from object node (path: {path})."); + } + + /// Read a JSON string. + /// The JSON string value. + /// The path to the current JSON node. + protected virtual T ReadString(string str, string path) + { + throw new SParseException($"Can't parse {typeof(T).Name} from string node (path: {path})."); + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/StringEnumConverter.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/StringEnumConverter.cs new file mode 100644 index 00000000..13e6e3a1 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/StringEnumConverter.cs @@ -0,0 +1,22 @@ +using System; +using Newtonsoft.Json.Converters; + +namespace StardewModdingAPI.Toolkit.Serialisation.Converters +{ + /// A variant of which only converts a specified enum. + /// The enum type. + internal class StringEnumConverter : StringEnumConverter + { + /********* + ** Public methods + *********/ + /// Get whether this instance can convert the specified object type. + /// The object type. + public override bool CanConvert(Type type) + { + return + base.CanConvert(type) + && (Nullable.GetUnderlyingType(type) ?? type) == typeof(T); + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/InternalExtensions.cs b/src/StardewModdingAPI.Toolkit/Serialisation/InternalExtensions.cs new file mode 100644 index 00000000..12b2c933 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Serialisation/InternalExtensions.cs @@ -0,0 +1,21 @@ +using System; +using Newtonsoft.Json.Linq; + +namespace StardewModdingAPI.Toolkit.Serialisation +{ + /// Provides extension methods for parsing JSON. + public static class JsonExtensions + { + /// Get a JSON field value from a case-insensitive field name. This will check for an exact match first, then search without case sensitivity. + /// The value type. + /// The JSON object to search. + /// The field name. + public static T ValueIgnoreCase(this JObject obj, string fieldName) + { + JToken token = obj.GetValue(fieldName, StringComparison.InvariantCultureIgnoreCase); + return token != null + ? token.Value() + : default(T); + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/JsonHelper.cs b/src/StardewModdingAPI.Toolkit/Serialisation/JsonHelper.cs new file mode 100644 index 00000000..00f334ad --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Serialisation/JsonHelper.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Newtonsoft.Json; +using StardewModdingAPI.Toolkit.Serialisation.Converters; + +namespace StardewModdingAPI.Toolkit.Serialisation +{ + /// Encapsulates SMAPI's JSON file parsing. + public class JsonHelper + { + /********* + ** Accessors + *********/ + /// The JSON settings to use when serialising and deserialising files. + public JsonSerializerSettings JsonSettings { get; } = new JsonSerializerSettings + { + Formatting = Formatting.Indented, + ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection values are duplicated each time the config is loaded + Converters = new List { new SemanticVersionConverter() } + }; + + + /********* + ** Public methods + *********/ + /// Read a JSON file. + /// The model type. + /// The absolete file path. + /// Returns the deserialised model, or null if the file doesn't exist or is empty. + /// The given path is empty or invalid. + public TModel ReadJsonFile(string fullPath) + where TModel : class + { + // validate + if (string.IsNullOrWhiteSpace(fullPath)) + throw new ArgumentException("The file path is empty or invalid.", nameof(fullPath)); + + // read file + string json; + try + { + json = File.ReadAllText(fullPath); + } + catch (Exception ex) when (ex is DirectoryNotFoundException || ex is FileNotFoundException) + { + return null; + } + + // deserialise model + try + { + return this.Deserialise(json); + } + catch (Exception ex) + { + string error = $"Can't parse JSON file at {fullPath}."; + + if (ex is JsonReaderException) + { + error += " This doesn't seem to be valid JSON."; + if (json.Contains("“") || json.Contains("”")) + error += " Found curly quotes in the text; note that only straight quotes are allowed in JSON."; + } + error += $"\nTechnical details: {ex.Message}"; + throw new JsonReaderException(error); + } + } + + /// Save to a JSON file. + /// The model type. + /// The absolete file path. + /// The model to save. + /// The given path is empty or invalid. + public void WriteJsonFile(string fullPath, TModel model) + where TModel : class + { + // validate + if (string.IsNullOrWhiteSpace(fullPath)) + throw new ArgumentException("The file path is empty or invalid.", nameof(fullPath)); + + // create directory if needed + string dir = Path.GetDirectoryName(fullPath); + if (dir == null) + throw new ArgumentException("The file path is invalid.", nameof(fullPath)); + if (!Directory.Exists(dir)) + Directory.CreateDirectory(dir); + + // write file + string json = JsonConvert.SerializeObject(model, this.JsonSettings); + File.WriteAllText(fullPath, json); + } + + + /********* + ** Private methods + *********/ + /// Deserialize JSON text if possible. + /// The model type. + /// The raw JSON text. + private TModel Deserialise(string json) + { + try + { + return JsonConvert.DeserializeObject(json, this.JsonSettings); + } + catch (JsonReaderException) + { + // try replacing curly quotes + if (json.Contains("“") || json.Contains("”")) + { + try + { + return JsonConvert.DeserializeObject(json.Replace('“', '"').Replace('”', '"'), this.JsonSettings); + } + catch { /* rethrow original error */ } + } + + throw; + } + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Models/LegacyManifestVersion.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Models/LegacyManifestVersion.cs new file mode 100644 index 00000000..12f6755b --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Models/LegacyManifestVersion.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; + +namespace StardewModdingAPI.Toolkit.Serialisation.Models +{ + /// An implementation of that hamdles the legacy version format. + public class LegacyManifestVersion : SemanticVersion + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The major version incremented for major API changes. + /// The minor version incremented for backwards-compatible changes. + /// The patch version for backwards-compatible bug fixes. + /// An optional build tag. + [JsonConstructor] + public LegacyManifestVersion(int majorVersion, int minorVersion, int patchVersion, string build = null) + : base( + majorVersion, + minorVersion, + patchVersion, + build != "0" ? build : null // '0' from incorrect examples in old SMAPI documentation + ) + { } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs new file mode 100644 index 00000000..68987dd1 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using StardewModdingAPI.Toolkit.Serialisation.Converters; + +namespace StardewModdingAPI.Toolkit.Serialisation.Models +{ + /// A manifest which describes a mod for SMAPI. + public class Manifest + { + /********* + ** Accessors + *********/ + /// The mod name. + public string Name { get; set; } + + /// A brief description of the mod. + public string Description { get; set; } + + /// The mod author's name. + public string Author { get; set; } + + /// The mod version. + public SemanticVersion Version { get; set; } + + /// The minimum SMAPI version required by this mod, if any. + public SemanticVersion MinimumApiVersion { get; set; } + + /// The name of the DLL in the directory that has the Entry method. Mutually exclusive with . + public string EntryDll { get; set; } + + /// The mod which will read this as a content pack. Mutually exclusive with . + [JsonConverter(typeof(ManifestContentPackForConverter))] + public ManifestContentPackFor ContentPackFor { get; set; } + + /// The other mods that must be loaded before this mod. + [JsonConverter(typeof(ManifestDependencyArrayConverter))] + public ManifestDependency[] Dependencies { get; set; } + + /// The namespaced mod IDs to query for updates (like Nexus:541). + public string[] UpdateKeys { get; set; } + + /// The unique mod ID. + public string UniqueID { get; set; } + + /// Any manifest fields which didn't match a valid field. + [JsonExtensionData] + public IDictionary ExtraFields { get; set; } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs new file mode 100644 index 00000000..00546533 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Toolkit.Serialisation.Models +{ + /// Indicates which mod can read the content pack represented by the containing manifest. + public class ManifestContentPackFor + { + /********* + ** Accessors + *********/ + /// The unique ID of the mod which can read this content pack. + public string UniqueID { get; set; } + + /// The minimum required version (if any). + public SemanticVersion MinimumVersion { get; set; } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs new file mode 100644 index 00000000..d902f9ac --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs @@ -0,0 +1,35 @@ +namespace StardewModdingAPI.Toolkit.Serialisation.Models +{ + /// A mod dependency listed in a mod manifest. + public class ManifestDependency + { + /********* + ** Accessors + *********/ + /// The unique mod ID to require. + public string UniqueID { get; set; } + + /// The minimum required version (if any). + public SemanticVersion MinimumVersion { get; set; } + + /// Whether the dependency must be installed to use the mod. + public bool IsRequired { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique mod ID to require. + /// The minimum required version (if any). + /// Whether the dependency must be installed to use the mod. + public ManifestDependency(string uniqueID, string minimumVersion, bool required = true) + { + this.UniqueID = uniqueID; + this.MinimumVersion = !string.IsNullOrWhiteSpace(minimumVersion) + ? new SemanticVersion(minimumVersion) + : null; + this.IsRequired = required; + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/SParseException.cs b/src/StardewModdingAPI.Toolkit/Serialisation/SParseException.cs new file mode 100644 index 00000000..61a7b305 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Serialisation/SParseException.cs @@ -0,0 +1,17 @@ +using System; + +namespace StardewModdingAPI.Toolkit.Serialisation +{ + /// A format exception which provides a user-facing error message. + internal class SParseException : FormatException + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The error message. + /// The underlying exception, if any. + public SParseException(string message, Exception ex = null) + : base(message, ex) { } + } +} -- cgit From 6eba10948bf39d5e05505ec060f6920f84610d58 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 5 Jun 2018 23:03:26 -0400 Subject: fix version parsing issues in new toolkit code (#532) --- src/SMAPI.Tests/Utilities/SemanticVersionTests.cs | 17 --------- .../Framework/Models/ManifestContentPackFor.cs | 2 +- .../Serialisation/SemanticVersionConverter.cs | 40 ++++++++++++++++++++++ src/SMAPI/Program.cs | 3 +- src/SMAPI/StardewModdingAPI.csproj | 1 + .../Converters/SemanticVersionConverter.cs | 5 ++- .../Serialisation/Models/LegacyManifestVersion.cs | 26 -------------- 7 files changed, 48 insertions(+), 46 deletions(-) create mode 100644 src/SMAPI/Framework/Serialisation/SemanticVersionConverter.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/Models/LegacyManifestVersion.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs index b797393b..feab452a 100644 --- a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs +++ b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs @@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis; using Newtonsoft.Json; using NUnit.Framework; using StardewModdingAPI.Framework; -using StardewModdingAPI.Toolkit.Serialisation.Models; namespace StardewModdingAPI.Tests.Utilities { @@ -272,22 +271,6 @@ namespace StardewModdingAPI.Tests.Utilities Assert.IsTrue(version.IsOlderThan(new SemanticVersion("1.2.30")), "The game version should be considered older than the later semantic versions."); } - /**** - ** LegacyManifestVersion - ****/ - [Test(Description = "Assert that the LegacyManifestVersion subclass correctly parses legacy manifest versions.")] - [TestCase(1, 0, 0, null, ExpectedResult = "1.0")] - [TestCase(3000, 4000, 5000, null, ExpectedResult = "3000.4000.5000")] - [TestCase(1, 2, 3, "", ExpectedResult = "1.2.3")] - [TestCase(1, 2, 3, " ", ExpectedResult = "1.2.3")] - [TestCase(1, 2, 3, "0", ExpectedResult = "1.2.3")] // special case: drop '0' tag for legacy manifest versions - [TestCase(1, 2, 3, "some-tag.4", ExpectedResult = "1.2.3-some-tag.4")] - [TestCase(1, 2, 3, "some-tag.4 ", ExpectedResult = "1.2.3-some-tag.4")] - public string LegacyManifestVersion(int major, int minor, int patch, string tag) - { - return new LegacyManifestVersion(major, minor, patch, tag).ToString(); - } - /********* ** Private methods diff --git a/src/SMAPI/Framework/Models/ManifestContentPackFor.cs b/src/SMAPI/Framework/Models/ManifestContentPackFor.cs index cdad8893..90e20c6a 100644 --- a/src/SMAPI/Framework/Models/ManifestContentPackFor.cs +++ b/src/SMAPI/Framework/Models/ManifestContentPackFor.cs @@ -21,7 +21,7 @@ namespace StardewModdingAPI.Framework.Models public ManifestContentPackFor(Toolkit.Serialisation.Models.ManifestContentPackFor contentPackFor) { this.UniqueID = contentPackFor.UniqueID; - this.MinimumVersion = new SemanticVersion(contentPackFor.MinimumVersion); + this.MinimumVersion = contentPackFor.MinimumVersion != null ? new SemanticVersion(contentPackFor.MinimumVersion) : null; } /// Construct an instance. diff --git a/src/SMAPI/Framework/Serialisation/SemanticVersionConverter.cs b/src/SMAPI/Framework/Serialisation/SemanticVersionConverter.cs new file mode 100644 index 00000000..3e05a440 --- /dev/null +++ b/src/SMAPI/Framework/Serialisation/SemanticVersionConverter.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation.Converters; + +namespace StardewModdingAPI.Framework.Serialisation +{ + /// Handles deserialisation of . + internal class SemanticVersionConverter : SimpleReadOnlyConverter + { + /********* + ** Protected methods + *********/ + /// Read a JSON object. + /// The JSON object to read. + /// The path to the current JSON node. + protected override ISemanticVersion ReadObject(JObject obj, string path) + { + int major = obj.ValueIgnoreCase("MajorVersion"); + int minor = obj.ValueIgnoreCase("MinorVersion"); + int patch = obj.ValueIgnoreCase("PatchVersion"); + string build = obj.ValueIgnoreCase("Build"); + if (build == "0") + build = null; // '0' from incorrect examples in old SMAPI documentation + + return new SemanticVersion(major, minor, patch, build); + } + + /// Read a JSON string. + /// The JSON string value. + /// The path to the current JSON node. + protected override ISemanticVersion ReadString(string str, string path) + { + if (string.IsNullOrWhiteSpace(str)) + return null; + if (!SemanticVersion.TryParse(str, out ISemanticVersion version)) + throw new SParseException($"Can't parse semantic version from invalid value '{str}', should be formatted like 1.2, 1.2.30, or 1.2.30-beta (path: {path})."); + return version; + } + } +} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 05a3134e..cc59c0cd 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -159,7 +159,8 @@ namespace StardewModdingAPI new StringEnumConverter(), new ColorConverter(), new PointConverter(), - new RectangleConverter() + new RectangleConverter(), + new Framework.Serialisation.SemanticVersionConverter() }; foreach (JsonConverter converter in converters) this.JsonHelper.JsonSettings.Converters.Add(converter); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 3c953ec5..7eb5f95a 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -121,6 +121,7 @@ + diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs index 2075d27e..2ddaa1bf 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs @@ -18,7 +18,10 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Converters int minor = obj.ValueIgnoreCase("MinorVersion"); int patch = obj.ValueIgnoreCase("PatchVersion"); string build = obj.ValueIgnoreCase("Build"); - return new LegacyManifestVersion(major, minor, patch, build); + if (build == "0") + build = null; // '0' from incorrect examples in old SMAPI documentation + + return new SemanticVersion(major, minor, patch, build); } /// Read a JSON string. diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Models/LegacyManifestVersion.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Models/LegacyManifestVersion.cs deleted file mode 100644 index 12f6755b..00000000 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Models/LegacyManifestVersion.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Newtonsoft.Json; - -namespace StardewModdingAPI.Toolkit.Serialisation.Models -{ - /// An implementation of that hamdles the legacy version format. - public class LegacyManifestVersion : SemanticVersion - { - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The major version incremented for major API changes. - /// The minor version incremented for backwards-compatible changes. - /// The patch version for backwards-compatible bug fixes. - /// An optional build tag. - [JsonConstructor] - public LegacyManifestVersion(int majorVersion, int minorVersion, int patchVersion, string build = null) - : base( - majorVersion, - minorVersion, - patchVersion, - build != "0" ? build : null // '0' from incorrect examples in old SMAPI documentation - ) - { } - } -} -- cgit From 9bc53145150d9209fb5b13e82857afb6c155b7a5 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 6 Jun 2018 21:42:09 -0400 Subject: add Harmony DLL (#541) --- build/common.targets | 2 ++ build/prepare-install-package.targets | 4 ++++ src/SMAPI.Installer/InteractiveInstaller.cs | 2 ++ src/SMAPI/StardewModdingAPI.csproj | 4 ++++ src/lib/0Harmony.dll | Bin 0 -> 99840 bytes src/lib/0Harmony.pdb | Bin 0 -> 323072 bytes 6 files changed, 12 insertions(+) create mode 100644 src/lib/0Harmony.dll create mode 100644 src/lib/0Harmony.pdb (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/build/common.targets b/build/common.targets index 099d7cc9..0b8f278f 100644 --- a/build/common.targets +++ b/build/common.targets @@ -103,6 +103,8 @@ + + diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index 9a5cde3f..5e00d663 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -27,6 +27,8 @@ + + @@ -43,6 +45,8 @@ + + diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 6e4cb95d..412c82a5 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -82,6 +82,8 @@ namespace StardewModdingApi.Installer string GetInstallPath(string path) => Path.Combine(installDir.FullName, path); // common + yield return GetInstallPath("0Harmony.dll"); + yield return GetInstallPath("0Harmony.pdb"); yield return GetInstallPath("Mono.Cecil.dll"); yield return GetInstallPath("Newtonsoft.Json.dll"); yield return GetInstallPath("StardewModdingAPI.exe"); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 7eb5f95a..d2a65f48 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -53,6 +53,10 @@ icon.ico + + False + ..\lib\0Harmony.dll + ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll True diff --git a/src/lib/0Harmony.dll b/src/lib/0Harmony.dll new file mode 100644 index 00000000..63619429 Binary files /dev/null and b/src/lib/0Harmony.dll differ diff --git a/src/lib/0Harmony.pdb b/src/lib/0Harmony.pdb new file mode 100644 index 00000000..d7a4c67c Binary files /dev/null and b/src/lib/0Harmony.pdb differ -- cgit From cd62dcc8cfa35a9b423f4d15359ec3083b4c6d44 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 6 Jun 2018 21:45:03 -0400 Subject: add simple Harmony wrapper for validation, error-handling, etc (#541) --- src/SMAPI/Constants.cs | 3 ++ src/SMAPI/Framework/Patching/GamePatcher.cs | 45 +++++++++++++++++++++++++++ src/SMAPI/Framework/Patching/IHarmonyPatch.cs | 15 +++++++++ src/SMAPI/Program.cs | 14 ++++----- src/SMAPI/StardewModdingAPI.csproj | 2 ++ 5 files changed, 71 insertions(+), 8 deletions(-) create mode 100644 src/SMAPI/Framework/Patching/GamePatcher.cs create mode 100644 src/SMAPI/Framework/Patching/IHarmonyPatch.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 786c1a39..d96c5839 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -103,6 +103,9 @@ namespace StardewModdingAPI /// The target game platform. internal static Platform Platform { get; } = EnvironmentUtility.DetectPlatform(); + /// The game's assembly name. + internal static string GameAssemblyName => Constants.Platform == Platform.Windows ? "Stardew Valley" : "StardewValley"; + /********* ** Internal methods diff --git a/src/SMAPI/Framework/Patching/GamePatcher.cs b/src/SMAPI/Framework/Patching/GamePatcher.cs new file mode 100644 index 00000000..71ca8e55 --- /dev/null +++ b/src/SMAPI/Framework/Patching/GamePatcher.cs @@ -0,0 +1,45 @@ +using System; +using Harmony; + +namespace StardewModdingAPI.Framework.Patching +{ + /// Encapsulates applying Harmony patches to the game. + internal class GamePatcher + { + /********* + ** Properties + *********/ + /// Encapsulates monitoring and logging. + private readonly IMonitor Monitor; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Encapsulates monitoring and logging. + public GamePatcher(IMonitor monitor) + { + this.Monitor = monitor; + } + + /// Apply all loaded patches to the game. + /// The patches to apply. + public void Apply(params IHarmonyPatch[] patches) + { + HarmonyInstance harmony = HarmonyInstance.Create("io.smapi"); + foreach (IHarmonyPatch patch in patches) + { + try + { + patch.Apply(harmony); + } + catch (Exception ex) + { + this.Monitor.Log($"Couldn't apply runtime patch '{patch.Name}' to the game. Some SMAPI features may not work correctly. See log file for details.", LogLevel.Error); + this.Monitor.Log(ex.GetLogSummary(), LogLevel.Trace); + } + } + } + } +} diff --git a/src/SMAPI/Framework/Patching/IHarmonyPatch.cs b/src/SMAPI/Framework/Patching/IHarmonyPatch.cs new file mode 100644 index 00000000..cb42f40e --- /dev/null +++ b/src/SMAPI/Framework/Patching/IHarmonyPatch.cs @@ -0,0 +1,15 @@ +using Harmony; + +namespace StardewModdingAPI.Framework.Patching +{ + /// A Harmony patch to apply. + internal interface IHarmonyPatch + { + /// A unique name for this patch. + string Name { get; } + + /// Apply the Harmony patch. + /// The Harmony instance. + void Apply(HarmonyInstance harmony); + } +} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index cc59c0cd..bf673fe6 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -24,6 +24,7 @@ using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Framework.Patching; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; using StardewModdingAPI.Internal; @@ -152,6 +153,10 @@ namespace StardewModdingAPI }; this.EventManager = new EventManager(this.Monitor, this.ModRegistry); + // apply game patches + new GamePatcher(this.Monitor).Apply( + ); + // init JSON parser JsonConverter[] converters = { new StringEnumConverter(), @@ -359,14 +364,7 @@ namespace StardewModdingAPI Console.ResetColor(); Program.PressAnyKeyToExit(showMessage: true); } - - // get game assembly name - const string gameAssemblyName = -#if SMAPI_FOR_WINDOWS - "Stardew Valley"; -#else - "StardewValley"; -#endif + string gameAssemblyName = Constants.GameAssemblyName; // game not present if (Type.GetType($"StardewValley.Game1, {gameAssemblyName}", throwOnError: false) == null) diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index d2a65f48..125e7746 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -107,6 +107,8 @@ + + -- cgit From a2d8a1be23f2351384a562b64807d2ce9c83170e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 6 Jun 2018 21:48:23 -0400 Subject: add Harmony patch to fix custom tilesheet handling (#541) --- docs/release-notes.md | 2 + src/SMAPI/Framework/Patching/GameLocationPatch.cs | 60 +++++++++++++++++++++++ src/SMAPI/Program.cs | 1 + src/SMAPI/StardewModdingAPI.csproj | 1 + 4 files changed, 64 insertions(+) create mode 100644 src/SMAPI/Framework/Patching/GameLocationPatch.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/docs/release-notes.md b/docs/release-notes.md index 11a2a04c..e62f5e7c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -29,6 +29,8 @@ * Added `Context.IsMultiplayer` and `Context.IsMainPlayer` flags. * Added `Constants.TargetPlatform` which says whether the game is running on Linux, Mac, or Windows. * Added `semanticVersion.IsPrerelease()` method. + * Added Harmony DLL managed by SMAPI. + * Fixed error when loading an unpacked `.tbin` map that references custom seasonal tilesheets. * Fixed error if a mod loads a PNG while the game is loading (e.g. custom map tilesheets via `IAssetLoader`). * Fixed assets loaded by temporary content managers not being editable by mods. * Fixed assets not reloaded consistently when the player switches language. diff --git a/src/SMAPI/Framework/Patching/GameLocationPatch.cs b/src/SMAPI/Framework/Patching/GameLocationPatch.cs new file mode 100644 index 00000000..669c700a --- /dev/null +++ b/src/SMAPI/Framework/Patching/GameLocationPatch.cs @@ -0,0 +1,60 @@ +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Reflection; +using Harmony; +using StardewValley; +using xTile.Tiles; + +namespace StardewModdingAPI.Framework.Patching +{ + /// A Harmony patch for the method. + internal class GameLocationPatch : IHarmonyPatch + { + /********* + ** Accessors + *********/ + /// A unique name for this patch. + public string Name => $"{nameof(GameLocation)}.{nameof(GameLocation.updateSeasonalTileSheets)}"; + + + /********* + ** Public methods + *********/ + /// Apply the Harmony patch. + /// The Harmony instance. + public void Apply(HarmonyInstance harmony) + { + MethodInfo method = AccessTools.Method(typeof(GameLocation), nameof(GameLocation.updateSeasonalTileSheets)); + MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(GameLocationPatch.Prefix)); + + harmony.Patch(method, new HarmonyMethod(prefix), null); + } + + + /********* + ** Private methods + *********/ + /// An implementation of which correctly handles custom map tilesheets. + /// The location instance being patched. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument name is defined by Harmony.")] + private static bool Prefix(ref GameLocation __instance) + { + if (!__instance.IsOutdoors || __instance.Name.Equals("Desert")) + return false; + foreach (TileSheet tilesheet in __instance.Map.TileSheets) + { + string imageSource = tilesheet.ImageSource; + string imageFile = Path.GetFileName(imageSource); + if (imageFile.StartsWith("spring_") || imageFile.StartsWith("summer_") || imageFile.StartsWith("fall_") || imageFile.StartsWith("winter_")) + { + string imageDir = Path.GetDirectoryName(imageSource); + if (string.IsNullOrWhiteSpace(imageDir)) + imageDir = "Maps"; + tilesheet.ImageSource = Path.Combine(imageDir, Game1.currentSeason + "_" + imageFile.Split('_')[1]); + } + } + + return false; + } + } +} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index bf673fe6..85306641 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -155,6 +155,7 @@ namespace StardewModdingAPI // apply game patches new GamePatcher(this.Monitor).Apply( + new GameLocationPatch() ); // init JSON parser diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 125e7746..2df3e63f 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -285,6 +285,7 @@ + -- cgit From 248ba90b75df31a657978c17ad4c6dff26210c96 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 9 Jun 2018 19:46:12 -0400 Subject: add metadata dump option for troubleshooting --- docs/release-notes.md | 1 + src/SMAPI/Framework/Models/ModFolderExport.cs | 21 +++++++++++++++++++++ src/SMAPI/Framework/Models/SConfig.cs | 3 +++ src/SMAPI/Program.cs | 16 +++++++++++++++- src/SMAPI/StardewModdingAPI.config.json | 6 ++++++ src/SMAPI/StardewModdingAPI.csproj | 1 + 6 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 src/SMAPI/Framework/Models/ModFolderExport.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/docs/release-notes.md b/docs/release-notes.md index 85d9eec7..a343cff3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -72,6 +72,7 @@ * Added prototype SMAPI 3.0 events accessible via `helper.Events`. * Added prototype mod handler toolkit. * Added Harmony for SMAPI's internal use to patch game functions for events. + * Added metadata dump option in `StardewModdingAPI.config.json` for troubleshooting some cases. * Rewrote input suppression using new SDV 1.3 APIs. * Rewrote world/player state tracking: * much more efficient than previous method; diff --git a/src/SMAPI/Framework/Models/ModFolderExport.cs b/src/SMAPI/Framework/Models/ModFolderExport.cs new file mode 100644 index 00000000..3b8d451a --- /dev/null +++ b/src/SMAPI/Framework/Models/ModFolderExport.cs @@ -0,0 +1,21 @@ +namespace StardewModdingAPI.Framework.Models +{ + /// Metadata exported to the mod folder. + internal class ModFolderExport + { + /// When the export was generated. + public string Exported { get; set; } + + /// The absolute path of the mod folder. + public string ModFolderPath { get; set; } + + /// The game version which last loaded the mods. + public string GameVersion { get; set; } + + /// The SMAPI version which last loaded the mods. + public string ApiVersion { get; set; } + + /// The detected mods. + public IModMetadata[] Mods { get; set; } + } +} diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index 98614933..15671af4 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -26,6 +26,9 @@ namespace StardewModdingAPI.Framework.Models /// Whether SMAPI should log more information about the game context. public bool VerboseLogging { get; set; } + /// Whether to generate a file in the mods folder with detailed metadata about the detected mods. + public bool DumpMetadata { get; set; } + /// The console color scheme to use. public MonitorColorScheme ColorScheme { get; set; } diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 4ede45d5..350a6195 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -428,6 +428,20 @@ namespace StardewModdingAPI // load mods this.LoadMods(mods, this.JsonHelper, this.ContentCore, modDatabase); + // write metadata file + if (this.Settings.DumpMetadata) + { + ModFolderExport export = new ModFolderExport + { + Exported = DateTime.UtcNow.ToString("O"), + ApiVersion = Constants.ApiVersion.ToString(), + GameVersion = Constants.GameVersion.ToString(), + ModFolderPath = Constants.ModPath, + Mods = mods + }; + this.JsonHelper.WriteJsonFile(Path.Combine(Constants.LogDir, $"{Constants.LogNamePrefix}.metadata-dump.json"), export); + } + // check for updates this.CheckForUpdatesAsync(mods); } @@ -1276,7 +1290,7 @@ namespace StardewModdingAPI if (!logsDir.Exists) return; - foreach (FileInfo logFile in logsDir.EnumerateFiles("*.txt")) + foreach (FileInfo logFile in logsDir.EnumerateFiles()) { if (logFile.Name.StartsWith(Constants.LogNamePrefix, StringComparison.InvariantCultureIgnoreCase)) { diff --git a/src/SMAPI/StardewModdingAPI.config.json b/src/SMAPI/StardewModdingAPI.config.json index 7aac6e4c..115997ba 100644 --- a/src/SMAPI/StardewModdingAPI.config.json +++ b/src/SMAPI/StardewModdingAPI.config.json @@ -44,6 +44,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha */ "VerboseLogging": false, + /** + * Whether to generate a 'SMAPI-latest.metadata-dump.json' file in the logs folder with the full mod + * metadata for detected mods. This is only needed when troubleshooting some cases. + */ + "DumpMetadata": false, + /** * The console color theme to use. The possible values are: * - AutoDetect: SMAPI will assume a light background on Mac, and detect the background color automatically on Linux or Windows. diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 2df3e63f..8e3ad83b 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -107,6 +107,7 @@ + -- cgit From 235d67623de648499db521606e4b9033d35388e5 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 10 Jun 2018 12:06:29 -0400 Subject: create watcher core (#310) --- src/SMAPI/Framework/CursorPosition.cs | 7 + src/SMAPI/Framework/SGame.cs | 169 ++++++--------------- .../Comparers/GenericEqualsComparer.cs | 31 ++++ .../StateTracking/FieldWatchers/WatcherFactory.cs | 8 + src/SMAPI/Framework/WatcherCore.cs | 119 +++++++++++++++ src/SMAPI/ICursorPosition.cs | 3 +- src/SMAPI/StardewModdingAPI.csproj | 2 + 7 files changed, 219 insertions(+), 120 deletions(-) create mode 100644 src/SMAPI/Framework/StateTracking/Comparers/GenericEqualsComparer.cs create mode 100644 src/SMAPI/Framework/WatcherCore.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI/Framework/CursorPosition.cs b/src/SMAPI/Framework/CursorPosition.cs index 6f716746..aaf089d3 100644 --- a/src/SMAPI/Framework/CursorPosition.cs +++ b/src/SMAPI/Framework/CursorPosition.cs @@ -36,5 +36,12 @@ namespace StardewModdingAPI.Framework this.Tile = tile; this.GrabTile = grabTile; } + + /// Get whether the current object is equal to another object of the same type. + /// An object to compare with this object. + public bool Equals(ICursorPosition other) + { + return other != null && this.ScreenPixels == other.ScreenPixels; + } } } diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index a4d149f3..588d30c8 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -14,7 +14,6 @@ using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.StateTracking; -using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewModdingAPI.Framework.Utilities; using StardewValley; using StardewValley.BellsAndWhistles; @@ -85,38 +84,8 @@ namespace StardewModdingAPI.Framework /**** ** Game state ****/ - /// The underlying watchers for convenience. These are accessible individually as separate properties. - private readonly List Watchers = new List(); - - /// Tracks changes to the window size. - private IValueWatcher WindowSizeWatcher; - - /// Tracks changes to the current player. - private PlayerTracker CurrentPlayerTracker; - - /// Tracks changes to the time of day (in 24-hour military format). - private IValueWatcher TimeWatcher; - - /// Tracks changes to the save ID. - private IValueWatcher SaveIdWatcher; - - /// Tracks changes to the game's locations. - private WorldLocationsTracker LocationsWatcher; - - /// Tracks changes to . - private IValueWatcher ActiveMenuWatcher; - - /// Tracks changes to the cursor position. - private IValueWatcher CursorWatcher; - - /// Tracks changes to the mouse wheel scroll. - private IValueWatcher MouseWheelScrollWatcher; - - /// The previous content locale. - private LocalizedContentManager.LanguageCode? PreviousLocale; - - /// The previous cursor position. - private ICursorPosition PreviousCursorPosition; + /// Monitors the entire game state for changes. + private WatcherCore Watchers; /// An index incremented on every tick and reset every 60th tick (0–59). private int CurrentUpdateTick; @@ -186,23 +155,7 @@ namespace StardewModdingAPI.Framework this.Input.TrueUpdate(); // init watchers - this.CursorWatcher = WatcherFactory.ForEquatable(() => this.Input.CursorPosition.ScreenPixels); - this.MouseWheelScrollWatcher = WatcherFactory.ForEquatable(() => this.Input.RealMouse.ScrollWheelValue); - this.SaveIdWatcher = WatcherFactory.ForEquatable(() => Game1.hasLoadedGame ? Game1.uniqueIDForThisGame : 0); - this.WindowSizeWatcher = WatcherFactory.ForEquatable(() => new Point(Game1.viewport.Width, Game1.viewport.Height)); - this.TimeWatcher = WatcherFactory.ForEquatable(() => Game1.timeOfDay); - this.ActiveMenuWatcher = WatcherFactory.ForReference(() => Game1.activeClickableMenu); - this.LocationsWatcher = new WorldLocationsTracker((ObservableCollection)Game1.locations); - this.Watchers.AddRange(new IWatcher[] - { - this.CursorWatcher, - this.MouseWheelScrollWatcher, - this.SaveIdWatcher, - this.WindowSizeWatcher, - this.TimeWatcher, - this.ActiveMenuWatcher, - this.LocationsWatcher - }); + this.Watchers = new WatcherCore(this.Input); // raise callback this.OnGameInitialised(); @@ -372,44 +325,20 @@ namespace StardewModdingAPI.Framework /********* ** Update watchers *********/ - // reset player - if (Context.IsWorldReady) - { - if (this.CurrentPlayerTracker == null || this.CurrentPlayerTracker.Player != Game1.player) - { - this.CurrentPlayerTracker?.Dispose(); - this.CurrentPlayerTracker = new PlayerTracker(Game1.player); - } - } - else - { - if (this.CurrentPlayerTracker != null) - { - this.CurrentPlayerTracker.Dispose(); - this.CurrentPlayerTracker = null; - } - } - - // update values - foreach (IWatcher watcher in this.Watchers) - watcher.Update(); - this.CurrentPlayerTracker?.Update(); - this.LocationsWatcher.Update(); + this.Watchers.Update(); /********* ** Locale changed events *********/ - if (this.PreviousLocale != LocalizedContentManager.CurrentLanguageCode) + if (this.Watchers.LocaleWatcher.IsChanged) { - var oldValue = this.PreviousLocale; - var newValue = LocalizedContentManager.CurrentLanguageCode; - - this.Monitor.Log($"Context: locale set to {newValue}.", LogLevel.Trace); + var was = this.Watchers.LocaleWatcher.PreviousValue; + var now = this.Watchers.LocaleWatcher.CurrentValue; - if (oldValue != null) - this.Events.Content_LocaleChanged.Raise(new EventArgsValueChanged(oldValue.ToString(), newValue.ToString())); + this.Monitor.Log($"Context: locale set to {now}.", LogLevel.Trace); + this.Events.Content_LocaleChanged.Raise(new EventArgsValueChanged(was.ToString(), now.ToString())); - this.PreviousLocale = newValue; + this.Watchers.LocaleWatcher.Reset(); } /********* @@ -450,12 +379,12 @@ namespace StardewModdingAPI.Framework // event because we need to notify mods after the game handles the resize, so the // game's metadata (like Game1.viewport) are updated. That's a bit complicated // since the game adds & removes its own handler on the fly. - if (this.WindowSizeWatcher.IsChanged) + if (this.Watchers.WindowSizeWatcher.IsChanged) { if (this.VerboseLogging) - this.Monitor.Log($"Context: window size changed to {this.WindowSizeWatcher.CurrentValue}.", LogLevel.Trace); + this.Monitor.Log($"Context: window size changed to {this.Watchers.WindowSizeWatcher.CurrentValue}.", LogLevel.Trace); this.Events.Graphics_Resize.Raise(); - this.WindowSizeWatcher.Reset(); + this.Watchers.WindowSizeWatcher.Reset(); } /********* @@ -470,21 +399,23 @@ namespace StardewModdingAPI.Framework ICursorPosition cursor = this.Input.CursorPosition; // raise cursor moved event - if (this.CursorWatcher.IsChanged && this.PreviousCursorPosition != null) + if (this.Watchers.CursorWatcher.IsChanged) { - this.CursorWatcher.Reset(); - this.Events.Input_CursorMoved.Raise(new InputCursorMovedArgsInput(this.PreviousCursorPosition, cursor)); + ICursorPosition was = this.Watchers.CursorWatcher.PreviousValue; + ICursorPosition now = this.Watchers.CursorWatcher.CurrentValue; + this.Watchers.CursorWatcher.Reset(); + + this.Events.Input_CursorMoved.Raise(new InputCursorMovedArgsInput(was, now)); } - this.PreviousCursorPosition = cursor; // raise mouse wheel scrolled - if (this.MouseWheelScrollWatcher.IsChanged) + if (this.Watchers.MouseWheelScrollWatcher.IsChanged) { - int oldValue = this.MouseWheelScrollWatcher.PreviousValue; - int newValue = this.MouseWheelScrollWatcher.CurrentValue; - this.MouseWheelScrollWatcher.Reset(); + int was = this.Watchers.MouseWheelScrollWatcher.PreviousValue; + int now = this.Watchers.MouseWheelScrollWatcher.CurrentValue; + this.Watchers.MouseWheelScrollWatcher.Reset(); - this.Events.Input_MouseWheelScrolled.Raise(new InputMouseWheelScrolledEventArgs(cursor, oldValue, newValue)); + this.Events.Input_MouseWheelScrolled.Raise(new InputMouseWheelScrolledEventArgs(cursor, was, now)); } // raise input button events @@ -544,20 +475,20 @@ namespace StardewModdingAPI.Framework /********* ** Menu events *********/ - if (this.ActiveMenuWatcher.IsChanged) + if (this.Watchers.ActiveMenuWatcher.IsChanged) { - IClickableMenu previousMenu = this.ActiveMenuWatcher.PreviousValue; - IClickableMenu newMenu = this.ActiveMenuWatcher.CurrentValue; - this.ActiveMenuWatcher.Reset(); // reset here so a mod changing the menu will be raised as a new event afterwards + IClickableMenu was = this.Watchers.ActiveMenuWatcher.PreviousValue; + IClickableMenu now = this.Watchers.ActiveMenuWatcher.CurrentValue; + this.Watchers.ActiveMenuWatcher.Reset(); // reset here so a mod changing the menu will be raised as a new event afterwards if (this.VerboseLogging) - this.Monitor.Log($"Context: menu changed from {previousMenu?.GetType().FullName ?? "none"} to {newMenu?.GetType().FullName ?? "none"}.", LogLevel.Trace); + this.Monitor.Log($"Context: menu changed from {was?.GetType().FullName ?? "none"} to {now?.GetType().FullName ?? "none"}.", LogLevel.Trace); // raise menu events - if (newMenu != null) - this.Events.Menu_Changed.Raise(new EventArgsClickableMenuChanged(previousMenu, newMenu)); + if (now != null) + this.Events.Menu_Changed.Raise(new EventArgsClickableMenuChanged(was, now)); else - this.Events.Menu_Closed.Raise(new EventArgsClickableMenuClosed(previousMenu)); + this.Events.Menu_Closed.Raise(new EventArgsClickableMenuClosed(was)); } /********* @@ -565,22 +496,22 @@ namespace StardewModdingAPI.Framework *********/ if (Context.IsWorldReady) { - bool raiseWorldEvents = !this.SaveIdWatcher.IsChanged; // don't report changes from unloaded => loaded + bool raiseWorldEvents = !this.Watchers.SaveIdWatcher.IsChanged; // don't report changes from unloaded => loaded // raise location changes - if (this.LocationsWatcher.IsChanged) + if (this.Watchers.LocationsWatcher.IsChanged) { // location list changes - if (this.LocationsWatcher.IsLocationListChanged) + if (this.Watchers.LocationsWatcher.IsLocationListChanged) { - GameLocation[] added = this.LocationsWatcher.Added.ToArray(); - GameLocation[] removed = this.LocationsWatcher.Removed.ToArray(); - this.LocationsWatcher.ResetLocationList(); + GameLocation[] added = this.Watchers.LocationsWatcher.Added.ToArray(); + GameLocation[] removed = this.Watchers.LocationsWatcher.Removed.ToArray(); + this.Watchers.LocationsWatcher.ResetLocationList(); if (this.VerboseLogging) { - string addedText = this.LocationsWatcher.Added.Any() ? string.Join(", ", added.Select(p => p.Name)) : "none"; - string removedText = this.LocationsWatcher.Removed.Any() ? string.Join(", ", removed.Select(p => p.Name)) : "none"; + string addedText = this.Watchers.LocationsWatcher.Added.Any() ? string.Join(", ", added.Select(p => p.Name)) : "none"; + string removedText = this.Watchers.LocationsWatcher.Removed.Any() ? string.Join(", ", removed.Select(p => p.Name)) : "none"; this.Monitor.Log($"Context: location list changed (added {addedText}; removed {removedText}).", LogLevel.Trace); } @@ -591,7 +522,7 @@ namespace StardewModdingAPI.Framework // raise location contents changed if (raiseWorldEvents) { - foreach (LocationTracker watcher in this.LocationsWatcher.Locations) + foreach (LocationTracker watcher in this.Watchers.LocationsWatcher.Locations) { // buildings changed if (watcher.BuildingsWatcher.IsChanged) @@ -652,15 +583,15 @@ namespace StardewModdingAPI.Framework } } else - this.LocationsWatcher.Reset(); + this.Watchers.LocationsWatcher.Reset(); } // raise time changed - if (raiseWorldEvents && this.TimeWatcher.IsChanged) + if (raiseWorldEvents && this.Watchers.TimeWatcher.IsChanged) { - int was = this.TimeWatcher.PreviousValue; - int now = this.TimeWatcher.CurrentValue; - this.TimeWatcher.Reset(); + int was = this.Watchers.TimeWatcher.PreviousValue; + int now = this.Watchers.TimeWatcher.CurrentValue; + this.Watchers.TimeWatcher.Reset(); if (this.VerboseLogging) this.Monitor.Log($"Context: time changed from {was} to {now}.", LogLevel.Trace); @@ -668,12 +599,12 @@ namespace StardewModdingAPI.Framework this.Events.Time_TimeOfDayChanged.Raise(new EventArgsIntChanged(was, now)); } else - this.TimeWatcher.Reset(); + this.Watchers.TimeWatcher.Reset(); // raise player events if (raiseWorldEvents) { - PlayerTracker curPlayer = this.CurrentPlayerTracker; + PlayerTracker curPlayer = this.Watchers.CurrentPlayerTracker; // raise current location changed if (curPlayer.TryGetNewLocation(out GameLocation newLocation)) @@ -708,11 +639,11 @@ namespace StardewModdingAPI.Framework this.Events.Mine_LevelChanged.Raise(new EventArgsMineLevelChanged(curPlayer.MineLevelWatcher.PreviousValue, mineLevel)); } } - this.CurrentPlayerTracker?.Reset(); + this.Watchers.CurrentPlayerTracker?.Reset(); } // update save ID watcher - this.SaveIdWatcher.Reset(); + this.Watchers.SaveIdWatcher.Reset(); /********* ** Game update diff --git a/src/SMAPI/Framework/StateTracking/Comparers/GenericEqualsComparer.cs b/src/SMAPI/Framework/StateTracking/Comparers/GenericEqualsComparer.cs new file mode 100644 index 00000000..cc1d6553 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/Comparers/GenericEqualsComparer.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace StardewModdingAPI.Framework.StateTracking.Comparers +{ + /// Compares values using their method. This should only be used when won't work, since this doesn't validate whether they're comparable. + /// The value type. + internal class GenericEqualsComparer : IEqualityComparer + { + /********* + ** Public methods + *********/ + /// Determines whether the specified objects are equal. + /// true if the specified objects are equal; otherwise, false. + /// The first object to compare. + /// The second object to compare. + public bool Equals(T x, T y) + { + if (x == null) + return y == null; + return x.Equals(y); + } + + /// Get a hash code for the specified object. + /// The value. + public int GetHashCode(T obj) + { + return RuntimeHelpers.GetHashCode(obj); + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs index 4f1ac9f4..d7a02668 100644 --- a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs @@ -12,6 +12,14 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers /********* ** Public methods *********/ + /// Get a watcher which compares values using their method. This method should only be used when won't work, since this doesn't validate whether they're comparable. + /// The value type. + /// Get the current value. + public static ComparableWatcher ForGenericEquality(Func getValue) where T : struct + { + return new ComparableWatcher(getValue, new GenericEqualsComparer()); + } + /// Get a watcher for an value. /// The value type. /// Get the current value. diff --git a/src/SMAPI/Framework/WatcherCore.cs b/src/SMAPI/Framework/WatcherCore.cs new file mode 100644 index 00000000..64b063cf --- /dev/null +++ b/src/SMAPI/Framework/WatcherCore.cs @@ -0,0 +1,119 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.Input; +using StardewModdingAPI.Framework.StateTracking; +using StardewModdingAPI.Framework.StateTracking.FieldWatchers; +using StardewValley; +using StardewValley.Menus; + +namespace StardewModdingAPI.Framework +{ + /// Monitors the entire game state for changes, virally spreading watchers into any new entities that get created. + internal class WatcherCore + { + /********* + ** Public methods + *********/ + /// The underlying watchers for convenience. These are accessible individually as separate properties. + private readonly List Watchers = new List(); + + + /********* + ** Accessors + *********/ + /// Tracks changes to the window size. + public readonly IValueWatcher WindowSizeWatcher; + + /// Tracks changes to the current player. + public PlayerTracker CurrentPlayerTracker; + + /// Tracks changes to the time of day (in 24-hour military format). + public readonly IValueWatcher TimeWatcher; + + /// Tracks changes to the save ID. + public readonly IValueWatcher SaveIdWatcher; + + /// Tracks changes to the game's locations. + public readonly WorldLocationsTracker LocationsWatcher; + + /// Tracks changes to . + public readonly IValueWatcher ActiveMenuWatcher; + + /// Tracks changes to the cursor position. + public readonly IValueWatcher CursorWatcher; + + /// Tracks changes to the mouse wheel scroll. + public readonly IValueWatcher MouseWheelScrollWatcher; + + /// Tracks changes to the content locale. + public readonly IValueWatcher LocaleWatcher; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Manages input visible to the game. + public WatcherCore(SInputState inputState) + { + // init watchers + this.CursorWatcher = WatcherFactory.ForEquatable(() => inputState.CursorPosition); + this.MouseWheelScrollWatcher = WatcherFactory.ForEquatable(() => inputState.RealMouse.ScrollWheelValue); + this.SaveIdWatcher = WatcherFactory.ForEquatable(() => Game1.hasLoadedGame ? Game1.uniqueIDForThisGame : 0); + this.WindowSizeWatcher = WatcherFactory.ForEquatable(() => new Point(Game1.viewport.Width, Game1.viewport.Height)); + this.TimeWatcher = WatcherFactory.ForEquatable(() => Game1.timeOfDay); + this.ActiveMenuWatcher = WatcherFactory.ForReference(() => Game1.activeClickableMenu); + this.LocationsWatcher = new WorldLocationsTracker((ObservableCollection)Game1.locations); + this.LocaleWatcher = WatcherFactory.ForGenericEquality(() => LocalizedContentManager.CurrentLanguageCode); + this.Watchers.AddRange(new IWatcher[] + { + this.CursorWatcher, + this.MouseWheelScrollWatcher, + this.SaveIdWatcher, + this.WindowSizeWatcher, + this.TimeWatcher, + this.ActiveMenuWatcher, + this.LocationsWatcher, + this.LocaleWatcher + }); + } + + /// Update the watchers and adjust for added or removed entities. + public void Update() + { + // reset player + if (Context.IsWorldReady) + { + if (this.CurrentPlayerTracker == null || this.CurrentPlayerTracker.Player != Game1.player) + { + this.CurrentPlayerTracker?.Dispose(); + this.CurrentPlayerTracker = new PlayerTracker(Game1.player); + } + } + else + { + if (this.CurrentPlayerTracker != null) + { + this.CurrentPlayerTracker.Dispose(); + this.CurrentPlayerTracker = null; + } + } + + // update values + foreach (IWatcher watcher in this.Watchers) + watcher.Update(); + this.CurrentPlayerTracker?.Update(); + this.LocationsWatcher.Update(); + } + + /// Reset the current values as the baseline. + public void Reset() + { + foreach (IWatcher watcher in this.Watchers) + watcher.Reset(); + this.CurrentPlayerTracker?.Reset(); + this.LocationsWatcher.Reset(); + } + } +} diff --git a/src/SMAPI/ICursorPosition.cs b/src/SMAPI/ICursorPosition.cs index ddb8eb49..78f4fc21 100644 --- a/src/SMAPI/ICursorPosition.cs +++ b/src/SMAPI/ICursorPosition.cs @@ -1,9 +1,10 @@ +using System; using Microsoft.Xna.Framework; namespace StardewModdingAPI { /// Represents a cursor position in the different coordinate systems. - public interface ICursorPosition + public interface ICursorPosition : IEquatable { /// The pixel position relative to the top-left corner of the visible screen. Vector2 ScreenPixels { get; } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 8e3ad83b..67c48a57 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -129,6 +129,8 @@ + + -- cgit From 0b2e46d55cb09a169c7bb64ade37c82fc8233cb3 Mon Sep 17 00:00:00 2001 From: Dan Volchek Date: Sun, 10 Jun 2018 15:05:59 -0700 Subject: refactor IModMetadata update info --- src/SMAPI/Framework/IModMetadata.cs | 28 ++++++---------- src/SMAPI/Framework/ModLoading/ModMetadata.cs | 36 ++++++++------------ .../Framework/ModUpdateChecking/ModUpdateStatus.cs | 32 ++++++++++++++++++ src/SMAPI/Program.cs | 39 +++++++++++----------- src/SMAPI/StardewModdingAPI.csproj | 1 + 5 files changed, 77 insertions(+), 59 deletions(-) create mode 100644 src/SMAPI/Framework/ModUpdateChecking/ModUpdateStatus.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index 7bf7d98c..b71c8056 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -1,5 +1,6 @@ using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Framework.ModUpdateChecking; namespace StardewModdingAPI.Framework { @@ -45,14 +46,11 @@ namespace StardewModdingAPI.Framework /// Whether the mod is a content pack. bool IsContentPack { get; } - /// The latest version of the mod. - ISemanticVersion LatestVersion { get; } + /// The update status of this mod (if any). + ModUpdateStatus UpdateStatus { get; } - /// The latest preview version of the mod, if any. - ISemanticVersion LatestPreviewVersion { get; } - - /// The error checking for updates for this mod, if any. - string UpdateCheckError { get; } + /// The preview update status of this mod (if any). + ModUpdateStatus PreviewUpdateStatus { get; } /********* ** Public methods @@ -80,17 +78,13 @@ namespace StardewModdingAPI.Framework /// The mod-provided API. IModMetadata SetApi(object api); - /// Set the update version. - /// The latest version. - IModMetadata SetUpdateVersion(ISemanticVersion latestVersion); - - /// Set the preview update version. - /// The latest preview version. - IModMetadata SetPreviewUpdateVersion(ISemanticVersion latestPreviewVersion); + /// Set the update status. + /// The mod update status. + IModMetadata SetUpdateStatus(ModUpdateStatus updateStatus); - /// Set the error that occured while checking for updates. - /// The error checking for updates. - IModMetadata SetUpdateError(string updateCheckError); + /// Set the preview update status. + /// The mod preview update status. + IModMetadata SetPreviewUpdateStatus(ModUpdateStatus previewUpdateStatus); /// Whether the mod manifest was loaded (regardless of whether the mod itself was loaded). bool HasManifest(); diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 1ead1387..88d2770c 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using StardewModdingAPI.Framework.ModData; +using StardewModdingAPI.Framework.ModUpdateChecking; namespace StardewModdingAPI.Framework.ModLoading { @@ -43,14 +44,11 @@ namespace StardewModdingAPI.Framework.ModLoading /// The mod-provided API (if any). public object Api { get; private set; } - /// The latest version of the mod. - public ISemanticVersion LatestVersion { get; private set; } + /// The update status of this mod (if any). + public ModUpdateStatus UpdateStatus { get; private set; } - /// The latest preview version of the mod, if any. - public ISemanticVersion LatestPreviewVersion { get; private set; } - - /// The error checking for updates for this mod, if any. - public string UpdateCheckError { get; private set; } + /// The preview update status of this mod (if any). + public ModUpdateStatus PreviewUpdateStatus { get; private set; } /// Whether the mod is a content pack. public bool IsContentPack => this.Manifest?.ContentPackFor != null; @@ -124,27 +122,19 @@ namespace StardewModdingAPI.Framework.ModLoading return this; } - /// Set the update version. - /// The latest version. - public IModMetadata SetUpdateVersion(ISemanticVersion latestVersion) - { - this.LatestVersion = latestVersion; - return this; - } - - /// Set the preview update version. - /// The latest preview version. - public IModMetadata SetPreviewUpdateVersion(ISemanticVersion latestPreviewVersion) + /// Set the update status. + /// The mod update status. + public IModMetadata SetUpdateStatus(ModUpdateStatus updateStatus) { - this.LatestPreviewVersion = latestPreviewVersion; + this.UpdateStatus = updateStatus; return this; } - /// Set the error that occured while checking for updates. - /// The error checking for updates. - public IModMetadata SetUpdateError(string updateCheckError) + /// Set the preview update status. + /// The mod preview update status. + public IModMetadata SetPreviewUpdateStatus(ModUpdateStatus previewUpdateStatus) { - this.UpdateCheckError = updateCheckError; + this.PreviewUpdateStatus = previewUpdateStatus; return this; } diff --git a/src/SMAPI/Framework/ModUpdateChecking/ModUpdateStatus.cs b/src/SMAPI/Framework/ModUpdateChecking/ModUpdateStatus.cs new file mode 100644 index 00000000..7f588b66 --- /dev/null +++ b/src/SMAPI/Framework/ModUpdateChecking/ModUpdateStatus.cs @@ -0,0 +1,32 @@ +namespace StardewModdingAPI.Framework.ModUpdateChecking +{ + /// Update status for a mod. + internal class ModUpdateStatus + { + /********* + ** Accessors + *********/ + /// The version that this mod can be updated to (if any). + public ISemanticVersion Version { get; } + + /// The error checking for updates of this mod (if any). + public string Error { get; } + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The version that this mod can be update to. + public ModUpdateStatus(ISemanticVersion version) + { + this.Version = version; + } + + /// Construct an instance. + /// The error checking for updates of this mod. + public ModUpdateStatus(string error) + { + this.Error = error; + } + } +} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index e6aafb2d..96061b2a 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -24,6 +24,7 @@ using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Framework.ModUpdateChecking; using StardewModdingAPI.Framework.Patching; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; @@ -671,46 +672,46 @@ namespace StardewModdingAPI // handle error if (remoteInfo.Error != null) { - if(mod.LatestVersion == null && mod.LatestPreviewVersion == null) - mod.SetUpdateError(remoteInfo.Error); + if (mod.UpdateStatus?.Version == null) + mod.SetUpdateStatus(new ModUpdateStatus(remoteInfo.Error)); + if (mod.PreviewUpdateStatus?.Version == null) + mod.SetUpdateStatus(new ModUpdateStatus(remoteInfo.Error)); + this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: {remoteInfo.Error}", LogLevel.Trace); continue; } // normalise versions ISemanticVersion localVersion = mod.DataRecord?.GetLocalVersionForUpdateChecks(mod.Manifest.Version) ?? mod.Manifest.Version; - bool validVersion = SemanticVersion.TryParse(mod.DataRecord?.GetRemoteVersionForUpdateChecks(remoteInfo.Version) ?? remoteInfo.Version, out ISemanticVersion remoteVersion); bool validPreviewVersion = SemanticVersion.TryParse(remoteInfo.PreviewVersion, out ISemanticVersion remotePreviewVersion); + if (!validVersion && mod.UpdateStatus?.Version == null) + mod.SetUpdateStatus(new ModUpdateStatus($"Version is invalid: {remoteInfo.Version}")); + if (!validPreviewVersion && mod.PreviewUpdateStatus?.Version == null) + mod.SetPreviewUpdateStatus(new ModUpdateStatus($"Version is invalid: {remoteInfo.PreviewVersion}")); + if (!validVersion && !validPreviewVersion) { - string errorInfo = $"Mod has invalid versions. version: {remoteInfo.Version}, preview version: {remoteInfo.PreviewVersion}"; - - if (mod.LatestVersion == null && mod.LatestPreviewVersion == null) - mod.SetUpdateError(errorInfo); - this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: {errorInfo}", LogLevel.Trace); + this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: Mod has invalid versions. version: {remoteInfo.Version}, preview version: {remoteInfo.PreviewVersion}", LogLevel.Trace); continue; } // compare versions - bool isNonPreviewUpdate = validVersion && remoteVersion.IsNewerThan(localVersion); + bool isPreviewUpdate = validPreviewVersion && localVersion.IsNewerThan(remoteVersion) && remotePreviewVersion.IsNewerThan(localVersion); + bool isUpdate = (validVersion && remoteVersion.IsNewerThan(localVersion)) || isPreviewUpdate; - bool isUpdate = isNonPreviewUpdate || - (validPreviewVersion && localVersion.IsNewerThan(remoteVersion) && remotePreviewVersion.IsNewerThan(localVersion)); - this.VerboseLog($" {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version}{(!localVersion.Equals(mod.Manifest.Version) ? $" [{localVersion}]" : "")} => {(isNonPreviewUpdate ? remoteInfo.Version : remoteInfo.PreviewVersion)}" : "okay")}."); + this.VerboseLog($" {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version}{(!localVersion.Equals(mod.Manifest.Version) ? $" [{localVersion}]" : "")} => {(isPreviewUpdate ? remoteInfo.PreviewVersion : remoteInfo.Version)}" : "okay")}."); if (isUpdate) { - if (!updatesByMod.TryGetValue(mod, out Tuple other) || (isNonPreviewUpdate ? remoteVersion : remotePreviewVersion).IsNewerThan(other.Item2 ? other.Item1.PreviewVersion : other.Item1.Version)) + if (!updatesByMod.TryGetValue(mod, out Tuple other) || (isPreviewUpdate ? remotePreviewVersion : remoteVersion).IsNewerThan(other.Item2 ? other.Item1.PreviewVersion : other.Item1.Version)) { - updatesByMod[mod] = new Tuple(remoteInfo, !isNonPreviewUpdate); + updatesByMod[mod] = new Tuple(remoteInfo, isPreviewUpdate); - if (isNonPreviewUpdate) - mod.SetUpdateVersion(remoteVersion); + if (isPreviewUpdate) + mod.SetPreviewUpdateStatus(new ModUpdateStatus(remotePreviewVersion)); else - mod.SetPreviewUpdateVersion(remotePreviewVersion); - - mod.SetUpdateError(null); + mod.SetUpdateStatus(new ModUpdateStatus(remoteVersion)); } } } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 67c48a57..beaad8e0 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -108,6 +108,7 @@ + -- cgit From 930a871018467683510ba39d092d401d7df50861 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 10 Jun 2018 21:33:17 -0400 Subject: add debris list changed event (#310) --- src/SMAPI/Events/IWorldEvents.cs | 3 ++ .../Events/WorldDebrisListChangedEventArgs.cs | 38 ++++++++++++++++++++++ src/SMAPI/Framework/Events/EventManager.cs | 4 +++ src/SMAPI/Framework/Events/ModWorldEvents.cs | 7 ++++ src/SMAPI/Framework/SGame.cs | 11 +++++++ .../Framework/StateTracking/LocationTracker.cs | 5 +++ src/SMAPI/Framework/WatcherCore.cs | 2 +- src/SMAPI/StardewModdingAPI.csproj | 1 + 8 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 src/SMAPI/Events/WorldDebrisListChangedEventArgs.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI/Events/IWorldEvents.cs b/src/SMAPI/Events/IWorldEvents.cs index 067a79bc..d4efb53b 100644 --- a/src/SMAPI/Events/IWorldEvents.cs +++ b/src/SMAPI/Events/IWorldEvents.cs @@ -11,6 +11,9 @@ namespace StardewModdingAPI.Events /// Raised after buildings are added or removed in a location. event EventHandler BuildingListChanged; + /// Raised after debris are added or removed in a location. + event EventHandler DebrisListChanged; + /// Raised after large terrain features (like bushes) are added or removed in a location. event EventHandler LargeTerrainFeatureListChanged; diff --git a/src/SMAPI/Events/WorldDebrisListChangedEventArgs.cs b/src/SMAPI/Events/WorldDebrisListChangedEventArgs.cs new file mode 100644 index 00000000..aad9c24d --- /dev/null +++ b/src/SMAPI/Events/WorldDebrisListChangedEventArgs.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class WorldDebrisListChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The debris added to the location. + public IEnumerable Added { get; } + + /// The debris removed from the location. + public IEnumerable Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The debris added to the location. + /// The debris removed from the location. + public WorldDebrisListChangedEventArgs(GameLocation location, IEnumerable added, IEnumerable removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 9f67244a..b05d82ce 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -20,6 +20,9 @@ namespace StardewModdingAPI.Framework.Events /// Raised after buildings are added or removed in a location. public readonly ManagedEvent World_BuildingListChanged; + /// Raised after debris are added or removed in a location. + public readonly ManagedEvent World_DebrisListChanged; + /// Raised after large terrain features (like bushes) are added or removed in a location. public readonly ManagedEvent World_LargeTerrainFeatureListChanged; @@ -255,6 +258,7 @@ namespace StardewModdingAPI.Framework.Events this.Input_MouseWheelScrolled = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled)); this.World_BuildingListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LocationListChanged)); + this.World_DebrisListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.DebrisListChanged)); this.World_LargeTerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LargeTerrainFeatureListChanged)); this.World_LocationListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.BuildingListChanged)); this.World_NpcListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.NpcListChanged)); diff --git a/src/SMAPI/Framework/Events/ModWorldEvents.cs b/src/SMAPI/Framework/Events/ModWorldEvents.cs index e1a53e0c..dc9c0f4c 100644 --- a/src/SMAPI/Framework/Events/ModWorldEvents.cs +++ b/src/SMAPI/Framework/Events/ModWorldEvents.cs @@ -23,6 +23,13 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.World_BuildingListChanged.Remove(value); } + /// Raised after debris are added or removed in a location. + public event EventHandler DebrisListChanged + { + add => this.EventManager.World_DebrisListChanged.Add(value, this.Mod); + remove => this.EventManager.World_DebrisListChanged.Remove(value); + } + /// Raised after large terrain features (like bushes) are added or removed in a location. public event EventHandler LargeTerrainFeatureListChanged { diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 588d30c8..984c1f57 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -536,6 +536,17 @@ namespace StardewModdingAPI.Framework this.Events.Legacy_Location_BuildingsChanged.Raise(new EventArgsLocationBuildingsChanged(location, added, removed)); } + // debris changed + if (watcher.DebrisWatcher.IsChanged) + { + GameLocation location = watcher.Location; + Debris[] added = watcher.DebrisWatcher.Added.ToArray(); + Debris[] removed = watcher.DebrisWatcher.Removed.ToArray(); + watcher.DebrisWatcher.Reset(); + + this.Events.World_DebrisListChanged.Raise(new WorldDebrisListChangedEventArgs(location, added, removed)); + } + // large terrain features changed if (watcher.LargeTerrainFeaturesWatcher.IsChanged) { diff --git a/src/SMAPI/Framework/StateTracking/LocationTracker.cs b/src/SMAPI/Framework/StateTracking/LocationTracker.cs index 1b4c0b19..708c0716 100644 --- a/src/SMAPI/Framework/StateTracking/LocationTracker.cs +++ b/src/SMAPI/Framework/StateTracking/LocationTracker.cs @@ -33,6 +33,9 @@ namespace StardewModdingAPI.Framework.StateTracking /// Tracks added or removed buildings. public ICollectionWatcher BuildingsWatcher { get; } + /// Tracks added or removed debris. + public ICollectionWatcher DebrisWatcher { get; } + /// Tracks added or removed large terrain features. public ICollectionWatcher LargeTerrainFeaturesWatcher { get; } @@ -59,6 +62,7 @@ namespace StardewModdingAPI.Framework.StateTracking this.BuildingsWatcher = location is BuildableGameLocation buildableLocation ? WatcherFactory.ForNetCollection(buildableLocation.buildings) : (ICollectionWatcher)WatcherFactory.ForObservableCollection(new ObservableCollection()); + this.DebrisWatcher = WatcherFactory.ForNetCollection(location.debris); this.LargeTerrainFeaturesWatcher = WatcherFactory.ForNetCollection(location.largeTerrainFeatures); this.NpcsWatcher = WatcherFactory.ForNetCollection(location.characters); this.ObjectsWatcher = WatcherFactory.ForNetDictionary(location.netObjects); @@ -67,6 +71,7 @@ namespace StardewModdingAPI.Framework.StateTracking this.Watchers.AddRange(new IWatcher[] { this.BuildingsWatcher, + this.DebrisWatcher, this.LargeTerrainFeaturesWatcher, this.NpcsWatcher, this.ObjectsWatcher, diff --git a/src/SMAPI/Framework/WatcherCore.cs b/src/SMAPI/Framework/WatcherCore.cs index 64b063cf..e06423b9 100644 --- a/src/SMAPI/Framework/WatcherCore.cs +++ b/src/SMAPI/Framework/WatcherCore.cs @@ -13,7 +13,7 @@ namespace StardewModdingAPI.Framework internal class WatcherCore { /********* - ** Public methods + ** Properties *********/ /// The underlying watchers for convenience. These are accessible individually as separate properties. private readonly List Watchers = new List(); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 67c48a57..ab3967c5 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -97,6 +97,7 @@ + -- cgit From 0043810e04239c897a61c097855ab08c382251ff Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 17 Jun 2018 13:23:24 -0400 Subject: set 'large address aware' flag on SMAPI executable to fix memory issues (#431) This is safe since the vanilla game has it set too. --- docs/release-notes.md | 1 + src/SMAPI/StardewModdingAPI.csproj | 8 ++++++++ src/SMAPI/packages.config | 1 + 3 files changed, 10 insertions(+) (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/docs/release-notes.md b/docs/release-notes.md index 5bb77762..df832c34 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ * Added friendly error when game can't start audio. * Added console warning for mods which don't have update checks configured. * Improved how mod warnings are shown in the console. + * Fixed `SEHException` errors and performance issues in some cases. * Fixed console color scheme on Mac or in PowerShell, configurable via `StardewModdingAPI.config.json`. * Fixed installer error on Linux/Mac in some cases. * Fixed installer not finding some game paths. diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index ab3967c5..fcd54c34 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -27,6 +27,7 @@ 1.0.0.%2a false true + true x86 @@ -344,4 +345,11 @@ + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + \ No newline at end of file diff --git a/src/SMAPI/packages.config b/src/SMAPI/packages.config index 3e876922..3347b037 100644 --- a/src/SMAPI/packages.config +++ b/src/SMAPI/packages.config @@ -1,5 +1,6 @@  + \ No newline at end of file -- cgit From d401aff3307f6e2e1641610fdd912b572d6b04c1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Jun 2018 22:10:15 -0400 Subject: rewrite update checks (#551) --- docs/release-notes.md | 14 +- docs/technical-docs.md | 43 ++--- src/SMAPI.Web/Controllers/ModsApiController.cs | 180 +++++++++++++++------ .../Framework/ModRepositories/ModInfoModel.cs | 56 +++++++ src/SMAPI/Framework/IModMetadata.cs | 18 +-- src/SMAPI/Framework/ModData/ParsedModDataRecord.cs | 7 +- src/SMAPI/Framework/ModLoading/ModMetadata.cs | 25 +-- .../Framework/ModUpdateChecking/ModUpdateStatus.cs | 37 ----- src/SMAPI/Program.cs | 143 +++++++--------- src/SMAPI/StardewModdingAPI.csproj | 1 - .../Framework/Clients/WebApi/ModEntryModel.cs | 30 ++++ .../Framework/Clients/WebApi/ModInfoModel.cs | 56 ------- .../Framework/Clients/WebApi/ModSeachModel.cs | 15 +- .../Clients/WebApi/ModSearchEntryModel.cs | 34 ++++ .../Framework/Clients/WebApi/WebApiClient.cs | 39 +---- 15 files changed, 366 insertions(+), 332 deletions(-) create mode 100644 src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs delete mode 100644 src/SMAPI/Framework/ModUpdateChecking/ModUpdateStatus.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/docs/release-notes.md b/docs/release-notes.md index df832c34..b3ab2481 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,8 @@ * Added prompt when in beta channel and a new version is found. * Added friendly error when game can't start audio. * Added console warning for mods which don't have update checks configured. + * Added update checks for optional mod files on Nexus. + * Added `player_add name` command, which lets you add items to your inventory by name instead of ID. * Improved how mod warnings are shown in the console. * Fixed `SEHException` errors and performance issues in some cases. * Fixed console color scheme on Mac or in PowerShell, configurable via `StardewModdingAPI.config.json`. @@ -16,6 +18,8 @@ * Fixed installer not removing some SMAPI files. * Fixed `smapi.io/install` not linking to a useful page. * Fixed `world_setseason` command not running season-change logic. + * Fixed `world_setseason` command not normalising the season value. + * Fixed `world_settime` sometimes breaking NPC schedules (e.g. so they stay in bed). * Fixed mod update checks failing if a mod only has prerelease versions on GitHub. * Fixed launch issue for Linux players with some terminals. (Thanks to HanFox and kurumushi!) * Fixed Nexus mod update alerts not showing HTTPS links. @@ -38,6 +42,7 @@ * Added support for custom seasonal tilesheets when loading an unpacked `.tbin` map. * Added Harmony DLL for internal use by SMAPI. (Mods should still include their own copy for backwards compatibility, and in case it's removed later. SMAPI will always load its own version though.) * Added option to suppress update checks for a specific mod in `StardewModdingAPI.config.json`. + * Update checks now use the update key order when deciding which to link to. * Fixed error if a mod loads a PNG while the game is loading (e.g. custom map tilesheets via `IAssetLoader`). * Fixed assets loaded by temporary content managers not being editable by mods. * Fixed assets not reloaded consistently when the player switches language. @@ -54,14 +59,9 @@ * Mods can't intercept chatbox input. * Mod IDs should only contain letters, numbers, hyphens, dots, and underscores. That allows their use in many contexts like URLs. This restriction is now enforced. (In regex form: `^[a-zA-Z0-9_.-]+$`.) -* In console commands: - * Added `player_add name`, which lets you add items to your inventory by name instead of ID. - * Fixed `world_settime` sometimes breaking NPC schedules (e.g. so they stay in bed). - * Fixed `world_setseason` not normalising the season value. - * For the web UI: - * Improved log parser design to make it more intuitive. - * Improved layout on small screens. + * Redesigned log parser to make it more intuitive. + * Redesigned UI to be more mobile-friendly. * Added option to download from Nexus. * Changed log parser filters to show `DEBUG` messages by default. * Fixed log parser issue when content packs have no description. diff --git a/docs/technical-docs.md b/docs/technical-docs.md index f4358e31..bdb731d1 100644 --- a/docs/technical-docs.md +++ b/docs/technical-docs.md @@ -161,7 +161,7 @@ The log parser lives at https://log.smapi.io. ### Mods API The mods API provides version info for mods hosted by Chucklefish, GitHub, or Nexus Mods. It's used by SMAPI to perform update checks. The `{version}` URL token is the version of SMAPI making the -request; it doesn't do anything currently, but lets us version breaking changes if needed. +request, and is used when needed for backwards compatibility. Each mod is identified by a repository key and unique identifier (like `nexus:541`). The following repositories are supported: @@ -173,32 +173,37 @@ key | repository `nexus` | A mod page on [Nexus Mods](https://www.nexusmods.com/stardewvalley), identified by the mod ID in the page URL. -The API accepts either `GET` or `POST` for convenience: -> ``` ->GET https://api.smapi.io/v2.0/mods?modKeys=nexus:541,chucklefish:4228 ->``` - +The API accepts a `POST` request with the mods to match, each of which **must** specify an ID and +update keys. >``` >POST https://api.smapi.io/v2.0/mods >{ -> "ModKeys": [ "nexus:541", "chucklefish:4228" ] +> "mods": [ +> { +> "id": "Pathoschild.LookupAnything", +> "updateKeys": [ "nexus:541", "chucklefish:4250" ] +> } +> ] >} >``` -It returns a response like this: +The API will automatically aggregate versions and errors, and return a response like this. The +latest version is the main mod version (e.g. 'latest version' field on Nexus); if available and +newer, the latest optional version will be shown as the 'preview version'. >``` >{ -> "chucklefish:4228": { -> "name": "Entoarox Framework", -> "version": "1.8.0", -> "url": "https://community.playstarbound.com/resources/4228" -> }, -> "nexus:541": { -> "name": "Lookup Anything", -> "version": "1.16", -> "url": "http://www.nexusmods.com/stardewvalley/mods/541" -> } ->} +> "Pathoschild.LookupAnything": { +> "id": "Pathoschild.LookupAnything", +> "name": "Lookup Anything", +> "version": "1.18", +> "url": "https://www.nexusmods.com/stardewvalley/mods/541", +> "previewVersion": "1.19-beta", +> "previewUrl": "https://www.nexusmods.com/stardewvalley/mods/541", +> "errors": [ +> "The update key 'chucklefish:4250' matches a mod with invalid semantic version '*'." +> ] +> } +} >``` ## Development diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 1ec855d5..c5a1705d 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; +using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.GitHub; @@ -67,70 +68,89 @@ namespace StardewModdingAPI.Web.Controllers } /// Fetch version metadata for the given mods. - /// The namespaced mod keys to search as a comma-delimited array. - /// Whether to allow non-semantic versions, instead of returning an error for those. - [HttpGet] - public async Task> GetAsync(string modKeys, bool allowInvalidVersions = false) - { - string[] modKeysArray = modKeys?.Split(',').ToArray(); - if (modKeysArray == null || !modKeysArray.Any()) - return new Dictionary(); - - return await this.PostAsync(new ModSearchModel(modKeysArray, allowInvalidVersions)); - } - - /// Fetch version metadata for the given mods. - /// The mod search criteria. + /// The mod search criteria. [HttpPost] - public async Task> PostAsync([FromBody] ModSearchModel search) + public async Task> PostAsync([FromBody] ModSearchModel model) { - // parse model - bool allowInvalidVersions = search?.AllowInvalidVersions ?? false; - string[] modKeys = (search?.ModKeys?.ToArray() ?? new string[0]) - .Distinct(StringComparer.CurrentCultureIgnoreCase) - .OrderBy(p => p, StringComparer.CurrentCultureIgnoreCase) - .ToArray(); - - // fetch mod info - IDictionary result = new Dictionary(StringComparer.CurrentCultureIgnoreCase); - foreach (string modKey in modKeys) + ModSearchEntryModel[] searchMods = this.GetSearchMods(model).ToArray(); + IDictionary mods = new Dictionary(StringComparer.CurrentCultureIgnoreCase); + foreach (ModSearchEntryModel mod in searchMods) { - // parse mod key - if (!this.TryParseModKey(modKey, out string vendorKey, out string modID)) - { - result[modKey] = new ModInfoModel("The mod key isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'."); + if (string.IsNullOrWhiteSpace(mod.ID)) continue; - } - // get matching repository - if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository)) + // get latest versions + ModEntryModel result = new ModEntryModel { ID = mod.ID }; + IList errors = new List(); + foreach (string updateKey in mod.UpdateKeys ?? new string[0]) { - result[modKey] = new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); - continue; - } + // fetch data + ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey); + if (data.Error != null) + { + errors.Add(data.Error); + continue; + } - // fetch mod info - result[modKey] = await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry => - { - // fetch info - ModInfoModel info = await repository.GetModInfoAsync(modID); + // handle main version + if (data.Version != null) + { + if (!SemanticVersion.TryParse(data.Version, out ISemanticVersion version)) + { + errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'."); + continue; + } + + if (result.Version == null || version.IsNewerThan(new SemanticVersion(result.Version))) + { + result.Name = data.Name; + result.Url = data.Url; + result.Version = version.ToString(); + } + } - // validate - if (info.Error == null) + // handle optional version + if (data.PreviewVersion != null) { - if (info.Version == null) - info = new ModInfoModel(name: info.Name, version: info.Version, url: info.Url, error: "Mod has no version number."); - if (!allowInvalidVersions && !Regex.IsMatch(info.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)) - info = new ModInfoModel(name: info.Name, version: info.Version, url: info.Url, error: $"Mod has invalid semantic version '{info.Version}'."); + if (!SemanticVersion.TryParse(data.PreviewVersion, out ISemanticVersion version)) + { + errors.Add($"The update key '{updateKey}' matches a mod with invalid optional semantic version '{data.PreviewVersion}'."); + continue; + } + + if (result.PreviewVersion == null || version.IsNewerThan(new SemanticVersion(data.PreviewVersion))) + { + result.Name = result.Name ?? data.Name; + result.PreviewUrl = data.Url; + result.PreviewVersion = version.ToString(); + } } + } + + // fallback to preview if latest is invalid + if (result.Version == null && result.PreviewVersion != null) + { + result.Version = result.PreviewVersion; + result.Url = result.PreviewUrl; + result.PreviewVersion = null; + result.PreviewUrl = null; + } + + // special cases + if (mod.ID == "Pathoschild.SMAPI") + { + result.Name = "SMAPI"; + result.Url = "https://smapi.io/"; + if (result.PreviewUrl != null) + result.PreviewUrl = "https://smapi.io/"; + } - // cache & return - entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(info.Error == null ? this.SuccessCacheMinutes : this.ErrorCacheMinutes); - return info; - }); + // add result + result.Errors = errors.ToArray(); + mods[mod.ID] = result; } - return result; + return mods; } @@ -158,5 +178,63 @@ namespace StardewModdingAPI.Web.Controllers modID = parts[1].Trim(); return true; } + + /// Get the mods for which the API should return data. + /// The search model. + private IEnumerable GetSearchMods(ModSearchModel model) + { + if (model == null) + yield break; + + // yield standard entries + if (model.Mods != null) + { + foreach (ModSearchEntryModel mod in model.Mods) + yield return mod; + } + + // yield mod update keys if backwards compatible + if (model.ModKeys != null && model.ModKeys.Any() && this.ShouldBeBackwardsCompatible("2.6-beta.17")) + { + foreach (string updateKey in model.ModKeys.Distinct()) + yield return new ModSearchEntryModel(updateKey, new[] { updateKey }); + } + } + + /// Get the mod info for an update key. + /// The namespaced update key. + private async Task GetInfoForUpdateKeyAsync(string updateKey) + { + // parse update key + if (!this.TryParseModKey(updateKey, out string vendorKey, out string modID)) + return new ModInfoModel($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'."); + + // get matching repository + if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository)) + return new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); + + // fetch mod info + return await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry => + { + ModInfoModel result = await repository.GetModInfoAsync(modID); + if (result.Error != null) + { + if (result.Version == null) + result.Error = $"The update key '{updateKey}' matches a mod with no version number."; + else if (!Regex.IsMatch(result.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)) + result.Error = $"The update key '{updateKey}' matches a mod with invalid semantic version '{result.Version}'."; + } + entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(result.Error == null ? this.SuccessCacheMinutes : this.ErrorCacheMinutes); + return result; + }); + } + + /// Get whether the API should return data in a backwards compatible way. + /// The last version for which data should be backwards compatible. + private bool ShouldBeBackwardsCompatible(string maxVersion) + { + string actualVersion = (string)this.RouteData.Values["version"]; + return !new SemanticVersion(actualVersion).IsNewerThan(new SemanticVersion(maxVersion)); + } } } diff --git a/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs b/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs new file mode 100644 index 00000000..ccb0699c --- /dev/null +++ b/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs @@ -0,0 +1,56 @@ +namespace StardewModdingAPI.Web.Framework.ModRepositories +{ + /// Generic metadata about a mod. + public class ModInfoModel + { + /********* + ** Accessors + *********/ + /// The mod name. + public string Name { get; set; } + + /// The mod's latest release number. + public string Version { get; set; } + + /// The mod's latest optional release, if newer than . + public string PreviewVersion { get; set; } + + /// The mod's web URL. + public string Url { get; set; } + + /// The error message indicating why the mod is invalid (if applicable). + public string Error { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public ModInfoModel() + { + // needed for JSON deserialising + } + + /// Construct an instance. + /// The mod name. + /// The semantic version for the mod's latest release. + /// The semantic version for the mod's latest preview release, if available and different from . + /// The mod's web URL. + /// The error message indicating why the mod is invalid (if applicable). + public ModInfoModel(string name, string version, string url, string previewVersion = null, string error = null) + { + this.Name = name; + this.Version = version; + this.PreviewVersion = previewVersion; + this.Url = url; + this.Error = error; + } + + /// Construct an instance. + /// The error message indicating why the mod is invalid. + public ModInfoModel(string error) + { + this.Error = error; + } + } +} diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index b71c8056..d3ec0035 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -1,6 +1,6 @@ using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.ModLoading; -using StardewModdingAPI.Framework.ModUpdateChecking; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; namespace StardewModdingAPI.Framework { @@ -46,11 +46,9 @@ namespace StardewModdingAPI.Framework /// Whether the mod is a content pack. bool IsContentPack { get; } - /// The update status of this mod (if any). - ModUpdateStatus UpdateStatus { get; } + /// The update-check metadata for this mod (if any). + ModEntryModel UpdateCheckData { get; } - /// The preview update status of this mod (if any). - ModUpdateStatus PreviewUpdateStatus { get; } /********* ** Public methods @@ -78,13 +76,9 @@ namespace StardewModdingAPI.Framework /// The mod-provided API. IModMetadata SetApi(object api); - /// Set the update status. - /// The mod update status. - IModMetadata SetUpdateStatus(ModUpdateStatus updateStatus); - - /// Set the preview update status. - /// The mod preview update status. - IModMetadata SetPreviewUpdateStatus(ModUpdateStatus previewUpdateStatus); + /// Set the update-check metadata for this mod. + /// The update-check metadata. + IModMetadata SetUpdateData(ModEntryModel data); /// Whether the mod manifest was loaded (regardless of whether the mod itself was loaded). bool HasManifest(); diff --git a/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs b/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs index deb12bdc..3801fac3 100644 --- a/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs +++ b/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs @@ -40,9 +40,12 @@ namespace StardewModdingAPI.Framework.ModData /// Get a semantic remote version for update checks. /// The remote version to normalise. - public string GetRemoteVersionForUpdateChecks(string version) + public ISemanticVersion GetRemoteVersionForUpdateChecks(string version) { - return this.DataRecord.GetRemoteVersionForUpdateChecks(version); + string rawVersion = this.DataRecord.GetRemoteVersionForUpdateChecks(version); + return rawVersion != null + ? new SemanticVersion(rawVersion) + : null; } } } diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 88d2770c..02a77778 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -1,7 +1,7 @@ using System; using System.Linq; using StardewModdingAPI.Framework.ModData; -using StardewModdingAPI.Framework.ModUpdateChecking; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; namespace StardewModdingAPI.Framework.ModLoading { @@ -44,11 +44,8 @@ namespace StardewModdingAPI.Framework.ModLoading /// The mod-provided API (if any). public object Api { get; private set; } - /// The update status of this mod (if any). - public ModUpdateStatus UpdateStatus { get; private set; } - - /// The preview update status of this mod (if any). - public ModUpdateStatus PreviewUpdateStatus { get; private set; } + /// The update-check metadata for this mod (if any). + public ModEntryModel UpdateCheckData { get; private set; } /// Whether the mod is a content pack. public bool IsContentPack => this.Manifest?.ContentPackFor != null; @@ -122,19 +119,11 @@ namespace StardewModdingAPI.Framework.ModLoading return this; } - /// Set the update status. - /// The mod update status. - public IModMetadata SetUpdateStatus(ModUpdateStatus updateStatus) - { - this.UpdateStatus = updateStatus; - return this; - } - - /// Set the preview update status. - /// The mod preview update status. - public IModMetadata SetPreviewUpdateStatus(ModUpdateStatus previewUpdateStatus) + /// Set the update-check metadata for this mod. + /// The update-check metadata. + public IModMetadata SetUpdateData(ModEntryModel data) { - this.PreviewUpdateStatus = previewUpdateStatus; + this.UpdateCheckData = data; return this; } diff --git a/src/SMAPI/Framework/ModUpdateChecking/ModUpdateStatus.cs b/src/SMAPI/Framework/ModUpdateChecking/ModUpdateStatus.cs deleted file mode 100644 index efb32aef..00000000 --- a/src/SMAPI/Framework/ModUpdateChecking/ModUpdateStatus.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace StardewModdingAPI.Framework.ModUpdateChecking -{ - /// Update status for a mod. - internal class ModUpdateStatus - { - /********* - ** Accessors - *********/ - /// The version that this mod can be updated to (if any). - public ISemanticVersion Version { get; } - - /// The error checking for updates of this mod (if any). - public string Error { get; } - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The version that this mod can be update to. - public ModUpdateStatus(ISemanticVersion version) - { - this.Version = version; - } - - /// Construct an instance. - /// The error checking for updates of this mod. - public ModUpdateStatus(string error) - { - this.Error = error; - } - - /// Construct an instance. - public ModUpdateStatus() - { - } - } -} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 2ee18a29..ccdf98ef 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -8,6 +8,7 @@ using System.Net; using System.Reflection; using System.Runtime.ExceptionServices; using System.Security; +using System.Text; using System.Text.RegularExpressions; using System.Threading; using Microsoft.Xna.Framework.Input; @@ -24,7 +25,6 @@ using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; -using StardewModdingAPI.Framework.ModUpdateChecking; using StardewModdingAPI.Framework.Patching; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; @@ -592,14 +592,14 @@ namespace StardewModdingAPI ISemanticVersion updateFound = null; try { - ModInfoModel response = client.GetModInfo($"GitHub:{this.Settings.GitHubProjectName}").Single().Value; + ModEntryModel response = client.GetModInfo(new ModSearchEntryModel("Pathoschild.SMAPI", new[] { $"GitHub:{this.Settings.GitHubProjectName}" })).Single().Value; ISemanticVersion latestStable = response.Version != null ? new SemanticVersion(response.Version) : null; ISemanticVersion latestBeta = response.PreviewVersion != null ? new SemanticVersion(response.PreviewVersion) : null; - if (response.Error != null) + if (latestStable == null && response.Errors.Any()) { this.Monitor.Log("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.", LogLevel.Warn); - this.Monitor.Log($"Error: {response.Error}"); + this.Monitor.Log($"Error: {string.Join("\n", response.Errors)}"); } else if (this.IsValidUpdate(Constants.ApiVersion, latestBeta, this.Settings.UseBetaChannel)) { @@ -634,103 +634,72 @@ namespace StardewModdingAPI { HashSet suppressUpdateChecks = new HashSet(this.Settings.SuppressUpdateChecks, StringComparer.InvariantCultureIgnoreCase); - // prepare update keys - Dictionary modsByKey = - ( - from mod in mods - where - mod.Manifest?.UpdateKeys != null - && !suppressUpdateChecks.Contains(mod.Manifest.UniqueID) - from key in mod.Manifest.UpdateKeys - select new { key, mod } - ) - .GroupBy(p => p.key, StringComparer.InvariantCultureIgnoreCase) - .ToDictionary( - group => group.Key, - group => group.Select(p => p.mod).ToArray(), - StringComparer.InvariantCultureIgnoreCase - ); - - // fetch results - this.Monitor.Log($" Checking {modsByKey.Count} mod update keys.", LogLevel.Trace); - var results = - ( - from entry in client.GetModInfo(modsByKey.Keys.ToArray()) - from mod in modsByKey[entry.Key] - orderby mod.DisplayName - select new { entry.Key, Mod = mod, Info = entry.Value } - ) - .ToArray(); - - // extract latest versions - IDictionary> updatesByMod = new Dictionary>(); - foreach (var result in results) + // prepare search model + List searchMods = new List(); + foreach (IModMetadata mod in mods) { - IModMetadata mod = result.Mod; - ModInfoModel remoteInfo = result.Info; - - // handle error - if (remoteInfo.Error != null) - { - if (mod.UpdateStatus?.Version == null) - mod.SetUpdateStatus(new ModUpdateStatus(remoteInfo.Error)); - if (mod.PreviewUpdateStatus?.Version == null) - mod.SetUpdateStatus(new ModUpdateStatus(remoteInfo.Error)); - - this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: {remoteInfo.Error}", LogLevel.Trace); + if (!mod.HasManifest()) continue; - } - // normalise versions - ISemanticVersion localVersion = mod.DataRecord?.GetLocalVersionForUpdateChecks(mod.Manifest.Version) ?? mod.Manifest.Version; - bool validVersion = SemanticVersion.TryParse(mod.DataRecord?.GetRemoteVersionForUpdateChecks(remoteInfo.Version) ?? remoteInfo.Version, out ISemanticVersion remoteVersion); - bool validPreviewVersion = SemanticVersion.TryParse(remoteInfo.PreviewVersion, out ISemanticVersion remotePreviewVersion); + string[] updateKeys = mod.Manifest.UpdateKeys ?? new string[0]; + searchMods.Add(new ModSearchEntryModel(mod.Manifest.UniqueID, updateKeys.Except(suppressUpdateChecks).ToArray())); + } - if (!validVersion && mod.UpdateStatus?.Version == null) - mod.SetUpdateStatus(new ModUpdateStatus($"Version is invalid: {remoteInfo.Version}")); - if (!validPreviewVersion && mod.PreviewUpdateStatus?.Version == null) - mod.SetPreviewUpdateStatus(new ModUpdateStatus($"Version is invalid: {remoteInfo.PreviewVersion}")); + // fetch results + this.Monitor.Log($" Checking for updates to {searchMods.Count} mods...", LogLevel.Trace); + IDictionary results = client.GetModInfo(searchMods.ToArray()); - if (!validVersion && !validPreviewVersion) - { - this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: Mod has invalid versions. version: {remoteInfo.Version}, preview version: {remoteInfo.PreviewVersion}", LogLevel.Trace); + // extract update alerts & errors + var updates = new List>(); + var errors = new StringBuilder(); + foreach (IModMetadata mod in mods.OrderBy(p => p.DisplayName)) + { + // link to update-check data + if (!mod.HasManifest() || !results.TryGetValue(mod.Manifest.UniqueID, out ModEntryModel result)) continue; - } - - // compare versions - bool isPreviewUpdate = validPreviewVersion && localVersion.IsNewerThan(remoteVersion) && remotePreviewVersion.IsNewerThan(localVersion); - bool isUpdate = (validVersion && remoteVersion.IsNewerThan(localVersion)) || isPreviewUpdate; + mod.SetUpdateData(result); - this.VerboseLog($" {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version}{(!localVersion.Equals(mod.Manifest.Version) ? $" [{localVersion}]" : "")} => {(isPreviewUpdate ? remoteInfo.PreviewVersion : remoteInfo.Version)}" : "okay")}."); - if (isUpdate) + // handle errors + if (result.Errors != null && result.Errors.Any()) { - if (!updatesByMod.TryGetValue(mod, out Tuple other) || (isPreviewUpdate ? remotePreviewVersion : remoteVersion).IsNewerThan(other.Item2 ? other.Item1.PreviewVersion : other.Item1.Version)) - { - updatesByMod[mod] = new Tuple(remoteInfo, isPreviewUpdate); - - if (isPreviewUpdate) - mod.SetPreviewUpdateStatus(new ModUpdateStatus(remotePreviewVersion)); - else - mod.SetUpdateStatus(new ModUpdateStatus(remoteVersion)); - } + errors.AppendLine(result.Errors.Length == 1 + ? $" {mod.DisplayName} update error: {result.Errors[0]}" + : $" {mod.DisplayName} update errors:\n - {string.Join("\n - ", result.Errors)}" + ); } - } - // set mods to have no updates - foreach (IModMetadata mod in results.Select(item => item.Mod) - .Where(item => !updatesByMod.ContainsKey(item))) - { - mod.SetUpdateStatus(new ModUpdateStatus()); - mod.SetPreviewUpdateStatus(new ModUpdateStatus()); + // parse versions + ISemanticVersion localVersion = mod.DataRecord?.GetLocalVersionForUpdateChecks(mod.Manifest.Version) ?? mod.Manifest.Version; + ISemanticVersion latestVersion = result.Version != null + ? mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.Version) ?? new SemanticVersion(result.Version) + : null; + ISemanticVersion optionalVersion = result.PreviewVersion != null + ? (mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.PreviewVersion) ?? new SemanticVersion(result.PreviewVersion)) + : null; + + // show update alerts + if (this.IsValidUpdate(localVersion, latestVersion, useBetaChannel: true)) + updates.Add(Tuple.Create(mod, latestVersion, result.Url)); + else if (this.IsValidUpdate(localVersion, optionalVersion, useBetaChannel: localVersion.IsPrerelease())) + updates.Add(Tuple.Create(mod, optionalVersion, result.Url)); } - // output - if (updatesByMod.Any()) + // show update errors + if (errors.Length != 0) + this.Monitor.Log("Encountered errors fetching updates for some mods:\n" + errors.ToString(), LogLevel.Trace); + + // show update alerts + if (updates.Any()) { this.Monitor.Newline(); - this.Monitor.Log($"You can update {updatesByMod.Count} mod{(updatesByMod.Count != 1 ? "s" : "")}:", LogLevel.Alert); - foreach (var entry in updatesByMod.OrderBy(p => p.Key.DisplayName)) - this.Monitor.Log($" {entry.Key.DisplayName} {(entry.Value.Item2 ? entry.Value.Item1.PreviewVersion : entry.Value.Item1.Version)}: {entry.Value.Item1.Url}", LogLevel.Alert); + this.Monitor.Log($"You can update {updates.Count} mod{(updates.Count != 1 ? "s" : "")}:", LogLevel.Alert); + foreach (var entry in updates) + { + IModMetadata mod = entry.Item1; + ISemanticVersion newVersion = entry.Item2; + string newUrl = entry.Item3; + this.Monitor.Log($" {mod.DisplayName} {newVersion}: {newUrl}", LogLevel.Alert); + } } else this.Monitor.Log(" All mods up to date.", LogLevel.Trace); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 916dd053..fcd54c34 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -110,7 +110,6 @@ - diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs new file mode 100644 index 00000000..0f268231 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs @@ -0,0 +1,30 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +{ + /// Metadata about a mod. + public class ModEntryModel + { + /********* + ** Accessors + *********/ + /// The mod's unique ID (if known). + public string ID { get; set; } + + /// The mod name. + public string Name { get; set; } + + /// The mod's latest release number. + public string Version { get; set; } + + /// The mod's web URL. + public string Url { get; set; } + + /// The mod's latest optional release, if newer than . + public string PreviewVersion { get; set; } + + /// The web URL to the mod's latest optional release, if newer than . + public string PreviewUrl { get; set; } + + /// The errors that occurred while fetching update data. + public string[] Errors { get; set; } = new string[0]; + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs deleted file mode 100644 index c8e296f0..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi -{ - /// Generic metadata about a mod. - public class ModInfoModel - { - /********* - ** Accessors - *********/ - /// The mod name. - public string Name { get; set; } - - /// The mod's latest release number. - public string Version { get; set; } - - /// The mod's latest optional release, if newer than . - public string PreviewVersion { get; set; } - - /// The mod's web URL. - public string Url { get; set; } - - /// The error message indicating why the mod is invalid (if applicable). - public string Error { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an empty instance. - public ModInfoModel() - { - // needed for JSON deserialising - } - - /// Construct an instance. - /// The mod name. - /// The semantic version for the mod's latest release. - /// The semantic version for the mod's latest preview release, if available and different from . - /// The mod's web URL. - /// The error message indicating why the mod is invalid (if applicable). - public ModInfoModel(string name, string version, string url, string previewVersion = null, string error = null) - { - this.Name = name; - this.Version = version; - this.PreviewVersion = previewVersion; - this.Url = url; - this.Error = error; // mainly initialised here for the JSON deserialiser - } - - /// Construct an instance. - /// The error message indicating why the mod is invalid. - public ModInfoModel(string error) - { - this.Error = error; - } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs index c0ee34ea..ffca32ca 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System; using System.Linq; namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi @@ -10,10 +10,11 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi ** Accessors *********/ /// The namespaced mod keys to search. + [Obsolete] public string[] ModKeys { get; set; } - /// Whether to allow non-semantic versions, instead of returning an error for those. - public bool AllowInvalidVersions { get; set; } + /// The mods for which to find data. + public ModSearchEntryModel[] Mods { get; set; } /********* @@ -26,12 +27,10 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi } /// Construct an instance. - /// The namespaced mod keys to search. - /// Whether to allow non-semantic versions, instead of returning an error for those. - public ModSearchModel(IEnumerable modKeys, bool allowInvalidVersions) + /// The mods to search. + public ModSearchModel(ModSearchEntryModel[] mods) { - this.ModKeys = modKeys.ToArray(); - this.AllowInvalidVersions = allowInvalidVersions; + this.Mods = mods.ToArray(); } } } diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs new file mode 100644 index 00000000..bca47647 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs @@ -0,0 +1,34 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +{ + /// Specifies the identifiers for a mod to match. + public class ModSearchEntryModel + { + /********* + ** Accessors + *********/ + /// The unique mod ID. + public string ID { get; set; } + + /// The namespaced mod update keys (if available). + public string[] UpdateKeys { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public ModSearchEntryModel() + { + // needed for JSON deserialising + } + + /// Construct an instance. + /// The unique mod ID. + /// The namespaced mod update keys (if available). + public ModSearchEntryModel(string id, string[] updateKeys) + { + this.ID = id; + this.UpdateKeys = updateKeys ?? new string[0]; + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs index d94b0259..892dfeba 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Net; using Newtonsoft.Json; @@ -31,44 +30,16 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi this.Version = version; } - /// Get the latest SMAPI version. - /// The mod keys for which to fetch the latest version. - public IDictionary GetModInfo(params string[] modKeys) + /// Get metadata about a set of mods from the web API. + /// The mod keys for which to fetch the latest version. + public IDictionary GetModInfo(params ModSearchEntryModel[] mods) { - return this.Post>( + return this.Post>( $"v{this.Version}/mods", - new ModSearchModel(modKeys, allowInvalidVersions: true) + new ModSearchModel(mods) ); } - /// Get the latest version for a mod. - /// The update keys to search. - public ISemanticVersion GetLatestVersion(string[] updateKeys) - { - if (!updateKeys.Any()) - return null; - - // fetch update results - ModInfoModel[] results = this - .GetModInfo(updateKeys) - .Values - .Where(p => p.Error == null) - .ToArray(); - if (!results.Any()) - return null; - - ISemanticVersion latest = null; - foreach (ModInfoModel result in results) - { - if (!SemanticVersion.TryParse(result.PreviewVersion ?? result.Version, out ISemanticVersion cur)) - continue; - - if (latest == null || cur.IsNewerThan(latest)) - latest = cur; - } - return latest; - } - /********* ** Private methods -- cgit From e149f20583b90c37964c48b8f13777493611aecf Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 Jun 2018 16:50:15 -0400 Subject: remove seasonal tilesheet patch (#552) This is no longer needed (the changes were added to the game in SDV 1.3.19), and caused an issue since it left out the tilesheet reloading. --- src/SMAPI/Framework/Patching/GameLocationPatch.cs | 60 ----------------------- src/SMAPI/Program.cs | 2 +- src/SMAPI/StardewModdingAPI.csproj | 1 - 3 files changed, 1 insertion(+), 62 deletions(-) delete mode 100644 src/SMAPI/Framework/Patching/GameLocationPatch.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI/Framework/Patching/GameLocationPatch.cs b/src/SMAPI/Framework/Patching/GameLocationPatch.cs deleted file mode 100644 index c0216b8f..00000000 --- a/src/SMAPI/Framework/Patching/GameLocationPatch.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Reflection; -using Harmony; -using StardewValley; -using xTile.Tiles; - -namespace StardewModdingAPI.Framework.Patching -{ - /// A Harmony patch for the method. - internal class GameLocationPatch : IHarmonyPatch - { - /********* - ** Accessors - *********/ - /// A unique name for this patch. - public string Name => $"{nameof(GameLocation)}.{nameof(GameLocation.updateSeasonalTileSheets)}"; - - - /********* - ** Public methods - *********/ - /// Apply the Harmony patch. - /// The Harmony instance. - public void Apply(HarmonyInstance harmony) - { - MethodInfo method = AccessTools.Method(typeof(GameLocation), nameof(GameLocation.updateSeasonalTileSheets)); - MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(GameLocationPatch.Prefix)); - - harmony.Patch(method, new HarmonyMethod(prefix), null); - } - - - /********* - ** Private methods - *********/ - /// An implementation of which correctly handles custom map tilesheets. - /// The location instance being patched. - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument name is defined by Harmony.")] - private static bool Prefix(GameLocation __instance) - { - if (!__instance.IsOutdoors || __instance.Name.Equals("Desert")) - return false; - foreach (TileSheet tilesheet in __instance.Map.TileSheets) - { - string imageSource = tilesheet.ImageSource; - string imageFile = Path.GetFileName(imageSource); - if (imageFile.StartsWith("spring_") || imageFile.StartsWith("summer_") || imageFile.StartsWith("fall_") || imageFile.StartsWith("winter_")) - { - string imageDir = Path.GetDirectoryName(imageSource); - if (string.IsNullOrWhiteSpace(imageDir)) - imageDir = "Maps"; - tilesheet.ImageSource = Path.Combine(imageDir, Game1.currentSeason + "_" + imageFile.Split('_')[1]); - } - } - - return false; - } - } -} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index ccdf98ef..53f3749a 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -168,7 +168,7 @@ namespace StardewModdingAPI // apply game patches new GamePatcher(this.Monitor).Apply( - new GameLocationPatch() + // new GameLocationPatch() ); } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index fcd54c34..bd9e2422 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -290,7 +290,6 @@ - -- cgit From ebc603844a3931bedbd512761ba8f152a4f5a09c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Jun 2018 19:49:34 -0400 Subject: update to Mono.Cecil 0.10 --- docs/release-notes.md | 1 + .../ModLoading/AssemblyDefinitionResolver.cs | 9 ------ src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 37 +++++++++++++++------- .../Framework/ModLoading/PlatformAssemblyMap.cs | 12 +++++-- .../ModLoading/Rewriters/FieldReplaceRewriter.cs | 2 +- .../Rewriters/FieldToPropertyRewriter.cs | 2 +- .../ModLoading/Rewriters/MethodParentRewriter.cs | 2 +- .../ModLoading/Rewriters/TypeReferenceRewriter.cs | 2 +- src/SMAPI/Program.cs | 4 +-- src/SMAPI/StardewModdingAPI.csproj | 12 +++---- src/SMAPI/packages.config | 2 +- 11 files changed, 49 insertions(+), 36 deletions(-) (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/docs/release-notes.md b/docs/release-notes.md index fa6501a3..5a9e4e92 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -83,6 +83,7 @@ * uses net field events where available; * lays groundwork for tracking events for multiple players. * Split mod DB out of `StardewModdingAPI.config.json` into its own file. + * Updated to Mono.Cecil 0.10. ## 2.5.5 * For players: diff --git a/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs b/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs index d85a9a28..33cd6ebd 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs @@ -36,15 +36,6 @@ namespace StardewModdingAPI.Framework.ModLoading /// The assembly reader parameters. public override AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters) => this.ResolveName(name.Name) ?? base.Resolve(name, parameters); - /// Resolve an assembly reference. - /// The assembly full name (including version, etc). - public override AssemblyDefinition Resolve(string fullName) => this.ResolveName(fullName) ?? base.Resolve(fullName); - - /// Resolve an assembly reference. - /// The assembly full name (including version, etc). - /// The assembly reader parameters. - public override AssemblyDefinition Resolve(string fullName, ReaderParameters parameters) => this.ResolveName(fullName) ?? base.Resolve(fullName, parameters); - /********* ** Private methods diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 2fb2aba7..d41774a9 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -12,7 +12,7 @@ using StardewModdingAPI.Metadata; namespace StardewModdingAPI.Framework.ModLoading { /// Preprocesses and loads mod assemblies. - internal class AssemblyLoader + internal class AssemblyLoader : IDisposable { /********* ** Properties @@ -20,9 +20,6 @@ namespace StardewModdingAPI.Framework.ModLoading /// Encapsulates monitoring and logging. private readonly IMonitor Monitor; - /// Whether to enable developer mode logging. - private readonly bool IsDeveloperMode; - /// Metadata for mapping assemblies to the current platform. private readonly PlatformAssemblyMap AssemblyMap; @@ -32,6 +29,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// A minimal assembly definition resolver which resolves references to known loaded assemblies. private readonly AssemblyDefinitionResolver AssemblyDefinitionResolver; + /// The objects to dispose as part of this instance. + private readonly HashSet Disposables = new HashSet(); + /********* ** Public methods @@ -39,13 +39,11 @@ namespace StardewModdingAPI.Framework.ModLoading /// Construct an instance. /// The current game platform. /// Encapsulates monitoring and logging. - /// Whether to enable developer mode logging. - public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool isDeveloperMode) + public AssemblyLoader(Platform targetPlatform, IMonitor monitor) { this.Monitor = monitor; - this.IsDeveloperMode = isDeveloperMode; - this.AssemblyMap = Constants.GetAssemblyMap(targetPlatform); - this.AssemblyDefinitionResolver = new AssemblyDefinitionResolver(); + this.AssemblyMap = this.TrackForDisposal(Constants.GetAssemblyMap(targetPlatform)); + this.AssemblyDefinitionResolver = this.TrackForDisposal(new AssemblyDefinitionResolver()); // generate type => assembly lookup for types which should be rewritten this.TypeAssemblies = new Dictionary(); @@ -144,10 +142,26 @@ namespace StardewModdingAPI.Framework.ModLoading .FirstOrDefault(p => p.GetName().Name == shortName); } + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + foreach (IDisposable instance in this.Disposables) + instance.Dispose(); + } + /********* ** Private methods *********/ + /// Track an object for disposal as part of the assembly loader. + /// The instance type. + /// The disposable instance. + private T TrackForDisposal(T instance) where T : IDisposable + { + this.Disposables.Add(instance); + return instance; + } + /**** ** Assembly parsing ****/ @@ -166,9 +180,8 @@ namespace StardewModdingAPI.Framework.ModLoading // read assembly byte[] assemblyBytes = File.ReadAllBytes(file.FullName); - AssemblyDefinition assembly; - using (Stream readStream = new MemoryStream(assemblyBytes)) - assembly = AssemblyDefinition.ReadAssembly(readStream, new ReaderParameters(ReadingMode.Deferred) { AssemblyResolver = assemblyResolver }); + Stream readStream = this.TrackForDisposal(new MemoryStream(assemblyBytes)); + AssemblyDefinition assembly = this.TrackForDisposal(AssemblyDefinition.ReadAssembly(readStream, new ReaderParameters(ReadingMode.Immediate) { AssemblyResolver = assemblyResolver, InMemory = true })); // skip if already visited if (visitedAssemblyNames.Contains(assembly.Name.Name)) diff --git a/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs b/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs index f0a28b4a..01460dce 100644 --- a/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs +++ b/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -7,7 +8,7 @@ using StardewModdingAPI.Internal; namespace StardewModdingAPI.Framework.ModLoading { /// Metadata for mapping assemblies to the current . - internal class PlatformAssemblyMap + internal class PlatformAssemblyMap : IDisposable { /********* ** Accessors @@ -50,7 +51,14 @@ namespace StardewModdingAPI.Framework.ModLoading // cache assembly metadata this.Targets = targetAssemblies; this.TargetReferences = this.Targets.ToDictionary(assembly => assembly, assembly => AssemblyNameReference.Parse(assembly.FullName)); - this.TargetModules = this.Targets.ToDictionary(assembly => assembly, assembly => ModuleDefinition.ReadModule(assembly.Modules.Single().FullyQualifiedName)); + this.TargetModules = this.Targets.ToDictionary(assembly => assembly, assembly => ModuleDefinition.ReadModule(assembly.Modules.Single().FullyQualifiedName, new ReaderParameters { InMemory = true })); + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + foreach (ModuleDefinition module in this.TargetModules.Values) + module.Dispose(); } } } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs index 63358b39..806a074f 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs @@ -42,7 +42,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters if (!this.IsMatch(instruction)) return InstructionHandleResult.None; - FieldReference newRef = module.Import(this.ToField); + FieldReference newRef = module.ImportReference(this.ToField); cil.Replace(instruction, cil.Create(instruction.OpCode, newRef)); return InstructionHandleResult.Rewritten; } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs index b1fa377a..e6ede9e3 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs @@ -50,7 +50,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters return InstructionHandleResult.None; string methodPrefix = instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldfld ? "get" : "set"; - MethodReference propertyRef = module.Import(this.Type.GetMethod($"{methodPrefix}_{this.PropertyName}")); + MethodReference propertyRef = module.ImportReference(this.Type.GetMethod($"{methodPrefix}_{this.PropertyName}")); cil.Replace(instruction, cil.Create(OpCodes.Call, propertyRef)); return InstructionHandleResult.Rewritten; diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs index 974fcf4c..99bd9125 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs @@ -64,7 +64,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters return InstructionHandleResult.None; MethodReference methodRef = (MethodReference)instruction.Operand; - methodRef.DeclaringType = module.Import(this.ToType); + methodRef.DeclaringType = module.ImportReference(this.ToType); return InstructionHandleResult.Rewritten; } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs index 74f2fcdd..5c7db902 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs @@ -135,7 +135,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters { // root type if (type.FullName == this.FromTypeName) - return module.Import(this.ToType); + return module.ImportReference(this.ToType); // generic arguments if (type is GenericInstanceType genericType) diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 53f3749a..7b5176a0 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -686,7 +686,7 @@ namespace StardewModdingAPI // show update errors if (errors.Length != 0) - this.Monitor.Log("Encountered errors fetching updates for some mods:\n" + errors.ToString(), LogLevel.Trace); + this.Monitor.Log("Encountered errors fetching updates for some mods:\n" + errors, LogLevel.Trace); // show update alerts if (updates.Any()) @@ -796,7 +796,7 @@ namespace StardewModdingAPI ); // get assembly loaders - AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor, this.Settings.DeveloperMode); + AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor); AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); InterfaceProxyFactory proxyFactory = new InterfaceProxyFactory(); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index bd9e2422..2e3ba1cd 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -58,16 +58,16 @@ False ..\lib\0Harmony.dll - - ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll + + ..\packages\Mono.Cecil.0.10.0\lib\net40\Mono.Cecil.dll True - - ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Mdb.dll + + ..\packages\Mono.Cecil.0.10.0\lib\net40\Mono.Cecil.Mdb.dll True - - ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Pdb.dll + + ..\packages\Mono.Cecil.0.10.0\lib\net40\Mono.Cecil.Pdb.dll True diff --git a/src/SMAPI/packages.config b/src/SMAPI/packages.config index 3347b037..84c6bed0 100644 --- a/src/SMAPI/packages.config +++ b/src/SMAPI/packages.config @@ -1,6 +1,6 @@  - + \ No newline at end of file -- cgit From 71efadf2322a622bc5a74614b1575d2680a84165 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Jun 2018 21:26:42 -0400 Subject: add project for toolkit interfaces visible to SMAPI mods (#532) --- build/common.targets | 8 +- build/prepare-install-package.targets | 4 + build/prepare-nuget-package.targets | 2 + src/SMAPI.Installer/InteractiveInstaller.cs | 2 + src/SMAPI.ModBuildConfig/build/smapi.targets | 5 ++ src/SMAPI.Tests/StardewModdingAPI.Tests.csproj | 4 + src/SMAPI.sln | 89 ++++++++++++---------- src/SMAPI/StardewModdingAPI.csproj | 4 + .../Properties/AssemblyInfo.cs | 4 + ...StardewModdingAPI.Toolkit.CoreInterfaces.csproj | 19 +++++ .../Properties/AssemblyInfo.cs | 3 - .../StardewModdingAPI.Toolkit.csproj | 8 ++ 12 files changed, 105 insertions(+), 47 deletions(-) create mode 100644 src/StardewModdingAPI.Toolkit.CoreInterfaces/Properties/AssemblyInfo.cs create mode 100644 src/StardewModdingAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/build/common.targets b/build/common.targets index 0b8f278f..5b6511f8 100644 --- a/build/common.targets +++ b/build/common.targets @@ -25,7 +25,7 @@ - + @@ -113,10 +113,14 @@ - + + + + + diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index 5e00d663..33a92f71 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -41,6 +41,8 @@ + + @@ -56,6 +58,8 @@ + + diff --git a/build/prepare-nuget-package.targets b/build/prepare-nuget-package.targets index 5dbc5508..11d1845e 100644 --- a/build/prepare-nuget-package.targets +++ b/build/prepare-nuget-package.targets @@ -13,6 +13,8 @@ + + diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index f9239604..1221f659 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -96,6 +96,8 @@ namespace StardewModdingApi.Installer yield return GetInstallPath("StardewModdingAPI.metadata.json"); yield return GetInstallPath("StardewModdingAPI.Toolkit.dll"); yield return GetInstallPath("StardewModdingAPI.Toolkit.pdb"); + yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.dll"); + yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.pdb"); yield return GetInstallPath("StardewModdingAPI.xml"); yield return GetInstallPath("System.ValueTuple.dll"); yield return GetInstallPath("steam_appid.txt"); diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index d33a9637..0869be66 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -92,6 +92,11 @@ false true + + $(GamePath)\StardewModdingAPI.Toolkit.CoreInterfaces.dll + false + true + $(GamePath)\xTile.dll false diff --git a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj index 9a203b3b..b2d98d23 100644 --- a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj +++ b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj @@ -76,6 +76,10 @@ {f1a573b0-f436-472c-ae29-0b91ea6b9f8f} StardewModdingAPI + + {d5cfd923-37f1-4bc3-9be8-e506e202ac28} + StardewModdingAPI.Toolkit.CoreInterfaces + {ea5cfd2e-9453-4d29-b80f-8e0ea23f4ac6} StardewModdingAPI.Toolkit diff --git a/src/SMAPI.sln b/src/SMAPI.sln index 0eb42cce..005537de 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -62,6 +62,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Mods.Save EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Toolkit", "StardewModdingAPI.Toolkit\StardewModdingAPI.Toolkit.csproj", "{EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Toolkit.CoreInterfaces", "StardewModdingAPI.Toolkit.CoreInterfaces\StardewModdingAPI.Toolkit.CoreInterfaces.csproj", "{D5CFD923-37F1-4BC3-9BE8-E506E202AC28}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution SMAPI.Internal\SMAPI.Internal.projitems*{443ddf81-6aaf-420a-a610-3459f37e5575}*SharedItemsImports = 4 @@ -70,50 +72,53 @@ Global SMAPI.Internal\SMAPI.Internal.projitems*{f1a573b0-f436-472c-ae29-0b91ea6b9f8f}*SharedItemsImports = 4 EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Default = Debug|Default - Release|Default = Release|Default + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {28480467-1A48-46A7-99F8-236D95225359}.Debug|Default.ActiveCfg = Debug|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Debug|Default.Build.0 = Debug|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Release|Default.ActiveCfg = Release|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Release|Default.Build.0 = Release|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|Default.ActiveCfg = Debug|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|Default.Build.0 = Debug|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|Default.ActiveCfg = Release|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|Default.Build.0 = Release|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|Default.ActiveCfg = Debug|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|Default.Build.0 = Debug|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Default.ActiveCfg = Release|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Default.Build.0 = Release|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Default.ActiveCfg = Debug|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Default.Build.0 = Debug|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Default.ActiveCfg = Release|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Default.Build.0 = Release|x86 - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Default.ActiveCfg = Debug|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Default.Build.0 = Debug|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Default.ActiveCfg = Release|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Default.Build.0 = Release|Any CPU - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Default.ActiveCfg = Debug|x86 - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Default.Build.0 = Debug|x86 - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Default.ActiveCfg = Release|x86 - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Default.Build.0 = Release|x86 - {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|Default.ActiveCfg = Debug|Any CPU - {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|Default.Build.0 = Debug|Any CPU - {80AD8528-AA49-4731-B4A6-C691845815A1}.Release|Default.ActiveCfg = Release|Any CPU - {80AD8528-AA49-4731-B4A6-C691845815A1}.Release|Default.Build.0 = Release|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|Default.ActiveCfg = Debug|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|Default.Build.0 = Debug|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|Default.ActiveCfg = Release|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|Default.Build.0 = Release|Any CPU - {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Debug|Default.ActiveCfg = Debug|x86 - {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Debug|Default.Build.0 = Debug|x86 - {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Release|Default.ActiveCfg = Release|x86 - {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Release|Default.Build.0 = Release|x86 - {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Debug|Default.ActiveCfg = Debug|Any CPU - {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Debug|Default.Build.0 = Debug|Any CPU - {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Release|Default.ActiveCfg = Release|Any CPU - {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Release|Default.Build.0 = Release|Any CPU + {28480467-1A48-46A7-99F8-236D95225359}.Debug|Any CPU.ActiveCfg = Debug|x86 + {28480467-1A48-46A7-99F8-236D95225359}.Debug|Any CPU.Build.0 = Debug|x86 + {28480467-1A48-46A7-99F8-236D95225359}.Release|Any CPU.ActiveCfg = Release|x86 + {28480467-1A48-46A7-99F8-236D95225359}.Release|Any CPU.Build.0 = Release|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|Any CPU.ActiveCfg = Debug|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|Any CPU.Build.0 = Debug|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|Any CPU.ActiveCfg = Release|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|Any CPU.Build.0 = Release|x86 + {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|Any CPU.ActiveCfg = Debug|x86 + {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|Any CPU.Build.0 = Debug|x86 + {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Any CPU.ActiveCfg = Release|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Any CPU.ActiveCfg = Debug|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Any CPU.Build.0 = Debug|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Any CPU.ActiveCfg = Release|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Any CPU.Build.0 = Release|x86 + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Any CPU.Build.0 = Release|Any CPU + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Any CPU.ActiveCfg = Debug|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Any CPU.Build.0 = Debug|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Any CPU.ActiveCfg = Release|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Any CPU.Build.0 = Release|x86 + {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {80AD8528-AA49-4731-B4A6-C691845815A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {80AD8528-AA49-4731-B4A6-C691845815A1}.Release|Any CPU.Build.0 = Release|Any CPU + {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|Any CPU.Build.0 = Release|Any CPU + {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Debug|Any CPU.ActiveCfg = Debug|x86 + {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Debug|Any CPU.Build.0 = Debug|x86 + {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Release|Any CPU.ActiveCfg = Release|x86 + {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Release|Any CPU.Build.0 = Release|x86 + {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Release|Any CPU.Build.0 = Release|Any CPU + {D5CFD923-37F1-4BC3-9BE8-E506E202AC28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D5CFD923-37F1-4BC3-9BE8-E506E202AC28}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D5CFD923-37F1-4BC3-9BE8-E506E202AC28}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D5CFD923-37F1-4BC3-9BE8-E506E202AC28}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 2e3ba1cd..a429d204 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -336,6 +336,10 @@ + + {d5cfd923-37f1-4bc3-9be8-e506e202ac28} + StardewModdingAPI.Toolkit.CoreInterfaces + {ea5cfd2e-9453-4d29-b80f-8e0ea23f4ac6} StardewModdingAPI.Toolkit diff --git a/src/StardewModdingAPI.Toolkit.CoreInterfaces/Properties/AssemblyInfo.cs b/src/StardewModdingAPI.Toolkit.CoreInterfaces/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..a29ba6cf --- /dev/null +++ b/src/StardewModdingAPI.Toolkit.CoreInterfaces/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Reflection; + +[assembly: AssemblyTitle("SMAPI.Toolkit.CoreInterfaces")] +[assembly: AssemblyDescription("Provides toolkit interfaces which are available to SMAPI mods.")] diff --git a/src/StardewModdingAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj b/src/StardewModdingAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj new file mode 100644 index 00000000..e003122e --- /dev/null +++ b/src/StardewModdingAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj @@ -0,0 +1,19 @@ + + + + net4.5;netstandard2.0 + false + + + + ..\..\bin\$(Configuration)\SMAPI.Toolkit.CoreInterfaces + StardewModdingAPI + + + + + + + + + diff --git a/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs b/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs index 22dcdd96..1bb19e8c 100644 --- a/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs +++ b/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs @@ -3,8 +3,5 @@ using System.Runtime.CompilerServices; [assembly: AssemblyTitle("SMAPI.Toolkit")] [assembly: AssemblyDescription("A library which encapsulates mod-handling logic for mod managers and tools. Not intended for use by mods.")] -[assembly: AssemblyProduct("SMAPI Toolkit")] -[assembly: AssemblyVersion("0.1.0")] -[assembly: AssemblyFileVersion("0.1.0")] [assembly: InternalsVisibleTo("StardewModdingAPI")] [assembly: InternalsVisibleTo("StardewModdingAPI.Web")] diff --git a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj index dda7c17c..904f6786 100644 --- a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj +++ b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj @@ -9,12 +9,20 @@ ..\..\bin\$(Configuration)\SMAPI.Toolkit + + + + + + + + -- cgit From 316242eeb2b6b6e711ab98f64c147a59c1d0aab8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Jun 2018 21:29:10 -0400 Subject: merge ISemanticVersion interfaces into new project (#532) --- .../StardewModdingAPI.Mods.SaveBackup.csproj | 4 + src/SMAPI.Web/Controllers/IndexController.cs | 2 +- src/SMAPI/Constants.cs | 16 +--- .../ModLoading/Rewriters/TypeReferenceRewriter.cs | 11 ++- src/SMAPI/ISemanticVersion.cs | 62 -------------- src/SMAPI/Metadata/InstructionMetadata.cs | 3 + src/SMAPI/Program.cs | 2 +- src/SMAPI/SemanticVersion.cs | 31 ++++--- src/SMAPI/StardewModdingAPI.csproj | 1 - .../ISemanticVersion.cs | 62 ++++++++++++++ src/StardewModdingAPI.Toolkit/ISemanticVersion.cs | 46 ----------- src/StardewModdingAPI.Toolkit/SemanticVersion.cs | 95 ++++++++++++++-------- 12 files changed, 158 insertions(+), 177 deletions(-) delete mode 100644 src/SMAPI/ISemanticVersion.cs create mode 100644 src/StardewModdingAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs delete mode 100644 src/StardewModdingAPI.Toolkit/ISemanticVersion.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj b/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj index 44fff536..afba15a1 100644 --- a/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj +++ b/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj @@ -51,6 +51,10 @@ StardewModdingAPI False + + {d5cfd923-37f1-4bc3-9be8-e506e202ac28} + StardewModdingAPI.Toolkit.CoreInterfaces + diff --git a/src/SMAPI.Web/Controllers/IndexController.cs b/src/SMAPI.Web/Controllers/IndexController.cs index f4ade7de..09bad112 100644 --- a/src/SMAPI.Web/Controllers/IndexController.cs +++ b/src/SMAPI.Web/Controllers/IndexController.cs @@ -130,7 +130,7 @@ namespace StardewModdingAPI.Web.Controllers Match match = Regex.Match(asset.FileName, @"SMAPI-(?[\d\.]+(?:-.+)?)-installer(?-for-developers)?.zip"); if (!match.Success || !SemanticVersion.TryParse(match.Groups["version"].Value, out ISemanticVersion version)) continue; - bool isBeta = version.Tag != null; + bool isBeta = version.IsPrerelease(); bool isForDevs = match.Groups["forDevs"].Success; yield return new ReleaseVersion(release, asset, version, isBeta, isForDevs); diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 65ca7866..df70d603 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -38,10 +38,10 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } + public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.6-beta.17"); /// The minimum supported version of Stardew Valley. - public static ISemanticVersion MinimumGameVersion { get; } + public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.21"); /// The maximum supported version of Stardew Valley. public static ISemanticVersion MaximumGameVersion { get; } = null; @@ -70,9 +70,6 @@ namespace StardewModdingAPI /**** ** Internal ****/ - /// SMAPI's current semantic version as a mod toolkit version. - internal static Toolkit.ISemanticVersion ApiVersionForToolkit { get; } - /// The URL of the SMAPI home page. internal const string HomePageUrl = "https://smapi.io"; @@ -113,15 +110,6 @@ namespace StardewModdingAPI /********* ** Internal methods *********/ - /// Initialise the static values. - static Constants() - { - Constants.ApiVersionForToolkit = new Toolkit.SemanticVersion("2.6-beta.17"); - Constants.MinimumGameVersion = new GameVersion("1.3.20"); - - Constants.ApiVersion = new SemanticVersion(Constants.ApiVersionForToolkit); - } - /// Get metadata for mapping assemblies to the current platform. /// The target game platform. internal static PlatformAssemblyMap GetAssemblyMap(Platform targetPlatform) diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs index 5c7db902..de9c439a 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs @@ -17,6 +17,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// The new type to reference. private readonly Type ToType; + /// A lambda which indicates whether a matching type reference should be rewritten. + private readonly Func ShouldRewrite; + /********* ** Public methods @@ -24,11 +27,13 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// Construct an instance. /// The full type name to which to find references. /// The new type to reference. - public TypeReferenceRewriter(string fromTypeFullName, Type toType) + /// A lambda which indicates whether a matching type reference should be rewritten. + public TypeReferenceRewriter(string fromTypeFullName, Type toType, Func shouldRewrite = null) : base(fromTypeFullName, InstructionHandleResult.None) { this.FromTypeName = fromTypeFullName; this.ToType = toType; + this.ShouldRewrite = shouldRewrite ?? (type => true); } /// Perform the predefined logic for a method if applicable. @@ -135,7 +140,11 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters { // root type if (type.FullName == this.FromTypeName) + { + if (!this.ShouldRewrite(type)) + return type; return module.ImportReference(this.ToType); + } // generic arguments if (type is GenericInstanceType genericType) diff --git a/src/SMAPI/ISemanticVersion.cs b/src/SMAPI/ISemanticVersion.cs deleted file mode 100644 index 961ef777..00000000 --- a/src/SMAPI/ISemanticVersion.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; - -namespace StardewModdingAPI -{ - /// A semantic version with an optional release tag. - public interface ISemanticVersion : IComparable, IEquatable - { - /********* - ** Accessors - *********/ - /// The major version incremented for major API changes. - int MajorVersion { get; } - - /// The minor version incremented for backwards-compatible changes. - int MinorVersion { get; } - - /// The patch version for backwards-compatible bug fixes. - int PatchVersion { get; } - - /// An optional build tag. - string Build { get; } - - - /********* - ** Accessors - *********/ - /// Whether this is a pre-release version. - bool IsPrerelease(); - - /// Get whether this version is older than the specified version. - /// The version to compare with this instance. - bool IsOlderThan(ISemanticVersion other); - - /// Get whether this version is older than the specified version. - /// The version to compare with this instance. - /// The specified version is not a valid semantic version. - bool IsOlderThan(string other); - - /// Get whether this version is newer than the specified version. - /// The version to compare with this instance. - bool IsNewerThan(ISemanticVersion other); - - /// Get whether this version is newer than the specified version. - /// The version to compare with this instance. - /// The specified version is not a valid semantic version. - bool IsNewerThan(string other); - - /// Get whether this version is between two specified versions (inclusively). - /// The minimum version. - /// The maximum version. - bool IsBetween(ISemanticVersion min, ISemanticVersion max); - - /// Get whether this version is between two specified versions (inclusively). - /// The minimum version. - /// The maximum version. - /// One of the specified versions is not a valid semantic version. - bool IsBetween(string min, string max); - - /// Get a string representation of the version. - string ToString(); - } -} diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 1063ae71..543d44a7 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -37,6 +37,9 @@ namespace StardewModdingAPI.Metadata // rewrite for SMAPI 2.0 new VirtualEntryCallRemover(), + // rewrite for SMAPI 2.6 + new TypeReferenceRewriter("StardewModdingAPI.ISemanticVersion", typeof(ISemanticVersion), type => type.Scope.Name == "StardewModdingAPI"), // moved to SMAPI.Toolkit.CoreInterfaces + // rewrite for Stardew Valley 1.3 new StaticFieldToConstantRewriter(typeof(Game1), "tileSize", Game1.tileSize), diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 7b5176a0..a51d6380 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -585,7 +585,7 @@ namespace StardewModdingAPI #if !SMAPI_FOR_WINDOWS url = url.Replace("https://", "http://"); // workaround for OpenSSL issues with the game's bundled Mono on Linux/Mac #endif - WebApiClient client = new WebApiClient(url, Constants.ApiVersionForToolkit); + WebApiClient client = new WebApiClient(url, Constants.ApiVersion); this.Monitor.Log("Checking for updates...", LogLevel.Trace); // check SMAPI version diff --git a/src/SMAPI/SemanticVersion.cs b/src/SMAPI/SemanticVersion.cs index c4dd1912..587ff286 100644 --- a/src/SMAPI/SemanticVersion.cs +++ b/src/SMAPI/SemanticVersion.cs @@ -10,23 +10,23 @@ namespace StardewModdingAPI ** Properties *********/ /// The underlying semantic version implementation. - private readonly Toolkit.ISemanticVersion Version; + private readonly ISemanticVersion Version; /********* ** Accessors *********/ /// The major version incremented for major API changes. - public int MajorVersion => this.Version.Major; + public int MajorVersion => this.Version.MajorVersion; /// The minor version incremented for backwards-compatible changes. - public int MinorVersion => this.Version.Minor; + public int MinorVersion => this.Version.MinorVersion; /// The patch version for backwards-compatible bug fixes. - public int PatchVersion => this.Version.Patch; + public int PatchVersion => this.Version.PatchVersion; /// An optional build tag. - public string Build => this.Version.Tag; + public string Build => this.Version.Build; /********* @@ -56,7 +56,7 @@ namespace StardewModdingAPI /// Construct an instance. /// The underlying semantic version implementation. - internal SemanticVersion(Toolkit.ISemanticVersion version) + internal SemanticVersion(ISemanticVersion version) { this.Version = version; } @@ -64,7 +64,7 @@ namespace StardewModdingAPI /// Whether this is a pre-release version. public bool IsPrerelease() { - return !string.IsNullOrWhiteSpace(this.Build); + return this.Version.IsPrerelease(); } /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. @@ -73,15 +73,14 @@ namespace StardewModdingAPI /// The implementation is defined by Semantic Version 2.0 (http://semver.org/). public int CompareTo(ISemanticVersion other) { - Toolkit.ISemanticVersion toolkitOther = new Toolkit.SemanticVersion(other.MajorVersion, other.MinorVersion, other.PatchVersion, other.Build); - return this.Version.CompareTo(toolkitOther); + return this.Version.CompareTo(other); } /// Get whether this version is older than the specified version. /// The version to compare with this instance. public bool IsOlderThan(ISemanticVersion other) { - return this.CompareTo(other) < 0; + return this.Version.IsOlderThan(other); } /// Get whether this version is older than the specified version. @@ -89,14 +88,14 @@ namespace StardewModdingAPI /// The specified version is not a valid semantic version. public bool IsOlderThan(string other) { - return this.IsOlderThan(new SemanticVersion(other)); + return this.Version.IsOlderThan(other); } /// Get whether this version is newer than the specified version. /// The version to compare with this instance. public bool IsNewerThan(ISemanticVersion other) { - return this.CompareTo(other) > 0; + return this.Version.IsNewerThan(other); } /// Get whether this version is newer than the specified version. @@ -104,7 +103,7 @@ namespace StardewModdingAPI /// The specified version is not a valid semantic version. public bool IsNewerThan(string other) { - return this.IsNewerThan(new SemanticVersion(other)); + return this.Version.IsNewerThan(other); } /// Get whether this version is between two specified versions (inclusively). @@ -112,7 +111,7 @@ namespace StardewModdingAPI /// The maximum version. public bool IsBetween(ISemanticVersion min, ISemanticVersion max) { - return this.CompareTo(min) >= 0 && this.CompareTo(max) <= 0; + return this.Version.IsBetween(min, max); } /// Get whether this version is between two specified versions (inclusively). @@ -121,7 +120,7 @@ namespace StardewModdingAPI /// One of the specified versions is not a valid semantic version. public bool IsBetween(string min, string max) { - return this.IsBetween(new SemanticVersion(min), new SemanticVersion(max)); + return this.Version.IsBetween(min, max); } /// Indicates whether the current object is equal to another object of the same type. @@ -144,7 +143,7 @@ namespace StardewModdingAPI /// Returns whether parsing the version succeeded. internal static bool TryParse(string version, out ISemanticVersion parsed) { - if (Toolkit.SemanticVersion.TryParse(version, out Toolkit.ISemanticVersion versionImpl)) + if (Toolkit.SemanticVersion.TryParse(version, out ISemanticVersion versionImpl)) { parsed = new SemanticVersion(versionImpl); return true; diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index a429d204..c872391c 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -280,7 +280,6 @@ - diff --git a/src/StardewModdingAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs b/src/StardewModdingAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs new file mode 100644 index 00000000..961ef777 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs @@ -0,0 +1,62 @@ +using System; + +namespace StardewModdingAPI +{ + /// A semantic version with an optional release tag. + public interface ISemanticVersion : IComparable, IEquatable + { + /********* + ** Accessors + *********/ + /// The major version incremented for major API changes. + int MajorVersion { get; } + + /// The minor version incremented for backwards-compatible changes. + int MinorVersion { get; } + + /// The patch version for backwards-compatible bug fixes. + int PatchVersion { get; } + + /// An optional build tag. + string Build { get; } + + + /********* + ** Accessors + *********/ + /// Whether this is a pre-release version. + bool IsPrerelease(); + + /// Get whether this version is older than the specified version. + /// The version to compare with this instance. + bool IsOlderThan(ISemanticVersion other); + + /// Get whether this version is older than the specified version. + /// The version to compare with this instance. + /// The specified version is not a valid semantic version. + bool IsOlderThan(string other); + + /// Get whether this version is newer than the specified version. + /// The version to compare with this instance. + bool IsNewerThan(ISemanticVersion other); + + /// Get whether this version is newer than the specified version. + /// The version to compare with this instance. + /// The specified version is not a valid semantic version. + bool IsNewerThan(string other); + + /// Get whether this version is between two specified versions (inclusively). + /// The minimum version. + /// The maximum version. + bool IsBetween(ISemanticVersion min, ISemanticVersion max); + + /// Get whether this version is between two specified versions (inclusively). + /// The minimum version. + /// The maximum version. + /// One of the specified versions is not a valid semantic version. + bool IsBetween(string min, string max); + + /// Get a string representation of the version. + string ToString(); + } +} diff --git a/src/StardewModdingAPI.Toolkit/ISemanticVersion.cs b/src/StardewModdingAPI.Toolkit/ISemanticVersion.cs deleted file mode 100644 index ca62d393..00000000 --- a/src/StardewModdingAPI.Toolkit/ISemanticVersion.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; - -namespace StardewModdingAPI.Toolkit -{ - /// A semantic version with an optional release tag. - public interface ISemanticVersion : IComparable, IEquatable - { - /********* - ** Accessors - *********/ - /// The major version incremented for major API changes. - int Major { get; } - - /// The minor version incremented for backwards-compatible changes. - int Minor { get; } - - /// The patch version for backwards-compatible bug fixes. - int Patch { get; } - - /// An optional prerelease tag. - string Tag { get; } - - - /********* - ** Accessors - *********/ - /// Whether this is a pre-release version. - bool IsPrerelease(); - - /// Get whether this version is older than the specified version. - /// The version to compare with this instance. - bool IsOlderThan(ISemanticVersion other); - - /// Get whether this version is newer than the specified version. - /// The version to compare with this instance. - bool IsNewerThan(ISemanticVersion other); - - /// Get whether this version is between two specified versions (inclusively). - /// The minimum version. - /// The maximum version. - bool IsBetween(ISemanticVersion min, ISemanticVersion max); - - /// Get a string representation of the version. - string ToString(); - } -} diff --git a/src/StardewModdingAPI.Toolkit/SemanticVersion.cs b/src/StardewModdingAPI.Toolkit/SemanticVersion.cs index de480416..156d58ce 100644 --- a/src/StardewModdingAPI.Toolkit/SemanticVersion.cs +++ b/src/StardewModdingAPI.Toolkit/SemanticVersion.cs @@ -31,16 +31,16 @@ namespace StardewModdingAPI.Toolkit ** Accessors *********/ /// The major version incremented for major API changes. - public int Major { get; } + public int MajorVersion { get; } /// The minor version incremented for backwards-compatible changes. - public int Minor { get; } + public int MinorVersion { get; } /// The patch version for backwards-compatible bug fixes. - public int Patch { get; } + public int PatchVersion { get; } /// An optional prerelease tag. - public string Tag { get; } + public string Build { get; } /********* @@ -53,10 +53,10 @@ namespace StardewModdingAPI.Toolkit /// An optional prerelease tag. public SemanticVersion(int major, int minor, int patch, string tag = null) { - this.Major = major; - this.Minor = minor; - this.Patch = patch; - this.Tag = this.GetNormalisedTag(tag); + this.MajorVersion = major; + this.MinorVersion = minor; + this.PatchVersion = patch; + this.Build = this.GetNormalisedTag(tag); this.AssertValid(); } @@ -69,9 +69,9 @@ namespace StardewModdingAPI.Toolkit if (version == null) throw new ArgumentNullException(nameof(version), "The input version can't be null."); - this.Major = version.Major; - this.Minor = version.Minor; - this.Patch = version.Build; + this.MajorVersion = version.Major; + this.MinorVersion = version.Minor; + this.PatchVersion = version.Build; this.AssertValid(); } @@ -90,10 +90,10 @@ namespace StardewModdingAPI.Toolkit throw new FormatException($"The input '{version}' isn't a valid semantic version."); // initialise - this.Major = int.Parse(match.Groups["major"].Value); - this.Minor = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0; - this.Patch = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0; - this.Tag = match.Groups["prerelease"].Success ? this.GetNormalisedTag(match.Groups["prerelease"].Value) : null; + 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["prerelease"].Success ? this.GetNormalisedTag(match.Groups["prerelease"].Value) : null; this.AssertValid(); } @@ -105,7 +105,7 @@ namespace StardewModdingAPI.Toolkit { if (other == null) throw new ArgumentNullException(nameof(other)); - return this.CompareTo(other.Major, other.Minor, other.Patch, other.Tag); + return this.CompareTo(other.MajorVersion, other.MinorVersion, other.PatchVersion, other.Build); } /// Indicates whether the current object is equal to another object of the same type. @@ -119,7 +119,7 @@ namespace StardewModdingAPI.Toolkit /// Whether this is a pre-release version. public bool IsPrerelease() { - return !string.IsNullOrWhiteSpace(this.Tag); + return !string.IsNullOrWhiteSpace(this.Build); } /// Get whether this version is older than the specified version. @@ -129,6 +129,14 @@ namespace StardewModdingAPI.Toolkit return this.CompareTo(other) < 0; } + /// Get whether this version is older than the specified version. + /// The version to compare with this instance. + /// The specified version is not a valid semantic version. + public bool IsOlderThan(string other) + { + return this.IsOlderThan(new SemanticVersion(other)); + } + /// Get whether this version is newer than the specified version. /// The version to compare with this instance. public bool IsNewerThan(ISemanticVersion other) @@ -136,6 +144,14 @@ namespace StardewModdingAPI.Toolkit return this.CompareTo(other) > 0; } + /// Get whether this version is newer than the specified version. + /// The version to compare with this instance. + /// The specified version is not a valid semantic version. + public bool IsNewerThan(string other) + { + return this.IsNewerThan(new SemanticVersion(other)); + } + /// Get whether this version is between two specified versions (inclusively). /// The minimum version. /// The maximum version. @@ -144,16 +160,25 @@ namespace StardewModdingAPI.Toolkit return this.CompareTo(min) >= 0 && this.CompareTo(max) <= 0; } + /// Get whether this version is between two specified versions (inclusively). + /// The minimum version. + /// The maximum version. + /// One of the specified versions is not a valid semantic version. + public bool IsBetween(string min, string max) + { + return this.IsBetween(new SemanticVersion(min), new SemanticVersion(max)); + } + /// Get a string representation of the version. public override string ToString() { // version - string result = this.Patch != 0 - ? $"{this.Major}.{this.Minor}.{this.Patch}" - : $"{this.Major}.{this.Minor}"; + string result = this.PatchVersion != 0 + ? $"{this.MajorVersion}.{this.MinorVersion}.{this.PatchVersion}" + : $"{this.MajorVersion}.{this.MinorVersion}"; // tag - string tag = this.Tag; + string tag = this.Build; if (tag != null) result += $"-{tag}"; return result; @@ -201,17 +226,17 @@ namespace StardewModdingAPI.Toolkit const int curOlder = -1; // compare stable versions - if (this.Major != otherMajor) - return this.Major.CompareTo(otherMajor); - if (this.Minor != otherMinor) - return this.Minor.CompareTo(otherMinor); - if (this.Patch != otherPatch) - return this.Patch.CompareTo(otherPatch); - if (this.Tag == otherTag) + if (this.MajorVersion != otherMajor) + return this.MajorVersion.CompareTo(otherMajor); + if (this.MinorVersion != otherMinor) + return this.MinorVersion.CompareTo(otherMinor); + if (this.PatchVersion != otherPatch) + return this.PatchVersion.CompareTo(otherPatch); + if (this.Build == otherTag) return same; // stable supercedes pre-release - bool curIsStable = string.IsNullOrWhiteSpace(this.Tag); + bool curIsStable = string.IsNullOrWhiteSpace(this.Build); bool otherIsStable = string.IsNullOrWhiteSpace(otherTag); if (curIsStable) return curNewer; @@ -219,7 +244,7 @@ namespace StardewModdingAPI.Toolkit return curOlder; // compare two pre-release tag values - string[] curParts = this.Tag.Split('.', '-'); + string[] curParts = this.Build.Split('.', '-'); string[] otherParts = otherTag.Split('.', '-'); for (int i = 0; i < curParts.Length; i++) { @@ -248,15 +273,15 @@ namespace StardewModdingAPI.Toolkit /// Assert that the current version is valid. private void AssertValid() { - if (this.Major < 0 || this.Minor < 0 || this.Patch < 0) + if (this.MajorVersion < 0 || this.MinorVersion < 0 || this.PatchVersion < 0) throw new FormatException($"{this} isn't a valid semantic version. The major, minor, and patch numbers can't be negative."); - if (this.Major == 0 && this.Minor == 0 && this.Patch == 0) + if (this.MajorVersion == 0 && this.MinorVersion == 0 && this.PatchVersion == 0) throw new FormatException($"{this} isn't a valid semantic version. At least one of the major, minor, and patch numbers must be more than zero."); - if (this.Tag != null) + if (this.Build != null) { - if (this.Tag.Trim() == "") + if (this.Build.Trim() == "") throw new FormatException($"{this} isn't a valid semantic version. The tag cannot be a blank string (but may be omitted)."); - if (!Regex.IsMatch(this.Tag, $"^{SemanticVersion.TagPattern}$", RegexOptions.IgnoreCase)) + if (!Regex.IsMatch(this.Build, $"^{SemanticVersion.TagPattern}$", RegexOptions.IgnoreCase)) throw new FormatException($"{this} isn't a valid semantic version. The tag is invalid."); } } -- cgit From b08e27d13a1f0c82656df95212fc40588b3b5314 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Jun 2018 21:51:51 -0400 Subject: merge IManifest interfaces into new project (#532) --- src/SMAPI.Tests/Core/ModResolverTests.cs | 25 +++--- src/SMAPI/Framework/ModHelpers/ModHelper.cs | 4 +- src/SMAPI/Framework/ModLoading/ModResolver.cs | 9 +- src/SMAPI/Framework/Models/Manifest.cs | 97 ---------------------- .../Framework/Models/ManifestContentPackFor.cs | 36 -------- src/SMAPI/Framework/Models/ManifestDependency.cs | 40 --------- .../Serialisation/SemanticVersionConverter.cs | 40 --------- src/SMAPI/IManifest.cs | 44 ---------- src/SMAPI/IManifestContentPackFor.cs | 12 --- src/SMAPI/IManifestDependency.cs | 18 ---- src/SMAPI/Metadata/InstructionMetadata.cs | 7 +- src/SMAPI/Program.cs | 3 +- src/SMAPI/StardewModdingAPI.csproj | 7 -- .../IManifest.cs | 44 ++++++++++ .../IManifestContentPackFor.cs | 12 +++ .../IManifestDependency.cs | 18 ++++ .../Converters/SemanticVersionConverter.cs | 1 - .../Serialisation/Models/Manifest.cs | 37 +++++++-- .../Serialisation/Models/ManifestContentPackFor.cs | 8 +- .../Serialisation/Models/ManifestDependency.cs | 8 +- 20 files changed, 142 insertions(+), 328 deletions(-) delete mode 100644 src/SMAPI/Framework/Models/Manifest.cs delete mode 100644 src/SMAPI/Framework/Models/ManifestContentPackFor.cs delete mode 100644 src/SMAPI/Framework/Models/ManifestDependency.cs delete mode 100644 src/SMAPI/Framework/Serialisation/SemanticVersionConverter.cs delete mode 100644 src/SMAPI/IManifest.cs delete mode 100644 src/SMAPI/IManifestContentPackFor.cs delete mode 100644 src/SMAPI/IManifestDependency.cs create mode 100644 src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifest.cs create mode 100644 src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestContentPackFor.cs create mode 100644 src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestDependency.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index 2fbeb9b6..a0fe2023 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -7,9 +7,9 @@ using Newtonsoft.Json; using NUnit.Framework; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.ModData; -using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation.Models; namespace StardewModdingAPI.Tests.Core { @@ -472,17 +472,18 @@ namespace StardewModdingAPI.Tests.Core /// The value. private Manifest GetManifest(string id = null, string name = null, string version = null, string entryDll = null, string contentPackForID = null, string minimumApiVersion = null, IManifestDependency[] dependencies = null) { - return new Manifest( - uniqueID: id ?? $"{Sample.String()}.{Sample.String()}", - name: name ?? id ?? Sample.String(), - author: Sample.String(), - description: Sample.String(), - version: version != null ? new SemanticVersion(version) : new SemanticVersion(Sample.Int(), Sample.Int(), Sample.Int(), Sample.String()), - entryDll: entryDll ?? $"{Sample.String()}.dll", - contentPackFor: contentPackForID != null ? new ManifestContentPackFor(contentPackForID) : null, - minimumApiVersion: minimumApiVersion != null ? new SemanticVersion(minimumApiVersion) : null, - dependencies: dependencies - ); + return new Manifest + { + UniqueID = id ?? $"{Sample.String()}.{Sample.String()}", + Name = name ?? id ?? Sample.String(), + Author = Sample.String(), + Description = Sample.String(), + Version = version != null ? new SemanticVersion(version) : new SemanticVersion(Sample.Int(), Sample.Int(), Sample.Int(), Sample.String()), + EntryDll = entryDll ?? $"{Sample.String()}.dll", + ContentPackFor = contentPackForID != null ? new ManifestContentPackFor { UniqueID = contentPackForID } : null, + MinimumApiVersion = minimumApiVersion != null ? new SemanticVersion(minimumApiVersion) : null, + Dependencies = dependencies + }; } /// Get a randomised basic manifest. diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index 18904857..d9498e83 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -4,8 +4,8 @@ using System.IO; using System.Linq; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Input; -using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation.Models; using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Framework.ModHelpers @@ -185,7 +185,7 @@ namespace StardewModdingAPI.Framework.ModHelpers author: author, description: description, version: version, - contentPackFor: new ManifestContentPackFor(this.ModID) + contentPackFor: this.ModID ); // create content pack diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 3366e8c1..fde921e6 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -4,10 +4,9 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using StardewModdingAPI.Framework.ModData; -using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation.Models; using StardewModdingAPI.Toolkit.Utilities; -using ToolkitManifest = StardewModdingAPI.Toolkit.Serialisation.Models.Manifest; namespace StardewModdingAPI.Framework.ModLoading { @@ -33,15 +32,13 @@ namespace StardewModdingAPI.Framework.ModLoading string path = Path.Combine(modDir.FullName, "manifest.json"); try { - ToolkitManifest rawManifest = jsonHelper.ReadJsonFile(path); - if (rawManifest == null) + manifest = jsonHelper.ReadJsonFile(path); + if (manifest == null) { error = File.Exists(path) ? "its manifest is invalid." : "it doesn't have a manifest."; } - else - manifest = new Manifest(rawManifest); } catch (SParseException ex) { diff --git a/src/SMAPI/Framework/Models/Manifest.cs b/src/SMAPI/Framework/Models/Manifest.cs deleted file mode 100644 index 92ffe0dc..00000000 --- a/src/SMAPI/Framework/Models/Manifest.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; - -namespace StardewModdingAPI.Framework.Models -{ - /// A manifest which describes a mod for SMAPI. - internal class Manifest : IManifest - { - /********* - ** Accessors - *********/ - /// The mod name. - public string Name { get; } - - /// A brief description of the mod. - public string Description { get; } - - /// The mod author's name. - public string Author { get; } - - /// The mod version. - public ISemanticVersion Version { get; } - - /// The minimum SMAPI version required by this mod, if any. - public ISemanticVersion MinimumApiVersion { get; } - - /// The name of the DLL in the directory that has the method. Mutually exclusive with . - public string EntryDll { get; } - - /// The mod which will read this as a content pack. Mutually exclusive with . - public IManifestContentPackFor ContentPackFor { get; } - - /// The other mods that must be loaded before this mod. - public IManifestDependency[] Dependencies { get; } - - /// The namespaced mod IDs to query for updates (like Nexus:541). - public string[] UpdateKeys { get; set; } - - /// The unique mod ID. - public string UniqueID { get; } - - /// Any manifest fields which didn't match a valid field. - [JsonExtensionData] - public IDictionary ExtraFields { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The toolkit manifest. - public Manifest(Toolkit.Serialisation.Models.Manifest manifest) - : this( - uniqueID: manifest.UniqueID, - name: manifest.Name, - author: manifest.Author, - description: manifest.Description, - version: manifest.Version != null ? new SemanticVersion(manifest.Version) : null, - entryDll: manifest.EntryDll, - minimumApiVersion: manifest.MinimumApiVersion != null ? new SemanticVersion(manifest.MinimumApiVersion) : null, - contentPackFor: manifest.ContentPackFor != null ? new ManifestContentPackFor(manifest.ContentPackFor) : null, - dependencies: manifest.Dependencies?.Select(p => p != null ? (IManifestDependency)new ManifestDependency(p) : null).ToArray(), - updateKeys: manifest.UpdateKeys, - extraFields: manifest.ExtraFields - ) - { } - - /// Construct an instance for a transitional content pack. - /// The unique mod ID. - /// The mod name. - /// The mod author's name. - /// A brief description of the mod. - /// The mod version. - /// The name of the DLL in the directory that has the method. Mutually exclusive with . - /// The minimum SMAPI version required by this mod, if any. - /// The modID which will read this as a content pack. Mutually exclusive with . - /// The other mods that must be loaded before this mod. - /// The namespaced mod IDs to query for updates (like Nexus:541). - /// Any manifest fields which didn't match a valid field. - public Manifest(string uniqueID, string name, string author, string description, ISemanticVersion version, string entryDll = null, ISemanticVersion minimumApiVersion = null, IManifestContentPackFor contentPackFor = null, IManifestDependency[] dependencies = null, string[] updateKeys = null, IDictionary extraFields = null) - { - this.Name = name; - this.Author = author; - this.Description = description; - this.Version = version; - this.UniqueID = uniqueID; - this.UpdateKeys = new string[0]; - this.EntryDll = entryDll; - this.ContentPackFor = contentPackFor; - this.MinimumApiVersion = minimumApiVersion; - this.Dependencies = dependencies ?? new IManifestDependency[0]; - this.UpdateKeys = updateKeys ?? new string[0]; - this.ExtraFields = extraFields; - } - } -} diff --git a/src/SMAPI/Framework/Models/ManifestContentPackFor.cs b/src/SMAPI/Framework/Models/ManifestContentPackFor.cs deleted file mode 100644 index 90e20c6a..00000000 --- a/src/SMAPI/Framework/Models/ManifestContentPackFor.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace StardewModdingAPI.Framework.Models -{ - /// Indicates which mod can read the content pack represented by the containing manifest. - internal class ManifestContentPackFor : IManifestContentPackFor - { - /********* - ** Accessors - *********/ - /// The unique ID of the mod which can read this content pack. - public string UniqueID { get; } - - /// The minimum required version (if any). - public ISemanticVersion MinimumVersion { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The toolkit instance. - public ManifestContentPackFor(Toolkit.Serialisation.Models.ManifestContentPackFor contentPackFor) - { - this.UniqueID = contentPackFor.UniqueID; - this.MinimumVersion = contentPackFor.MinimumVersion != null ? new SemanticVersion(contentPackFor.MinimumVersion) : null; - } - - /// Construct an instance. - /// The unique ID of the mod which can read this content pack. - /// The minimum required version (if any). - public ManifestContentPackFor(string uniqueID, ISemanticVersion minimumVersion = null) - { - this.UniqueID = uniqueID; - this.MinimumVersion = minimumVersion; - } - } -} diff --git a/src/SMAPI/Framework/Models/ManifestDependency.cs b/src/SMAPI/Framework/Models/ManifestDependency.cs deleted file mode 100644 index e92597f3..00000000 --- a/src/SMAPI/Framework/Models/ManifestDependency.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace StardewModdingAPI.Framework.Models -{ - /// A mod dependency listed in a mod manifest. - internal class ManifestDependency : IManifestDependency - { - /********* - ** Accessors - *********/ - /// The unique mod ID to require. - public string UniqueID { get; } - - /// The minimum required version (if any). - public ISemanticVersion MinimumVersion { get; } - - /// Whether the dependency must be installed to use the mod. - public bool IsRequired { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The toolkit instance. - public ManifestDependency(Toolkit.Serialisation.Models.ManifestDependency dependency) - : this(dependency.UniqueID, dependency.MinimumVersion?.ToString(), dependency.IsRequired) { } - - /// Construct an instance. - /// The unique mod ID to require. - /// The minimum required version (if any). - /// Whether the dependency must be installed to use the mod. - public ManifestDependency(string uniqueID, string minimumVersion, bool required = true) - { - this.UniqueID = uniqueID; - this.MinimumVersion = !string.IsNullOrWhiteSpace(minimumVersion) - ? new SemanticVersion(minimumVersion) - : null; - this.IsRequired = required; - } - } -} diff --git a/src/SMAPI/Framework/Serialisation/SemanticVersionConverter.cs b/src/SMAPI/Framework/Serialisation/SemanticVersionConverter.cs deleted file mode 100644 index 3e05a440..00000000 --- a/src/SMAPI/Framework/Serialisation/SemanticVersionConverter.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Newtonsoft.Json.Linq; -using StardewModdingAPI.Toolkit.Serialisation; -using StardewModdingAPI.Toolkit.Serialisation.Converters; - -namespace StardewModdingAPI.Framework.Serialisation -{ - /// Handles deserialisation of . - internal class SemanticVersionConverter : SimpleReadOnlyConverter - { - /********* - ** Protected methods - *********/ - /// Read a JSON object. - /// The JSON object to read. - /// The path to the current JSON node. - protected override ISemanticVersion ReadObject(JObject obj, string path) - { - int major = obj.ValueIgnoreCase("MajorVersion"); - int minor = obj.ValueIgnoreCase("MinorVersion"); - int patch = obj.ValueIgnoreCase("PatchVersion"); - string build = obj.ValueIgnoreCase("Build"); - if (build == "0") - build = null; // '0' from incorrect examples in old SMAPI documentation - - return new SemanticVersion(major, minor, patch, build); - } - - /// Read a JSON string. - /// The JSON string value. - /// The path to the current JSON node. - protected override ISemanticVersion ReadString(string str, string path) - { - if (string.IsNullOrWhiteSpace(str)) - return null; - if (!SemanticVersion.TryParse(str, out ISemanticVersion version)) - throw new SParseException($"Can't parse semantic version from invalid value '{str}', should be formatted like 1.2, 1.2.30, or 1.2.30-beta (path: {path})."); - return version; - } - } -} diff --git a/src/SMAPI/IManifest.cs b/src/SMAPI/IManifest.cs deleted file mode 100644 index 6c07d374..00000000 --- a/src/SMAPI/IManifest.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Collections.Generic; - -namespace StardewModdingAPI -{ - /// A manifest which describes a mod for SMAPI. - public interface IManifest - { - /********* - ** Accessors - *********/ - /// The mod name. - string Name { get; } - - /// A brief description of the mod. - string Description { get; } - - /// The mod author's name. - string Author { get; } - - /// The mod version. - ISemanticVersion Version { get; } - - /// The minimum SMAPI version required by this mod, if any. - ISemanticVersion MinimumApiVersion { get; } - - /// The unique mod ID. - string UniqueID { get; } - - /// The name of the DLL in the directory that has the method. Mutually exclusive with . - string EntryDll { get; } - - /// The mod which will read this as a content pack. Mutually exclusive with . - IManifestContentPackFor ContentPackFor { get; } - - /// The other mods that must be loaded before this mod. - IManifestDependency[] Dependencies { get; } - - /// The namespaced mod IDs to query for updates (like Nexus:541). - string[] UpdateKeys { get; } - - /// Any manifest fields which didn't match a valid field. - IDictionary ExtraFields { get; } - } -} diff --git a/src/SMAPI/IManifestContentPackFor.cs b/src/SMAPI/IManifestContentPackFor.cs deleted file mode 100644 index f05a3873..00000000 --- a/src/SMAPI/IManifestContentPackFor.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace StardewModdingAPI -{ - /// Indicates which mod can read the content pack represented by the containing manifest. - public interface IManifestContentPackFor - { - /// The unique ID of the mod which can read this content pack. - string UniqueID { get; } - - /// The minimum required version (if any). - ISemanticVersion MinimumVersion { get; } - } -} diff --git a/src/SMAPI/IManifestDependency.cs b/src/SMAPI/IManifestDependency.cs deleted file mode 100644 index e86cd1f4..00000000 --- a/src/SMAPI/IManifestDependency.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace StardewModdingAPI -{ - /// A mod dependency listed in a mod manifest. - public interface IManifestDependency - { - /********* - ** Accessors - *********/ - /// The unique mod ID to require. - string UniqueID { get; } - - /// The minimum required version (if any). - ISemanticVersion MinimumVersion { get; } - - /// Whether the dependency must be installed to use the mod. - bool IsRequired { get; } - } -} diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 543d44a7..c5128eb1 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -37,8 +37,11 @@ namespace StardewModdingAPI.Metadata // rewrite for SMAPI 2.0 new VirtualEntryCallRemover(), - // rewrite for SMAPI 2.6 - new TypeReferenceRewriter("StardewModdingAPI.ISemanticVersion", typeof(ISemanticVersion), type => type.Scope.Name == "StardewModdingAPI"), // moved to SMAPI.Toolkit.CoreInterfaces + // rewrite for SMAPI 2.6 (types moved into SMAPI.Toolkit.CoreInterfaces) + new TypeReferenceRewriter("StardewModdingAPI.IManifest", typeof(IManifest), type => type.Scope.Name == "StardewModdingAPI"), + new TypeReferenceRewriter("StardewModdingAPI.IManifestContentPackFor", typeof(IManifestContentPackFor), type => type.Scope.Name == "StardewModdingAPI"), + new TypeReferenceRewriter("StardewModdingAPI.IManifestDependency", typeof(IManifestDependency), type => type.Scope.Name == "StardewModdingAPI"), + new TypeReferenceRewriter("StardewModdingAPI.ISemanticVersion", typeof(ISemanticVersion), type => type.Scope.Name == "StardewModdingAPI"), // rewrite for Stardew Valley 1.3 new StaticFieldToConstantRewriter(typeof(Game1), "tileSize", Game1.tileSize), diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index a51d6380..1b276988 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -201,8 +201,7 @@ namespace StardewModdingAPI new StringEnumConverter(), new ColorConverter(), new PointConverter(), - new RectangleConverter(), - new Framework.Serialisation.SemanticVersionConverter() + new RectangleConverter() }; foreach (JsonConverter converter in converters) this.JsonHelper.JsonSettings.Converters.Add(converter); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index c872391c..4852f70c 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -126,11 +126,7 @@ - - - - @@ -186,7 +182,6 @@ - @@ -258,7 +253,6 @@ - @@ -275,7 +269,6 @@ - diff --git a/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifest.cs b/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifest.cs new file mode 100644 index 00000000..6c07d374 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifest.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI +{ + /// A manifest which describes a mod for SMAPI. + public interface IManifest + { + /********* + ** Accessors + *********/ + /// The mod name. + string Name { get; } + + /// A brief description of the mod. + string Description { get; } + + /// The mod author's name. + string Author { get; } + + /// The mod version. + ISemanticVersion Version { get; } + + /// The minimum SMAPI version required by this mod, if any. + ISemanticVersion MinimumApiVersion { get; } + + /// The unique mod ID. + string UniqueID { get; } + + /// The name of the DLL in the directory that has the method. Mutually exclusive with . + string EntryDll { get; } + + /// The mod which will read this as a content pack. Mutually exclusive with . + IManifestContentPackFor ContentPackFor { get; } + + /// The other mods that must be loaded before this mod. + IManifestDependency[] Dependencies { get; } + + /// The namespaced mod IDs to query for updates (like Nexus:541). + string[] UpdateKeys { get; } + + /// Any manifest fields which didn't match a valid field. + IDictionary ExtraFields { get; } + } +} diff --git a/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestContentPackFor.cs b/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestContentPackFor.cs new file mode 100644 index 00000000..f05a3873 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestContentPackFor.cs @@ -0,0 +1,12 @@ +namespace StardewModdingAPI +{ + /// Indicates which mod can read the content pack represented by the containing manifest. + public interface IManifestContentPackFor + { + /// The unique ID of the mod which can read this content pack. + string UniqueID { get; } + + /// The minimum required version (if any). + ISemanticVersion MinimumVersion { get; } + } +} diff --git a/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestDependency.cs b/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestDependency.cs new file mode 100644 index 00000000..e86cd1f4 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestDependency.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI +{ + /// A mod dependency listed in a mod manifest. + public interface IManifestDependency + { + /********* + ** Accessors + *********/ + /// The unique mod ID to require. + string UniqueID { get; } + + /// The minimum required version (if any). + ISemanticVersion MinimumVersion { get; } + + /// Whether the dependency must be installed to use the mod. + bool IsRequired { get; } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs index 2ddaa1bf..eff95d1f 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs @@ -1,5 +1,4 @@ using Newtonsoft.Json.Linq; -using StardewModdingAPI.Toolkit.Serialisation.Models; namespace StardewModdingAPI.Toolkit.Serialisation.Converters { diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs index 68987dd1..6ec57258 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs @@ -5,7 +5,7 @@ using StardewModdingAPI.Toolkit.Serialisation.Converters; namespace StardewModdingAPI.Toolkit.Serialisation.Models { /// A manifest which describes a mod for SMAPI. - public class Manifest + public class Manifest : IManifest { /********* ** Accessors @@ -20,21 +20,23 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Models public string Author { get; set; } /// The mod version. - public SemanticVersion Version { get; set; } + [JsonConverter(typeof(SemanticVersionConverter))] + public ISemanticVersion Version { get; set; } /// The minimum SMAPI version required by this mod, if any. - public SemanticVersion MinimumApiVersion { get; set; } + [JsonConverter(typeof(SemanticVersionConverter))] + public ISemanticVersion MinimumApiVersion { get; set; } /// The name of the DLL in the directory that has the Entry method. Mutually exclusive with . public string EntryDll { get; set; } /// The mod which will read this as a content pack. Mutually exclusive with . [JsonConverter(typeof(ManifestContentPackForConverter))] - public ManifestContentPackFor ContentPackFor { get; set; } + public IManifestContentPackFor ContentPackFor { get; set; } /// The other mods that must be loaded before this mod. [JsonConverter(typeof(ManifestDependencyArrayConverter))] - public ManifestDependency[] Dependencies { get; set; } + public IManifestDependency[] Dependencies { get; set; } /// The namespaced mod IDs to query for updates (like Nexus:541). public string[] UpdateKeys { get; set; } @@ -45,5 +47,30 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Models /// Any manifest fields which didn't match a valid field. [JsonExtensionData] public IDictionary ExtraFields { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public Manifest() { } + + /// Construct an instance for a transitional content pack. + /// The unique mod ID. + /// The mod name. + /// The mod author's name. + /// A brief description of the mod. + /// The mod version. + /// The modID which will read this as a content pack. + public Manifest(string uniqueID, string name, string author, string description, ISemanticVersion version, string contentPackFor = null) + { + this.Name = name; + this.Author = author; + this.Description = description; + this.Version = version; + this.UniqueID = uniqueID; + this.UpdateKeys = new string[0]; + this.ContentPackFor = new ManifestContentPackFor { UniqueID = contentPackFor }; + } } } diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs index 00546533..64808dcf 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs @@ -1,7 +1,10 @@ +using Newtonsoft.Json; +using StardewModdingAPI.Toolkit.Serialisation.Converters; + namespace StardewModdingAPI.Toolkit.Serialisation.Models { /// Indicates which mod can read the content pack represented by the containing manifest. - public class ManifestContentPackFor + public class ManifestContentPackFor : IManifestContentPackFor { /********* ** Accessors @@ -10,6 +13,7 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Models public string UniqueID { get; set; } /// The minimum required version (if any). - public SemanticVersion MinimumVersion { get; set; } + [JsonConverter(typeof(SemanticVersionConverter))] + public ISemanticVersion MinimumVersion { get; set; } } } diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs index d902f9ac..67e733dd 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs @@ -1,7 +1,10 @@ +using Newtonsoft.Json; +using StardewModdingAPI.Toolkit.Serialisation.Converters; + namespace StardewModdingAPI.Toolkit.Serialisation.Models { /// A mod dependency listed in a mod manifest. - public class ManifestDependency + public class ManifestDependency : IManifestDependency { /********* ** Accessors @@ -10,7 +13,8 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Models public string UniqueID { get; set; } /// The minimum required version (if any). - public SemanticVersion MinimumVersion { get; set; } + [JsonConverter(typeof(SemanticVersionConverter))] + public ISemanticVersion MinimumVersion { get; set; } /// Whether the dependency must be installed to use the mod. public bool IsRequired { get; set; } -- cgit From 5f19e4f2035c36f9c6c882da3767d6f29409db1c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 27 Jun 2018 00:05:53 -0400 Subject: move mod DB parsing into toolkit (#532) --- src/SMAPI.Tests/Core/ModResolverTests.cs | 2 +- src/SMAPI/Constants.cs | 26 ---- src/SMAPI/Framework/IModMetadata.cs | 2 +- src/SMAPI/Framework/ModData/ModDataField.cs | 82 ------------ src/SMAPI/Framework/ModData/ModDataFieldKey.cs | 18 --- src/SMAPI/Framework/ModData/ModDataRecord.cs | 146 --------------------- src/SMAPI/Framework/ModData/ModDatabase.cs | 140 -------------------- src/SMAPI/Framework/ModData/ModStatus.cs | 18 --- src/SMAPI/Framework/ModData/ParsedModDataRecord.cs | 51 ------- src/SMAPI/Framework/ModLoading/ModMetadata.cs | 2 +- src/SMAPI/Framework/ModLoading/ModResolver.cs | 2 +- src/SMAPI/Framework/Models/SMetadata.cs | 15 --- src/SMAPI/Program.cs | 9 +- src/SMAPI/StardewModdingAPI.csproj | 7 - .../Framework/ModData/ModDataField.cs | 82 ++++++++++++ .../Framework/ModData/ModDataFieldKey.cs | 18 +++ .../Framework/ModData/ModDataRecord.cs | 146 +++++++++++++++++++++ .../Framework/ModData/ModDatabase.cs | 140 ++++++++++++++++++++ .../Framework/ModData/ModStatus.cs | 18 +++ .../Framework/ModData/ParsedModDataRecord.cs | 51 +++++++ .../Framework/ModData/SMetadata.cs | 14 ++ src/StardewModdingAPI.Toolkit/ModToolkit.cs | 39 ++++++ 22 files changed, 517 insertions(+), 511 deletions(-) delete mode 100644 src/SMAPI/Framework/ModData/ModDataField.cs delete mode 100644 src/SMAPI/Framework/ModData/ModDataFieldKey.cs delete mode 100644 src/SMAPI/Framework/ModData/ModDataRecord.cs delete mode 100644 src/SMAPI/Framework/ModData/ModDatabase.cs delete mode 100644 src/SMAPI/Framework/ModData/ModStatus.cs delete mode 100644 src/SMAPI/Framework/ModData/ParsedModDataRecord.cs delete mode 100644 src/SMAPI/Framework/Models/SMetadata.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataField.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataFieldKey.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ModStatus.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ParsedModDataRecord.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/SMetadata.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index a0fe2023..e63057b3 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -6,8 +6,8 @@ using Moq; using Newtonsoft.Json; using NUnit.Framework; using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Serialisation; using StardewModdingAPI.Toolkit.Serialisation.Models; diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 01b99d62..c4a3813e 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; @@ -22,14 +21,6 @@ namespace StardewModdingAPI /// Whether the directory containing the current save's data exists on disk. private static bool SavePathReady => Context.IsSaveLoaded && Directory.Exists(Constants.RawSavePath); - /// Maps vendor keys (like Nexus) to their mod URL template (where {0} is the mod ID). This doesn't affect update checks, which defer to the remote web API. - private static readonly IDictionary VendorModUrls = new Dictionary(StringComparer.InvariantCultureIgnoreCase) - { - ["Chucklefish"] = "https://community.playstarbound.com/resources/{0}", - ["GitHub"] = "https://github.com/{0}/releases", - ["Nexus"] = "https://www.nexusmods.com/stardewvalley/mods/{0}" - }; - /********* ** Accessors @@ -159,23 +150,6 @@ namespace StardewModdingAPI return new PlatformAssemblyMap(targetPlatform, removeAssemblyReferences, targetAssemblies); } - /// Get an update URL for an update key (if valid). - /// The update key. - internal static string GetUpdateUrl(string updateKey) - { - string[] parts = updateKey.Split(new[] { ':' }, 2); - if (parts.Length != 2) - return null; - - string vendorKey = parts[0].Trim(); - string modID = parts[1].Trim(); - - if (Constants.VendorModUrls.TryGetValue(vendorKey, out string urlTemplate)) - return string.Format(urlTemplate, modID); - - return null; - } - /********* ** Private methods diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index 6281c052..5a8689de 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -1,6 +1,6 @@ -using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; +using StardewModdingAPI.Toolkit.Framework.ModData; namespace StardewModdingAPI.Framework { diff --git a/src/SMAPI/Framework/ModData/ModDataField.cs b/src/SMAPI/Framework/ModData/ModDataField.cs deleted file mode 100644 index df906103..00000000 --- a/src/SMAPI/Framework/ModData/ModDataField.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Linq; - -namespace StardewModdingAPI.Framework.ModData -{ - /// A versioned mod metadata field. - internal class ModDataField - { - /********* - ** Accessors - *********/ - /// The field key. - public ModDataFieldKey Key { get; } - - /// The field value. - public string Value { get; } - - /// Whether this field should only be applied if it's not already set. - public bool IsDefault { get; } - - /// The lowest version in the range, or null for all past versions. - public ISemanticVersion LowerVersion { get; } - - /// The highest version in the range, or null for all future versions. - public ISemanticVersion UpperVersion { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The field key. - /// The field value. - /// Whether this field should only be applied if it's not already set. - /// The lowest version in the range, or null for all past versions. - /// The highest version in the range, or null for all future versions. - public ModDataField(ModDataFieldKey key, string value, bool isDefault, ISemanticVersion lowerVersion, ISemanticVersion upperVersion) - { - this.Key = key; - this.Value = value; - this.IsDefault = isDefault; - this.LowerVersion = lowerVersion; - this.UpperVersion = upperVersion; - } - - /// Get whether this data field applies for the given manifest. - /// The mod manifest. - public bool IsMatch(IManifest manifest) - { - return - manifest?.Version != null // ignore invalid manifest - && (!this.IsDefault || !this.HasFieldValue(manifest, this.Key)) - && (this.LowerVersion == null || !manifest.Version.IsOlderThan(this.LowerVersion)) - && (this.UpperVersion == null || !manifest.Version.IsNewerThan(this.UpperVersion)); - } - - - /********* - ** Private methods - *********/ - /// Get whether a manifest field has a meaningful value for the purposes of enforcing . - /// The mod manifest. - /// The field key matching . - private bool HasFieldValue(IManifest manifest, ModDataFieldKey key) - { - switch (key) - { - // update key - case ModDataFieldKey.UpdateKey: - return manifest.UpdateKeys != null && manifest.UpdateKeys.Any(p => !string.IsNullOrWhiteSpace(p)); - - // non-manifest fields - case ModDataFieldKey.AlternativeUrl: - case ModDataFieldKey.StatusReasonPhrase: - case ModDataFieldKey.Status: - return false; - - default: - return false; - } - } - } -} diff --git a/src/SMAPI/Framework/ModData/ModDataFieldKey.cs b/src/SMAPI/Framework/ModData/ModDataFieldKey.cs deleted file mode 100644 index f68f575c..00000000 --- a/src/SMAPI/Framework/ModData/ModDataFieldKey.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace StardewModdingAPI.Framework.ModData -{ - /// The valid field keys. - public enum ModDataFieldKey - { - /// A manifest update key. - UpdateKey, - - /// An alternative URL the player can check for an updated version. - AlternativeUrl, - - /// The mod's predefined compatibility status. - Status, - - /// A reason phrase for the , or null to use the default reason. - StatusReasonPhrase - } -} diff --git a/src/SMAPI/Framework/ModData/ModDataRecord.cs b/src/SMAPI/Framework/ModData/ModDataRecord.cs deleted file mode 100644 index 56275f53..00000000 --- a/src/SMAPI/Framework/ModData/ModDataRecord.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace StardewModdingAPI.Framework.ModData -{ - /// Raw mod metadata from SMAPI's internal mod list. - internal class ModDataRecord - { - /********* - ** Properties - *********/ - /// This field stores properties that aren't mapped to another field before they're parsed into . - [JsonExtensionData] - private IDictionary ExtensionData; - - - /********* - ** Accessors - *********/ - /// The mod's current unique ID. - public string ID { get; set; } - - /// The former mod IDs (if any). - /// - /// This uses a custom format which uniquely identifies a mod across multiple versions and - /// supports matching other fields if no ID was specified. This doesn't include the latest - /// ID, if any. Format rules: - /// 1. If the mod's ID changed over time, multiple variants can be separated by the - /// | character. - /// 2. Each variant can take one of two forms: - /// - A simple string matching the mod's UniqueID value. - /// - A JSON structure containing any of four manifest fields (ID, Name, Author, and - /// EntryDll) to match. - /// - public string FormerIDs { get; set; } - - /// Maps local versions to a semantic version for update checks. - public IDictionary MapLocalVersions { get; set; } = new Dictionary(); - - /// Maps remote versions to a semantic version for update checks. - public IDictionary MapRemoteVersions { get; set; } = new Dictionary(); - - /// The versioned field data. - /// - /// This maps field names to values. This should be accessed via . - /// Format notes: - /// - Each key consists of a field name prefixed with any combination of version range - /// and Default, separated by pipes (whitespace trimmed). For example, Name - /// will always override the name, Default | Name will only override a blank - /// name, and ~1.1 | Default | Name will override blank names up to version 1.1. - /// - The version format is min~max (where either side can be blank for unbounded), or - /// a single version number. - /// - The field name itself corresponds to a value. - /// - public IDictionary Fields { get; set; } = new Dictionary(); - - - /********* - ** Public methods - *********/ - /// Get a parsed representation of the . - public IEnumerable GetFields() - { - foreach (KeyValuePair pair in this.Fields) - { - // init fields - string packedKey = pair.Key; - string value = pair.Value; - bool isDefault = false; - ISemanticVersion lowerVersion = null; - ISemanticVersion upperVersion = null; - - // parse - string[] parts = packedKey.Split('|').Select(p => p.Trim()).ToArray(); - ModDataFieldKey fieldKey = (ModDataFieldKey)Enum.Parse(typeof(ModDataFieldKey), parts.Last(), ignoreCase: true); - foreach (string part in parts.Take(parts.Length - 1)) - { - // 'default' - if (part.Equals("Default", StringComparison.InvariantCultureIgnoreCase)) - { - isDefault = true; - continue; - } - - // version range - if (part.Contains("~")) - { - string[] versionParts = part.Split(new[] { '~' }, 2); - lowerVersion = versionParts[0] != "" ? new SemanticVersion(versionParts[0]) : null; - upperVersion = versionParts[1] != "" ? new SemanticVersion(versionParts[1]) : null; - continue; - } - - // single version - lowerVersion = new SemanticVersion(part); - upperVersion = new SemanticVersion(part); - } - - yield return new ModDataField(fieldKey, value, isDefault, lowerVersion, upperVersion); - } - } - - /// Get a semantic local version for update checks. - /// The remote version to normalise. - public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) - { - return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version.ToString(), out string newVersion) - ? new SemanticVersion(newVersion) - : version; - } - - /// Get a semantic remote version for update checks. - /// The remote version to normalise. - public string GetRemoteVersionForUpdateChecks(string version) - { - // normalise version if possible - if (SemanticVersion.TryParse(version, out ISemanticVersion parsed)) - version = parsed.ToString(); - - // fetch remote version - return this.MapRemoteVersions != null && this.MapRemoteVersions.TryGetValue(version, out string newVersion) - ? newVersion - : version; - } - - - /********* - ** Private methods - *********/ - /// The method invoked after JSON deserialisation. - /// The deserialisation context. - [OnDeserialized] - private void OnDeserialized(StreamingContext context) - { - if (this.ExtensionData != null) - { - this.Fields = this.ExtensionData.ToDictionary(p => p.Key, p => p.Value.ToString()); - this.ExtensionData = null; - } - } - } -} diff --git a/src/SMAPI/Framework/ModData/ModDatabase.cs b/src/SMAPI/Framework/ModData/ModDatabase.cs deleted file mode 100644 index 62f37d9b..00000000 --- a/src/SMAPI/Framework/ModData/ModDatabase.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace StardewModdingAPI.Framework.ModData -{ - /// Handles access to SMAPI's internal mod metadata list. - internal class ModDatabase - { - /********* - ** Properties - *********/ - /// The underlying mod data records indexed by default display name. - private readonly IDictionary Records; - - /// Get an update URL for an update key (if valid). - private readonly Func GetUpdateUrl; - - - /********* - ** Public methods - *********/ - /// Construct an empty instance. - public ModDatabase() - : this(new Dictionary(), key => null) { } - - /// Construct an instance. - /// The underlying mod data records indexed by default display name. - /// Get an update URL for an update key (if valid). - public ModDatabase(IDictionary records, Func getUpdateUrl) - { - this.Records = records; - this.GetUpdateUrl = getUpdateUrl; - } - - /// Get a parsed representation of the which match a given manifest. - /// The manifest to match. - public ParsedModDataRecord GetParsed(IManifest manifest) - { - // get raw record - if (!this.TryGetRaw(manifest?.UniqueID, out string displayName, out ModDataRecord record)) - return null; - - // parse fields - ParsedModDataRecord parsed = new ParsedModDataRecord { DisplayName = displayName, DataRecord = record }; - foreach (ModDataField field in record.GetFields().Where(field => field.IsMatch(manifest))) - { - switch (field.Key) - { - // update key - case ModDataFieldKey.UpdateKey: - parsed.UpdateKey = field.Value; - break; - - // alternative URL - case ModDataFieldKey.AlternativeUrl: - parsed.AlternativeUrl = field.Value; - break; - - // status - case ModDataFieldKey.Status: - parsed.Status = (ModStatus)Enum.Parse(typeof(ModStatus), field.Value, ignoreCase: true); - parsed.StatusUpperVersion = field.UpperVersion; - break; - - // status reason phrase - case ModDataFieldKey.StatusReasonPhrase: - parsed.StatusReasonPhrase = field.Value; - break; - } - } - - return parsed; - } - - /// Get the display name for a given mod ID (if available). - /// The unique mod ID. - public string GetDisplayNameFor(string id) - { - return this.TryGetRaw(id, out string displayName, out ModDataRecord _) - ? displayName - : null; - } - - /// Get the mod page URL for a mod (if available). - /// The unique mod ID. - public string GetModPageUrlFor(string id) - { - // get raw record - if (!this.TryGetRaw(id, out string _, out ModDataRecord record)) - return null; - - // get update key - ModDataField updateKeyField = record.GetFields().FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey); - if (updateKeyField == null) - return null; - - // get update URL - return this.GetUpdateUrl(updateKeyField.Value); - } - - - /********* - ** Private models - *********/ - /// Get a raw data record. - /// The mod ID to match. - /// The mod's default display name. - /// The raw mod record. - private bool TryGetRaw(string id, out string displayName, out ModDataRecord record) - { - if (!string.IsNullOrWhiteSpace(id)) - { - foreach (var entry in this.Records) - { - displayName = entry.Key; - record = entry.Value; - - // try main ID - if (record.ID != null && record.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase)) - return true; - - // try former IDs - if (record.FormerIDs != null) - { - foreach (string part in record.FormerIDs.Split('|')) - { - if (part.Trim().Equals(id, StringComparison.InvariantCultureIgnoreCase)) - return true; - } - } - } - } - - displayName = null; - record = null; - return false; - } - } -} diff --git a/src/SMAPI/Framework/ModData/ModStatus.cs b/src/SMAPI/Framework/ModData/ModStatus.cs deleted file mode 100644 index 0e1d94d4..00000000 --- a/src/SMAPI/Framework/ModData/ModStatus.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace StardewModdingAPI.Framework.ModData -{ - /// Indicates how SMAPI should treat a mod. - internal enum ModStatus - { - /// Don't override the status. - None, - - /// The mod is obsolete and shouldn't be used, regardless of version. - Obsolete, - - /// Assume the mod is not compatible, even if SMAPI doesn't detect any incompatible code. - AssumeBroken, - - /// Assume the mod is compatible, even if SMAPI detects incompatible code. - AssumeCompatible - } -} diff --git a/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs b/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs deleted file mode 100644 index 3801fac3..00000000 --- a/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace StardewModdingAPI.Framework.ModData -{ - /// A parsed representation of the fields from a for a specific manifest. - internal class ParsedModDataRecord - { - /********* - ** Accessors - *********/ - /// The underlying data record. - public ModDataRecord DataRecord { get; set; } - - /// The default mod name to display when the name isn't available (e.g. during dependency checks). - public string DisplayName { get; set; } - - /// The update key to apply. - public string UpdateKey { get; set; } - - /// The alternative URL the player can check for an updated version. - public string AlternativeUrl { get; set; } - - /// The predefined compatibility status. - public ModStatus Status { get; set; } = ModStatus.None; - - /// A reason phrase for the , or null to use the default reason. - public string StatusReasonPhrase { get; set; } - - /// The upper version for which the applies (if any). - public ISemanticVersion StatusUpperVersion { get; set; } - - - /********* - ** Public methods - *********/ - /// Get a semantic local version for update checks. - /// The remote version to normalise. - public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) - { - return this.DataRecord.GetLocalVersionForUpdateChecks(version); - } - - /// Get a semantic remote version for update checks. - /// The remote version to normalise. - public ISemanticVersion GetRemoteVersionForUpdateChecks(string version) - { - string rawVersion = this.DataRecord.GetRemoteVersionForUpdateChecks(version); - return rawVersion != null - ? new SemanticVersion(rawVersion) - : null; - } - } -} diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 3a412009..4db25932 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -1,7 +1,7 @@ using System; using System.Linq; -using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; +using StardewModdingAPI.Toolkit.Framework.ModData; namespace StardewModdingAPI.Framework.ModLoading { diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index fde921e6..c1bc51ec 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; -using StardewModdingAPI.Framework.ModData; +using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Serialisation; using StardewModdingAPI.Toolkit.Serialisation.Models; using StardewModdingAPI.Toolkit.Utilities; diff --git a/src/SMAPI/Framework/Models/SMetadata.cs b/src/SMAPI/Framework/Models/SMetadata.cs deleted file mode 100644 index 9ff495e9..00000000 --- a/src/SMAPI/Framework/Models/SMetadata.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; -using StardewModdingAPI.Framework.ModData; - -namespace StardewModdingAPI.Framework.Models -{ - /// The SMAPI predefined metadata. - internal class SMetadata - { - /******** - ** Accessors - ********/ - /// Extra metadata about mods. - public IDictionary ModData { get; set; } - } -} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 09d9969c..6f1fe761 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -21,7 +21,6 @@ using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Logging; -using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; @@ -29,7 +28,9 @@ using StardewModdingAPI.Framework.Patching; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; using StardewModdingAPI.Internal; +using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; +using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Serialisation; using StardewModdingAPI.Toolkit.Serialisation.Converters; using StardewModdingAPI.Toolkit.Utilities; @@ -413,8 +414,8 @@ namespace StardewModdingAPI this.Monitor.Log("SMAPI found problems in your game's content files which are likely to cause errors or crashes. Consider uninstalling XNB mods or reinstalling the game.", LogLevel.Error); // load mod data - SMetadata metadata = JsonConvert.DeserializeObject(File.ReadAllText(Constants.ApiMetadataPath)); - ModDatabase modDatabase = new ModDatabase(metadata.ModData, Constants.GetUpdateUrl); + ModToolkit toolkit = new ModToolkit(); + ModDatabase modDatabase = toolkit.GetModDatabase(Constants.ApiMetadataPath, toolkit.GetUpdateUrl); // load mods { @@ -423,7 +424,7 @@ namespace StardewModdingAPI // load manifests IModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, this.JsonHelper, modDatabase).ToArray(); - resolver.ValidateManifests(mods, Constants.ApiVersion, Constants.GetUpdateUrl); + resolver.ValidateManifests(mods, Constants.ApiVersion, toolkit.GetUpdateUrl); // process dependencies mods = resolver.ProcessDependencies(mods, modDatabase).ToArray(); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 4852f70c..f849ee53 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -132,11 +132,6 @@ - - - - - @@ -239,7 +234,6 @@ - @@ -263,7 +257,6 @@ - diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataField.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataField.cs new file mode 100644 index 00000000..b3954693 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataField.cs @@ -0,0 +1,82 @@ +using System.Linq; + +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// A versioned mod metadata field. + public class ModDataField + { + /********* + ** Accessors + *********/ + /// The field key. + public ModDataFieldKey Key { get; } + + /// The field value. + public string Value { get; } + + /// Whether this field should only be applied if it's not already set. + public bool IsDefault { get; } + + /// The lowest version in the range, or null for all past versions. + public ISemanticVersion LowerVersion { get; } + + /// The highest version in the range, or null for all future versions. + public ISemanticVersion UpperVersion { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The field key. + /// The field value. + /// Whether this field should only be applied if it's not already set. + /// The lowest version in the range, or null for all past versions. + /// The highest version in the range, or null for all future versions. + public ModDataField(ModDataFieldKey key, string value, bool isDefault, ISemanticVersion lowerVersion, ISemanticVersion upperVersion) + { + this.Key = key; + this.Value = value; + this.IsDefault = isDefault; + this.LowerVersion = lowerVersion; + this.UpperVersion = upperVersion; + } + + /// Get whether this data field applies for the given manifest. + /// The mod manifest. + public bool IsMatch(IManifest manifest) + { + return + manifest?.Version != null // ignore invalid manifest + && (!this.IsDefault || !this.HasFieldValue(manifest, this.Key)) + && (this.LowerVersion == null || !manifest.Version.IsOlderThan(this.LowerVersion)) + && (this.UpperVersion == null || !manifest.Version.IsNewerThan(this.UpperVersion)); + } + + + /********* + ** Private methods + *********/ + /// Get whether a manifest field has a meaningful value for the purposes of enforcing . + /// The mod manifest. + /// The field key matching . + private bool HasFieldValue(IManifest manifest, ModDataFieldKey key) + { + switch (key) + { + // update key + case ModDataFieldKey.UpdateKey: + return manifest.UpdateKeys != null && manifest.UpdateKeys.Any(p => !string.IsNullOrWhiteSpace(p)); + + // non-manifest fields + case ModDataFieldKey.AlternativeUrl: + case ModDataFieldKey.StatusReasonPhrase: + case ModDataFieldKey.Status: + return false; + + default: + return false; + } + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataFieldKey.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataFieldKey.cs new file mode 100644 index 00000000..09dd0cc5 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataFieldKey.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// The valid field keys. + public enum ModDataFieldKey + { + /// A manifest update key. + UpdateKey, + + /// An alternative URL the player can check for an updated version. + AlternativeUrl, + + /// The mod's predefined compatibility status. + Status, + + /// A reason phrase for the , or null to use the default reason. + StatusReasonPhrase + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs new file mode 100644 index 00000000..97ad0ca4 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// Raw mod metadata from SMAPI's internal mod list. + public class ModDataRecord + { + /********* + ** Properties + *********/ + /// This field stores properties that aren't mapped to another field before they're parsed into . + [JsonExtensionData] + private IDictionary ExtensionData; + + + /********* + ** Accessors + *********/ + /// The mod's current unique ID. + public string ID { get; set; } + + /// The former mod IDs (if any). + /// + /// This uses a custom format which uniquely identifies a mod across multiple versions and + /// supports matching other fields if no ID was specified. This doesn't include the latest + /// ID, if any. Format rules: + /// 1. If the mod's ID changed over time, multiple variants can be separated by the + /// | character. + /// 2. Each variant can take one of two forms: + /// - A simple string matching the mod's UniqueID value. + /// - A JSON structure containing any of four manifest fields (ID, Name, Author, and + /// EntryDll) to match. + /// + public string FormerIDs { get; set; } + + /// Maps local versions to a semantic version for update checks. + public IDictionary MapLocalVersions { get; set; } = new Dictionary(); + + /// Maps remote versions to a semantic version for update checks. + public IDictionary MapRemoteVersions { get; set; } = new Dictionary(); + + /// The versioned field data. + /// + /// This maps field names to values. This should be accessed via . + /// Format notes: + /// - Each key consists of a field name prefixed with any combination of version range + /// and Default, separated by pipes (whitespace trimmed). For example, Name + /// will always override the name, Default | Name will only override a blank + /// name, and ~1.1 | Default | Name will override blank names up to version 1.1. + /// - The version format is min~max (where either side can be blank for unbounded), or + /// a single version number. + /// - The field name itself corresponds to a value. + /// + public IDictionary Fields { get; set; } = new Dictionary(); + + + /********* + ** Public methods + *********/ + /// Get a parsed representation of the . + public IEnumerable GetFields() + { + foreach (KeyValuePair pair in this.Fields) + { + // init fields + string packedKey = pair.Key; + string value = pair.Value; + bool isDefault = false; + ISemanticVersion lowerVersion = null; + ISemanticVersion upperVersion = null; + + // parse + string[] parts = packedKey.Split('|').Select(p => p.Trim()).ToArray(); + ModDataFieldKey fieldKey = (ModDataFieldKey)Enum.Parse(typeof(ModDataFieldKey), parts.Last(), ignoreCase: true); + foreach (string part in parts.Take(parts.Length - 1)) + { + // 'default' + if (part.Equals("Default", StringComparison.InvariantCultureIgnoreCase)) + { + isDefault = true; + continue; + } + + // version range + if (part.Contains("~")) + { + string[] versionParts = part.Split(new[] { '~' }, 2); + lowerVersion = versionParts[0] != "" ? new SemanticVersion(versionParts[0]) : null; + upperVersion = versionParts[1] != "" ? new SemanticVersion(versionParts[1]) : null; + continue; + } + + // single version + lowerVersion = new SemanticVersion(part); + upperVersion = new SemanticVersion(part); + } + + yield return new ModDataField(fieldKey, value, isDefault, lowerVersion, upperVersion); + } + } + + /// Get a semantic local version for update checks. + /// The remote version to normalise. + public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) + { + return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version.ToString(), out string newVersion) + ? new SemanticVersion(newVersion) + : version; + } + + /// Get a semantic remote version for update checks. + /// The remote version to normalise. + public string GetRemoteVersionForUpdateChecks(string version) + { + // normalise version if possible + if (SemanticVersion.TryParse(version, out ISemanticVersion parsed)) + version = parsed.ToString(); + + // fetch remote version + return this.MapRemoteVersions != null && this.MapRemoteVersions.TryGetValue(version, out string newVersion) + ? newVersion + : version; + } + + + /********* + ** Private methods + *********/ + /// The method invoked after JSON deserialisation. + /// The deserialisation context. + [OnDeserialized] + private void OnDeserialized(StreamingContext context) + { + if (this.ExtensionData != null) + { + this.Fields = this.ExtensionData.ToDictionary(p => p.Key, p => p.Value.ToString()); + this.ExtensionData = null; + } + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs new file mode 100644 index 00000000..c60d2bcb --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// Handles access to SMAPI's internal mod metadata list. + public class ModDatabase + { + /********* + ** Properties + *********/ + /// The underlying mod data records indexed by default display name. + private readonly IDictionary Records; + + /// Get an update URL for an update key (if valid). + private readonly Func GetUpdateUrl; + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public ModDatabase() + : this(new Dictionary(), key => null) { } + + /// Construct an instance. + /// The underlying mod data records indexed by default display name. + /// Get an update URL for an update key (if valid). + public ModDatabase(IDictionary records, Func getUpdateUrl) + { + this.Records = records; + this.GetUpdateUrl = getUpdateUrl; + } + + /// Get a parsed representation of the which match a given manifest. + /// The manifest to match. + public ParsedModDataRecord GetParsed(IManifest manifest) + { + // get raw record + if (!this.TryGetRaw(manifest?.UniqueID, out string displayName, out ModDataRecord record)) + return null; + + // parse fields + ParsedModDataRecord parsed = new ParsedModDataRecord { DisplayName = displayName, DataRecord = record }; + foreach (ModDataField field in record.GetFields().Where(field => field.IsMatch(manifest))) + { + switch (field.Key) + { + // update key + case ModDataFieldKey.UpdateKey: + parsed.UpdateKey = field.Value; + break; + + // alternative URL + case ModDataFieldKey.AlternativeUrl: + parsed.AlternativeUrl = field.Value; + break; + + // status + case ModDataFieldKey.Status: + parsed.Status = (ModStatus)Enum.Parse(typeof(ModStatus), field.Value, ignoreCase: true); + parsed.StatusUpperVersion = field.UpperVersion; + break; + + // status reason phrase + case ModDataFieldKey.StatusReasonPhrase: + parsed.StatusReasonPhrase = field.Value; + break; + } + } + + return parsed; + } + + /// Get the display name for a given mod ID (if available). + /// The unique mod ID. + public string GetDisplayNameFor(string id) + { + return this.TryGetRaw(id, out string displayName, out ModDataRecord _) + ? displayName + : null; + } + + /// Get the mod page URL for a mod (if available). + /// The unique mod ID. + public string GetModPageUrlFor(string id) + { + // get raw record + if (!this.TryGetRaw(id, out string _, out ModDataRecord record)) + return null; + + // get update key + ModDataField updateKeyField = record.GetFields().FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey); + if (updateKeyField == null) + return null; + + // get update URL + return this.GetUpdateUrl(updateKeyField.Value); + } + + + /********* + ** Private models + *********/ + /// Get a raw data record. + /// The mod ID to match. + /// The mod's default display name. + /// The raw mod record. + private bool TryGetRaw(string id, out string displayName, out ModDataRecord record) + { + if (!string.IsNullOrWhiteSpace(id)) + { + foreach (var entry in this.Records) + { + displayName = entry.Key; + record = entry.Value; + + // try main ID + if (record.ID != null && record.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase)) + return true; + + // try former IDs + if (record.FormerIDs != null) + { + foreach (string part in record.FormerIDs.Split('|')) + { + if (part.Trim().Equals(id, StringComparison.InvariantCultureIgnoreCase)) + return true; + } + } + } + } + + displayName = null; + record = null; + return false; + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModStatus.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModStatus.cs new file mode 100644 index 00000000..09da74bf --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModStatus.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// Indicates how SMAPI should treat a mod. + public enum ModStatus + { + /// Don't override the status. + None, + + /// The mod is obsolete and shouldn't be used, regardless of version. + Obsolete, + + /// Assume the mod is not compatible, even if SMAPI doesn't detect any incompatible code. + AssumeBroken, + + /// Assume the mod is compatible, even if SMAPI detects incompatible code. + AssumeCompatible + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ParsedModDataRecord.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ParsedModDataRecord.cs new file mode 100644 index 00000000..74f11ea5 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ParsedModDataRecord.cs @@ -0,0 +1,51 @@ +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// A parsed representation of the fields from a for a specific manifest. + public class ParsedModDataRecord + { + /********* + ** Accessors + *********/ + /// The underlying data record. + public ModDataRecord DataRecord { get; set; } + + /// The default mod name to display when the name isn't available (e.g. during dependency checks). + public string DisplayName { get; set; } + + /// The update key to apply. + public string UpdateKey { get; set; } + + /// The alternative URL the player can check for an updated version. + public string AlternativeUrl { get; set; } + + /// The predefined compatibility status. + public ModStatus Status { get; set; } = ModStatus.None; + + /// A reason phrase for the , or null to use the default reason. + public string StatusReasonPhrase { get; set; } + + /// The upper version for which the applies (if any). + public ISemanticVersion StatusUpperVersion { get; set; } + + + /********* + ** Public methods + *********/ + /// Get a semantic local version for update checks. + /// The remote version to normalise. + public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) + { + return this.DataRecord.GetLocalVersionForUpdateChecks(version); + } + + /// Get a semantic remote version for update checks. + /// The remote version to normalise. + public ISemanticVersion GetRemoteVersionForUpdateChecks(string version) + { + string rawVersion = this.DataRecord.GetRemoteVersionForUpdateChecks(version); + return rawVersion != null + ? new SemanticVersion(rawVersion) + : null; + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/SMetadata.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/SMetadata.cs new file mode 100644 index 00000000..9553cca9 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/SMetadata.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// The SMAPI predefined metadata. + internal class SMetadata + { + /******** + ** Accessors + ********/ + /// Extra metadata about mods. + public IDictionary ModData { get; set; } + } +} diff --git a/src/StardewModdingAPI.Toolkit/ModToolkit.cs b/src/StardewModdingAPI.Toolkit/ModToolkit.cs index 6136186e..1723991e 100644 --- a/src/StardewModdingAPI.Toolkit/ModToolkit.cs +++ b/src/StardewModdingAPI.Toolkit/ModToolkit.cs @@ -1,5 +1,10 @@ +using System; +using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; +using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; +using StardewModdingAPI.Toolkit.Framework.ModData; namespace StardewModdingAPI.Toolkit { @@ -12,6 +17,14 @@ namespace StardewModdingAPI.Toolkit /// The default HTTP user agent for the toolkit. private readonly string UserAgent; + /// Maps vendor keys (like Nexus) to their mod URL template (where {0} is the mod ID). This doesn't affect update checks, which defer to the remote web API. + private readonly IDictionary VendorModUrls = new Dictionary(StringComparer.InvariantCultureIgnoreCase) + { + ["Chucklefish"] = "https://community.playstarbound.com/resources/{0}", + ["GitHub"] = "https://github.com/{0}/releases", + ["Nexus"] = "https://www.nexusmods.com/stardewvalley/mods/{0}" + }; + /********* ** Public methods @@ -29,5 +42,31 @@ namespace StardewModdingAPI.Toolkit var client = new WikiCompatibilityClient(this.UserAgent); return await client.FetchAsync(); } + + /// Get SMAPI's internal mod database. + /// The file path for the SMAPI metadata file. + /// Get an update URL for an update key (if valid). + public ModDatabase GetModDatabase(string metadataPath, Func getUpdateUrl) + { + SMetadata metadata = JsonConvert.DeserializeObject(File.ReadAllText(metadataPath)); + return new ModDatabase(metadata.ModData, getUpdateUrl); + } + + /// Get an update URL for an update key (if valid). + /// The update key. + internal string GetUpdateUrl(string updateKey) + { + string[] parts = updateKey.Split(new[] { ':' }, 2); + if (parts.Length != 2) + return null; + + string vendorKey = parts[0].Trim(); + string modID = parts[1].Trim(); + + if (this.VendorModUrls.TryGetValue(vendorKey, out string urlTemplate)) + return string.Format(urlTemplate, modID); + + return null; + } } } -- cgit From 583cb91f4a3429549b8e56081737e6a410ebd1a4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 28 Jun 2018 20:59:39 -0400 Subject: use mod DB in web API to get default update keys for mod IDs (#532) --- docs/release-notes.md | 15 +- src/SMAPI.Web/Controllers/ModsApiController.cs | 22 +- src/SMAPI.Web/StardewModdingAPI.Web.csproj | 5 + .../wwwroot/StardewModdingAPI.metadata.json | 1668 ++++++++++++++++++++ src/SMAPI/StardewModdingAPI.csproj | 7 +- src/SMAPI/StardewModdingAPI.metadata.json | 1668 -------------------- 6 files changed, 1707 insertions(+), 1678 deletions(-) create mode 100644 src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json delete mode 100644 src/SMAPI/StardewModdingAPI.metadata.json (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/docs/release-notes.md b/docs/release-notes.md index 329ea3ad..062f902e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,13 +3,16 @@ * For players: * Updated for Stardew Valley 1.3 (no longer compatible with earlier versions). * Added a bundled Save Backup mod. - * Added beta update channel. * Added prompt when in beta channel and a new version is found. * Added friendly error when game can't start audio. - * Added console warning for mods which don't have update checks configured. - * Added update checks for optional mod files on Nexus. * Added `player_add name` command, which lets you add items to your inventory by name instead of ID. * Improved how mod warnings are shown in the console. + * Improved update checks: + * added beta update channel; + * added support for optional files on Nexus; + * added console warning for mods which don't have update checks configured; + * fixed mod update checks failing if a mod only has prerelease versions on GitHub; + * fixed Nexus mod update alerts not showing HTTPS links. * Fixed `SEHException` errors and performance issues in some cases. * Fixed console color scheme on Mac or in PowerShell, configurable via `StardewModdingAPI.config.json`. * Fixed installer error on Linux/Mac in some cases. @@ -20,9 +23,7 @@ * Fixed `world_setseason` command not running season-change logic. * Fixed `world_setseason` command not normalising the season value. * Fixed `world_settime` sometimes breaking NPC schedules (e.g. so they stay in bed). - * Fixed mod update checks failing if a mod only has prerelease versions on GitHub. * Fixed launch issue for Linux players with some terminals. (Thanks to HanFox and kurumushi!) - * Fixed Nexus mod update alerts not showing HTTPS links. * Fixed issue where a mod crashing in `CanEdit` or `CanLoad` could cause an abort-retry loop. * Renamed `install.exe` to `install on Windows.exe` to avoid confusion. * Updated compatibility list. @@ -79,6 +80,10 @@ * Added Harmony for SMAPI's internal use to patch game functions for events. * Added metadata dump option in `StardewModdingAPI.config.json` for troubleshooting some cases. * Rewrote input suppression using new SDV 1.3 APIs. + * Rewrote update checks: + * Moved most logic into the web API. + * Changed web API to require mod ID. + * Changed web API to also fetch update keys from SMAPI's internal mod DB. * Rewrote world/player state tracking: * much more efficient than previous method; * uses net field events where available; diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index c5a1705d..960602f4 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -1,13 +1,16 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; +using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.Framework.Clients.Nexus; @@ -39,18 +42,23 @@ namespace StardewModdingAPI.Web.Controllers /// A regex which matches SMAPI-style semantic version. private readonly string VersionRegex; + /// The internal mod metadata list. + private readonly ModDatabase ModDatabase; + /********* ** Public methods *********/ /// Construct an instance. + /// The web hosting environment. /// The cache in which to store mod metadata. /// The config settings for mod update checks. /// The Chucklefish API client. /// The GitHub API client. /// The Nexus API client. - public ModsApiController(IMemoryCache cache, IOptions configProvider, IChucklefishClient chucklefish, IGitHubClient github, INexusClient nexus) + public ModsApiController(IHostingEnvironment environment, IMemoryCache cache, IOptions configProvider, IChucklefishClient chucklefish, IGitHubClient github, INexusClient nexus) { + this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "StardewModdingAPI.metadata.json")); ModUpdateCheckConfig config = configProvider.Value; this.Cache = cache; @@ -79,10 +87,20 @@ namespace StardewModdingAPI.Web.Controllers if (string.IsNullOrWhiteSpace(mod.ID)) continue; + // resolve update keys + var updateKeys = new HashSet(mod.UpdateKeys ?? new string[0], StringComparer.InvariantCultureIgnoreCase); + ModDataRecord record = this.ModDatabase.Get(mod.ID); + if (record?.Fields != null) + { + string defaultUpdateKey = record.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey && p.IsDefault)?.Value; + if (!string.IsNullOrWhiteSpace(defaultUpdateKey)) + updateKeys.Add(defaultUpdateKey); + } + // get latest versions ModEntryModel result = new ModEntryModel { ID = mod.ID }; IList errors = new List(); - foreach (string updateKey in mod.UpdateKeys ?? new string[0]) + foreach (string updateKey in updateKeys) { // fetch data ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey); diff --git a/src/SMAPI.Web/StardewModdingAPI.Web.csproj b/src/SMAPI.Web/StardewModdingAPI.Web.csproj index a409e6eb..6761c7ad 100644 --- a/src/SMAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/SMAPI.Web/StardewModdingAPI.Web.csproj @@ -26,5 +26,10 @@ + + + PreserveNewest + + diff --git a/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json b/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json new file mode 100644 index 00000000..343257f1 --- /dev/null +++ b/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json @@ -0,0 +1,1668 @@ +{ + /** + * Metadata about some SMAPI mods used in compatibility, update, and dependency checks. This + * field shouldn't be edited by players in most cases. + * + * Standard fields + * =============== + * The predefined fields are documented below (only 'ID' is required). Each entry's key is the + * default display name for the mod if one isn't available (e.g. in dependency checks). + * + * - ID: the mod's latest unique ID (if any). + * + * - FormerIDs: uniquely identifies the mod across multiple versions, and supports matching + * other fields if no ID was specified. This doesn't include the latest ID, if any. Multiple + * variants can be separated with '|'. + * + * - MapLocalVersions and MapRemoteVersions correct local manifest versions and remote versions + * during update checks. For example, if the API returns version '1.1-1078' where '1078' is + * intended to be a build number, MapRemoteVersions can map it to '1.1' when comparing to the + * mod's current version. This is only meant to support legacy mods with injected update keys. + * + * Versioned metadata + * ================== + * Each record can also specify extra metadata using the field keys below. + * + * Each key consists of a field name prefixed with any combination of version range and 'Default', + * separated by pipes (whitespace trimmed). For example, 'UpdateKey' will always override, + * 'Default | UpdateKey' will only override if the mod has no update keys, and + * '~1.1 | Default | Name' will do the same up to version 1.1. + * + * The version format is 'min~max' (where either side can be blank for unbounded), or a single + * version number. + * + * These are the valid field names: + * + * - UpdateKey: the update key to set in the mod's manifest. This is used to enable update + * checks for older mods that haven't been updated to use it yet. + * + * - Status: overrides compatibility checks. The possible values are Obsolete (SMAPI won't load + * it because the mod should no longer be used), AssumeBroken (SMAPI won't load it because + * the specified version isn't compatible), or AssumeCompatible (SMAPI will try to load it + * even if it detects incompatible code). + * + * Note that this shouldn't be set to 'AssumeBroken' if SMAPI can detect the incompatibility + * automatically, since that hides the details from trace logs. + * + * - StatusReasonPhrase: a message to show to the player explaining why the mod can't be loaded + * (if applicable). If blank, will default to a generic not-compatible message. + * + * - AlternativeUrl: a URL where the player can find an unofficial update or alternative if the + * mod is no longer compatible. + */ + "ModData": { + "AccessChestAnywhere": { + "ID": "AccessChestAnywhere", + "MapLocalVersions": { "1.1-1078": "1.1" }, + "Default | UpdateKey": "Nexus:257", + "~1.1 | Status": "AssumeBroken" + }, + + "Adjust Artisan Prices": { + "ID": "ThatNorthernMonkey.AdjustArtisanPrices", + "FormerIDs": "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc", // changed in 0.0.2-pathoschild-update + "MapRemoteVersions": { "0.01": "0.0.1" }, + "Default | UpdateKey": "Chucklefish:3532" + }, + + "Adjust Monster": { + "ID": "mmanlapat.AdjustMonster", + "Default | UpdateKey": "Nexus:1161" + }, + + "Advanced Location Loader": { + "ID": "Entoarox.AdvancedLocationLoader", + "~1.3.7 | UpdateKey": "Chucklefish:3619" // only enable update checks up to 1.3.7 by request (has its own update-check feature) + }, + + "Adventure Shop Inventory": { + "ID": "HammurabiAdventureShopInventory", + "Default | UpdateKey": "Chucklefish:4608" + }, + + "AgingMod": { + "ID": "skn.AgingMod", + "Default | UpdateKey": "Nexus:1129", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "All Crops All Seasons": { + "ID": "cantorsdust.AllCropsAllSeasons", + "FormerIDs": "29ee8246-d67b-4242-a340-35a9ae0d5dd7 | community.AllCropsAllSeasons", // changed in 1.3 and 1.5 + "Default | UpdateKey": "Nexus:170" + }, + + "All Professions": { + "ID": "cantorsdust.AllProfessions", + "FormerIDs": "8c37b1a7-4bfb-4916-9d8a-9533e6363ea3 | community.AllProfessions", // changed in 1.2 and 1.3.1 + "Default | UpdateKey": "Nexus:174" + }, + + "Almighty Farming Tool": { + "ID": "439", + "MapRemoteVersions": { "1.21": "1.2.1" }, + "Default | UpdateKey": "Nexus:439" + }, + + "Animal Husbandry": { + "ID": "DIGUS.ANIMALHUSBANDRYMOD", + "FormerIDs": "DIGUS.BUTCHER", // changed in 2.0.1 + "Default | UpdateKey": "Nexus:1538" + }, + + "Animal Mood Fix": { + "ID": "GPeters-AnimalMoodFix", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2." + }, + + "Animal Sitter": { + "ID": "jwdred.AnimalSitter", + "Default | UpdateKey": "Nexus:581", + "~1.0.8 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Arcade Pong": { + "ID": "Platonymous.ArcadePong", + "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.16 due to reflection into SMAPI internals + }, + + "A Tapper's Dream": { + "ID": "ddde5195-8f85-4061-90cc-0d4fd5459358", + "Default | UpdateKey": "Nexus:260" + }, + + "Auto Animal Doors": { + "ID": "AaronTaggart.AutoAnimalDoors", + "Default | UpdateKey": "Nexus:1019" + }, + + "Auto-Eat": { + "ID": "Permamiss.AutoEat", + "FormerIDs": "BALANCEMOD_AutoEat", // changed in 1.1.1 + "Default | UpdateKey": "Nexus:643" + }, + + "AutoFish": { + "ID": "WhiteMind.AF", + "Default | UpdateKey": "Nexus:1895" + }, + + "AutoGate": { + "ID": "AutoGate", + "Default | UpdateKey": "Nexus:820" + }, + + "Automate": { + "ID": "Pathoschild.Automate", + "Default | UpdateKey": "Nexus:1063", + "~1.10-beta.7 | Status": "AssumeBroken" // broke in SDV 1.3.20 + }, + + "Automated Doors": { + "ID": "azah.automated-doors", + "FormerIDs": "1abcfa07-2cf4-4dc3-a6e9-6068b642112b", // changed in 1.4.1 + "Default | UpdateKey": "GitHub:azah/AutomatedDoors" // added in 1.4.2 + }, + + "AutoSpeed": { + "ID": "Omegasis.AutoSpeed", + "Default | UpdateKey": "Nexus:443" // added in 1.4.1 + }, + + "Basic Sprinklers Improved": { + "ID": "lrsk_sdvm_bsi.0117171308", + "MapRemoteVersions": { "1.0.2": "1.0.1-release" }, // manifest not updated + "Default | UpdateKey": "Nexus:833" + }, + + "Better Hay": { + "ID": "cat.betterhay", + "Default | UpdateKey": "Nexus:1430" + }, + + "Better Quality More Seasons": { + "ID": "SB_BQMS", + "Default | UpdateKey": "Nexus:935" + }, + + "Better Quarry": { + "ID": "BetterQuarry", + "Default | UpdateKey": "Nexus:771" + }, + + "Better Ranching": { + "ID": "BetterRanching", + "Default | UpdateKey": "Nexus:859" + }, + + "Better Shipping Box": { + "ID": "Kithio:BetterShippingBox", + "MapLocalVersions": { "1.0.1": "1.0.2" }, + "Default | UpdateKey": "Chucklefish:4302" + }, + + "Better Sprinklers": { + "ID": "Speeder.BetterSprinklers", + "FormerIDs": "SPDSprinklersMod", // changed in 2.3 + "Default | UpdateKey": "Nexus:41" + }, + + "Billboard Anywhere": { + "ID": "Omegasis.BillboardAnywhere", + "Default | UpdateKey": "Nexus:492" // added in 1.4.1 + }, + + "Birthday Mail": { + "ID": "KathrynHazuka.BirthdayMail", + "FormerIDs": "005e02dc-d900-425c-9c68-1ff55c5a295d", // changed in 1.2.3-pathoschild-update + "Default | UpdateKey": "Nexus:276", + "MapRemoteVersions": { "1.3.1": "1.3" } // manifest not updated + }, + + "Breed Like Rabbits": { + "ID": "dycedarger.breedlikerabbits", + "Default | UpdateKey": "Nexus:948" + }, + + "Build Endurance": { + "ID": "Omegasis.BuildEndurance", + "Default | UpdateKey": "Nexus:445" // added in 1.4.1 + }, + + "Build Health": { + "ID": "Omegasis.BuildHealth", + "Default | UpdateKey": "Nexus:446" // added in 1.4.1 + }, + + "Buy Cooking Recipes": { + "ID": "Denifia.BuyRecipes", + "Default | UpdateKey": "Nexus:1126" // added in 1.0.1 (2017-10-04) + }, + + "Buy Back Collectables": { + "ID": "Omegasis.BuyBackCollectables", + "FormerIDs": "BuyBackCollectables", // changed in 1.4 + "Default | UpdateKey": "Nexus:507" // added in 1.4.1 + }, + + "Carry Chest": { + "ID": "spacechase0.CarryChest", + "Default | UpdateKey": "Nexus:1333" + }, + + "Casks Anywhere": { + "ID": "CasksAnywhere", + "MapLocalVersions": { "1.1-alpha": "1.1" }, + "Default | UpdateKey": "Nexus:878" + }, + + "Categorize Chests": { + "ID": "CategorizeChests", + "Default | UpdateKey": "Nexus:1300", + "~1.4.3-unofficial.2.mizzion | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.18 (in-game errors) + }, + + "Chefs Closet": { + "ID": "Duder.ChefsCloset", + "MapLocalVersions": { "1.3-1": "1.3" }, + "Default | UpdateKey": "Nexus:1030" + }, + + "Chest Label System": { + "ID": "Speeder.ChestLabel", + "FormerIDs": "SPDChestLabel", // changed in 1.5.1-pathoschild-update + "Default | UpdateKey": "Nexus:242" + }, + + "Chest Pooling": { + "ID": "mralbobo.ChestPooling", + "Default | UpdateKey": "GitHub:mralbobo/stardew-chest-pooling" + }, + + "Chests Anywhere": { + "ID": "Pathoschild.ChestsAnywhere", + "FormerIDs": "ChestsAnywhere", // changed in 1.9 + "Default | UpdateKey": "Nexus:518", + "~1.12.4 | Status": "AssumeBroken" // broke in SDV 1.3 + }, + + "CJB Automation": { + "ID": "CJBAutomation", + "Default | UpdateKey": "Nexus:211", + "~1.4 | Status": "AssumeBroken", // broke in SDV 1.2 + "~1.4 | AlternativeUrl": "http://www.nexusmods.com/stardewvalley/mods/1063" + }, + + "CJB Cheats Menu": { + "ID": "CJBok.CheatsMenu", + "FormerIDs": "CJBCheatsMenu", // changed in 1.14 + "Default | UpdateKey": "Nexus:4", + "~1.18-beta | Status": "AssumeBroken" // broke in SDV 1.3, first beta causes significant friendship bugs + }, + + "CJB Item Spawner": { + "ID": "CJBok.ItemSpawner", + "FormerIDs": "CJBItemSpawner", // changed in 1.7 + "Default | UpdateKey": "Nexus:93", + "~1.10 | Status": "AssumeBroken" // broke in SDV 1.3 + }, + + "CJB Show Item Sell Price": { + "ID": "CJBok.ShowItemSellPrice", + "FormerIDs": "CJBShowItemSellPrice", // changed in 1.7 + "Default | UpdateKey": "Nexus:5", + "~1.8 | Status": "AssumeBroken" // broke in SDV 1.3 + }, + + "Clean Farm": { + "ID": "tstaples.CleanFarm", + "Default | UpdateKey": "Nexus:794" + }, + + "Climates of Ferngill": { + "ID": "KoihimeNakamura.ClimatesOfFerngill", + "Default | UpdateKey": "Nexus:604" + }, + + "Coal Regen": { + "ID": "Blucifer.CoalRegen", + "Default | UpdateKey": "Nexus:1664" + }, + + "Cobalt": { + "ID": "spacechase0.Cobalt", + "MapRemoteVersions": { "1.1.3": "1.1.2" } // not updated in manifest + }, + + "Cold Weather Haley": { + "ID": "LordXamon.ColdWeatherHaleyPRO", + "Default | UpdateKey": "Nexus:1169", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Colored Chests": { + "ID": "4befde5c-731c-4853-8e4b-c5cdf946805f", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "colored chests were added in Stardew Valley 1.1." + }, + + "Combat with Farm Implements": { + "ID": "SPDFarmingImplementsInCombat", + "Default | UpdateKey": "Nexus:313" + }, + + "Community Bundle Item Tooltip": { + "ID": "musbah.bundleTooltip", + "Default | UpdateKey": "Nexus:1329" + }, + + "Concentration on Farming": { + "ID": "punyo.ConcentrationOnFarming", + "Default | UpdateKey": "Nexus:1445" + }, + + "Configurable Machines": { + "ID": "21da6619-dc03-4660-9794-8e5b498f5b97", + "MapLocalVersions": { "1.2-beta": "1.2" }, + "Default | UpdateKey": "Nexus:280" + }, + + "Configurable Shipping Dates": { + "ID": "ConfigurableShippingDates", + "Default | UpdateKey": "Nexus:675", + "~1.1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Content Patcher": { + "ID": "Pathoschild.ContentPatcher", + "Default | UpdateKey": "Nexus:1915", + "~1.4-beta.5 | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.18 (in-game errors) + }, + + "Cooking Skill": { + "ID": "spacechase0.CookingSkill", + "FormerIDs": "CookingSkill", // changed in 1.0.4–6 + "Default | UpdateKey": "Nexus:522" + }, + + "CrabNet": { + "ID": "jwdred.CrabNet", + "Default | UpdateKey": "Nexus:584" + }, + + "Crafting Counter": { + "ID": "lolpcgaming.CraftingCounter", + "Default | UpdateKey": "Nexus:1585", + "MapRemoteVersions": { "1.1": "1.0" } // not updated in manifest + }, + + "Current Location": { + "ID": "CurrentLocation102120161203", + "Default | UpdateKey": "Nexus:638" + }, + + "Custom Asset Modifier": { + "ID": "Omegasis.CustomAssetModifier", + "Default | UpdateKey": "1836" + }, + + "Custom Critters": { + "ID": "spacechase0.CustomCritters", + "Default | UpdateKey": "Nexus:1255" + }, + + "Custom Crops": { + "ID": "spacechase0.CustomCrops", + "Default | UpdateKey": "Nexus:1592" + }, + + "Custom Element Handler": { + "ID": "Platonymous.CustomElementHandler", + "Default | UpdateKey": "Nexus:1068" // added in 1.3.1 + }, + + "Custom Farming Redux": { + "ID": "Platonymous.CustomFarming", + "Default | UpdateKey": "Nexus:991" // added in 0.6.1 + }, + + "Custom Farming Automate Bridge": { + "ID": "Platonymous.CFAutomate", + "~1.0.1 | Status": "AssumeBroken", // no longer compatible with Automate + "~1.0.1 | AlternativeUrl": "https://www.nexusmods.com/stardewvalley/mods/991" + }, + + "Custom Farm Types": { + "ID": "spacechase0.CustomFarmTypes", + "Default | UpdateKey": "Nexus:1140" + }, + + "Custom Furniture": { + "ID": "Platonymous.CustomFurniture", + "Default | UpdateKey": "Nexus:1254" // added in 0.4.1 + }, + + "Customize Exterior": { + "ID": "spacechase0.CustomizeExterior", + "FormerIDs": "CustomizeExterior", // changed in 1.0.3 + "Default | UpdateKey": "Nexus:1099", + "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Customizable Cart Redux": { + "ID": "KoihimeNakamura.CCR", + "MapLocalVersions": { "1.1-20170917": "1.1" }, + "Default | UpdateKey": "Nexus:1402" + }, + + "Customizable Traveling Cart Days": { + "ID": "TravelingCartYyeahdude", + "Default | UpdateKey": "Nexus:567" + }, + + "Custom Linens": { + "ID": "Mevima.CustomLinens", + "MapRemoteVersions": { "1.1": "1.0" }, // manifest not updated + "Default | UpdateKey": "Nexus:1027" + }, + + "Custom NPC": { + "ID": "Platonymous.CustomNPC", + "Default | UpdateKey": "Nexus:1607" + }, + + "Custom Shops Redux": { + "ID": "Omegasis.CustomShopReduxGui", + "Default | UpdateKey": "Nexus:1378" // added in 1.4.1 + }, + + "Custom TV": { + "ID": "Platonymous.CustomTV", + "Default | UpdateKey": "Nexus:1139" // added in 1.0.6 + }, + + "Daily Luck Message": { + "ID": "Schematix.DailyLuckMessage", + "Default | UpdateKey": "Nexus:1327" + }, + + "Daily News": { + "ID": "bashNinja.DailyNews", + "Default | UpdateKey": "Nexus:1141", + "~1.2 | Status": "AssumeBroken" // broke in Stardew Valley 1.3 (or depends on CustomTV which broke) + }, + + "Daily Quest Anywhere": { + "ID": "Omegasis.DailyQuestAnywhere", + "FormerIDs": "DailyQuest", // changed in 1.4 + "Default | UpdateKey": "Nexus:513" // added in 1.4.1 + }, + + "Data Maps": { + "ID": "Pathoschild.DataMaps", + "Default | UpdateKey": "Nexus:1691", + "~1.3 | Status": "AssumeBroken" // broke in SDV 1.3 + }, + + "Debug Mode": { + "ID": "Pathoschild.DebugMode", + "FormerIDs": "Pathoschild.Stardew.DebugMode", // changed in 1.4 + "Default | UpdateKey": "Nexus:679", + "~1.8 | Status": "AssumeBroken" // broke in SDV 1.3 + }, + + "Did You Water Your Crops?": { + "ID": "Nishtra.DidYouWaterYourCrops", + "Default | UpdateKey": "Nexus:1583" + }, + + "Dynamic Checklist": { + "ID": "gunnargolf.DynamicChecklist", + "Default | UpdateKey": "Nexus:1145" // added in 1.0.1-pathoschild-update + }, + + "Dynamic Horses": { + "ID": "Bpendragon-DynamicHorses", + "MapRemoteVersions": { "1.2": "1.1-release" }, // manifest not updated + "Default | UpdateKey": "Nexus:874" + }, + + "Dynamic Machines": { + "ID": "DynamicMachines", + "MapLocalVersions": { "1.1": "1.1.1" }, + "Default | UpdateKey": "Nexus:374", + "~1.1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Dynamic NPC Sprites": { + "ID": "BashNinja.DynamicNPCSprites", + "Default | UpdateKey": "Nexus:1183" + }, + + "Easier Farming": { + "ID": "cautiouswafffle.EasierFarming", + "Default | UpdateKey": "Nexus:1426" + }, + + "Empty Hands": { + "ID": "QuicksilverFox.EmptyHands", + "Default | UpdateKey": "Nexus:1176" // added in 1.0.1-pathoschild-update + }, + + "Enemy Health Bars": { + "ID": "Speeder.HealthBars", + "FormerIDs": "SPDHealthBar", // changed in 1.7.1-pathoschild-update + "Default | UpdateKey": "Nexus:193" + }, + + "Entoarox Framework": { + "ID": "Entoarox.EntoaroxFramework", + "FormerIDs": "eacdb74b-4080-4452-b16b-93773cda5cf9", // changed in ??? + "~2.0.6 | UpdateKey": "Chucklefish:4228", // only enable update checks up to 2.0.6 by request (has its own update-check feature) + "~2.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.5 (error reflecting into SMAPI internals) + }, + + "Expanded Fridge": { + "ID": "Uwazouri.ExpandedFridge", + "Default | UpdateKey": "Nexus:1191" + }, + + "Experience Bars": { + "ID": "spacechase0.ExperienceBars", + "FormerIDs": "ExperienceBars", // changed in 1.0.2 + "Default | UpdateKey": "Nexus:509" + }, + + "Extended Bus System": { + "ID": "ExtendedBusSystem", + "Default | UpdateKey": "Chucklefish:4373" + }, + + "Extended Fridge": { + "ID": "Crystalmir.ExtendedFridge", + "FormerIDs": "Mystra007ExtendedFridge", // changed in 1.0.1 + "Default | UpdateKey": "Nexus:485" + }, + + "Extended Greenhouse": { + "ID": "ExtendedGreenhouse", + "Default | UpdateKey": "Chucklefish:4303", + "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Extended Minecart": { + "ID": "Entoarox.ExtendedMinecart", + "~1.7.1 | UpdateKey": "Chucklefish:4359" // only enable update checks up to 1.7.1 by request (has its own update-check feature) + }, + + "Extended Reach": { + "ID": "spacechase0.ExtendedReach", + "Default | UpdateKey": "Nexus:1493" + }, + + "Fall 28 Snow Day": { + "ID": "Omegasis.Fall28SnowDay", + "Default | UpdateKey": "Nexus:486", // added in 1.4.1 + "~1.4.1 | Status": "AssumeBroken" // broke in SMAPI 2.0, and update for SMAPI 2.0 doesn't do anything + }, + + "Farm Automation Unofficial: Item Collector": { + "ID": "Maddy99.FarmAutomation.ItemCollector", + "~0.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Farm Expansion": { + "ID": "Advize.FarmExpansion", + "FormerIDs": "3888bdfd-73f6-4776-8bb7-8ad45aea1915 | AdvizeFarmExpansionMod-2-0 | AdvizeFarmExpansionMod-2-0-5", // changed in 2.0, 2.0.5, and 3.0 + "Default | UpdateKey": "Nexus:130" + }, + + "Fast Animations": { + "ID": "Pathoschild.FastAnimations", + "Default | UpdateKey": "Nexus:1089", + "~1.5 | Status": "AssumeBroken" // broke in SDV 1.3 + }, + + "Faster Grass": { + "ID": "IceGladiador.FasterGrass", + "Default | UpdateKey": "Nexus:1772" + }, + + "Faster Paths": { + "ID": "Entoarox.FasterPaths", + "FormerIDs": "615f85f8-5c89-44ee-aecc-c328f172e413", // changed in 1.3 + "~1.3.3 | UpdateKey": "Chucklefish:3641" // only enable update checks up to 1.3.3 by request (has its own update-check feature) + }, + + "Fishing Adjust": { + "ID": "shuaiz.FishingAdjustMod", + "Default | UpdateKey": "Nexus:1350", + "~2.0.1 | Status": "AssumeBroken" // Method not found: 'Void Harmony.HarmonyInstance.Patch(System.Reflection.MethodBase, Harmony.HarmonyMethod, Harmony.HarmonyMethod, Harmony.HarmonyMethod)' + }, + + "Fishing Tuner Redux": { + "ID": "HammurabiFishingTunerRedux", + "Default | UpdateKey": "Chucklefish:4578" + }, + + "Fixed Secret Woods Debris": { + "ID": "f4iTh.WoodsDebrisFix", + "Default | UpdateKey": "Nexus:1941" + }, + + "Flower Color Picker": { + "ID": "spacechase0.FlowerColorPicker", + "Default | UpdateKey": "Nexus:1229" + }, + + "Forage at the Farm": { + "ID": "Nishtra.ForageAtTheFarm", + "FormerIDs": "ForageAtTheFarm", // changed in <=1.6 + "Default | UpdateKey": "Nexus:673" + }, + + "Furniture Anywhere": { + "ID": "Entoarox.FurnitureAnywhere", + "~1.1.5 | UpdateKey": "Chucklefish:4324" // only enable update checks up to 1.1.5 by request (has its own update-check feature) + }, + + "Game Reminder": { + "ID": "mmanlapat.GameReminder", + "Default | UpdateKey": "Nexus:1153" + }, + + "Gate Opener": { + "ID": "mralbobo.GateOpener", + "Default | UpdateKey": "GitHub:mralbobo/stardew-gate-opener" + }, + + "GenericShopExtender": { + "ID": "GenericShopExtender", + "Default | UpdateKey": "Nexus:814" // added in 0.1.3 + }, + + "Geode Info Menu": { + "ID": "cat.geodeinfomenu", + "Default | UpdateKey": "Nexus:1448" + }, + + "Get Dressed": { + "ID": "Advize.GetDressed", + "Default | UpdateKey": "Nexus:331" + }, + + "Giant Crop Ring": { + "ID": "cat.giantcropring", + "Default | UpdateKey": "Nexus:1182" + }, + + "Gift Taste Helper": { + "ID": "tstaples.GiftTasteHelper", + "FormerIDs": "8008db57-fa67-4730-978e-34b37ef191d6", // changed in 2.5 + "Default | UpdateKey": "Nexus:229" + }, + + "Grandfather's Gift": { + "ID": "ShadowDragon.GrandfathersGift", + "Default | UpdateKey": "Nexus:985" + }, + + "Happy Animals": { + "ID": "HappyAnimals", + "~1.0.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Happy Birthday (Omegasis)": { + "ID": "Omegasis.HappyBirthday", + "Default | UpdateKey": "Nexus:520" // added in 1.4.1 + }, + + "Hardcore Mines": { + "ID": "kibbe.hardcore_mines", + "Default | UpdateKey": "Nexus:1674" + }, + + "Harp of Yoba Redux": { + "ID": "Platonymous.HarpOfYobaRedux", + "Default | UpdateKey": "Nexus:914" // added in 2.0.3 + }, + + "Harvest Moon Witch Princess": { + "ID": "Sasara.WitchPrincess", + "Default | UpdateKey": "Nexus:1157" + }, + + "Harvest With Scythe": { + "ID": "965169fd-e1ed-47d0-9f12-b104535fb4bc", + "Default | UpdateKey": "Nexus:236" + }, + + "Horse Whistle (icepuente)": { + "ID": "icepuente.HorseWhistle", + "Default | UpdateKey": "Nexus:1131", + "~1.1.2-unofficial.1-pathoschild | Status": "AssumeBroken" // causes significant lag, fixed in unofficial.2 + }, + + "Hunger (Yyeadude)": { + "ID": "HungerYyeadude", + "Default | UpdateKey": "Nexus:613" + }, + + "Hunger for Food (Tigerle)": { + "ID": "HungerForFoodByTigerle", + "Default | UpdateKey": "Nexus:810", + "~0.1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Hunger Mod (skn)": { + "ID": "skn.HungerMod", + "MapRemoteVersions": { "1.2.1": "1.0" }, // manifest not updated + "Default | UpdateKey": "Nexus:1127" + }, + + "Idle Pause": { + "ID": "Veleek.IdlePause", + "MapRemoteVersions": { "1.2": "1.1" }, // manifest not updated + "Default | UpdateKey": "Nexus:1092" + }, + + "Improved Quality of Life": { + "ID": "Demiacle.ImprovedQualityOfLife", + "Default | UpdateKey": "Nexus:1025", + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Instant Geode": { + "ID": "InstantGeode", + "~1.12 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Instant Grow Trees": { + "ID": "cantorsdust.InstantGrowTrees", + "FormerIDs": "dc50c58b-c7d8-4e60-86cc-e27b5d95ee59 | community.InstantGrowTrees", // changed in 1.2 and 1.3.1 + "Default | UpdateKey": "Nexus:173" + }, + + "Interaction Helper": { + "ID": "HammurabiInteractionHelper", + "Default | UpdateKey": "Chucklefish:4640" // added in 1.0.4-pathoschild-update + }, + + "Item Auto Stacker": { + "ID": "cat.autostacker", + "MapRemoteVersions": { "1.0.1": "1.0" }, // manifest not updated + "Default | UpdateKey": "Nexus:1184" + }, + + "Json Assets": { + "ID": "spacechase0.JsonAssets", + "Default | UpdateKey": "Nexus:1720" + }, + + "Junimo Farm": { + "ID": "Platonymous.JunimoFarm", + "MapRemoteVersions": { "1.1.2": "1.1.1" }, // manifest not updated + "Default | UpdateKey": "Nexus:984" // added in 1.1.3 + }, + + "Less Strict Over-Exertion (AntiExhaustion)": { + "ID": "BALANCEMOD_AntiExhaustion", + "MapLocalVersions": { "0.0": "1.1" }, + "Default | UpdateKey": "Nexus:637", + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Level Extender": { + "ID": "DevinLematty.LevelExtender", + "FormerIDs": "Devin Lematty.Level Extender", // changed in 1.3 + "Default | UpdateKey": "Nexus:1471" + }, + + "Level Up Notifications": { + "ID": "Level Up Notifications", + "MapRemoteVersions": { "0.0.1a": "0.0.1" }, + "Default | UpdateKey": "Nexus:855" + }, + + "Location and Music Logging": { + "ID": "Brandy Lover.LMlog", + "Default | UpdateKey": "Nexus:1366" + }, + + "Longevity": { + "ID": "RTGOAT.Longevity", + "MapRemoteVersions": { "1.6.8h": "1.6.8" }, + "Default | UpdateKey": "Nexus:649" + }, + + "Lookup Anything": { + "ID": "Pathoschild.LookupAnything", + "FormerIDs": "LookupAnything", // changed in 1.10.1 + "Default | UpdateKey": "Nexus:541", + "~1.18.1 | Status": "AssumeBroken" // broke in SDV 1.3 + }, + + "Love Bubbles": { + "ID": "LoveBubbles", + "Default | UpdateKey": "Nexus:1318" + }, + + "Loved Labels": { + "ID": "Advize.LovedLabels", + "Default | UpdateKey": "Nexus:279" + }, + + "Luck Skill": { + "ID": "spacechase0.LuckSkill", + "FormerIDs": "LuckSkill", // changed in 0.1.4 + "Default | UpdateKey": "Nexus:521" + }, + + "Magic": { + "ID": "spacechase0.Magic", + "MapRemoteVersions": { "0.1.2": "0.1.1" } // not updated in manifest + }, + + "Mail Framework": { + "ID": "DIGUS.MailFrameworkMod", + "Default | UpdateKey": "Nexus:1536" + }, + + "MailOrderPigs": { + "ID": "jwdred.MailOrderPigs", + "Default | UpdateKey": "Nexus:632" + }, + + "Makeshift Multiplayer": { + "ID": "spacechase0.StardewValleyMP", + "FormerIDs": "StardewValleyMP", // changed in 0.3 + "Default | UpdateKey": "Nexus:501" + }, + + "Map Image Exporter": { + "ID": "spacechase0.MapImageExporter", + "FormerIDs": "MapImageExporter", // changed in 1.0.2 + "Default | UpdateKey": "Nexus:1073" + }, + + "Message Box [API]? (ChatMod)": { + "ID": "Kithio:ChatMod", + "Default | UpdateKey": "Chucklefish:4296", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Mining at the Farm": { + "ID": "Nishtra.MiningAtTheFarm", + "FormerIDs": "MiningAtTheFarm", // changed in <=1.7 + "Default | UpdateKey": "Nexus:674" + }, + + "Mining With Explosives": { + "ID": "Nishtra.MiningWithExplosives", + "FormerIDs": "MiningWithExplosives", // changed in 1.1 + "Default | UpdateKey": "Nexus:770" + }, + + "Modder Serialization Utility": { + "ID": "SerializerUtils-0-1", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "it's no longer maintained or used." + }, + + "Monster Level Tip": { + "ID": "WhiteMind.MonsterLT", + "Default | UpdateKey": "Nexus:1896" + }, + + "More Animals": { + "ID": "Entoarox.MoreAnimals", + "FormerIDs": "821ce8f6-e629-41ad-9fde-03b54f68b0b6MOREPETS | Entoarox.MorePets", // changed in 1.3 and 2.0 + "~2.0.2 | UpdateKey": "Chucklefish:4288" // only enable update checks up to 2.0.2 by request (has its own update-check feature) + }, + + "More Artifact Spots": { + "ID": "451", + "Default | UpdateKey": "Nexus:451" + }, + + "More Map Layers": { + "ID": "Platonymous.MoreMapLayers", + "Default | UpdateKey": "Nexus:1134" // added in 1.1.1 + }, + + "More Rain": { + "ID": "Omegasis.MoreRain", + "Default | UpdateKey": "Nexus:441", // added in 1.5.1 + "~1.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "More Weapons": { + "ID": "Joco80.MoreWeapons", + "Default | UpdateKey": "Nexus:1168" + }, + + "Move Faster": { + "ID": "shuaiz.MoveFasterMod", + "Default | UpdateKey": "Nexus:1351", + "1.0.1 | Status": "AssumeBroken" // doesn't do anything as of SDV 1.2.33 (bad Harmony patch?) + }, + + "Multiple Sprites and Portraits On Rotation (File Loading)": { + "ID": "FileLoading", + "MapLocalVersions": { "1.1": "1.12" }, + "Default | UpdateKey": "Nexus:1094", + "~1.12 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Museum Rearranger": { + "ID": "Omegasis.MuseumRearranger", + "Default | UpdateKey": "Nexus:428" // added in 1.4.1 + }, + + "Mushroom Level Tip": { + "ID": "WhiteMind.MLT", + "Default | UpdateKey": "Nexus:1894" + }, + + "New Machines": { + "ID": "F70D4FAB-0AB2-4B78-9F1B-AF2CA2236A59", + "Default | UpdateKey": "Chucklefish:3683", + "~4.2.1343 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Night Owl": { + "ID": "Omegasis.NightOwl", + "MapLocalVersions": { "2.1": "1.3" }, // 1.3 had wrong version in manifest + "Default | UpdateKey": "Nexus:433" // added in 1.4.1 + }, + + "No Crows": { + "ID": "cat.nocrows", + "Default | UpdateKey": "Nexus:1682" + }, + + "No Kids Ever": { + "ID": "Hangy.NoKidsEver", + "Default | UpdateKey": "Nexus:1464" + }, + + "No Debug Mode": { + "ID": "NoDebugMode", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "debug mode was removed in SMAPI 1.0." + }, + + "No Fence Decay": { + "ID": "cat.nofencedecay", + "Default | UpdateKey": "Nexus:1180" + }, + + "No More Pets": { + "ID": "Omegasis.NoMorePets", + "FormerIDs": "NoMorePets", // changed in 1.4 + "Default | UpdateKey": "Nexus:506" // added in 1.4.1 + }, + + "No Rumble Horse": { + "ID": "Xangria.NoRumbleHorse", + "Default | UpdateKey": "Nexus:1779" + }, + + "No Soil Decay": { + "ID": "289dee03-5f38-4d8e-8ffc-e440198e8610", + "Default | UpdateKey": "Nexus:237", + "~0.5 | Status": "AssumeBroken" // broke in SDV 1.2 and uses Assembly.GetExecutingAssembly().Location + }, + + "No Soil Decay Redux": { + "ID": "Platonymous.NoSoilDecayRedux", + "Default | UpdateKey": "Nexus:1084" // added in 1.1.9 + }, + + "NPC Map Locations": { + "ID": "Bouhm.NPCMapLocations", + "FormerIDs": "NPCMapLocationsMod", // changed in 2.0 + "Default | UpdateKey": "Nexus:239" + }, + + "Object Time Left": { + "ID": "spacechase0.ObjectTimeLeft", + "Default | UpdateKey": "Nexus:1315" + }, + + "OmniFarm": { + "ID": "PhthaloBlue.OmniFarm", + "FormerIDs": "BlueMod_OmniFarm", // changed in 2.0.2-pathoschild-update + "Default | UpdateKey": "GitHub:lambui/StardewValleyMod_OmniFarm" + }, + + "One Click Shed": { + "ID": "BitwiseJonMods.OneClickShedReloader", + "Default | UpdateKey": "Nexus:2052" + }, + + "Out of Season Bonuses (Seasonal Items)": { + "ID": "midoriarmstrong.seasonalitems", + "Default | UpdateKey": "Nexus:1452" + }, + + "Part of the Community": { + "ID": "SB_PotC", + "Default | UpdateKey": "Nexus:923" + }, + + "PelicanFiber": { + "ID": "jwdred.PelicanFiber", + "Default | UpdateKey": "Nexus:631" + }, + + "PelicanTTS": { + "ID": "Platonymous.PelicanTTS", + "Default | UpdateKey": "Nexus:1079" // added in 1.6.1 + }, + + "Persia the Mermaid - Standalone Custom NPC": { + "ID": "63b9f419-7449-42db-ab2e-440b4d05c073", + "Default | UpdateKey": "Nexus:1419" + }, + + "Persistent Game Options": { + "ID": "Xangria.PersistentGameOptions", + "Default | UpdateKey": "Nexus:1778" + }, + + "Plant on Grass": { + "ID": "Demiacle.PlantOnGrass", + "Default | UpdateKey": "Nexus:1026" + }, + + "PyTK - Platonymous Toolkit": { + "ID": "Platonymous.Toolkit", + "Default | UpdateKey": "Nexus:1726" + }, + + "Point-and-Plant": { + "ID": "jwdred.PointAndPlant", + "Default | UpdateKey": "Nexus:572", + "MapRemoteVersions": { "1.0.3": "1.0.2" } // manifest not updated + }, + + "Pony Weight Loss Program": { + "ID": "BadNetCode.PonyWeightLossProgram", + "Default | UpdateKey": "Nexus:1232" + }, + + "Portraiture": { + "ID": "Platonymous.Portraiture", + "Default | UpdateKey": "Nexus:999" // added in 1.3.1 + }, + + "Prairie King Made Easy": { + "ID": "Mucchan.PrairieKingMadeEasy", + "Default | UpdateKey": "Chucklefish:3594", + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Purchasable Recipes": { + "ID": "Paracosm.PurchasableRecipes", + "Default | UpdateKey": "Nexus:1722" + }, + + "Quest Delay": { + "ID": "BadNetCode.QuestDelay", + "Default | UpdateKey": "Nexus:1239" + }, + + "Recatch Legendary Fish": { + "ID": "cantorsdust.RecatchLegendaryFish", + "FormerIDs": "b3af8c31-48f0-43cf-8343-3eb08bcfa1f9 | community.RecatchLegendaryFish", // changed in 1.3 and 1.5.1 + "Default | UpdateKey": "Nexus:172" + }, + + "Regeneration": { + "ID": "HammurabiRegeneration", + "Default | UpdateKey": "Chucklefish:4584" + }, + + "Relationship Bar UI": { + "ID": "RelationshipBar", + "Default | UpdateKey": "Nexus:1009" + }, + + "RelationshipsEnhanced": { + "ID": "relationshipsenhanced", + "Default | UpdateKey": "Chucklefish:4435", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Relationship Status": { + "ID": "relationshipstatus", + "MapRemoteVersions": { "1.0.5": "1.0.4" }, // not updated in manifest + "Default | UpdateKey": "Nexus:751", + "~1.0.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Rented Tools": { + "ID": "JarvieK.RentedTools", + "Default | UpdateKey": "Nexus:1307" + }, + + "Replanter": { + "ID": "jwdred.Replanter", + "Default | UpdateKey": "Nexus:589" + }, + + "ReRegeneration": { + "ID": "lrsk_sdvm_rerg.0925160827", + "MapLocalVersions": { "1.1.2-release": "1.1.2" }, + "Default | UpdateKey": "Chucklefish:4465" + }, + + "Reseed": { + "ID": "Roc.Reseed", + "Default | UpdateKey": "Nexus:887" + }, + + "Reusable Wallpapers and Floors (Wallpaper Retain)": { + "ID": "dae1b553-2e39-43e7-8400-c7c5c836134b", + "Default | UpdateKey": "Nexus:356", + "~1.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Ring of Fire": { + "ID": "Platonymous.RingOfFire", + "Default | UpdateKey": "Nexus:1166" // added in 1.0.1 + }, + + "Rope Bridge": { + "ID": "RopeBridge", + "Default | UpdateKey": "Nexus:824" + }, + + "Rotate Toolbar": { + "ID": "Pathoschild.RotateToolbar", + "Default | UpdateKey": "Nexus:1100", + "~1.2.1 | Status": "AssumeBroken" // broke in SDV 1.3 + }, + + "Rush Orders": { + "ID": "spacechase0.RushOrders", + "FormerIDs": "RushOrders", // changed in 1.1 + "Default | UpdateKey": "Nexus:605" + }, + + "Save Anywhere": { + "ID": "Omegasis.SaveAnywhere", + "Default | UpdateKey": "Nexus:444", // added in 2.6.1 + "MapRemoteVersions": { "2.6.2": "2.6.1" } // not updated in manifest + }, + + "Save Backup": { + "ID": "Omegasis.SaveBackup", + "Default | UpdateKey": "Nexus:435", // added in 1.3.1 + "~1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Scroll to Blank": { + "ID": "caraxian.scroll.to.blank", + "Default | UpdateKey": "Chucklefish:4405" + }, + + "Scythe Harvesting": { + "ID": "mmanlapat.ScytheHarvesting", + "FormerIDs": "ScytheHarvesting", // changed in 1.6 + "Default | UpdateKey": "Nexus:1106" + }, + + "SDV Twitch": { + "ID": "MTD.SDVTwitch", + "Default | UpdateKey": "Nexus:1760" + }, + + "Seasonal Immersion": { + "ID": "Entoarox.SeasonalImmersion", + "FormerIDs": "EntoaroxSeasonalHouse | EntoaroxSeasonalBuildings | EntoaroxSeasonalImmersion", // changed in 1.1, 1.6 or earlier, and 1.7 + "~1.11 | UpdateKey": "Chucklefish:4262" // only enable update checks up to 1.11 by request (has its own update-check feature) + }, + + "Seed Bag": { + "ID": "Platonymous.SeedBag", + "Default | UpdateKey": "Nexus:1133" // added in 1.1.2 + }, + + "Seed Catalogue": { + "ID": "spacechase0.SeedCatalogue", + "Default | UpdateKey": "Nexus:1640" + }, + + "Self Service": { + "ID": "JarvieK.SelfService", + "MapRemoteVersions": { "0.2.1": "0.2" }, // manifest not updated + "Default | UpdateKey": "Nexus:1304" + }, + + "Send Items": { + "ID": "Denifia.SendItems", + "Default | UpdateKey": "Nexus:1087" // added in 1.0.3 (2017-10-04) + }, + + "Shed Notifications (BuildingsNotifications)": { + "ID": "TheCroak.BuildingsNotifications", + "Default | UpdateKey": "Nexus:620", + "~0.4.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Shenandoah Project": { + "ID": "Nishtra.ShenandoahProject", + "FormerIDs": "Shenandoah Project", // changed in 1.2 + "MapRemoteVersions": { "1.1.1": "1.1" }, // not updated in manifest + "Default | UpdateKey": "Nexus:756" + }, + + "Ship Anywhere": { + "ID": "spacechase0.ShipAnywhere", + "Default | UpdateKey": "Nexus:1379" + }, + + "Shipment Tracker": { + "ID": "7e474181-e1a0-40f9-9c11-d08a3dcefaf3", + "Default | UpdateKey": "Nexus:321" + }, + + "Shop Expander": { + "ID": "Entoarox.ShopExpander", + "FormerIDs": "EntoaroxShopExpander", // changed in 1.5 and 1.5.2; disambiguate from Faster Paths + "MapRemoteVersions": { "1.6.0b": "1.6.0" }, + "~1.6 | UpdateKey": "Chucklefish:4381" // only enable update checks up to 1.6 by request (has its own update-check feature) + }, + + "Showcase Mod": { + "ID": "Igorious.Showcase", + "MapLocalVersions": { "0.9-500": "0.9" }, + "Default | UpdateKey": "Chucklefish:4487", + "~0.9 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Shroom Spotter": { + "ID": "TehPers.ShroomSpotter", + "Default | UpdateKey": "Nexus:908" + }, + + "Simple Crop Label": { + "ID": "SimpleCropLabel", + "Default | UpdateKey": "Nexus:314" + }, + + "Simple Sound Manager": { + "ID": "Omegasis.SimpleSoundManager", + "Default | UpdateKey": "Nexus:1410" // added in 1.0.1 + }, + + "Simple Sprinklers": { + "ID": "tZed.SimpleSprinkler", + "Default | UpdateKey": "Nexus:76" + }, + + "Siv's Marriage Mod": { + "ID": "6266959802", // official version + "FormerIDs": "Siv.MarriageMod | medoli900.Siv's Marriage Mod", // 1.2.3-unofficial versions + "MapLocalVersions": { "0.0": "1.4" }, + "Default | UpdateKey": "Nexus:366" + }, + + "Skill Prestige": { + "ID": "alphablackwolf.skillPrestige", + "FormerIDs": "6b843e60-c8fc-4a25-a67b-4a38ac8dcf9b", // changed circa 1.2.3 + "Default | UpdateKey": "Nexus:569" + }, + + "Skill Prestige: Cooking Adapter": { + "ID": "Alphablackwolf.CookingSkillPrestigeAdapter", + "FormerIDs": "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63", // changed circa 1.1 + "MapRemoteVersions": { "1.2.3": "1.1" }, // manifest not updated + "Default | UpdateKey": "Nexus:569" + }, + + "Skip Intro": { + "ID": "Pathoschild.SkipIntro", + "FormerIDs": "SkipIntro", // changed in 1.4 + "Default | UpdateKey": "Nexus:533", + "~1.7.2 | Status": "AssumeBroken" // broke in SDV 1.3 + }, + + "Skull Cavern Elevator": { + "ID": "SkullCavernElevator", + "Default | UpdateKey": "Nexus:963" + }, + + "Skull Cave Saver": { + "ID": "cantorsdust.SkullCaveSaver", + "FormerIDs": "8ac06349-26f7-4394-806c-95d48fd35774 | community.SkullCaveSaver", // changed in 1.1 and 1.2.2 + "Default | UpdateKey": "Nexus:175", + "1.3-beta | Status": "AssumeBroken" // doesn't work in multiplayer, no longer maintained + }, + + "Sleepy Eye": { + "ID": "spacechase0.SleepyEye", + "Default | UpdateKey": "Nexus:1152" + }, + + "Slower Fence Decay": { + "ID": "Speeder.SlowerFenceDecay", + "FormerIDs": "SPDSlowFenceDecay", // changed in 0.5.2-pathoschild-update + "Default | UpdateKey": "Nexus:252", + "~0.5.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Smart Mod": { + "ID": "KuroBear.SmartMod", + "~2.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Solar Eclipse Event": { + "ID": "KoihimeNakamura.SolarEclipseEvent", + "Default | UpdateKey": "Nexus:897", + "MapLocalVersions": { "1.3.1-20180131": "1.3.1" } + }, + + "SpaceCore": { + "ID": "spacechase0.SpaceCore", + "Default | UpdateKey": "Nexus:1348" + }, + + "Speedster": { + "ID": "Platonymous.Speedster", + "Default | UpdateKey": "Nexus:1102" // added in 1.3.1 + }, + + "Split Screen": { + "ID": "Ilyaki.SplitScreen", + "~3.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.16 due to reflection into SMAPI internals + }, + + "Sprinkler Range": { + "ID": "cat.sprinklerrange", + "Default | UpdateKey": "Nexus:1179" + }, + + "Sprinkles": { + "ID": "Platonymous.Sprinkles", + "Default | UpdateKey": "Chucklefish:4592" + }, + + "Sprint and Dash": { + "ID": "SPDSprintAndDash", + "Default | UpdateKey": "Nexus:235", + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Sprint and Dash Redux": { + "ID": "littleraskol.SprintAndDashRedux", + "FormerIDs": "lrsk_sdvm_sndr.0921161059", // changed in 1.3 + "Default | UpdateKey": "Chucklefish:4201" + }, + + "StackSplitX": { + "ID": "tstaples.StackSplitX", + "Default | UpdateKey": "Nexus:798" + }, + + "Stardew Config Menu": { + "ID": "Juice805.StardewConfigMenu", + "Default | UpdateKey": "Nexus:1312" + }, + + "Stardew Content Compatibility Layer (SCCL)": { + "ID": "SCCL", + "Default | UpdateKey": "Nexus:889", + "~0.1 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Stardew Editor Game Integration": { + "ID": "spacechase0.StardewEditor.GameIntegration", + "Default | UpdateKey": "Nexus:1298" + }, + + "Stardew Notification": { + "ID": "stardewnotification", + "Default | UpdateKey": "GitHub:monopandora/StardewNotification" + }, + + "Stardew Symphony": { + "ID": "Omegasis.StardewSymphony", + "Default | UpdateKey": "Nexus:425" // added in 1.4.1 + }, + + "StarDustCore": { + "ID": "StarDustCore", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "it was only used by earlier versions of Save Anywhere, and is no longer used or maintained." + }, + + "Starting Money": { + "ID": "mmanlapat.StartingMoney", + "FormerIDs": "StartingMoney", // changed in 1.1 + "Default | UpdateKey": "Nexus:1138" + }, + + "StashItemsToChest": { + "ID": "BlueMod_StashItemsToChest", + "Default | UpdateKey": "GitHub:lambui/StardewValleyMod_StashItemsToChest", + "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Stephan's Lots of Crops": { + "ID": "stephansstardewcrops", + "MapRemoteVersions": { "1.41": "1.1" }, // manifest not updated + "Default | UpdateKey": "Chucklefish:4314" + }, + + "Stumps to Hardwood Stumps": { + "ID": "StumpsToHardwoodStumps", + "Default | UpdateKey": "Nexus:691" + }, + + "Summit Reborn": { + "ID": "KoihimeNakamura.summitreborn", + "FormerIDs": "emissaryofinfinity.summitreborn", // changed in 1.0.2 + "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.3 (runtime errors) + }, + + "Super Greenhouse Warp Modifier": { + "ID": "SuperGreenhouse", + "Default | UpdateKey": "Chucklefish:4334", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Swim Almost Anywhere / Swim Suit": { + "ID": "Platonymous.SwimSuit", + "Default | UpdateKey": "Nexus:1215" // added in 0.5.1 + }, + + "Tapper Ready": { + "ID": "skunkkk.TapperReady", + "Default | UpdateKey": "Nexus:1219" + }, + + "Teh's Fishing Overhaul": { + "ID": "TehPers.FishingOverhaul", + "Default | UpdateKey": "Nexus:866" + }, + + "Teleporter": { + "ID": "Teleporter", + "Default | UpdateKey": "Chucklefish:4374", + "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "The Long Night": { + "ID": "Pathoschild.TheLongNight", + "Default | UpdateKey": "Nexus:1369", + "~1.1.1 | Status": "AssumeBroken" // broke in SDV 1.3 + }, + + "Three-heart Dance Partner": { + "ID": "ThreeHeartDancePartner", + "Default | UpdateKey": "Nexus:500" + }, + + "TimeFreeze": { + "ID": "Omegasis.TimeFreeze", + "FormerIDs": "4108e859-333c-4fec-a1a7-d2e18c1019fe", // changed in 1.2 + "Default | UpdateKey": "Nexus:973" // added in 1.2.1 + }, + + "Time Reminder": { + "ID": "KoihimeNakamura.TimeReminder", + "MapLocalVersions": { "1.0-20170314": "1.0.2" }, + "Default | UpdateKey": "Nexus:1000" + }, + + "TimeSpeed": { + "ID": "cantorsdust.TimeSpeed", + "FormerIDs": "community.TimeSpeed", // changed in 2.3.3 + "Default | UpdateKey": "Nexus:169" + }, + + "To Do List": { + "ID": "eleanor.todolist", + "Default | UpdateKey": "Nexus:1630" + }, + + "Tool Charging": { + "ID": "mralbobo.ToolCharging", + "Default | UpdateKey": "GitHub:mralbobo/stardew-tool-charging" + }, + + "TractorMod": { + "ID": "Pathoschild.TractorMod", + "FormerIDs": "BlueMod_TractorMod | PhthaloBlue.TractorMod | community.TractorMod", // changed in 3.2, 4.0 beta, and 4.0 + "Default | UpdateKey": "Nexus:1401", + "~4.5-beta | Status": "AssumeBroken" // broke in SDV 1.3 + }, + + "TrainerMod": { + "ID": "SMAPI.TrainerMod", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "replaced by ConsoleCommands, which is added by the SMAPI installer." + }, + + "Tree Transplant": { + "ID": "TreeTransplant", + "Default | UpdateKey": "Nexus:1342" + }, + + "UI Info Suite": { + "ID": "Cdaragorn.UiInfoSuite", + "Default | UpdateKey": "Nexus:1150" + }, + + "UiModSuite": { + "ID": "Demiacle.UiModSuite", + "MapLocalVersions": { "0.5": "1.0" }, // not updated in manifest + "Default | UpdateKey": "Nexus:1023", + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Variable Grass": { + "ID": "dantheman999.VariableGrass", + "Default | UpdateKey": "GitHub:dantheman999301/StardewMods" + }, + + "Vertical Toolbar": { + "ID": "SB_VerticalToolMenu", + "Default | UpdateKey": "Nexus:943" + }, + + "WarpAnimals": { + "ID": "Symen.WarpAnimals", + "Default | UpdateKey": "Nexus:1400" + }, + + "What Farm Cave / WhatAMush": { + "ID": "WhatAMush", + "Default | UpdateKey": "Nexus:1097" + }, + + "WHats Up": { + "ID": "wHatsUp", + "Default | UpdateKey": "Nexus:1082" + }, + + "Winter Grass": { + "ID": "cat.wintergrass", + "Default | UpdateKey": "Nexus:1601" + }, + + "Xnb Loader": { + "ID": "Entoarox.XnbLoader", + "~1.1.10 | UpdateKey": "Chucklefish:4506" // only enable update checks up to 1.1.10 by request (has its own update-check feature) + }, + + "zDailyIncrease": { + "ID": "zdailyincrease", + "MapRemoteVersions": { "1.3.5": "1.3.4" }, // not updated in manifest + "Default | UpdateKey": "Chucklefish:4247" + }, + + "Zoom Out Extreme": { + "ID": "RockinMods.ZoomMod", + "FormerIDs": "ZoomMod", // changed circa 1.2.1 + "Default | UpdateKey": "Nexus:1326", + "~0.1 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoryn's Better RNG": { + "ID": "Zoryn.BetterRNG", + "FormerIDs": "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoryn's Calendar Anywhere": { + "ID": "Zoryn.CalendarAnywhere", + "FormerIDs": "a41c01cd-0437-43eb-944f-78cb5a53002a", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoryn's Durable Fences": { + "ID": "Zoryn.DurableFences", + "FormerIDs": "56d3439c-7b9b-497e-9496-0c4890e8a00e", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods" + }, + + "Zoryn's Health Bars": { + "ID": "Zoryn.HealthBars", + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoryn's Fishing Mod": { + "ID": "Zoryn.FishingMod", + "FormerIDs": "fa277b1f-265e-47c3-a84f-cd320cc74949", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods" + }, + + "Zoryn's Junimo Deposit Anywhere": { + "ID": "Zoryn.JunimoDepositAnywhere", + "FormerIDs": "f93a4fe8-cade-4146-9335-b5f82fbbf7bc", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", + "~1.7 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoryn's Movement Mod": { + "ID": "Zoryn.MovementModifier", + "FormerIDs": "8a632929-8335-484f-87dd-c29d2ba3215d", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods" + }, + + "Zoryn's Regen Mod": { + "ID": "Zoryn.RegenMod", + "FormerIDs": "dfac4383-1b6b-4f33-ae4e-37fc23e5252e", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 + } + } +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index f849ee53..c13f5e30 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -293,16 +293,17 @@ Designer - Always + PreserveNewest - + + StardewModdingAPI.metadata.json PreserveNewest - Always + PreserveNewest diff --git a/src/SMAPI/StardewModdingAPI.metadata.json b/src/SMAPI/StardewModdingAPI.metadata.json deleted file mode 100644 index 343257f1..00000000 --- a/src/SMAPI/StardewModdingAPI.metadata.json +++ /dev/null @@ -1,1668 +0,0 @@ -{ - /** - * Metadata about some SMAPI mods used in compatibility, update, and dependency checks. This - * field shouldn't be edited by players in most cases. - * - * Standard fields - * =============== - * The predefined fields are documented below (only 'ID' is required). Each entry's key is the - * default display name for the mod if one isn't available (e.g. in dependency checks). - * - * - ID: the mod's latest unique ID (if any). - * - * - FormerIDs: uniquely identifies the mod across multiple versions, and supports matching - * other fields if no ID was specified. This doesn't include the latest ID, if any. Multiple - * variants can be separated with '|'. - * - * - MapLocalVersions and MapRemoteVersions correct local manifest versions and remote versions - * during update checks. For example, if the API returns version '1.1-1078' where '1078' is - * intended to be a build number, MapRemoteVersions can map it to '1.1' when comparing to the - * mod's current version. This is only meant to support legacy mods with injected update keys. - * - * Versioned metadata - * ================== - * Each record can also specify extra metadata using the field keys below. - * - * Each key consists of a field name prefixed with any combination of version range and 'Default', - * separated by pipes (whitespace trimmed). For example, 'UpdateKey' will always override, - * 'Default | UpdateKey' will only override if the mod has no update keys, and - * '~1.1 | Default | Name' will do the same up to version 1.1. - * - * The version format is 'min~max' (where either side can be blank for unbounded), or a single - * version number. - * - * These are the valid field names: - * - * - UpdateKey: the update key to set in the mod's manifest. This is used to enable update - * checks for older mods that haven't been updated to use it yet. - * - * - Status: overrides compatibility checks. The possible values are Obsolete (SMAPI won't load - * it because the mod should no longer be used), AssumeBroken (SMAPI won't load it because - * the specified version isn't compatible), or AssumeCompatible (SMAPI will try to load it - * even if it detects incompatible code). - * - * Note that this shouldn't be set to 'AssumeBroken' if SMAPI can detect the incompatibility - * automatically, since that hides the details from trace logs. - * - * - StatusReasonPhrase: a message to show to the player explaining why the mod can't be loaded - * (if applicable). If blank, will default to a generic not-compatible message. - * - * - AlternativeUrl: a URL where the player can find an unofficial update or alternative if the - * mod is no longer compatible. - */ - "ModData": { - "AccessChestAnywhere": { - "ID": "AccessChestAnywhere", - "MapLocalVersions": { "1.1-1078": "1.1" }, - "Default | UpdateKey": "Nexus:257", - "~1.1 | Status": "AssumeBroken" - }, - - "Adjust Artisan Prices": { - "ID": "ThatNorthernMonkey.AdjustArtisanPrices", - "FormerIDs": "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc", // changed in 0.0.2-pathoschild-update - "MapRemoteVersions": { "0.01": "0.0.1" }, - "Default | UpdateKey": "Chucklefish:3532" - }, - - "Adjust Monster": { - "ID": "mmanlapat.AdjustMonster", - "Default | UpdateKey": "Nexus:1161" - }, - - "Advanced Location Loader": { - "ID": "Entoarox.AdvancedLocationLoader", - "~1.3.7 | UpdateKey": "Chucklefish:3619" // only enable update checks up to 1.3.7 by request (has its own update-check feature) - }, - - "Adventure Shop Inventory": { - "ID": "HammurabiAdventureShopInventory", - "Default | UpdateKey": "Chucklefish:4608" - }, - - "AgingMod": { - "ID": "skn.AgingMod", - "Default | UpdateKey": "Nexus:1129", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "All Crops All Seasons": { - "ID": "cantorsdust.AllCropsAllSeasons", - "FormerIDs": "29ee8246-d67b-4242-a340-35a9ae0d5dd7 | community.AllCropsAllSeasons", // changed in 1.3 and 1.5 - "Default | UpdateKey": "Nexus:170" - }, - - "All Professions": { - "ID": "cantorsdust.AllProfessions", - "FormerIDs": "8c37b1a7-4bfb-4916-9d8a-9533e6363ea3 | community.AllProfessions", // changed in 1.2 and 1.3.1 - "Default | UpdateKey": "Nexus:174" - }, - - "Almighty Farming Tool": { - "ID": "439", - "MapRemoteVersions": { "1.21": "1.2.1" }, - "Default | UpdateKey": "Nexus:439" - }, - - "Animal Husbandry": { - "ID": "DIGUS.ANIMALHUSBANDRYMOD", - "FormerIDs": "DIGUS.BUTCHER", // changed in 2.0.1 - "Default | UpdateKey": "Nexus:1538" - }, - - "Animal Mood Fix": { - "ID": "GPeters-AnimalMoodFix", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2." - }, - - "Animal Sitter": { - "ID": "jwdred.AnimalSitter", - "Default | UpdateKey": "Nexus:581", - "~1.0.8 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Arcade Pong": { - "ID": "Platonymous.ArcadePong", - "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.16 due to reflection into SMAPI internals - }, - - "A Tapper's Dream": { - "ID": "ddde5195-8f85-4061-90cc-0d4fd5459358", - "Default | UpdateKey": "Nexus:260" - }, - - "Auto Animal Doors": { - "ID": "AaronTaggart.AutoAnimalDoors", - "Default | UpdateKey": "Nexus:1019" - }, - - "Auto-Eat": { - "ID": "Permamiss.AutoEat", - "FormerIDs": "BALANCEMOD_AutoEat", // changed in 1.1.1 - "Default | UpdateKey": "Nexus:643" - }, - - "AutoFish": { - "ID": "WhiteMind.AF", - "Default | UpdateKey": "Nexus:1895" - }, - - "AutoGate": { - "ID": "AutoGate", - "Default | UpdateKey": "Nexus:820" - }, - - "Automate": { - "ID": "Pathoschild.Automate", - "Default | UpdateKey": "Nexus:1063", - "~1.10-beta.7 | Status": "AssumeBroken" // broke in SDV 1.3.20 - }, - - "Automated Doors": { - "ID": "azah.automated-doors", - "FormerIDs": "1abcfa07-2cf4-4dc3-a6e9-6068b642112b", // changed in 1.4.1 - "Default | UpdateKey": "GitHub:azah/AutomatedDoors" // added in 1.4.2 - }, - - "AutoSpeed": { - "ID": "Omegasis.AutoSpeed", - "Default | UpdateKey": "Nexus:443" // added in 1.4.1 - }, - - "Basic Sprinklers Improved": { - "ID": "lrsk_sdvm_bsi.0117171308", - "MapRemoteVersions": { "1.0.2": "1.0.1-release" }, // manifest not updated - "Default | UpdateKey": "Nexus:833" - }, - - "Better Hay": { - "ID": "cat.betterhay", - "Default | UpdateKey": "Nexus:1430" - }, - - "Better Quality More Seasons": { - "ID": "SB_BQMS", - "Default | UpdateKey": "Nexus:935" - }, - - "Better Quarry": { - "ID": "BetterQuarry", - "Default | UpdateKey": "Nexus:771" - }, - - "Better Ranching": { - "ID": "BetterRanching", - "Default | UpdateKey": "Nexus:859" - }, - - "Better Shipping Box": { - "ID": "Kithio:BetterShippingBox", - "MapLocalVersions": { "1.0.1": "1.0.2" }, - "Default | UpdateKey": "Chucklefish:4302" - }, - - "Better Sprinklers": { - "ID": "Speeder.BetterSprinklers", - "FormerIDs": "SPDSprinklersMod", // changed in 2.3 - "Default | UpdateKey": "Nexus:41" - }, - - "Billboard Anywhere": { - "ID": "Omegasis.BillboardAnywhere", - "Default | UpdateKey": "Nexus:492" // added in 1.4.1 - }, - - "Birthday Mail": { - "ID": "KathrynHazuka.BirthdayMail", - "FormerIDs": "005e02dc-d900-425c-9c68-1ff55c5a295d", // changed in 1.2.3-pathoschild-update - "Default | UpdateKey": "Nexus:276", - "MapRemoteVersions": { "1.3.1": "1.3" } // manifest not updated - }, - - "Breed Like Rabbits": { - "ID": "dycedarger.breedlikerabbits", - "Default | UpdateKey": "Nexus:948" - }, - - "Build Endurance": { - "ID": "Omegasis.BuildEndurance", - "Default | UpdateKey": "Nexus:445" // added in 1.4.1 - }, - - "Build Health": { - "ID": "Omegasis.BuildHealth", - "Default | UpdateKey": "Nexus:446" // added in 1.4.1 - }, - - "Buy Cooking Recipes": { - "ID": "Denifia.BuyRecipes", - "Default | UpdateKey": "Nexus:1126" // added in 1.0.1 (2017-10-04) - }, - - "Buy Back Collectables": { - "ID": "Omegasis.BuyBackCollectables", - "FormerIDs": "BuyBackCollectables", // changed in 1.4 - "Default | UpdateKey": "Nexus:507" // added in 1.4.1 - }, - - "Carry Chest": { - "ID": "spacechase0.CarryChest", - "Default | UpdateKey": "Nexus:1333" - }, - - "Casks Anywhere": { - "ID": "CasksAnywhere", - "MapLocalVersions": { "1.1-alpha": "1.1" }, - "Default | UpdateKey": "Nexus:878" - }, - - "Categorize Chests": { - "ID": "CategorizeChests", - "Default | UpdateKey": "Nexus:1300", - "~1.4.3-unofficial.2.mizzion | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.18 (in-game errors) - }, - - "Chefs Closet": { - "ID": "Duder.ChefsCloset", - "MapLocalVersions": { "1.3-1": "1.3" }, - "Default | UpdateKey": "Nexus:1030" - }, - - "Chest Label System": { - "ID": "Speeder.ChestLabel", - "FormerIDs": "SPDChestLabel", // changed in 1.5.1-pathoschild-update - "Default | UpdateKey": "Nexus:242" - }, - - "Chest Pooling": { - "ID": "mralbobo.ChestPooling", - "Default | UpdateKey": "GitHub:mralbobo/stardew-chest-pooling" - }, - - "Chests Anywhere": { - "ID": "Pathoschild.ChestsAnywhere", - "FormerIDs": "ChestsAnywhere", // changed in 1.9 - "Default | UpdateKey": "Nexus:518", - "~1.12.4 | Status": "AssumeBroken" // broke in SDV 1.3 - }, - - "CJB Automation": { - "ID": "CJBAutomation", - "Default | UpdateKey": "Nexus:211", - "~1.4 | Status": "AssumeBroken", // broke in SDV 1.2 - "~1.4 | AlternativeUrl": "http://www.nexusmods.com/stardewvalley/mods/1063" - }, - - "CJB Cheats Menu": { - "ID": "CJBok.CheatsMenu", - "FormerIDs": "CJBCheatsMenu", // changed in 1.14 - "Default | UpdateKey": "Nexus:4", - "~1.18-beta | Status": "AssumeBroken" // broke in SDV 1.3, first beta causes significant friendship bugs - }, - - "CJB Item Spawner": { - "ID": "CJBok.ItemSpawner", - "FormerIDs": "CJBItemSpawner", // changed in 1.7 - "Default | UpdateKey": "Nexus:93", - "~1.10 | Status": "AssumeBroken" // broke in SDV 1.3 - }, - - "CJB Show Item Sell Price": { - "ID": "CJBok.ShowItemSellPrice", - "FormerIDs": "CJBShowItemSellPrice", // changed in 1.7 - "Default | UpdateKey": "Nexus:5", - "~1.8 | Status": "AssumeBroken" // broke in SDV 1.3 - }, - - "Clean Farm": { - "ID": "tstaples.CleanFarm", - "Default | UpdateKey": "Nexus:794" - }, - - "Climates of Ferngill": { - "ID": "KoihimeNakamura.ClimatesOfFerngill", - "Default | UpdateKey": "Nexus:604" - }, - - "Coal Regen": { - "ID": "Blucifer.CoalRegen", - "Default | UpdateKey": "Nexus:1664" - }, - - "Cobalt": { - "ID": "spacechase0.Cobalt", - "MapRemoteVersions": { "1.1.3": "1.1.2" } // not updated in manifest - }, - - "Cold Weather Haley": { - "ID": "LordXamon.ColdWeatherHaleyPRO", - "Default | UpdateKey": "Nexus:1169", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Colored Chests": { - "ID": "4befde5c-731c-4853-8e4b-c5cdf946805f", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "colored chests were added in Stardew Valley 1.1." - }, - - "Combat with Farm Implements": { - "ID": "SPDFarmingImplementsInCombat", - "Default | UpdateKey": "Nexus:313" - }, - - "Community Bundle Item Tooltip": { - "ID": "musbah.bundleTooltip", - "Default | UpdateKey": "Nexus:1329" - }, - - "Concentration on Farming": { - "ID": "punyo.ConcentrationOnFarming", - "Default | UpdateKey": "Nexus:1445" - }, - - "Configurable Machines": { - "ID": "21da6619-dc03-4660-9794-8e5b498f5b97", - "MapLocalVersions": { "1.2-beta": "1.2" }, - "Default | UpdateKey": "Nexus:280" - }, - - "Configurable Shipping Dates": { - "ID": "ConfigurableShippingDates", - "Default | UpdateKey": "Nexus:675", - "~1.1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Content Patcher": { - "ID": "Pathoschild.ContentPatcher", - "Default | UpdateKey": "Nexus:1915", - "~1.4-beta.5 | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.18 (in-game errors) - }, - - "Cooking Skill": { - "ID": "spacechase0.CookingSkill", - "FormerIDs": "CookingSkill", // changed in 1.0.4–6 - "Default | UpdateKey": "Nexus:522" - }, - - "CrabNet": { - "ID": "jwdred.CrabNet", - "Default | UpdateKey": "Nexus:584" - }, - - "Crafting Counter": { - "ID": "lolpcgaming.CraftingCounter", - "Default | UpdateKey": "Nexus:1585", - "MapRemoteVersions": { "1.1": "1.0" } // not updated in manifest - }, - - "Current Location": { - "ID": "CurrentLocation102120161203", - "Default | UpdateKey": "Nexus:638" - }, - - "Custom Asset Modifier": { - "ID": "Omegasis.CustomAssetModifier", - "Default | UpdateKey": "1836" - }, - - "Custom Critters": { - "ID": "spacechase0.CustomCritters", - "Default | UpdateKey": "Nexus:1255" - }, - - "Custom Crops": { - "ID": "spacechase0.CustomCrops", - "Default | UpdateKey": "Nexus:1592" - }, - - "Custom Element Handler": { - "ID": "Platonymous.CustomElementHandler", - "Default | UpdateKey": "Nexus:1068" // added in 1.3.1 - }, - - "Custom Farming Redux": { - "ID": "Platonymous.CustomFarming", - "Default | UpdateKey": "Nexus:991" // added in 0.6.1 - }, - - "Custom Farming Automate Bridge": { - "ID": "Platonymous.CFAutomate", - "~1.0.1 | Status": "AssumeBroken", // no longer compatible with Automate - "~1.0.1 | AlternativeUrl": "https://www.nexusmods.com/stardewvalley/mods/991" - }, - - "Custom Farm Types": { - "ID": "spacechase0.CustomFarmTypes", - "Default | UpdateKey": "Nexus:1140" - }, - - "Custom Furniture": { - "ID": "Platonymous.CustomFurniture", - "Default | UpdateKey": "Nexus:1254" // added in 0.4.1 - }, - - "Customize Exterior": { - "ID": "spacechase0.CustomizeExterior", - "FormerIDs": "CustomizeExterior", // changed in 1.0.3 - "Default | UpdateKey": "Nexus:1099", - "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Customizable Cart Redux": { - "ID": "KoihimeNakamura.CCR", - "MapLocalVersions": { "1.1-20170917": "1.1" }, - "Default | UpdateKey": "Nexus:1402" - }, - - "Customizable Traveling Cart Days": { - "ID": "TravelingCartYyeahdude", - "Default | UpdateKey": "Nexus:567" - }, - - "Custom Linens": { - "ID": "Mevima.CustomLinens", - "MapRemoteVersions": { "1.1": "1.0" }, // manifest not updated - "Default | UpdateKey": "Nexus:1027" - }, - - "Custom NPC": { - "ID": "Platonymous.CustomNPC", - "Default | UpdateKey": "Nexus:1607" - }, - - "Custom Shops Redux": { - "ID": "Omegasis.CustomShopReduxGui", - "Default | UpdateKey": "Nexus:1378" // added in 1.4.1 - }, - - "Custom TV": { - "ID": "Platonymous.CustomTV", - "Default | UpdateKey": "Nexus:1139" // added in 1.0.6 - }, - - "Daily Luck Message": { - "ID": "Schematix.DailyLuckMessage", - "Default | UpdateKey": "Nexus:1327" - }, - - "Daily News": { - "ID": "bashNinja.DailyNews", - "Default | UpdateKey": "Nexus:1141", - "~1.2 | Status": "AssumeBroken" // broke in Stardew Valley 1.3 (or depends on CustomTV which broke) - }, - - "Daily Quest Anywhere": { - "ID": "Omegasis.DailyQuestAnywhere", - "FormerIDs": "DailyQuest", // changed in 1.4 - "Default | UpdateKey": "Nexus:513" // added in 1.4.1 - }, - - "Data Maps": { - "ID": "Pathoschild.DataMaps", - "Default | UpdateKey": "Nexus:1691", - "~1.3 | Status": "AssumeBroken" // broke in SDV 1.3 - }, - - "Debug Mode": { - "ID": "Pathoschild.DebugMode", - "FormerIDs": "Pathoschild.Stardew.DebugMode", // changed in 1.4 - "Default | UpdateKey": "Nexus:679", - "~1.8 | Status": "AssumeBroken" // broke in SDV 1.3 - }, - - "Did You Water Your Crops?": { - "ID": "Nishtra.DidYouWaterYourCrops", - "Default | UpdateKey": "Nexus:1583" - }, - - "Dynamic Checklist": { - "ID": "gunnargolf.DynamicChecklist", - "Default | UpdateKey": "Nexus:1145" // added in 1.0.1-pathoschild-update - }, - - "Dynamic Horses": { - "ID": "Bpendragon-DynamicHorses", - "MapRemoteVersions": { "1.2": "1.1-release" }, // manifest not updated - "Default | UpdateKey": "Nexus:874" - }, - - "Dynamic Machines": { - "ID": "DynamicMachines", - "MapLocalVersions": { "1.1": "1.1.1" }, - "Default | UpdateKey": "Nexus:374", - "~1.1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Dynamic NPC Sprites": { - "ID": "BashNinja.DynamicNPCSprites", - "Default | UpdateKey": "Nexus:1183" - }, - - "Easier Farming": { - "ID": "cautiouswafffle.EasierFarming", - "Default | UpdateKey": "Nexus:1426" - }, - - "Empty Hands": { - "ID": "QuicksilverFox.EmptyHands", - "Default | UpdateKey": "Nexus:1176" // added in 1.0.1-pathoschild-update - }, - - "Enemy Health Bars": { - "ID": "Speeder.HealthBars", - "FormerIDs": "SPDHealthBar", // changed in 1.7.1-pathoschild-update - "Default | UpdateKey": "Nexus:193" - }, - - "Entoarox Framework": { - "ID": "Entoarox.EntoaroxFramework", - "FormerIDs": "eacdb74b-4080-4452-b16b-93773cda5cf9", // changed in ??? - "~2.0.6 | UpdateKey": "Chucklefish:4228", // only enable update checks up to 2.0.6 by request (has its own update-check feature) - "~2.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.5 (error reflecting into SMAPI internals) - }, - - "Expanded Fridge": { - "ID": "Uwazouri.ExpandedFridge", - "Default | UpdateKey": "Nexus:1191" - }, - - "Experience Bars": { - "ID": "spacechase0.ExperienceBars", - "FormerIDs": "ExperienceBars", // changed in 1.0.2 - "Default | UpdateKey": "Nexus:509" - }, - - "Extended Bus System": { - "ID": "ExtendedBusSystem", - "Default | UpdateKey": "Chucklefish:4373" - }, - - "Extended Fridge": { - "ID": "Crystalmir.ExtendedFridge", - "FormerIDs": "Mystra007ExtendedFridge", // changed in 1.0.1 - "Default | UpdateKey": "Nexus:485" - }, - - "Extended Greenhouse": { - "ID": "ExtendedGreenhouse", - "Default | UpdateKey": "Chucklefish:4303", - "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Extended Minecart": { - "ID": "Entoarox.ExtendedMinecart", - "~1.7.1 | UpdateKey": "Chucklefish:4359" // only enable update checks up to 1.7.1 by request (has its own update-check feature) - }, - - "Extended Reach": { - "ID": "spacechase0.ExtendedReach", - "Default | UpdateKey": "Nexus:1493" - }, - - "Fall 28 Snow Day": { - "ID": "Omegasis.Fall28SnowDay", - "Default | UpdateKey": "Nexus:486", // added in 1.4.1 - "~1.4.1 | Status": "AssumeBroken" // broke in SMAPI 2.0, and update for SMAPI 2.0 doesn't do anything - }, - - "Farm Automation Unofficial: Item Collector": { - "ID": "Maddy99.FarmAutomation.ItemCollector", - "~0.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Farm Expansion": { - "ID": "Advize.FarmExpansion", - "FormerIDs": "3888bdfd-73f6-4776-8bb7-8ad45aea1915 | AdvizeFarmExpansionMod-2-0 | AdvizeFarmExpansionMod-2-0-5", // changed in 2.0, 2.0.5, and 3.0 - "Default | UpdateKey": "Nexus:130" - }, - - "Fast Animations": { - "ID": "Pathoschild.FastAnimations", - "Default | UpdateKey": "Nexus:1089", - "~1.5 | Status": "AssumeBroken" // broke in SDV 1.3 - }, - - "Faster Grass": { - "ID": "IceGladiador.FasterGrass", - "Default | UpdateKey": "Nexus:1772" - }, - - "Faster Paths": { - "ID": "Entoarox.FasterPaths", - "FormerIDs": "615f85f8-5c89-44ee-aecc-c328f172e413", // changed in 1.3 - "~1.3.3 | UpdateKey": "Chucklefish:3641" // only enable update checks up to 1.3.3 by request (has its own update-check feature) - }, - - "Fishing Adjust": { - "ID": "shuaiz.FishingAdjustMod", - "Default | UpdateKey": "Nexus:1350", - "~2.0.1 | Status": "AssumeBroken" // Method not found: 'Void Harmony.HarmonyInstance.Patch(System.Reflection.MethodBase, Harmony.HarmonyMethod, Harmony.HarmonyMethod, Harmony.HarmonyMethod)' - }, - - "Fishing Tuner Redux": { - "ID": "HammurabiFishingTunerRedux", - "Default | UpdateKey": "Chucklefish:4578" - }, - - "Fixed Secret Woods Debris": { - "ID": "f4iTh.WoodsDebrisFix", - "Default | UpdateKey": "Nexus:1941" - }, - - "Flower Color Picker": { - "ID": "spacechase0.FlowerColorPicker", - "Default | UpdateKey": "Nexus:1229" - }, - - "Forage at the Farm": { - "ID": "Nishtra.ForageAtTheFarm", - "FormerIDs": "ForageAtTheFarm", // changed in <=1.6 - "Default | UpdateKey": "Nexus:673" - }, - - "Furniture Anywhere": { - "ID": "Entoarox.FurnitureAnywhere", - "~1.1.5 | UpdateKey": "Chucklefish:4324" // only enable update checks up to 1.1.5 by request (has its own update-check feature) - }, - - "Game Reminder": { - "ID": "mmanlapat.GameReminder", - "Default | UpdateKey": "Nexus:1153" - }, - - "Gate Opener": { - "ID": "mralbobo.GateOpener", - "Default | UpdateKey": "GitHub:mralbobo/stardew-gate-opener" - }, - - "GenericShopExtender": { - "ID": "GenericShopExtender", - "Default | UpdateKey": "Nexus:814" // added in 0.1.3 - }, - - "Geode Info Menu": { - "ID": "cat.geodeinfomenu", - "Default | UpdateKey": "Nexus:1448" - }, - - "Get Dressed": { - "ID": "Advize.GetDressed", - "Default | UpdateKey": "Nexus:331" - }, - - "Giant Crop Ring": { - "ID": "cat.giantcropring", - "Default | UpdateKey": "Nexus:1182" - }, - - "Gift Taste Helper": { - "ID": "tstaples.GiftTasteHelper", - "FormerIDs": "8008db57-fa67-4730-978e-34b37ef191d6", // changed in 2.5 - "Default | UpdateKey": "Nexus:229" - }, - - "Grandfather's Gift": { - "ID": "ShadowDragon.GrandfathersGift", - "Default | UpdateKey": "Nexus:985" - }, - - "Happy Animals": { - "ID": "HappyAnimals", - "~1.0.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Happy Birthday (Omegasis)": { - "ID": "Omegasis.HappyBirthday", - "Default | UpdateKey": "Nexus:520" // added in 1.4.1 - }, - - "Hardcore Mines": { - "ID": "kibbe.hardcore_mines", - "Default | UpdateKey": "Nexus:1674" - }, - - "Harp of Yoba Redux": { - "ID": "Platonymous.HarpOfYobaRedux", - "Default | UpdateKey": "Nexus:914" // added in 2.0.3 - }, - - "Harvest Moon Witch Princess": { - "ID": "Sasara.WitchPrincess", - "Default | UpdateKey": "Nexus:1157" - }, - - "Harvest With Scythe": { - "ID": "965169fd-e1ed-47d0-9f12-b104535fb4bc", - "Default | UpdateKey": "Nexus:236" - }, - - "Horse Whistle (icepuente)": { - "ID": "icepuente.HorseWhistle", - "Default | UpdateKey": "Nexus:1131", - "~1.1.2-unofficial.1-pathoschild | Status": "AssumeBroken" // causes significant lag, fixed in unofficial.2 - }, - - "Hunger (Yyeadude)": { - "ID": "HungerYyeadude", - "Default | UpdateKey": "Nexus:613" - }, - - "Hunger for Food (Tigerle)": { - "ID": "HungerForFoodByTigerle", - "Default | UpdateKey": "Nexus:810", - "~0.1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Hunger Mod (skn)": { - "ID": "skn.HungerMod", - "MapRemoteVersions": { "1.2.1": "1.0" }, // manifest not updated - "Default | UpdateKey": "Nexus:1127" - }, - - "Idle Pause": { - "ID": "Veleek.IdlePause", - "MapRemoteVersions": { "1.2": "1.1" }, // manifest not updated - "Default | UpdateKey": "Nexus:1092" - }, - - "Improved Quality of Life": { - "ID": "Demiacle.ImprovedQualityOfLife", - "Default | UpdateKey": "Nexus:1025", - "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Instant Geode": { - "ID": "InstantGeode", - "~1.12 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Instant Grow Trees": { - "ID": "cantorsdust.InstantGrowTrees", - "FormerIDs": "dc50c58b-c7d8-4e60-86cc-e27b5d95ee59 | community.InstantGrowTrees", // changed in 1.2 and 1.3.1 - "Default | UpdateKey": "Nexus:173" - }, - - "Interaction Helper": { - "ID": "HammurabiInteractionHelper", - "Default | UpdateKey": "Chucklefish:4640" // added in 1.0.4-pathoschild-update - }, - - "Item Auto Stacker": { - "ID": "cat.autostacker", - "MapRemoteVersions": { "1.0.1": "1.0" }, // manifest not updated - "Default | UpdateKey": "Nexus:1184" - }, - - "Json Assets": { - "ID": "spacechase0.JsonAssets", - "Default | UpdateKey": "Nexus:1720" - }, - - "Junimo Farm": { - "ID": "Platonymous.JunimoFarm", - "MapRemoteVersions": { "1.1.2": "1.1.1" }, // manifest not updated - "Default | UpdateKey": "Nexus:984" // added in 1.1.3 - }, - - "Less Strict Over-Exertion (AntiExhaustion)": { - "ID": "BALANCEMOD_AntiExhaustion", - "MapLocalVersions": { "0.0": "1.1" }, - "Default | UpdateKey": "Nexus:637", - "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Level Extender": { - "ID": "DevinLematty.LevelExtender", - "FormerIDs": "Devin Lematty.Level Extender", // changed in 1.3 - "Default | UpdateKey": "Nexus:1471" - }, - - "Level Up Notifications": { - "ID": "Level Up Notifications", - "MapRemoteVersions": { "0.0.1a": "0.0.1" }, - "Default | UpdateKey": "Nexus:855" - }, - - "Location and Music Logging": { - "ID": "Brandy Lover.LMlog", - "Default | UpdateKey": "Nexus:1366" - }, - - "Longevity": { - "ID": "RTGOAT.Longevity", - "MapRemoteVersions": { "1.6.8h": "1.6.8" }, - "Default | UpdateKey": "Nexus:649" - }, - - "Lookup Anything": { - "ID": "Pathoschild.LookupAnything", - "FormerIDs": "LookupAnything", // changed in 1.10.1 - "Default | UpdateKey": "Nexus:541", - "~1.18.1 | Status": "AssumeBroken" // broke in SDV 1.3 - }, - - "Love Bubbles": { - "ID": "LoveBubbles", - "Default | UpdateKey": "Nexus:1318" - }, - - "Loved Labels": { - "ID": "Advize.LovedLabels", - "Default | UpdateKey": "Nexus:279" - }, - - "Luck Skill": { - "ID": "spacechase0.LuckSkill", - "FormerIDs": "LuckSkill", // changed in 0.1.4 - "Default | UpdateKey": "Nexus:521" - }, - - "Magic": { - "ID": "spacechase0.Magic", - "MapRemoteVersions": { "0.1.2": "0.1.1" } // not updated in manifest - }, - - "Mail Framework": { - "ID": "DIGUS.MailFrameworkMod", - "Default | UpdateKey": "Nexus:1536" - }, - - "MailOrderPigs": { - "ID": "jwdred.MailOrderPigs", - "Default | UpdateKey": "Nexus:632" - }, - - "Makeshift Multiplayer": { - "ID": "spacechase0.StardewValleyMP", - "FormerIDs": "StardewValleyMP", // changed in 0.3 - "Default | UpdateKey": "Nexus:501" - }, - - "Map Image Exporter": { - "ID": "spacechase0.MapImageExporter", - "FormerIDs": "MapImageExporter", // changed in 1.0.2 - "Default | UpdateKey": "Nexus:1073" - }, - - "Message Box [API]? (ChatMod)": { - "ID": "Kithio:ChatMod", - "Default | UpdateKey": "Chucklefish:4296", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Mining at the Farm": { - "ID": "Nishtra.MiningAtTheFarm", - "FormerIDs": "MiningAtTheFarm", // changed in <=1.7 - "Default | UpdateKey": "Nexus:674" - }, - - "Mining With Explosives": { - "ID": "Nishtra.MiningWithExplosives", - "FormerIDs": "MiningWithExplosives", // changed in 1.1 - "Default | UpdateKey": "Nexus:770" - }, - - "Modder Serialization Utility": { - "ID": "SerializerUtils-0-1", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "it's no longer maintained or used." - }, - - "Monster Level Tip": { - "ID": "WhiteMind.MonsterLT", - "Default | UpdateKey": "Nexus:1896" - }, - - "More Animals": { - "ID": "Entoarox.MoreAnimals", - "FormerIDs": "821ce8f6-e629-41ad-9fde-03b54f68b0b6MOREPETS | Entoarox.MorePets", // changed in 1.3 and 2.0 - "~2.0.2 | UpdateKey": "Chucklefish:4288" // only enable update checks up to 2.0.2 by request (has its own update-check feature) - }, - - "More Artifact Spots": { - "ID": "451", - "Default | UpdateKey": "Nexus:451" - }, - - "More Map Layers": { - "ID": "Platonymous.MoreMapLayers", - "Default | UpdateKey": "Nexus:1134" // added in 1.1.1 - }, - - "More Rain": { - "ID": "Omegasis.MoreRain", - "Default | UpdateKey": "Nexus:441", // added in 1.5.1 - "~1.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "More Weapons": { - "ID": "Joco80.MoreWeapons", - "Default | UpdateKey": "Nexus:1168" - }, - - "Move Faster": { - "ID": "shuaiz.MoveFasterMod", - "Default | UpdateKey": "Nexus:1351", - "1.0.1 | Status": "AssumeBroken" // doesn't do anything as of SDV 1.2.33 (bad Harmony patch?) - }, - - "Multiple Sprites and Portraits On Rotation (File Loading)": { - "ID": "FileLoading", - "MapLocalVersions": { "1.1": "1.12" }, - "Default | UpdateKey": "Nexus:1094", - "~1.12 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Museum Rearranger": { - "ID": "Omegasis.MuseumRearranger", - "Default | UpdateKey": "Nexus:428" // added in 1.4.1 - }, - - "Mushroom Level Tip": { - "ID": "WhiteMind.MLT", - "Default | UpdateKey": "Nexus:1894" - }, - - "New Machines": { - "ID": "F70D4FAB-0AB2-4B78-9F1B-AF2CA2236A59", - "Default | UpdateKey": "Chucklefish:3683", - "~4.2.1343 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Night Owl": { - "ID": "Omegasis.NightOwl", - "MapLocalVersions": { "2.1": "1.3" }, // 1.3 had wrong version in manifest - "Default | UpdateKey": "Nexus:433" // added in 1.4.1 - }, - - "No Crows": { - "ID": "cat.nocrows", - "Default | UpdateKey": "Nexus:1682" - }, - - "No Kids Ever": { - "ID": "Hangy.NoKidsEver", - "Default | UpdateKey": "Nexus:1464" - }, - - "No Debug Mode": { - "ID": "NoDebugMode", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "debug mode was removed in SMAPI 1.0." - }, - - "No Fence Decay": { - "ID": "cat.nofencedecay", - "Default | UpdateKey": "Nexus:1180" - }, - - "No More Pets": { - "ID": "Omegasis.NoMorePets", - "FormerIDs": "NoMorePets", // changed in 1.4 - "Default | UpdateKey": "Nexus:506" // added in 1.4.1 - }, - - "No Rumble Horse": { - "ID": "Xangria.NoRumbleHorse", - "Default | UpdateKey": "Nexus:1779" - }, - - "No Soil Decay": { - "ID": "289dee03-5f38-4d8e-8ffc-e440198e8610", - "Default | UpdateKey": "Nexus:237", - "~0.5 | Status": "AssumeBroken" // broke in SDV 1.2 and uses Assembly.GetExecutingAssembly().Location - }, - - "No Soil Decay Redux": { - "ID": "Platonymous.NoSoilDecayRedux", - "Default | UpdateKey": "Nexus:1084" // added in 1.1.9 - }, - - "NPC Map Locations": { - "ID": "Bouhm.NPCMapLocations", - "FormerIDs": "NPCMapLocationsMod", // changed in 2.0 - "Default | UpdateKey": "Nexus:239" - }, - - "Object Time Left": { - "ID": "spacechase0.ObjectTimeLeft", - "Default | UpdateKey": "Nexus:1315" - }, - - "OmniFarm": { - "ID": "PhthaloBlue.OmniFarm", - "FormerIDs": "BlueMod_OmniFarm", // changed in 2.0.2-pathoschild-update - "Default | UpdateKey": "GitHub:lambui/StardewValleyMod_OmniFarm" - }, - - "One Click Shed": { - "ID": "BitwiseJonMods.OneClickShedReloader", - "Default | UpdateKey": "Nexus:2052" - }, - - "Out of Season Bonuses (Seasonal Items)": { - "ID": "midoriarmstrong.seasonalitems", - "Default | UpdateKey": "Nexus:1452" - }, - - "Part of the Community": { - "ID": "SB_PotC", - "Default | UpdateKey": "Nexus:923" - }, - - "PelicanFiber": { - "ID": "jwdred.PelicanFiber", - "Default | UpdateKey": "Nexus:631" - }, - - "PelicanTTS": { - "ID": "Platonymous.PelicanTTS", - "Default | UpdateKey": "Nexus:1079" // added in 1.6.1 - }, - - "Persia the Mermaid - Standalone Custom NPC": { - "ID": "63b9f419-7449-42db-ab2e-440b4d05c073", - "Default | UpdateKey": "Nexus:1419" - }, - - "Persistent Game Options": { - "ID": "Xangria.PersistentGameOptions", - "Default | UpdateKey": "Nexus:1778" - }, - - "Plant on Grass": { - "ID": "Demiacle.PlantOnGrass", - "Default | UpdateKey": "Nexus:1026" - }, - - "PyTK - Platonymous Toolkit": { - "ID": "Platonymous.Toolkit", - "Default | UpdateKey": "Nexus:1726" - }, - - "Point-and-Plant": { - "ID": "jwdred.PointAndPlant", - "Default | UpdateKey": "Nexus:572", - "MapRemoteVersions": { "1.0.3": "1.0.2" } // manifest not updated - }, - - "Pony Weight Loss Program": { - "ID": "BadNetCode.PonyWeightLossProgram", - "Default | UpdateKey": "Nexus:1232" - }, - - "Portraiture": { - "ID": "Platonymous.Portraiture", - "Default | UpdateKey": "Nexus:999" // added in 1.3.1 - }, - - "Prairie King Made Easy": { - "ID": "Mucchan.PrairieKingMadeEasy", - "Default | UpdateKey": "Chucklefish:3594", - "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Purchasable Recipes": { - "ID": "Paracosm.PurchasableRecipes", - "Default | UpdateKey": "Nexus:1722" - }, - - "Quest Delay": { - "ID": "BadNetCode.QuestDelay", - "Default | UpdateKey": "Nexus:1239" - }, - - "Recatch Legendary Fish": { - "ID": "cantorsdust.RecatchLegendaryFish", - "FormerIDs": "b3af8c31-48f0-43cf-8343-3eb08bcfa1f9 | community.RecatchLegendaryFish", // changed in 1.3 and 1.5.1 - "Default | UpdateKey": "Nexus:172" - }, - - "Regeneration": { - "ID": "HammurabiRegeneration", - "Default | UpdateKey": "Chucklefish:4584" - }, - - "Relationship Bar UI": { - "ID": "RelationshipBar", - "Default | UpdateKey": "Nexus:1009" - }, - - "RelationshipsEnhanced": { - "ID": "relationshipsenhanced", - "Default | UpdateKey": "Chucklefish:4435", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Relationship Status": { - "ID": "relationshipstatus", - "MapRemoteVersions": { "1.0.5": "1.0.4" }, // not updated in manifest - "Default | UpdateKey": "Nexus:751", - "~1.0.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Rented Tools": { - "ID": "JarvieK.RentedTools", - "Default | UpdateKey": "Nexus:1307" - }, - - "Replanter": { - "ID": "jwdred.Replanter", - "Default | UpdateKey": "Nexus:589" - }, - - "ReRegeneration": { - "ID": "lrsk_sdvm_rerg.0925160827", - "MapLocalVersions": { "1.1.2-release": "1.1.2" }, - "Default | UpdateKey": "Chucklefish:4465" - }, - - "Reseed": { - "ID": "Roc.Reseed", - "Default | UpdateKey": "Nexus:887" - }, - - "Reusable Wallpapers and Floors (Wallpaper Retain)": { - "ID": "dae1b553-2e39-43e7-8400-c7c5c836134b", - "Default | UpdateKey": "Nexus:356", - "~1.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Ring of Fire": { - "ID": "Platonymous.RingOfFire", - "Default | UpdateKey": "Nexus:1166" // added in 1.0.1 - }, - - "Rope Bridge": { - "ID": "RopeBridge", - "Default | UpdateKey": "Nexus:824" - }, - - "Rotate Toolbar": { - "ID": "Pathoschild.RotateToolbar", - "Default | UpdateKey": "Nexus:1100", - "~1.2.1 | Status": "AssumeBroken" // broke in SDV 1.3 - }, - - "Rush Orders": { - "ID": "spacechase0.RushOrders", - "FormerIDs": "RushOrders", // changed in 1.1 - "Default | UpdateKey": "Nexus:605" - }, - - "Save Anywhere": { - "ID": "Omegasis.SaveAnywhere", - "Default | UpdateKey": "Nexus:444", // added in 2.6.1 - "MapRemoteVersions": { "2.6.2": "2.6.1" } // not updated in manifest - }, - - "Save Backup": { - "ID": "Omegasis.SaveBackup", - "Default | UpdateKey": "Nexus:435", // added in 1.3.1 - "~1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Scroll to Blank": { - "ID": "caraxian.scroll.to.blank", - "Default | UpdateKey": "Chucklefish:4405" - }, - - "Scythe Harvesting": { - "ID": "mmanlapat.ScytheHarvesting", - "FormerIDs": "ScytheHarvesting", // changed in 1.6 - "Default | UpdateKey": "Nexus:1106" - }, - - "SDV Twitch": { - "ID": "MTD.SDVTwitch", - "Default | UpdateKey": "Nexus:1760" - }, - - "Seasonal Immersion": { - "ID": "Entoarox.SeasonalImmersion", - "FormerIDs": "EntoaroxSeasonalHouse | EntoaroxSeasonalBuildings | EntoaroxSeasonalImmersion", // changed in 1.1, 1.6 or earlier, and 1.7 - "~1.11 | UpdateKey": "Chucklefish:4262" // only enable update checks up to 1.11 by request (has its own update-check feature) - }, - - "Seed Bag": { - "ID": "Platonymous.SeedBag", - "Default | UpdateKey": "Nexus:1133" // added in 1.1.2 - }, - - "Seed Catalogue": { - "ID": "spacechase0.SeedCatalogue", - "Default | UpdateKey": "Nexus:1640" - }, - - "Self Service": { - "ID": "JarvieK.SelfService", - "MapRemoteVersions": { "0.2.1": "0.2" }, // manifest not updated - "Default | UpdateKey": "Nexus:1304" - }, - - "Send Items": { - "ID": "Denifia.SendItems", - "Default | UpdateKey": "Nexus:1087" // added in 1.0.3 (2017-10-04) - }, - - "Shed Notifications (BuildingsNotifications)": { - "ID": "TheCroak.BuildingsNotifications", - "Default | UpdateKey": "Nexus:620", - "~0.4.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Shenandoah Project": { - "ID": "Nishtra.ShenandoahProject", - "FormerIDs": "Shenandoah Project", // changed in 1.2 - "MapRemoteVersions": { "1.1.1": "1.1" }, // not updated in manifest - "Default | UpdateKey": "Nexus:756" - }, - - "Ship Anywhere": { - "ID": "spacechase0.ShipAnywhere", - "Default | UpdateKey": "Nexus:1379" - }, - - "Shipment Tracker": { - "ID": "7e474181-e1a0-40f9-9c11-d08a3dcefaf3", - "Default | UpdateKey": "Nexus:321" - }, - - "Shop Expander": { - "ID": "Entoarox.ShopExpander", - "FormerIDs": "EntoaroxShopExpander", // changed in 1.5 and 1.5.2; disambiguate from Faster Paths - "MapRemoteVersions": { "1.6.0b": "1.6.0" }, - "~1.6 | UpdateKey": "Chucklefish:4381" // only enable update checks up to 1.6 by request (has its own update-check feature) - }, - - "Showcase Mod": { - "ID": "Igorious.Showcase", - "MapLocalVersions": { "0.9-500": "0.9" }, - "Default | UpdateKey": "Chucklefish:4487", - "~0.9 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Shroom Spotter": { - "ID": "TehPers.ShroomSpotter", - "Default | UpdateKey": "Nexus:908" - }, - - "Simple Crop Label": { - "ID": "SimpleCropLabel", - "Default | UpdateKey": "Nexus:314" - }, - - "Simple Sound Manager": { - "ID": "Omegasis.SimpleSoundManager", - "Default | UpdateKey": "Nexus:1410" // added in 1.0.1 - }, - - "Simple Sprinklers": { - "ID": "tZed.SimpleSprinkler", - "Default | UpdateKey": "Nexus:76" - }, - - "Siv's Marriage Mod": { - "ID": "6266959802", // official version - "FormerIDs": "Siv.MarriageMod | medoli900.Siv's Marriage Mod", // 1.2.3-unofficial versions - "MapLocalVersions": { "0.0": "1.4" }, - "Default | UpdateKey": "Nexus:366" - }, - - "Skill Prestige": { - "ID": "alphablackwolf.skillPrestige", - "FormerIDs": "6b843e60-c8fc-4a25-a67b-4a38ac8dcf9b", // changed circa 1.2.3 - "Default | UpdateKey": "Nexus:569" - }, - - "Skill Prestige: Cooking Adapter": { - "ID": "Alphablackwolf.CookingSkillPrestigeAdapter", - "FormerIDs": "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63", // changed circa 1.1 - "MapRemoteVersions": { "1.2.3": "1.1" }, // manifest not updated - "Default | UpdateKey": "Nexus:569" - }, - - "Skip Intro": { - "ID": "Pathoschild.SkipIntro", - "FormerIDs": "SkipIntro", // changed in 1.4 - "Default | UpdateKey": "Nexus:533", - "~1.7.2 | Status": "AssumeBroken" // broke in SDV 1.3 - }, - - "Skull Cavern Elevator": { - "ID": "SkullCavernElevator", - "Default | UpdateKey": "Nexus:963" - }, - - "Skull Cave Saver": { - "ID": "cantorsdust.SkullCaveSaver", - "FormerIDs": "8ac06349-26f7-4394-806c-95d48fd35774 | community.SkullCaveSaver", // changed in 1.1 and 1.2.2 - "Default | UpdateKey": "Nexus:175", - "1.3-beta | Status": "AssumeBroken" // doesn't work in multiplayer, no longer maintained - }, - - "Sleepy Eye": { - "ID": "spacechase0.SleepyEye", - "Default | UpdateKey": "Nexus:1152" - }, - - "Slower Fence Decay": { - "ID": "Speeder.SlowerFenceDecay", - "FormerIDs": "SPDSlowFenceDecay", // changed in 0.5.2-pathoschild-update - "Default | UpdateKey": "Nexus:252", - "~0.5.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Smart Mod": { - "ID": "KuroBear.SmartMod", - "~2.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Solar Eclipse Event": { - "ID": "KoihimeNakamura.SolarEclipseEvent", - "Default | UpdateKey": "Nexus:897", - "MapLocalVersions": { "1.3.1-20180131": "1.3.1" } - }, - - "SpaceCore": { - "ID": "spacechase0.SpaceCore", - "Default | UpdateKey": "Nexus:1348" - }, - - "Speedster": { - "ID": "Platonymous.Speedster", - "Default | UpdateKey": "Nexus:1102" // added in 1.3.1 - }, - - "Split Screen": { - "ID": "Ilyaki.SplitScreen", - "~3.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.16 due to reflection into SMAPI internals - }, - - "Sprinkler Range": { - "ID": "cat.sprinklerrange", - "Default | UpdateKey": "Nexus:1179" - }, - - "Sprinkles": { - "ID": "Platonymous.Sprinkles", - "Default | UpdateKey": "Chucklefish:4592" - }, - - "Sprint and Dash": { - "ID": "SPDSprintAndDash", - "Default | UpdateKey": "Nexus:235", - "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Sprint and Dash Redux": { - "ID": "littleraskol.SprintAndDashRedux", - "FormerIDs": "lrsk_sdvm_sndr.0921161059", // changed in 1.3 - "Default | UpdateKey": "Chucklefish:4201" - }, - - "StackSplitX": { - "ID": "tstaples.StackSplitX", - "Default | UpdateKey": "Nexus:798" - }, - - "Stardew Config Menu": { - "ID": "Juice805.StardewConfigMenu", - "Default | UpdateKey": "Nexus:1312" - }, - - "Stardew Content Compatibility Layer (SCCL)": { - "ID": "SCCL", - "Default | UpdateKey": "Nexus:889", - "~0.1 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Stardew Editor Game Integration": { - "ID": "spacechase0.StardewEditor.GameIntegration", - "Default | UpdateKey": "Nexus:1298" - }, - - "Stardew Notification": { - "ID": "stardewnotification", - "Default | UpdateKey": "GitHub:monopandora/StardewNotification" - }, - - "Stardew Symphony": { - "ID": "Omegasis.StardewSymphony", - "Default | UpdateKey": "Nexus:425" // added in 1.4.1 - }, - - "StarDustCore": { - "ID": "StarDustCore", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "it was only used by earlier versions of Save Anywhere, and is no longer used or maintained." - }, - - "Starting Money": { - "ID": "mmanlapat.StartingMoney", - "FormerIDs": "StartingMoney", // changed in 1.1 - "Default | UpdateKey": "Nexus:1138" - }, - - "StashItemsToChest": { - "ID": "BlueMod_StashItemsToChest", - "Default | UpdateKey": "GitHub:lambui/StardewValleyMod_StashItemsToChest", - "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Stephan's Lots of Crops": { - "ID": "stephansstardewcrops", - "MapRemoteVersions": { "1.41": "1.1" }, // manifest not updated - "Default | UpdateKey": "Chucklefish:4314" - }, - - "Stumps to Hardwood Stumps": { - "ID": "StumpsToHardwoodStumps", - "Default | UpdateKey": "Nexus:691" - }, - - "Summit Reborn": { - "ID": "KoihimeNakamura.summitreborn", - "FormerIDs": "emissaryofinfinity.summitreborn", // changed in 1.0.2 - "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.3 (runtime errors) - }, - - "Super Greenhouse Warp Modifier": { - "ID": "SuperGreenhouse", - "Default | UpdateKey": "Chucklefish:4334", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Swim Almost Anywhere / Swim Suit": { - "ID": "Platonymous.SwimSuit", - "Default | UpdateKey": "Nexus:1215" // added in 0.5.1 - }, - - "Tapper Ready": { - "ID": "skunkkk.TapperReady", - "Default | UpdateKey": "Nexus:1219" - }, - - "Teh's Fishing Overhaul": { - "ID": "TehPers.FishingOverhaul", - "Default | UpdateKey": "Nexus:866" - }, - - "Teleporter": { - "ID": "Teleporter", - "Default | UpdateKey": "Chucklefish:4374", - "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "The Long Night": { - "ID": "Pathoschild.TheLongNight", - "Default | UpdateKey": "Nexus:1369", - "~1.1.1 | Status": "AssumeBroken" // broke in SDV 1.3 - }, - - "Three-heart Dance Partner": { - "ID": "ThreeHeartDancePartner", - "Default | UpdateKey": "Nexus:500" - }, - - "TimeFreeze": { - "ID": "Omegasis.TimeFreeze", - "FormerIDs": "4108e859-333c-4fec-a1a7-d2e18c1019fe", // changed in 1.2 - "Default | UpdateKey": "Nexus:973" // added in 1.2.1 - }, - - "Time Reminder": { - "ID": "KoihimeNakamura.TimeReminder", - "MapLocalVersions": { "1.0-20170314": "1.0.2" }, - "Default | UpdateKey": "Nexus:1000" - }, - - "TimeSpeed": { - "ID": "cantorsdust.TimeSpeed", - "FormerIDs": "community.TimeSpeed", // changed in 2.3.3 - "Default | UpdateKey": "Nexus:169" - }, - - "To Do List": { - "ID": "eleanor.todolist", - "Default | UpdateKey": "Nexus:1630" - }, - - "Tool Charging": { - "ID": "mralbobo.ToolCharging", - "Default | UpdateKey": "GitHub:mralbobo/stardew-tool-charging" - }, - - "TractorMod": { - "ID": "Pathoschild.TractorMod", - "FormerIDs": "BlueMod_TractorMod | PhthaloBlue.TractorMod | community.TractorMod", // changed in 3.2, 4.0 beta, and 4.0 - "Default | UpdateKey": "Nexus:1401", - "~4.5-beta | Status": "AssumeBroken" // broke in SDV 1.3 - }, - - "TrainerMod": { - "ID": "SMAPI.TrainerMod", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "replaced by ConsoleCommands, which is added by the SMAPI installer." - }, - - "Tree Transplant": { - "ID": "TreeTransplant", - "Default | UpdateKey": "Nexus:1342" - }, - - "UI Info Suite": { - "ID": "Cdaragorn.UiInfoSuite", - "Default | UpdateKey": "Nexus:1150" - }, - - "UiModSuite": { - "ID": "Demiacle.UiModSuite", - "MapLocalVersions": { "0.5": "1.0" }, // not updated in manifest - "Default | UpdateKey": "Nexus:1023", - "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Variable Grass": { - "ID": "dantheman999.VariableGrass", - "Default | UpdateKey": "GitHub:dantheman999301/StardewMods" - }, - - "Vertical Toolbar": { - "ID": "SB_VerticalToolMenu", - "Default | UpdateKey": "Nexus:943" - }, - - "WarpAnimals": { - "ID": "Symen.WarpAnimals", - "Default | UpdateKey": "Nexus:1400" - }, - - "What Farm Cave / WhatAMush": { - "ID": "WhatAMush", - "Default | UpdateKey": "Nexus:1097" - }, - - "WHats Up": { - "ID": "wHatsUp", - "Default | UpdateKey": "Nexus:1082" - }, - - "Winter Grass": { - "ID": "cat.wintergrass", - "Default | UpdateKey": "Nexus:1601" - }, - - "Xnb Loader": { - "ID": "Entoarox.XnbLoader", - "~1.1.10 | UpdateKey": "Chucklefish:4506" // only enable update checks up to 1.1.10 by request (has its own update-check feature) - }, - - "zDailyIncrease": { - "ID": "zdailyincrease", - "MapRemoteVersions": { "1.3.5": "1.3.4" }, // not updated in manifest - "Default | UpdateKey": "Chucklefish:4247" - }, - - "Zoom Out Extreme": { - "ID": "RockinMods.ZoomMod", - "FormerIDs": "ZoomMod", // changed circa 1.2.1 - "Default | UpdateKey": "Nexus:1326", - "~0.1 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Zoryn's Better RNG": { - "ID": "Zoryn.BetterRNG", - "FormerIDs": "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", - "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Zoryn's Calendar Anywhere": { - "ID": "Zoryn.CalendarAnywhere", - "FormerIDs": "a41c01cd-0437-43eb-944f-78cb5a53002a", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", - "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Zoryn's Durable Fences": { - "ID": "Zoryn.DurableFences", - "FormerIDs": "56d3439c-7b9b-497e-9496-0c4890e8a00e", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods" - }, - - "Zoryn's Health Bars": { - "ID": "Zoryn.HealthBars", - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", - "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Zoryn's Fishing Mod": { - "ID": "Zoryn.FishingMod", - "FormerIDs": "fa277b1f-265e-47c3-a84f-cd320cc74949", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods" - }, - - "Zoryn's Junimo Deposit Anywhere": { - "ID": "Zoryn.JunimoDepositAnywhere", - "FormerIDs": "f93a4fe8-cade-4146-9335-b5f82fbbf7bc", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", - "~1.7 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Zoryn's Movement Mod": { - "ID": "Zoryn.MovementModifier", - "FormerIDs": "8a632929-8335-484f-87dd-c29d2ba3215d", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods" - }, - - "Zoryn's Regen Mod": { - "ID": "Zoryn.RegenMod", - "FormerIDs": "dfac4383-1b6b-4f33-ae4e-37fc23e5252e", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", - "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 - } - } -} -- cgit From 0079110870e4944e734be507ede91e7b0b655df6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 8 Jul 2018 13:58:37 -0400 Subject: encapsulate type reference comparison --- src/SMAPI/Framework/ModLoading/RewriteHelper.cs | 188 ++---------------- .../Framework/ModLoading/TypeReferenceComparer.cs | 209 +++++++++++++++++++++ src/SMAPI/StardewModdingAPI.csproj | 1 + 3 files changed, 221 insertions(+), 177 deletions(-) create mode 100644 src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs index f8684cde..2f79809c 100644 --- a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs +++ b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Text.RegularExpressions; using Mono.Cecil; using Mono.Cecil.Cil; @@ -14,8 +12,8 @@ namespace StardewModdingAPI.Framework.ModLoading /********* ** Properties *********/ - /// A pattern matching type name substrings to strip for display. - private static readonly Regex StripTypeNamePattern = new Regex(@"`\d+(?=<)", RegexOptions.Compiled); + /// The comparer which heuristically compares type definitions. + private static readonly TypeReferenceComparer TypeDefinitionComparer = new TypeReferenceComparer(); /********* @@ -68,6 +66,15 @@ namespace StardewModdingAPI.Framework.ModLoading return true; } + /// Determine whether two type IDs look like the same type, accounting for placeholder values such as !0. + /// The type ID to compare. + /// The other type ID to compare. + /// true if the type IDs look like the same type, false if not. + public static bool LooksLikeSameType(TypeReference typeA, TypeReference typeB) + { + return RewriteHelper.TypeDefinitionComparer.Equals(typeA, typeB); + } + /// Get whether a method definition matches the signature expected by a method reference. /// The method definition. /// The method reference. @@ -99,178 +106,5 @@ namespace StardewModdingAPI.Framework.ModLoading .GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public) .Any(method => RewriteHelper.HasMatchingSignature(method, reference)); } - - /// Determine whether this type ID has a placeholder such as !0. - /// The type to check. - /// true if the type ID contains a placeholder, false if not. - public static bool HasPlaceholder(string typeID) - { - return typeID.Contains("!0"); - } - - /// returns whether this type ID is a placeholder, i.e., it begins with "!". - /// The symbol to validate. - /// true if the symbol is a placeholder, false if not - public static bool IsPlaceholder(string symbol) - { - return symbol.StartsWith("!"); - } - - /// Determine whether two type IDs look like the same type, accounting for placeholder values such as !0. - /// The type ID to compare. - /// The other type ID to compare. - /// true if the type IDs look like the same type, false if not. - public static bool LooksLikeSameType(TypeReference a, TypeReference b) - { - string typeA = RewriteHelper.GetComparableTypeID(a); - string typeB = RewriteHelper.GetComparableTypeID(b); - - string placeholderType = "", actualType = ""; - - if (RewriteHelper.HasPlaceholder(typeA)) - { - placeholderType = typeA; - actualType = typeB; - } - else if (RewriteHelper.HasPlaceholder(typeB)) - { - placeholderType = typeB; - actualType = typeA; - } - else - return typeA == typeB; - - return RewriteHelper.PlaceholderTypeValidates(placeholderType, actualType); - } - - /// Get a unique string representation of a type. - /// The type reference. - private static string GetComparableTypeID(TypeReference type) - { - return RewriteHelper.StripTypeNamePattern.Replace(type.FullName, ""); - } - - protected class SymbolLocation - { - public string symbol; - public int depth; - - public SymbolLocation(string symbol, int depth) - { - this.symbol = symbol; - this.depth = depth; - } - } - - private static List symbolBoundaries = new List { '<', '>', ',' }; - - /// Traverses and parses out symbols from a type which does not contain placeholder values. - /// The type to traverse. - /// A List in which to store the parsed symbols. - private static void TraverseActualType(string type, List typeSymbols) - { - int depth = 0; - string symbol = ""; - - foreach (char c in type) - { - if (RewriteHelper.symbolBoundaries.Contains(c)) - { - typeSymbols.Add(new SymbolLocation(symbol, depth)); - symbol = ""; - switch (c) - { - case '<': - depth++; - break; - case '>': - depth--; - break; - default: - break; - } - } - else - symbol += c; - } - } - - /// Determines whether two symbols in a type ID match, accounting for placeholders such as !0. - /// A symbol in a typename which contains placeholders. - /// A symbol in a typename which does not contain placeholders. - /// A dictionary containing a mapping of placeholders to concrete types. - /// true if the symbols match, false if not. - private static bool SymbolsMatch(SymbolLocation symbolA, SymbolLocation symbolB, Dictionary placeholderMap) - { - if (symbolA.depth != symbolB.depth) - return false; - - if (!RewriteHelper.IsPlaceholder(symbolA.symbol)) - { - return symbolA.symbol == symbolB.symbol; - } - - if (placeholderMap.ContainsKey(symbolA.symbol)) - { - return placeholderMap[symbolA.symbol] == symbolB.symbol; - } - - placeholderMap[symbolA.symbol] = symbolB.symbol; - - return true; - } - - /// Determines whether a type which has placeholders correctly resolves to the concrete type provided. - /// A type containing placeholders such as !0. - /// The list of symbols extracted from the concrete type. - /// true if the type resolves correctly, false if not. - private static bool PlaceholderTypeResolvesToActualType(string type, List typeSymbols) - { - Dictionary placeholderMap = new Dictionary(); - - int depth = 0, symbolCount = 0; - string symbol = ""; - - foreach (char c in type) - { - if (symbolBoundaries.Contains(c)) - { - bool match = RewriteHelper.SymbolsMatch(new SymbolLocation(symbol, depth), typeSymbols[symbolCount], placeholderMap); - if (typeSymbols.Count <= symbolCount || - !match) - return false; - - symbolCount++; - symbol = ""; - switch (c) - { - case '<': - depth++; - break; - case '>': - depth--; - break; - default: - break; - } - } - else - symbol += c; - } - - return true; - } - - /// Determines whether a type with placeholders in it matches a type without placeholders. - /// The type with placeholders in it. - /// The type without placeholders. - /// true if the placeholder type can resolve to the actual type, false if not. - private static bool PlaceholderTypeValidates(string placeholderType, string actualType) - { - List typeSymbols = new List(); - - RewriteHelper.TraverseActualType(actualType, typeSymbols); - return PlaceholderTypeResolvesToActualType(placeholderType, typeSymbols); - } } } diff --git a/src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs b/src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs new file mode 100644 index 00000000..8d128b37 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs @@ -0,0 +1,209 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; +using Mono.Cecil; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Performs heuristic equality checks for instances. + internal class TypeReferenceComparer : IEqualityComparer + { + /********* + ** Properties + *********/ + /// A pattern matching type name substrings to strip for display. + private readonly Regex StripTypeNamePattern = new Regex(@"`\d+(?=<)", RegexOptions.Compiled); + + private List symbolBoundaries = new List { '<', '>', ',' }; + + + /********* + ** Public methods + *********/ + /// Get whether the specified objects are equal. + /// The first object to compare. + /// The second object to compare. + public bool Equals(TypeReference a, TypeReference b) + { + string typeA = this.GetComparableTypeID(a); + string typeB = this.GetComparableTypeID(b); + + string placeholderType = "", actualType = ""; + + if (this.HasPlaceholder(typeA)) + { + placeholderType = typeA; + actualType = typeB; + } + else if (this.HasPlaceholder(typeB)) + { + placeholderType = typeB; + actualType = typeA; + } + else + return typeA == typeB; + + return this.PlaceholderTypeValidates(placeholderType, actualType); + } + + /// Get a hash code for the specified object. + /// The object for which a hash code is to be returned. + /// The object type is a reference type and is null. + public int GetHashCode(TypeReference obj) + { + return obj.GetHashCode(); + } + + + /********* + ** Private methods + *********/ + /// Get a unique string representation of a type. + /// The type reference. + private string GetComparableTypeID(TypeReference type) + { + return this.StripTypeNamePattern.Replace(type.FullName, ""); + } + + /// Determine whether this type ID has a placeholder such as !0. + /// The type to check. + /// true if the type ID contains a placeholder, false if not. + private bool HasPlaceholder(string typeID) + { + return typeID.Contains("!0"); + } + + /// returns whether this type ID is a placeholder, i.e., it begins with "!". + /// The symbol to validate. + /// true if the symbol is a placeholder, false if not + private bool IsPlaceholder(string symbol) + { + return symbol.StartsWith("!"); + } + + /// Traverses and parses out symbols from a type which does not contain placeholder values. + /// The type to traverse. + /// A List in which to store the parsed symbols. + private void TraverseActualType(string type, List typeSymbols) + { + int depth = 0; + string symbol = ""; + + foreach (char c in type) + { + if (this.symbolBoundaries.Contains(c)) + { + typeSymbols.Add(new SymbolLocation(symbol, depth)); + symbol = ""; + switch (c) + { + case '<': + depth++; + break; + case '>': + depth--; + break; + default: + break; + } + } + else + symbol += c; + } + } + + /// Determines whether two symbols in a type ID match, accounting for placeholders such as !0. + /// A symbol in a typename which contains placeholders. + /// A symbol in a typename which does not contain placeholders. + /// A dictionary containing a mapping of placeholders to concrete types. + /// true if the symbols match, false if not. + private bool SymbolsMatch(SymbolLocation symbolA, SymbolLocation symbolB, Dictionary placeholderMap) + { + if (symbolA.depth != symbolB.depth) + return false; + + if (!this.IsPlaceholder(symbolA.symbol)) + { + return symbolA.symbol == symbolB.symbol; + } + + if (placeholderMap.ContainsKey(symbolA.symbol)) + { + return placeholderMap[symbolA.symbol] == symbolB.symbol; + } + + placeholderMap[symbolA.symbol] = symbolB.symbol; + + return true; + } + + /// Determines whether a type which has placeholders correctly resolves to the concrete type provided. + /// A type containing placeholders such as !0. + /// The list of symbols extracted from the concrete type. + /// true if the type resolves correctly, false if not. + private bool PlaceholderTypeResolvesToActualType(string type, List typeSymbols) + { + Dictionary placeholderMap = new Dictionary(); + + int depth = 0, symbolCount = 0; + string symbol = ""; + + foreach (char c in type) + { + if (this.symbolBoundaries.Contains(c)) + { + bool match = this.SymbolsMatch(new SymbolLocation(symbol, depth), typeSymbols[symbolCount], placeholderMap); + if (typeSymbols.Count <= symbolCount || + !match) + return false; + + symbolCount++; + symbol = ""; + switch (c) + { + case '<': + depth++; + break; + case '>': + depth--; + break; + default: + break; + } + } + else + symbol += c; + } + + return true; + } + + /// Determines whether a type with placeholders in it matches a type without placeholders. + /// The type with placeholders in it. + /// The type without placeholders. + /// true if the placeholder type can resolve to the actual type, false if not. + private bool PlaceholderTypeValidates(string placeholderType, string actualType) + { + List typeSymbols = new List(); + + this.TraverseActualType(actualType, typeSymbols); + return PlaceholderTypeResolvesToActualType(placeholderType, typeSymbols); + } + + + + /********* + ** Inner classes + *********/ + protected class SymbolLocation + { + public string symbol; + public int depth; + + public SymbolLocation(string symbol, int depth) + { + this.symbol = symbol; + this.depth = depth; + } + } + } +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index c13f5e30..57c2c9e8 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -110,6 +110,7 @@ + -- cgit From 3b078d55daccd13332e2cba1fd5c76f775505d7a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 8 Jul 2018 20:06:33 -0400 Subject: add GameLoop events for SMAPI 3.0 (#310) --- src/SMAPI/Events/GameLoopLaunchedEventArgs.cs | 7 ++++ src/SMAPI/Events/GameLoopUpdatedEventArgs.cs | 36 ++++++++++++++++++++ src/SMAPI/Events/GameLoopUpdatingEventArgs.cs | 36 ++++++++++++++++++++ src/SMAPI/Events/IGameLoopEvents.cs | 17 ++++++++++ src/SMAPI/Events/IModEvents.cs | 3 ++ src/SMAPI/Framework/Events/EventManager.cs | 45 ++++++++++++++++--------- src/SMAPI/Framework/Events/ModEvents.cs | 4 +++ src/SMAPI/Framework/Events/ModGameLoopEvents.cs | 39 +++++++++++++++++++++ src/SMAPI/Framework/SGame.cs | 17 +++++----- src/SMAPI/StardewModdingAPI.csproj | 5 +++ 10 files changed, 186 insertions(+), 23 deletions(-) create mode 100644 src/SMAPI/Events/GameLoopLaunchedEventArgs.cs create mode 100644 src/SMAPI/Events/GameLoopUpdatedEventArgs.cs create mode 100644 src/SMAPI/Events/GameLoopUpdatingEventArgs.cs create mode 100644 src/SMAPI/Events/IGameLoopEvents.cs create mode 100644 src/SMAPI/Framework/Events/ModGameLoopEvents.cs (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/src/SMAPI/Events/GameLoopLaunchedEventArgs.cs b/src/SMAPI/Events/GameLoopLaunchedEventArgs.cs new file mode 100644 index 00000000..6a42e4f9 --- /dev/null +++ b/src/SMAPI/Events/GameLoopLaunchedEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class GameLoopLaunchedEventArgs : EventArgs { } +} diff --git a/src/SMAPI/Events/GameLoopUpdatedEventArgs.cs b/src/SMAPI/Events/GameLoopUpdatedEventArgs.cs new file mode 100644 index 00000000..3ad34b69 --- /dev/null +++ b/src/SMAPI/Events/GameLoopUpdatedEventArgs.cs @@ -0,0 +1,36 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class GameLoopUpdatedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The number of ticks elapsed since the game started, including the current tick. + public uint Ticks { get; } + + /// Whether is a multiple of 60, which happens approximately once per second. + public bool IsOneSecond { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The number of ticks elapsed since the game started, including the current tick. + public GameLoopUpdatedEventArgs(uint ticks) + { + this.Ticks = ticks; + this.IsOneSecond = this.IsMultipleOf(60); + } + + /// Get whether is a multiple of the given . This is mainly useful if you want to run logic intermittently (e.g. e.IsMultipleOf(30) for every half-second). + /// The factor to check. + public bool IsMultipleOf(uint number) + { + return this.Ticks % number == 0; + } + } +} diff --git a/src/SMAPI/Events/GameLoopUpdatingEventArgs.cs b/src/SMAPI/Events/GameLoopUpdatingEventArgs.cs new file mode 100644 index 00000000..d6a8b5c2 --- /dev/null +++ b/src/SMAPI/Events/GameLoopUpdatingEventArgs.cs @@ -0,0 +1,36 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class GameLoopUpdatingEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The number of ticks elapsed since the game started, including the current tick. + public uint Ticks { get; } + + /// Whether is a multiple of 60, which happens approximately once per second. + public bool IsOneSecond { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The number of ticks elapsed since the game started, including the current tick. + public GameLoopUpdatingEventArgs(uint ticks) + { + this.Ticks = ticks; + this.IsOneSecond = this.IsMultipleOf(60); + } + + /// Get whether is a multiple of the given . This is mainly useful if you want to run logic intermittently (e.g. e.IsMultipleOf(30) for every half-second). + /// The factor to check. + public bool IsMultipleOf(uint number) + { + return this.Ticks % number == 0; + } + } +} diff --git a/src/SMAPI/Events/IGameLoopEvents.cs b/src/SMAPI/Events/IGameLoopEvents.cs new file mode 100644 index 00000000..a56b3de3 --- /dev/null +++ b/src/SMAPI/Events/IGameLoopEvents.cs @@ -0,0 +1,17 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Events linked to the game's update loop. The update loop runs roughly ≈60 times/second to run game logic like state changes, action handling, etc. These can be useful, but you should consider more semantic events like if possible. + public interface IGameLoopEvents + { + /// Raised after the game is launched, right before the first update tick. This happens once per game session (unrelated to loading saves). All mods are loaded and initialised at this point, so this is a good time to set up mod integrations. + event EventHandler Launched; + + /// Raised before the game performs its overall update tick (≈60 times per second). + event EventHandler Updating; + + /// Raised after the game performs its overall update tick (≈60 times per second). + event EventHandler Updated; + } +} diff --git a/src/SMAPI/Events/IModEvents.cs b/src/SMAPI/Events/IModEvents.cs index 16ec6557..cf2f8cb8 100644 --- a/src/SMAPI/Events/IModEvents.cs +++ b/src/SMAPI/Events/IModEvents.cs @@ -3,6 +3,9 @@ namespace StardewModdingAPI.Events /// Manages access to events raised by SMAPI. public interface IModEvents { + /// Events linked to the game's update loop. The update loop runs roughly ≈60 times/second to run game logic like state changes, action handling, etc. These can be useful, but you should consider more semantic events like if possible. + IGameLoopEvents GameLoop { get; } + /// Events raised when the player provides input using a controller, keyboard, or mouse. IInputEvents Input { get; } diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index b05d82ce..3d5d0124 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -11,6 +11,33 @@ namespace StardewModdingAPI.Framework.Events /********* ** Events (new) *********/ + /**** + ** Game loop + ****/ + /// Raised after the game is launched, right before the first update tick. + public readonly ManagedEvent GameLoop_Launched; + + /// Raised before the game performs its overall update tick (≈60 times per second). + public readonly ManagedEvent GameLoop_Updating; + + /// Raised after the game performs its overall update tick (≈60 times per second). + public readonly ManagedEvent GameLoop_Updated; + + /**** + ** Input + ****/ + /// Raised after the player presses a button on the keyboard, controller, or mouse. + public readonly ManagedEvent Input_ButtonPressed; + + /// Raised after the player released a button on the keyboard, controller, or mouse. + public readonly ManagedEvent Input_ButtonReleased; + + /// Raised after the player moves the in-game cursor. + public readonly ManagedEvent Input_CursorMoved; + + /// Raised after the player scrolls the mouse wheel. + public readonly ManagedEvent Input_MouseWheelScrolled; + /**** ** World ****/ @@ -35,21 +62,6 @@ namespace StardewModdingAPI.Framework.Events /// Raised after terrain features (like floors and trees) are added or removed in a location. public readonly ManagedEvent World_TerrainFeatureListChanged; - /**** - ** Input - ****/ - /// Raised after the player presses a button on the keyboard, controller, or mouse. - public readonly ManagedEvent Input_ButtonPressed; - - /// Raised after the player released a button on the keyboard, controller, or mouse. - public readonly ManagedEvent Input_ButtonReleased; - - /// Raised after the player moves the in-game cursor. - public readonly ManagedEvent Input_CursorMoved; - - /// Raised after the player scrolls the mouse wheel. - public readonly ManagedEvent Input_MouseWheelScrolled; - /********* ** Events (old) @@ -252,6 +264,9 @@ namespace StardewModdingAPI.Framework.Events ManagedEvent ManageEvent(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); // init events (new) + this.GameLoop_Updating = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Updating)); + this.GameLoop_Updated = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Updated)); + this.Input_ButtonPressed = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed)); this.Input_ButtonReleased = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased)); this.Input_CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); diff --git a/src/SMAPI/Framework/Events/ModEvents.cs b/src/SMAPI/Framework/Events/ModEvents.cs index 90853141..9e474457 100644 --- a/src/SMAPI/Framework/Events/ModEvents.cs +++ b/src/SMAPI/Framework/Events/ModEvents.cs @@ -8,6 +8,9 @@ namespace StardewModdingAPI.Framework.Events /********* ** Accessors *********/ + /// Events linked to the game's update loop. The update loop runs roughly ≈60 times/second to run game logic like state changes, action handling, etc. These can be useful, but you should consider more semantic events like if possible. + public IGameLoopEvents GameLoop { get; } + /// Events raised when the player provides input using a controller, keyboard, or mouse. public IInputEvents Input { get; } @@ -23,6 +26,7 @@ namespace StardewModdingAPI.Framework.Events /// The underlying event manager. public ModEvents(IModMetadata mod, EventManager eventManager) { + this.GameLoop = new ModGameLoopEvents(mod, eventManager); this.Input = new ModInputEvents(mod, eventManager); this.World = new ModWorldEvents(mod, eventManager); } diff --git a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs new file mode 100644 index 00000000..1a142b0f --- /dev/null +++ b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs @@ -0,0 +1,39 @@ +using System; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// Events linked to the game's update loop. The update loop runs roughly ≈60 times/second to run game logic like state changes, action handling, etc. These can be useful, but you should consider more semantic events like if possible. + internal class ModGameLoopEvents : ModEventsBase, IGameLoopEvents + { + /********* + ** Accessors + *********/ + /// Raised after the game is launched, right before the first update tick. + public event EventHandler Launched; + + /// Raised before the game performs its overall update tick (≈60 times per second). + public event EventHandler Updating + { + add => this.EventManager.GameLoop_Updating.Add(value); + remove => this.EventManager.GameLoop_Updating.Remove(value); + } + + /// Raised after the game performs its overall update tick (≈60 times per second). + public event EventHandler Updated + { + add => this.EventManager.GameLoop_Updated.Add(value); + remove => this.EventManager.GameLoop_Updated.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + internal ModGameLoopEvents(IModMetadata mod, EventManager eventManager) + : base(mod, eventManager) { } + } +} diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index d3865316..777bc478 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -94,8 +94,8 @@ namespace StardewModdingAPI.Framework /// Whether post-game-startup initialisation has been performed. private bool IsInitialised; - /// Whether this is the very first update tick since the game started. - private bool FirstUpdate; + /// The number of update ticks which have already executed. + private uint TicksElapsed = 0; /// Whether the next content manager requested by the game will be for . private bool NextContentManagerIsMain; @@ -138,7 +138,6 @@ namespace StardewModdingAPI.Framework // init SMAPI this.Monitor = monitor; this.Events = eventManager; - this.FirstUpdate = true; this.Reflection = reflection; this.OnGameInitialised = onGameInitialised; this.OnGameExiting = onGameExiting; @@ -669,25 +668,27 @@ namespace StardewModdingAPI.Framework /********* ** Game update *********/ - this.Input.UpdateSuppression(); + this.TicksElapsed++; + if (this.TicksElapsed == 1) + this.Events.GameLoop_Launched.Raise(new GameLoopLaunchedEventArgs()); + this.Events.GameLoop_Updating.Raise(new GameLoopUpdatingEventArgs(this.TicksElapsed)); try { + this.Input.UpdateSuppression(); base.Update(gameTime); } catch (Exception ex) { this.Monitor.Log($"An error occured in the base update loop: {ex.GetLogSummary()}", LogLevel.Error); } + this.Events.GameLoop_Updated.Raise(new GameLoopUpdatedEventArgs(this.TicksElapsed)); /********* ** Update events *********/ this.Events.Specialised_UnvalidatedUpdateTick.Raise(); - if (this.FirstUpdate) - { - this.FirstUpdate = false; + if (this.TicksElapsed == 1) this.Events.Game_FirstUpdateTick.Raise(); - } this.Events.Game_UpdateTick.Raise(); if (this.CurrentUpdateTick % 2 == 0) this.Events.Game_SecondUpdateTick.Raise(); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 57c2c9e8..27e98f3a 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -90,15 +90,19 @@ Properties\GlobalAssemblyInfo.cs + + + + @@ -125,6 +129,7 @@ + -- cgit From 4f854aea1530177f959fc01b1731ae4759830321 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 9 Jul 2018 22:50:35 -0400 Subject: fix various build issues - installer not waiting until SaveBackup mod is compiled before preparing release build; - missing XML doc files for new toolkit assemblies; - missing XML doc file in SMAPI release build; - SaveBackup including toolkit DLL. --- build/prepare-install-package.targets | 4 ++++ src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj | 1 + src/SMAPI.sln | 1 + src/SMAPI/StardewModdingAPI.csproj | 2 +- .../StardewModdingAPI.Toolkit.CoreInterfaces.csproj | 6 ++---- src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj | 4 +--- 6 files changed, 10 insertions(+), 8 deletions(-) (limited to 'src/SMAPI/StardewModdingAPI.csproj') diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index 33a92f71..79185896 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -41,8 +41,10 @@ + + @@ -58,8 +60,10 @@ + + diff --git a/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj b/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj index afba15a1..0ccbcc6c 100644 --- a/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj +++ b/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj @@ -54,6 +54,7 @@ {d5cfd923-37f1-4bc3-9be8-e506e202ac28} StardewModdingAPI.Toolkit.CoreInterfaces + False diff --git a/src/SMAPI.sln b/src/SMAPI.sln index 99e93d62..d870c30c 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -20,6 +20,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".root", ".root", "{86C452BE EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Installer", "SMAPI.Installer\StardewModdingAPI.Installer.csproj", "{443DDF81-6AAF-420A-A610-3459F37E5575}" ProjectSection(ProjectDependencies) = postProject + {E272EB5D-8C57-417E-8E60-C1079D3F53C4} = {E272EB5D-8C57-417E-8E60-C1079D3F53C4} {28480467-1A48-46A7-99F8-236D95225359} = {28480467-1A48-46A7-99F8-236D95225359} {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} = {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} EndProjectSection diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 27e98f3a..0d0a5fe9 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -43,7 +43,7 @@ x86 false $(SolutionDir)\..\bin\Release\SMAPI - $(SolutionDir)\..\bin\Debug\SMAPI\StardewModdingAPI.xml + $(SolutionDir)\..\bin\Release\SMAPI\StardewModdingAPI.xml TRACE true true diff --git a/src/StardewModdingAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj b/src/StardewModdingAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj index e003122e..525931e5 100644 --- a/src/StardewModdingAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj +++ b/src/StardewModdingAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj @@ -2,12 +2,10 @@ net4.5;netstandard2.0 + StardewModdingAPI false - - - ..\..\bin\$(Configuration)\SMAPI.Toolkit.CoreInterfaces - StardewModdingAPI + ..\..\bin\$(Configuration)\SMAPI.Toolkit.CoreInterfaces\$(TargetFramework)\StardewModdingAPI.Toolkit.CoreInterfaces.xml diff --git a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj index 904f6786..21c130b3 100644 --- a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj +++ b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj @@ -3,10 +3,8 @@ net4.5;netstandard2.0 false - - - ..\..\bin\$(Configuration)\SMAPI.Toolkit + ..\..\bin\$(Configuration)\SMAPI.Toolkit\$(TargetFramework)\StardewModdingAPI.Toolkit.xml -- cgit