summaryrefslogtreecommitdiff
path: root/src/SMAPI
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <github@jplamondonw.com>2018-08-01 11:07:29 -0400
committerJesse Plamondon-Willard <github@jplamondonw.com>2018-08-01 11:07:29 -0400
commit60b41195778af33fd609eab66d9ae3f1d1165e8f (patch)
tree7128b906d40e94c56c34ed6058f27bc31c31a08b /src/SMAPI
parentb9bc1a6d17cafa0a97b46ffecda432cfc2f23b51 (diff)
parent52cf953f685c65b2b6814e375ec9a5ffa03c440a (diff)
downloadSMAPI-60b41195778af33fd609eab66d9ae3f1d1165e8f.tar.gz
SMAPI-60b41195778af33fd609eab66d9ae3f1d1165e8f.tar.bz2
SMAPI-60b41195778af33fd609eab66d9ae3f1d1165e8f.zip
Merge branch 'develop' into stable
Diffstat (limited to 'src/SMAPI')
-rw-r--r--src/SMAPI/Constants.cs91
-rw-r--r--src/SMAPI/Context.cs4
-rw-r--r--src/SMAPI/Events/ControlEvents.cs36
-rw-r--r--src/SMAPI/Events/EventArgsGameLocationsChanged.cs27
-rw-r--r--src/SMAPI/Events/EventArgsInput.cs125
-rw-r--r--src/SMAPI/Events/EventArgsIntChanged.cs3
-rw-r--r--src/SMAPI/Events/EventArgsInventoryChanged.cs12
-rw-r--r--src/SMAPI/Events/EventArgsLocationBuildingsChanged.cs39
-rw-r--r--src/SMAPI/Events/EventArgsLocationObjectsChanged.cs41
-rw-r--r--src/SMAPI/Events/EventArgsLocationsChanged.cs33
-rw-r--r--src/SMAPI/Events/EventArgsPlayerWarped.cs (renamed from src/SMAPI/Events/EventArgsCurrentLocationChanged.cs)13
-rw-r--r--src/SMAPI/Events/GameLoopLaunchedEventArgs.cs7
-rw-r--r--src/SMAPI/Events/GameLoopUpdatedEventArgs.cs36
-rw-r--r--src/SMAPI/Events/GameLoopUpdatingEventArgs.cs36
-rw-r--r--src/SMAPI/Events/IGameLoopEvents.cs17
-rw-r--r--src/SMAPI/Events/IInputEvents.cs20
-rw-r--r--src/SMAPI/Events/IModEvents.cs15
-rw-r--r--src/SMAPI/Events/IWorldEvents.cs29
-rw-r--r--src/SMAPI/Events/InputButtonPressedEventArgs.cs60
-rw-r--r--src/SMAPI/Events/InputButtonReleasedEventArgs.cs60
-rw-r--r--src/SMAPI/Events/InputCursorMovedEventArgs.cs30
-rw-r--r--src/SMAPI/Events/InputEvents.cs8
-rw-r--r--src/SMAPI/Events/InputMouseWheelScrolledEventArgs.cs38
-rw-r--r--src/SMAPI/Events/LocationEvents.cs24
-rw-r--r--src/SMAPI/Events/MultiplayerEvents.cs58
-rw-r--r--src/SMAPI/Events/PlayerEvents.cs10
-rw-r--r--src/SMAPI/Events/WorldBuildingListChangedEventArgs.cs39
-rw-r--r--src/SMAPI/Events/WorldDebrisListChangedEventArgs.cs38
-rw-r--r--src/SMAPI/Events/WorldLargeTerrainFeatureListChangedEventArgs.cs39
-rw-r--r--src/SMAPI/Events/WorldLocationListChangedEventArgs.cs33
-rw-r--r--src/SMAPI/Events/WorldNpcListChangedEventArgs.cs38
-rw-r--r--src/SMAPI/Events/WorldObjectListChangedEventArgs.cs40
-rw-r--r--src/SMAPI/Events/WorldTerrainFeatureListChangedEventArgs.cs40
-rw-r--r--src/SMAPI/Framework/CommandManager.cs28
-rw-r--r--src/SMAPI/Framework/Content/AssetDataForImage.cs6
-rw-r--r--src/SMAPI/Framework/Content/ContentCache.cs6
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs315
-rw-r--r--src/SMAPI/Framework/ContentCore.cs882
-rw-r--r--src/SMAPI/Framework/ContentManagerShim.cs100
-rw-r--r--src/SMAPI/Framework/ContentManagers/BaseContentManager.cs297
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManager.cs252
-rw-r--r--src/SMAPI/Framework/ContentManagers/IContentManager.cs86
-rw-r--r--src/SMAPI/Framework/ContentManagers/ModContentManager.cs207
-rw-r--r--src/SMAPI/Framework/ContentPack.cs4
-rw-r--r--src/SMAPI/Framework/CursorPosition.cs14
-rw-r--r--src/SMAPI/Framework/Events/EventManager.cs168
-rw-r--r--src/SMAPI/Framework/Events/ManagedEvent.cs20
-rw-r--r--src/SMAPI/Framework/Events/ManagedEventBase.cs9
-rw-r--r--src/SMAPI/Framework/Events/ModEvents.cs34
-rw-r--r--src/SMAPI/Framework/Events/ModEventsBase.cs28
-rw-r--r--src/SMAPI/Framework/Events/ModGameLoopEvents.cs43
-rw-r--r--src/SMAPI/Framework/Events/ModInputEvents.cs50
-rw-r--r--src/SMAPI/Framework/Events/ModWorldEvents.cs71
-rw-r--r--src/SMAPI/Framework/Exceptions/SParseException.cs17
-rw-r--r--src/SMAPI/Framework/GameVersion.cs10
-rw-r--r--src/SMAPI/Framework/IModMetadata.cs28
-rw-r--r--src/SMAPI/Framework/Input/GamePadStateBuilder.cs162
-rw-r--r--src/SMAPI/Framework/Input/InputState.cs163
-rw-r--r--src/SMAPI/Framework/Input/SInputState.cs382
-rw-r--r--src/SMAPI/Framework/InternalExtensions.cs16
-rw-r--r--src/SMAPI/Framework/LegacyManifestVersion.cs26
-rw-r--r--src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs27
-rw-r--r--src/SMAPI/Framework/ModData/ModDataField.cs82
-rw-r--r--src/SMAPI/Framework/ModData/ModDataFieldKey.cs18
-rw-r--r--src/SMAPI/Framework/ModData/ModDataRecord.cs146
-rw-r--r--src/SMAPI/Framework/ModData/ModDatabase.cs206
-rw-r--r--src/SMAPI/Framework/ModData/ModStatus.cs18
-rw-r--r--src/SMAPI/Framework/ModData/ParsedModDataRecord.cs48
-rw-r--r--src/SMAPI/Framework/ModHelpers/ContentHelper.cs78
-rw-r--r--src/SMAPI/Framework/ModHelpers/InputHelper.cs54
-rw-r--r--src/SMAPI/Framework/ModHelpers/ModHelper.cs51
-rw-r--r--src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs4
-rw-r--r--src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs40
-rw-r--r--src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs116
-rw-r--r--src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs20
-rw-r--r--src/SMAPI/Framework/ModLoading/AssemblyLoader.cs90
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs37
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs11
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs10
-rw-r--r--src/SMAPI/Framework/ModLoading/ModMetadata.cs53
-rw-r--r--src/SMAPI/Framework/ModLoading/ModResolver.cs59
-rw-r--r--src/SMAPI/Framework/ModLoading/ModWarning.cs31
-rw-r--r--src/SMAPI/Framework/ModLoading/Platform.cs12
-rw-r--r--src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs13
-rw-r--r--src/SMAPI/Framework/ModLoading/RewriteHelper.cs18
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs2
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs2
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs2
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs56
-rw-r--r--src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs201
-rw-r--r--src/SMAPI/Framework/Models/Manifest.cs49
-rw-r--r--src/SMAPI/Framework/Models/ManifestContentPackFor.cs15
-rw-r--r--src/SMAPI/Framework/Models/ManifestDependency.cs35
-rw-r--r--src/SMAPI/Framework/Models/ModFolderExport.cs21
-rw-r--r--src/SMAPI/Framework/Models/SConfig.cs16
-rw-r--r--src/SMAPI/Framework/Monitor.cs116
-rw-r--r--src/SMAPI/Framework/Patching/GamePatcher.cs45
-rw-r--r--src/SMAPI/Framework/Patching/IHarmonyPatch.cs15
-rw-r--r--src/SMAPI/Framework/Reflection/ReflectedField.cs3
-rw-r--r--src/SMAPI/Framework/Reflection/ReflectedMethod.cs3
-rw-r--r--src/SMAPI/Framework/Reflection/ReflectedProperty.cs3
-rw-r--r--src/SMAPI/Framework/RewriteFacades/SpriteBatchMethods.cs61
-rw-r--r--src/SMAPI/Framework/SGame.cs1819
-rw-r--r--src/SMAPI/Framework/SMultiplayer.cs47
-rw-r--r--src/SMAPI/Framework/Serialisation/ColorConverter.cs (renamed from src/SMAPI/Framework/Serialisation/CrossplatformConverters/ColorConverter.cs)5
-rw-r--r--src/SMAPI/Framework/Serialisation/JsonHelper.cs139
-rw-r--r--src/SMAPI/Framework/Serialisation/PointConverter.cs (renamed from src/SMAPI/Framework/Serialisation/CrossplatformConverters/PointConverter.cs)5
-rw-r--r--src/SMAPI/Framework/Serialisation/RectangleConverter.cs (renamed from src/SMAPI/Framework/Serialisation/CrossplatformConverters/RectangleConverter.cs)5
-rw-r--r--src/SMAPI/Framework/Serialisation/SimpleReadOnlyConverter.cs77
-rw-r--r--src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestContentPackForConverter.cs50
-rw-r--r--src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestDependencyArrayConverter.cs60
-rw-r--r--src/SMAPI/Framework/Serialisation/SmapiConverters/SemanticVersionConverter.cs36
-rw-r--r--src/SMAPI/Framework/Serialisation/SmapiConverters/StringEnumConverter.cs22
-rw-r--r--src/SMAPI/Framework/StateTracking/Comparers/EquatableComparer.cs32
-rw-r--r--src/SMAPI/Framework/StateTracking/Comparers/GenericEqualsComparer.cs31
-rw-r--r--src/SMAPI/Framework/StateTracking/Comparers/ObjectReferenceComparer.cs29
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/BaseDisposableWatcher.cs36
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableWatcher.cs62
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs93
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/NetDictionaryWatcher.cs103
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/NetValueWatcher.cs83
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs86
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs79
-rw-r--r--src/SMAPI/Framework/StateTracking/ICollectionWatcher.cs17
-rw-r--r--src/SMAPI/Framework/StateTracking/IDictionaryWatcher.cs7
-rw-r--r--src/SMAPI/Framework/StateTracking/IValueWatcher.cs15
-rw-r--r--src/SMAPI/Framework/StateTracking/IWatcher.cs24
-rw-r--r--src/SMAPI/Framework/StateTracking/LocationTracker.cs103
-rw-r--r--src/SMAPI/Framework/StateTracking/PlayerTracker.cs169
-rw-r--r--src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs221
-rw-r--r--src/SMAPI/Framework/Utilities/PathUtilities.cs62
-rw-r--r--src/SMAPI/Framework/WatcherCore.cs119
-rw-r--r--src/SMAPI/Framework/WebApiClient.cs73
-rw-r--r--src/SMAPI/GamePlatform.cs17
-rw-r--r--src/SMAPI/IAssetDataForImage.cs4
-rw-r--r--src/SMAPI/ICursorPosition.cs6
-rw-r--r--src/SMAPI/IInputHelper.cs21
-rw-r--r--src/SMAPI/IManifest.cs44
-rw-r--r--src/SMAPI/IManifestContentPackFor.cs12
-rw-r--r--src/SMAPI/IManifestDependency.cs18
-rw-r--r--src/SMAPI/IModHelper.cs11
-rw-r--r--src/SMAPI/IMultiplayerHelper.cs15
-rw-r--r--src/SMAPI/IPrivateField.cs30
-rw-r--r--src/SMAPI/IPrivateMethod.cs31
-rw-r--r--src/SMAPI/IPrivateProperty.cs30
-rw-r--r--src/SMAPI/IReflectionHelper.cs69
-rw-r--r--src/SMAPI/ISemanticVersion.cs59
-rw-r--r--src/SMAPI/LogLevel.cs16
-rw-r--r--src/SMAPI/Metadata/CoreAssetPropagator.cs117
-rw-r--r--src/SMAPI/Metadata/InstructionMetadata.cs73
-rw-r--r--src/SMAPI/Program.cs746
-rw-r--r--src/SMAPI/SemanticVersion.cs58
-rw-r--r--src/SMAPI/StardewModdingAPI.config.json1853
-rw-r--r--src/SMAPI/StardewModdingAPI.csproj152
-rw-r--r--src/SMAPI/packages.config3
155 files changed, 6519 insertions, 7025 deletions
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index 1e35c030..a6cddbe4 100644
--- a/src/SMAPI/Constants.cs
+++ b/src/SMAPI/Constants.cs
@@ -1,10 +1,10 @@
using System;
-using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using StardewModdingAPI.Framework;
using StardewModdingAPI.Framework.ModLoading;
+using StardewModdingAPI.Internal;
using StardewValley;
namespace StardewModdingAPI
@@ -21,14 +21,6 @@ namespace StardewModdingAPI
/// <summary>Whether the directory containing the current save's data exists on disk.</summary>
private static bool SavePathReady => Context.IsSaveLoaded && Directory.Exists(Constants.RawSavePath);
- /// <summary>Maps vendor keys (like <c>Nexus</c>) to their mod URL template (where <c>{0}</c> is the mod ID). This doesn't affect update checks, which defer to the remote web API.</summary>
- private static readonly IDictionary<string, string> VendorModUrls = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase)
- {
- ["Chucklefish"] = "https://community.playstarbound.com/resources/{0}",
- ["GitHub"] = "https://github.com/{0}/releases",
- ["Nexus"] = "https://www.nexusmods.com/stardewvalley/mods/{0}"
- };
-
/*********
** Accessors
@@ -37,28 +29,16 @@ namespace StardewModdingAPI
** Public
****/
/// <summary>SMAPI's current semantic version.</summary>
- public static ISemanticVersion ApiVersion { get; } =
-#if STARDEW_VALLEY_1_3
- new SemanticVersion($"2.6-alpha.{DateTime.UtcNow:yyyyMMddHHmm}");
-#else
- new SemanticVersion("2.5.5");
-#endif
+ public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.6.0");
/// <summary>The minimum supported version of Stardew Valley.</summary>
- public static ISemanticVersion MinimumGameVersion { get; } =
-#if STARDEW_VALLEY_1_3
- new GameVersion("1.3.0.4");
-#else
- new SemanticVersion("1.2.30");
-#endif
+ public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.27");
/// <summary>The maximum supported version of Stardew Valley.</summary>
- public static ISemanticVersion MaximumGameVersion { get; } =
-#if STARDEW_VALLEY_1_3
- null;
-#else
- new SemanticVersion("1.2.33");
-#endif
+ public static ISemanticVersion MaximumGameVersion { get; } = null;
+
+ /// <summary>The target game platform.</summary>
+ public static GamePlatform TargetPlatform => (GamePlatform)Constants.Platform;
/// <summary>The path to the game folder.</summary>
public static string ExecutionPath { get; } = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
@@ -87,8 +67,14 @@ namespace StardewModdingAPI
/// <summary>The file path for the SMAPI configuration file.</summary>
internal static string ApiConfigPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.config.json");
- /// <summary>The file path to the log where the latest output should be saved.</summary>
- internal static string DefaultLogPath => Path.Combine(Constants.LogDir, "SMAPI-latest.txt");
+ /// <summary>The file path for the SMAPI metadata file.</summary>
+ internal static string ApiMetadataPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.metadata.json");
+
+ /// <summary>The filename prefix for SMAPI log files.</summary>
+ internal static string LogNamePrefix { get; } = "SMAPI-latest";
+
+ /// <summary>The filename extension for SMAPI log files.</summary>
+ internal static string LogNameExtension { get; } = "txt";
/// <summary>A copy of the log leading up to the previous fatal crash, if any.</summary>
internal static string FatalCrashLog => Path.Combine(Constants.LogDir, "SMAPI-crash.txt");
@@ -96,6 +82,9 @@ namespace StardewModdingAPI
/// <summary>The file path which stores a fatal crash message for the next run.</summary>
internal static string FatalCrashMarker => Path.Combine(Constants.ExecutionPath, "StardewModdingAPI.crash.marker");
+ /// <summary>The file path which stores the detected update version for the next run.</summary>
+ internal static string UpdateMarker => Path.Combine(Constants.ExecutionPath, "StardewModdingAPI.update.marker");
+
/// <summary>The full path to the folder containing mods.</summary>
internal static string ModPath { get; } = Path.Combine(Constants.ExecutionPath, "Mods");
@@ -103,12 +92,10 @@ namespace StardewModdingAPI
internal static ISemanticVersion GameVersion { get; } = new GameVersion(Constants.GetGameVersion());
/// <summary>The target game platform.</summary>
- internal static Platform TargetPlatform { get; } =
-#if SMAPI_FOR_WINDOWS
- Platform.Windows;
-#else
- Platform.Mono;
-#endif
+ internal static Platform Platform { get; } = EnvironmentUtility.DetectPlatform();
+
+ /// <summary>The game's assembly name.</summary>
+ internal static string GameAssemblyName => Constants.Platform == Platform.Windows ? "Stardew Valley" : "StardewValley";
/*********
@@ -123,17 +110,20 @@ namespace StardewModdingAPI
Assembly[] targetAssemblies;
switch (targetPlatform)
{
- case Platform.Mono:
+ case Platform.Linux:
+ case Platform.Mac:
removeAssemblyReferences = new[]
{
+ "Netcode",
"Stardew Valley",
"Microsoft.Xna.Framework",
"Microsoft.Xna.Framework.Game",
- "Microsoft.Xna.Framework.Graphics"
+ "Microsoft.Xna.Framework.Graphics",
+ "Microsoft.Xna.Framework.Xact"
};
targetAssemblies = new[]
{
- typeof(StardewValley.Game1).Assembly,
+ typeof(StardewValley.Game1).Assembly, // note: includes Netcode types on Linux/Mac
typeof(Microsoft.Xna.Framework.Vector2).Assembly
};
break;
@@ -146,6 +136,7 @@ namespace StardewModdingAPI
};
targetAssemblies = new[]
{
+ typeof(Netcode.NetBool).Assembly,
typeof(StardewValley.Game1).Assembly,
typeof(Microsoft.Xna.Framework.Vector2).Assembly,
typeof(Microsoft.Xna.Framework.Game).Assembly,
@@ -160,23 +151,6 @@ namespace StardewModdingAPI
return new PlatformAssemblyMap(targetPlatform, removeAssemblyReferences, targetAssemblies);
}
- /// <summary>Get an update URL for an update key (if valid).</summary>
- /// <param name="updateKey">The update key.</param>
- 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
@@ -184,12 +158,7 @@ namespace StardewModdingAPI
/// <summary>Get the name of a save directory for the current player.</summary>
private static string GetSaveFolderName()
{
- string prefix =
-#if STARDEW_VALLEY_1_3
- new string(Game1.player.Name.Where(char.IsLetterOrDigit).ToArray());
-#else
- new string(Game1.player.name.Where(char.IsLetterOrDigit).ToArray());
-#endif
+ string prefix = new string(Game1.player.Name.Where(char.IsLetterOrDigit).ToArray());
return $"{prefix}_{Game1.uniqueIDForThisGame}";
}
diff --git a/src/SMAPI/Context.cs b/src/SMAPI/Context.cs
index 7ed9b052..3905699e 100644
--- a/src/SMAPI/Context.cs
+++ b/src/SMAPI/Context.cs
@@ -17,7 +17,7 @@ namespace StardewModdingAPI
public static bool IsWorldReady { get; internal set; }
/// <summary>Whether <see cref="IsWorldReady"/> is true and the player is free to act in the world (no menu is displayed, no cutscene is in progress, etc).</summary>
- public static bool IsPlayerFree => Context.IsWorldReady && Game1.activeClickableMenu == null && !Game1.dialogueUp && !Game1.eventUp;
+ public static bool IsPlayerFree => Context.IsWorldReady && Game1.activeClickableMenu == null && !Game1.dialogueUp && (!Game1.eventUp || Game1.isFestival());
/// <summary>Whether <see cref="IsPlayerFree"/> is true and the player is free to move (e.g. not using a tool).</summary>
public static bool CanPlayerMove => Context.IsPlayerFree && Game1.player.CanMove;
@@ -35,7 +35,7 @@ namespace StardewModdingAPI
** Internal
****/
/// <summary>Whether a player save has been loaded.</summary>
- internal static bool IsSaveLoaded => Game1.hasLoadedGame && !string.IsNullOrEmpty(Game1.player.name);
+ internal static bool IsSaveLoaded => Game1.hasLoadedGame && !(Game1.activeClickableMenu is TitleMenu);
/// <summary>Whether the game is currently writing to the save file.</summary>
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/Events/ControlEvents.cs b/src/SMAPI/Events/ControlEvents.cs
index 973bb245..a3994d1d 100644
--- a/src/SMAPI/Events/ControlEvents.cs
+++ b/src/SMAPI/Events/ControlEvents.cs
@@ -20,57 +20,57 @@ namespace StardewModdingAPI.Events
/// <summary>Raised when the <see cref="KeyboardState"/> changes. That happens when the player presses or releases a key.</summary>
public static event EventHandler<EventArgsKeyboardStateChanged> 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);
}
- /// <summary>Raised when the player presses a keyboard key.</summary>
+ /// <summary>Raised after the player presses a keyboard key.</summary>
public static event EventHandler<EventArgsKeyPressed> 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);
}
- /// <summary>Raised when the player releases a keyboard key.</summary>
+ /// <summary>Raised after the player releases a keyboard key.</summary>
public static event EventHandler<EventArgsKeyPressed> 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);
}
/// <summary>Raised when the <see cref="MouseState"/> changes. That happens when the player moves the mouse, scrolls the mouse wheel, or presses/releases a button.</summary>
public static event EventHandler<EventArgsMouseStateChanged> 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);
}
/// <summary>The player pressed a controller button. This event isn't raised for trigger buttons.</summary>
public static event EventHandler<EventArgsControllerButtonPressed> 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);
}
/// <summary>The player released a controller button. This event isn't raised for trigger buttons.</summary>
public static event EventHandler<EventArgsControllerButtonReleased> 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);
}
/// <summary>The player pressed a controller trigger button.</summary>
public static event EventHandler<EventArgsControllerTriggerPressed> 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);
}
/// <summary>The player released a controller trigger button.</summary>
public static event EventHandler<EventArgsControllerTriggerReleased> 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/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
-{
- /// <summary>Event arguments for a <see cref="LocationEvents.LocationsChanged"/> event.</summary>
- public class EventArgsGameLocationsChanged : EventArgs
- {
- /*********
- ** Accessors
- *********/
- /// <summary>The current list of game locations.</summary>
- public IList<GameLocation> NewLocations { get; }
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Construct an instance.</summary>
- /// <param name="newLocations">The current list of game locations.</param>
- public EventArgsGameLocationsChanged(IList<GameLocation> newLocations)
- {
- this.NewLocations = newLocations;
- }
- }
-}
diff --git a/src/SMAPI/Events/EventArgsInput.cs b/src/SMAPI/Events/EventArgsInput.cs
index 75b9b8cd..0cafdba5 100644
--- a/src/SMAPI/Events/EventArgsInput.cs
+++ b/src/SMAPI/Events/EventArgsInput.cs
@@ -1,8 +1,5 @@
using System;
-using System.Linq;
-using Microsoft.Xna.Framework;
-using Microsoft.Xna.Framework.Input;
-using StardewValley;
+using System.Collections.Generic;
namespace StardewModdingAPI.Events
{
@@ -10,6 +7,13 @@ namespace StardewModdingAPI.Events
public class EventArgsInput : EventArgs
{
/*********
+ ** Properties
+ *********/
+ /// <summary>The buttons to suppress.</summary>
+ private readonly HashSet<SButton> SuppressButtons;
+
+
+ /*********
** Accessors
*********/
/// <summary>The button on the controller, keyboard, or mouse.</summary>
@@ -18,20 +22,14 @@ namespace StardewModdingAPI.Events
/// <summary>The current cursor position.</summary>
public ICursorPosition Cursor { get; }
-#if !STARDEW_VALLEY_1_3
- /// <summary>Whether the input is considered a 'click' by the game for enabling action.</summary>
- [Obsolete("Use " + nameof(EventArgsInput.IsActionButton) + " or " + nameof(EventArgsInput.IsUseToolButton) + " instead")] // deprecated in SMAPI 2.1
- public bool IsClick => this.IsActionButton;
-#endif
-
/// <summary>Whether the input should trigger actions on the affected tile.</summary>
- public bool IsActionButton { get; }
+ public bool IsActionButton => this.Button.IsActionButton();
/// <summary>Whether the input should use tools on the affected tile.</summary>
- public bool IsUseToolButton { get; }
+ public bool IsUseToolButton => this.Button.IsUseToolButton();
/// <summary>Whether a mod has indicated the key was already handled.</summary>
- public bool IsSuppressed { get; private set; }
+ public bool IsSuppressed => this.SuppressButtons.Contains(this.Button);
/*********
@@ -40,17 +38,15 @@ namespace StardewModdingAPI.Events
/// <summary>Construct an instance.</summary>
/// <param name="button">The button on the controller, keyboard, or mouse.</param>
/// <param name="cursor">The cursor position.</param>
- /// <param name="isActionButton">Whether the input should trigger actions on the affected tile.</param>
- /// <param name="isUseToolButton">Whether the input should use tools on the affected tile.</param>
- public EventArgsInput(SButton button, ICursorPosition cursor, bool isActionButton, bool isUseToolButton)
+ /// <param name="suppressButtons">The buttons to suppress.</param>
+ public EventArgsInput(SButton button, ICursorPosition cursor, HashSet<SButton> suppressButtons)
{
this.Button = button;
this.Cursor = cursor;
- this.IsActionButton = isActionButton;
- this.IsUseToolButton = isUseToolButton;
+ this.SuppressButtons = suppressButtons;
}
- /// <summary>Prevent the game from handling the vurrent button press. This doesn't prevent other mods from receiving the event.</summary>
+ /// <summary>Prevent the game from handling the current button press. This doesn't prevent other mods from receiving the event.</summary>
public void SuppressButton()
{
this.SuppressButton(this.Button);
@@ -60,96 +56,7 @@ namespace StardewModdingAPI.Events
/// <param name="button">The button to suppress.</param>
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/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
/// <summary>The current value.</summary>
public int NewInt { get; }
+
/*********
** Public methods
*********/
diff --git a/src/SMAPI/Events/EventArgsInventoryChanged.cs b/src/SMAPI/Events/EventArgsInventoryChanged.cs
index b85ae9db..1fdca834 100644
--- a/src/SMAPI/Events/EventArgsInventoryChanged.cs
+++ b/src/SMAPI/Events/EventArgsInventoryChanged.cs
@@ -12,11 +12,7 @@ namespace StardewModdingAPI.Events
** Accessors
*********/
/// <summary>The player's inventory.</summary>
-#if STARDEW_VALLEY_1_3
public IList<Item> Inventory { get; }
-#else
- public List<Item> Inventory { get; }
-#endif
/// <summary>The added items.</summary>
public List<ItemStackChange> Added { get; }
@@ -34,13 +30,7 @@ namespace StardewModdingAPI.Events
/// <summary>Construct an instance.</summary>
/// <param name="inventory">The player's inventory.</param>
/// <param name="changedItems">The inventory changes.</param>
- public EventArgsInventoryChanged(
-#if STARDEW_VALLEY_1_3
- IList<Item> inventory,
-#else
- List<Item> inventory,
-#endif
- List<ItemStackChange> changedItems)
+ public EventArgsInventoryChanged(IList<Item> inventory, List<ItemStackChange> changedItems)
{
this.Inventory = inventory;
this.Added = changedItems.Where(n => n.ChangeType == ChangeType.Added).ToList();
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
+{
+ /// <summary>Event arguments for a <see cref="LocationEvents.BuildingsChanged"/> event.</summary>
+ public class EventArgsLocationBuildingsChanged : EventArgs
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The location which changed.</summary>
+ public GameLocation Location { get; }
+
+ /// <summary>The buildings added to the location.</summary>
+ public IEnumerable<Building> Added { get; }
+
+ /// <summary>The buildings removed from the location.</summary>
+ public IEnumerable<Building> Removed { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="location">The location which changed.</param>
+ /// <param name="added">The buildings added to the location.</param>
+ /// <param name="removed">The buildings removed from the location.</param>
+ public EventArgsLocationBuildingsChanged(GameLocation location, IEnumerable<Building> added, IEnumerable<Building> 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 180e9d78..3bb387d5 100644
--- a/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs
+++ b/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs
@@ -1,43 +1,40 @@
using System;
-using Microsoft.Xna.Framework;
-#if STARDEW_VALLEY_1_3
using System.Collections.Generic;
-using Netcode;
-#else
+using System.Linq;
+using Microsoft.Xna.Framework;
using StardewValley;
-#endif
-using Object = StardewValley.Object;
+using SObject = StardewValley.Object;
namespace StardewModdingAPI.Events
{
- /// <summary>Event arguments for a <see cref="LocationEvents.LocationObjectsChanged"/> event.</summary>
+ /// <summary>Event arguments for a <see cref="LocationEvents.ObjectsChanged"/> event.</summary>
public class EventArgsLocationObjectsChanged : EventArgs
{
/*********
** Accessors
*********/
- /// <summary>The current list of objects in the current location.</summary>
-#if STARDEW_VALLEY_1_3
- public IDictionary<Vector2, NetRef<Object>> NewObjects { get; }
-#else
- public SerializableDictionary<Vector2, Object> NewObjects { get; }
-#endif
+ /// <summary>The location which changed.</summary>
+ public GameLocation Location { get; }
+
+ /// <summary>The objects added to the location.</summary>
+ public IEnumerable<KeyValuePair<Vector2, SObject>> Added { get; }
+
+ /// <summary>The objects removed from the location.</summary>
+ public IEnumerable<KeyValuePair<Vector2, SObject>> Removed { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
- /// <param name="newObjects">The current list of objects in the current location.</param>
- public EventArgsLocationObjectsChanged(
-#if STARDEW_VALLEY_1_3
- IDictionary<Vector2, NetRef<Object>> newObjects
-#else
- SerializableDictionary<Vector2, Object> newObjects
-#endif
- )
+ /// <param name="location">The location which changed.</param>
+ /// <param name="added">The objects added to the location.</param>
+ /// <param name="removed">The objects removed from the location.</param>
+ public EventArgsLocationObjectsChanged(GameLocation location, IEnumerable<KeyValuePair<Vector2, SObject>> added, IEnumerable<KeyValuePair<Vector2, SObject>> removed)
{
- this.NewObjects = newObjects;
+ this.Location = location;
+ this.Added = added.ToArray();
+ this.Removed = removed.ToArray();
}
}
}
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
+{
+ /// <summary>Event arguments for a <see cref="LocationEvents.LocationsChanged"/> event.</summary>
+ public class EventArgsLocationsChanged : EventArgs
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The added locations.</summary>
+ public IEnumerable<GameLocation> Added { get; }
+
+ /// <summary>The removed locations.</summary>
+ public IEnumerable<GameLocation> Removed { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="added">The added locations.</param>
+ /// <param name="removed">The removed locations.</param>
+ public EventArgsLocationsChanged(IEnumerable<GameLocation> added, IEnumerable<GameLocation> removed)
+ {
+ this.Added = added.ToArray();
+ this.Removed = removed.ToArray();
+ }
+ }
+}
diff --git a/src/SMAPI/Events/EventArgsCurrentLocationChanged.cs b/src/SMAPI/Events/EventArgsPlayerWarped.cs
index 25d3ebf3..93026aea 100644
--- a/src/SMAPI/Events/EventArgsCurrentLocationChanged.cs
+++ b/src/SMAPI/Events/EventArgsPlayerWarped.cs
@@ -1,19 +1,20 @@
-using System;
+using System;
using StardewValley;
namespace StardewModdingAPI.Events
{
- /// <summary>Event arguments for a <see cref="LocationEvents.CurrentLocationChanged"/> event.</summary>
- public class EventArgsCurrentLocationChanged : EventArgs
+ /// <summary>Event arguments for a <see cref="PlayerEvents.Warped"/> event.</summary>
+ public class EventArgsPlayerWarped : EventArgs
{
/*********
** Accessors
*********/
+ /// <summary>The player's previous location.</summary>
+ public GameLocation PriorLocation { get; }
+
/// <summary>The player's current location.</summary>
public GameLocation NewLocation { get; }
- /// <summary>The player's previous location.</summary>
- public GameLocation PriorLocation { get; }
/*********
@@ -22,7 +23,7 @@ namespace StardewModdingAPI.Events
/// <summary>Construct an instance.</summary>
/// <param name="priorLocation">The player's previous location.</param>
/// <param name="newLocation">The player's current location.</param>
- public EventArgsCurrentLocationChanged(GameLocation priorLocation, GameLocation newLocation)
+ public EventArgsPlayerWarped(GameLocation priorLocation, GameLocation newLocation)
{
this.NewLocation = newLocation;
this.PriorLocation = priorLocation;
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
+{
+ /// <summary>Event arguments for an <see cref="IGameLoopEvents.Launched"/> event.</summary>
+ 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
+{
+ /// <summary>Event arguments for an <see cref="IGameLoopEvents.Updated"/> event.</summary>
+ public class GameLoopUpdatedEventArgs : EventArgs
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The number of ticks elapsed since the game started, including the current tick.</summary>
+ public uint Ticks { get; }
+
+ /// <summary>Whether <see cref="Ticks"/> is a multiple of 60, which happens approximately once per second.</summary>
+ public bool IsOneSecond { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="ticks">The number of ticks elapsed since the game started, including the current tick.</param>
+ public GameLoopUpdatedEventArgs(uint ticks)
+ {
+ this.Ticks = ticks;
+ this.IsOneSecond = this.IsMultipleOf(60);
+ }
+
+ /// <summary>Get whether <see cref="Ticks"/> is a multiple of the given <paramref name="number"/>. This is mainly useful if you want to run logic intermittently (e.g. <code>e.IsMultipleOf(30)</code> for every half-second).</summary>
+ /// <param name="number">The factor to check.</param>
+ 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
+{
+ /// <summary>Event arguments for an <see cref="IGameLoopEvents.Updating"/> event.</summary>
+ public class GameLoopUpdatingEventArgs : EventArgs
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The number of ticks elapsed since the game started, including the current tick.</summary>
+ public uint Ticks { get; }
+
+ /// <summary>Whether <see cref="Ticks"/> is a multiple of 60, which happens approximately once per second.</summary>
+ public bool IsOneSecond { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="ticks">The number of ticks elapsed since the game started, including the current tick.</param>
+ public GameLoopUpdatingEventArgs(uint ticks)
+ {
+ this.Ticks = ticks;
+ this.IsOneSecond = this.IsMultipleOf(60);
+ }
+
+ /// <summary>Get whether <see cref="Ticks"/> is a multiple of the given <paramref name="number"/>. This is mainly useful if you want to run logic intermittently (e.g. <code>e.IsMultipleOf(30)</code> for every half-second).</summary>
+ /// <param name="number">The factor to check.</param>
+ 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
+{
+ /// <summary>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 <see cref="IInputEvents"/> if possible.</summary>
+ public interface IGameLoopEvents
+ {
+ /// <summary>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.</summary>
+ event EventHandler<GameLoopLaunchedEventArgs> Launched;
+
+ /// <summary>Raised before the game performs its overall update tick (≈60 times per second).</summary>
+ event EventHandler<GameLoopUpdatingEventArgs> Updating;
+
+ /// <summary>Raised after the game performs its overall update tick (≈60 times per second).</summary>
+ event EventHandler<GameLoopUpdatedEventArgs> Updated;
+ }
+}
diff --git a/src/SMAPI/Events/IInputEvents.cs b/src/SMAPI/Events/IInputEvents.cs
new file mode 100644
index 00000000..8e2ef406
--- /dev/null
+++ b/src/SMAPI/Events/IInputEvents.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace StardewModdingAPI.Events
+{
+ /// <summary>Events raised when the player provides input using a controller, keyboard, or mouse.</summary>
+ public interface IInputEvents
+ {
+ /// <summary>Raised after the player presses a button on the keyboard, controller, or mouse.</summary>
+ event EventHandler<InputButtonPressedEventArgs> ButtonPressed;
+
+ /// <summary>Raised after the player releases a button on the keyboard, controller, or mouse.</summary>
+ event EventHandler<InputButtonReleasedEventArgs> ButtonReleased;
+
+ /// <summary>Raised after the player moves the in-game cursor.</summary>
+ event EventHandler<InputCursorMovedEventArgs> CursorMoved;
+
+ /// <summary>Raised after the player scrolls the mouse wheel.</summary>
+ event EventHandler<InputMouseWheelScrolledEventArgs> MouseWheelScrolled;
+ }
+}
diff --git a/src/SMAPI/Events/IModEvents.cs b/src/SMAPI/Events/IModEvents.cs
new file mode 100644
index 00000000..cf2f8cb8
--- /dev/null
+++ b/src/SMAPI/Events/IModEvents.cs
@@ -0,0 +1,15 @@
+namespace StardewModdingAPI.Events
+{
+ /// <summary>Manages access to events raised by SMAPI.</summary>
+ public interface IModEvents
+ {
+ /// <summary>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 <see cref="Input"/> if possible.</summary>
+ IGameLoopEvents GameLoop { get; }
+
+ /// <summary>Events raised when the player provides input using a controller, keyboard, or mouse.</summary>
+ IInputEvents Input { get; }
+
+ /// <summary>Events raised when something changes in the world.</summary>
+ IWorldEvents World { get; }
+ }
+}
diff --git a/src/SMAPI/Events/IWorldEvents.cs b/src/SMAPI/Events/IWorldEvents.cs
new file mode 100644
index 00000000..d4efb53b
--- /dev/null
+++ b/src/SMAPI/Events/IWorldEvents.cs
@@ -0,0 +1,29 @@
+using System;
+
+namespace StardewModdingAPI.Events
+{
+ /// <summary>Events raised when something changes in the world.</summary>
+ public interface IWorldEvents
+ {
+ /// <summary>Raised after a game location is added or removed.</summary>
+ event EventHandler<WorldLocationListChangedEventArgs> LocationListChanged;
+
+ /// <summary>Raised after buildings are added or removed in a location.</summary>
+ event EventHandler<WorldBuildingListChangedEventArgs> BuildingListChanged;
+
+ /// <summary>Raised after debris are added or removed in a location.</summary>
+ event EventHandler<WorldDebrisListChangedEventArgs> DebrisListChanged;
+
+ /// <summary>Raised after large terrain features (like bushes) are added or removed in a location.</summary>
+ event EventHandler<WorldLargeTerrainFeatureListChangedEventArgs> LargeTerrainFeatureListChanged;
+
+ /// <summary>Raised after NPCs are added or removed in a location.</summary>
+ event EventHandler<WorldNpcListChangedEventArgs> NpcListChanged;
+
+ /// <summary>Raised after objects are added or removed in a location.</summary>
+ event EventHandler<WorldObjectListChangedEventArgs> ObjectListChanged;
+
+ /// <summary>Raised after terrain features (like floors and trees) are added or removed in a location.</summary>
+ event EventHandler<WorldTerrainFeatureListChangedEventArgs> TerrainFeatureListChanged;
+ }
+}
diff --git a/src/SMAPI/Events/InputButtonPressedEventArgs.cs b/src/SMAPI/Events/InputButtonPressedEventArgs.cs
new file mode 100644
index 00000000..8c6844dd
--- /dev/null
+++ b/src/SMAPI/Events/InputButtonPressedEventArgs.cs
@@ -0,0 +1,60 @@
+using System;
+using StardewModdingAPI.Framework.Input;
+
+namespace StardewModdingAPI.Events
+{
+ /// <summary>Event arguments when a button is pressed.</summary>
+ public class InputButtonPressedEventArgs : EventArgs
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The game's current input state.</summary>
+ private readonly SInputState InputState;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The button on the controller, keyboard, or mouse.</summary>
+ public SButton Button { get; }
+
+ /// <summary>The current cursor position.</summary>
+ public ICursorPosition Cursor { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="button">The button on the controller, keyboard, or mouse.</param>
+ /// <param name="cursor">The cursor position.</param>
+ /// <param name="inputState">The game's current input state.</param>
+ internal InputButtonPressedEventArgs(SButton button, ICursorPosition cursor, SInputState inputState)
+ {
+ this.Button = button;
+ this.Cursor = cursor;
+ this.InputState = inputState;
+ }
+
+ /// <summary>Whether a mod has indicated the key was already handled, so the game should handle it.</summary>
+ public bool IsSuppressed()
+ {
+ return this.IsSuppressed(this.Button);
+ }
+
+ /// <summary>Whether a mod has indicated the key was already handled, so the game should handle it.</summary>
+ /// <param name="button">The button to check.</param>
+ public bool IsSuppressed(SButton button)
+ {
+ return this.InputState.SuppressButtons.Contains(button);
+ }
+
+ /// <summary>Get whether a given button was pressed or held.</summary>
+ /// <param name="button">The button to check.</param>
+ public bool IsDown(SButton button)
+ {
+ return this.InputState.IsDown(button);
+ }
+ }
+}
diff --git a/src/SMAPI/Events/InputButtonReleasedEventArgs.cs b/src/SMAPI/Events/InputButtonReleasedEventArgs.cs
new file mode 100644
index 00000000..4b0bc326
--- /dev/null
+++ b/src/SMAPI/Events/InputButtonReleasedEventArgs.cs
@@ -0,0 +1,60 @@
+using System;
+using StardewModdingAPI.Framework.Input;
+
+namespace StardewModdingAPI.Events
+{
+ /// <summary>Event arguments when a button is released.</summary>
+ public class InputButtonReleasedEventArgs : EventArgs
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The game's current input state.</summary>
+ private readonly SInputState InputState;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The button on the controller, keyboard, or mouse.</summary>
+ public SButton Button { get; }
+
+ /// <summary>The current cursor position.</summary>
+ public ICursorPosition Cursor { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="button">The button on the controller, keyboard, or mouse.</param>
+ /// <param name="cursor">The cursor position.</param>
+ /// <param name="inputState">The game's current input state.</param>
+ internal InputButtonReleasedEventArgs(SButton button, ICursorPosition cursor, SInputState inputState)
+ {
+ this.Button = button;
+ this.Cursor = cursor;
+ this.InputState = inputState;
+ }
+
+ /// <summary>Whether a mod has indicated the key was already handled, so the game should handle it.</summary>
+ public bool IsSuppressed()
+ {
+ return this.IsSuppressed(this.Button);
+ }
+
+ /// <summary>Whether a mod has indicated the key was already handled, so the game should handle it.</summary>
+ /// <param name="button">The button to check.</param>
+ public bool IsSuppressed(SButton button)
+ {
+ return this.InputState.SuppressButtons.Contains(button);
+ }
+
+ /// <summary>Get whether a given button was pressed or held.</summary>
+ /// <param name="button">The button to check.</param>
+ public bool IsDown(SButton button)
+ {
+ return this.InputState.IsDown(button);
+ }
+ }
+}
diff --git a/src/SMAPI/Events/InputCursorMovedEventArgs.cs b/src/SMAPI/Events/InputCursorMovedEventArgs.cs
new file mode 100644
index 00000000..53aac5b3
--- /dev/null
+++ b/src/SMAPI/Events/InputCursorMovedEventArgs.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace StardewModdingAPI.Events
+{
+ /// <summary>Event arguments when the in-game cursor is moved.</summary>
+ public class InputCursorMovedEventArgs : EventArgs
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The previous cursor position.</summary>
+ public ICursorPosition OldPosition { get; }
+
+ /// <summary>The current cursor position.</summary>
+ public ICursorPosition NewPosition { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="oldPosition">The previous cursor position.</param>
+ /// <param name="newPosition">The new cursor position.</param>
+ public InputCursorMovedEventArgs(ICursorPosition oldPosition, ICursorPosition newPosition)
+ {
+ this.OldPosition = oldPosition;
+ this.NewPosition = newPosition;
+ }
+ }
+}
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
/// <summary>Raised when the player presses a button on the keyboard, controller, or mouse.</summary>
public static event EventHandler<EventArgsInput> 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);
}
/// <summary>Raised when the player releases a keyboard key on the keyboard, controller, or mouse.</summary>
public static event EventHandler<EventArgsInput> 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/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
+{
+ /// <summary>Event arguments when the player scrolls the mouse wheel.</summary>
+ public class InputMouseWheelScrolledEventArgs : EventArgs
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The cursor position.</summary>
+ public ICursorPosition Position { get; }
+
+ /// <summary>The old scroll value.</summary>
+ public int OldValue { get; }
+
+ /// <summary>The new scroll value.</summary>
+ public int NewValue { get; }
+
+ /// <summary>The amount by which the scroll value changed.</summary>
+ public int Delta => this.NewValue - this.OldValue;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="position">The cursor position.</param>
+ /// <param name="oldValue">The old scroll value.</param>
+ /// <param name="newValue">The new scroll value.</param>
+ public InputMouseWheelScrolledEventArgs(ICursorPosition position, int oldValue, int newValue)
+ {
+ this.Position = position;
+ this.OldValue = oldValue;
+ this.NewValue = newValue;
+ }
+ }
+}
diff --git a/src/SMAPI/Events/LocationEvents.cs b/src/SMAPI/Events/LocationEvents.cs
index 81d13e9f..e2108de0 100644
--- a/src/SMAPI/Events/LocationEvents.cs
+++ b/src/SMAPI/Events/LocationEvents.cs
@@ -16,25 +16,25 @@ namespace StardewModdingAPI.Events
/*********
** Events
*********/
- /// <summary>Raised after the player warps to a new location.</summary>
- public static event EventHandler<EventArgsCurrentLocationChanged> CurrentLocationChanged
+ /// <summary>Raised after a game location is added or removed.</summary>
+ public static event EventHandler<EventArgsLocationsChanged> LocationsChanged
{
- add => LocationEvents.EventManager.Location_CurrentLocationChanged.Add(value);
- remove => LocationEvents.EventManager.Location_CurrentLocationChanged.Remove(value);
+ add => LocationEvents.EventManager.Legacy_Location_LocationsChanged.Add(value);
+ remove => LocationEvents.EventManager.Legacy_Location_LocationsChanged.Remove(value);
}
- /// <summary>Raised after a game location is added or removed.</summary>
- public static event EventHandler<EventArgsGameLocationsChanged> LocationsChanged
+ /// <summary>Raised after buildings are added or removed in a location.</summary>
+ public static event EventHandler<EventArgsLocationBuildingsChanged> BuildingsChanged
{
- add => LocationEvents.EventManager.Location_LocationsChanged.Add(value);
- remove => LocationEvents.EventManager.Location_LocationsChanged.Remove(value);
+ add => LocationEvents.EventManager.Legacy_Location_BuildingsChanged.Add(value);
+ remove => LocationEvents.EventManager.Legacy_Location_BuildingsChanged.Remove(value);
}
- /// <summary>Raised after the list of objects in the current location changes (e.g. an object is added or removed).</summary>
- public static event EventHandler<EventArgsLocationObjectsChanged> LocationObjectsChanged
+ /// <summary>Raised after objects are added or removed in a location.</summary>
+ public static event EventHandler<EventArgsLocationObjectsChanged> ObjectsChanged
{
- add => LocationEvents.EventManager.Location_LocationObjectsChanged.Add(value);
- remove => LocationEvents.EventManager.Location_LocationObjectsChanged.Remove(value);
+ add => LocationEvents.EventManager.Legacy_Location_ObjectsChanged.Add(value);
+ remove => LocationEvents.EventManager.Legacy_Location_ObjectsChanged.Remove(value);
}
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
+{
+ /// <summary>Events raised during the multiplayer sync process.</summary>
+ public static class MultiplayerEvents
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The core event manager.</summary>
+ private static EventManager EventManager;
+
+
+ /*********
+ ** Events
+ *********/
+ /// <summary>Raised before the game syncs changes from other players.</summary>
+ public static event EventHandler BeforeMainSync
+ {
+ add => MultiplayerEvents.EventManager.Multiplayer_BeforeMainSync.Add(value);
+ remove => MultiplayerEvents.EventManager.Multiplayer_BeforeMainSync.Remove(value);
+ }
+
+ /// <summary>Raised after the game syncs changes from other players.</summary>
+ public static event EventHandler AfterMainSync
+ {
+ add => MultiplayerEvents.EventManager.Multiplayer_AfterMainSync.Add(value);
+ remove => MultiplayerEvents.EventManager.Multiplayer_AfterMainSync.Remove(value);
+ }
+
+ /// <summary>Raised before the game broadcasts changes to other players.</summary>
+ public static event EventHandler BeforeMainBroadcast
+ {
+ add => MultiplayerEvents.EventManager.Multiplayer_BeforeMainBroadcast.Add(value);
+ remove => MultiplayerEvents.EventManager.Multiplayer_BeforeMainBroadcast.Remove(value);
+ }
+
+ /// <summary>Raised after the game broadcasts changes to other players.</summary>
+ public static event EventHandler AfterMainBroadcast
+ {
+ add => MultiplayerEvents.EventManager.Multiplayer_AfterMainBroadcast.Add(value);
+ remove => MultiplayerEvents.EventManager.Multiplayer_AfterMainBroadcast.Remove(value);
+ }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Initialise the events.</summary>
+ /// <param name="eventManager">The core event manager.</param>
+ internal static void Init(EventManager eventManager)
+ {
+ MultiplayerEvents.EventManager = eventManager;
+ }
+ }
+}
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);
}
- /// <summary> 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.</summary>
+ /// <summary>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.</summary>
public static event EventHandler<EventArgsLevelUp> LeveledUp
{
add => PlayerEvents.EventManager.Player_LeveledUp.Add(value);
remove => PlayerEvents.EventManager.Player_LeveledUp.Remove(value);
}
+ /// <summary>Raised after the player warps to a new location.</summary>
+ public static event EventHandler<EventArgsPlayerWarped> Warped
+ {
+ add => PlayerEvents.EventManager.Player_Warped.Add(value);
+ remove => PlayerEvents.EventManager.Player_Warped.Remove(value);
+ }
+
+
/*********
** Public methods
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
+{
+ /// <summary>Event arguments for a <see cref="IWorldEvents.BuildingListChanged"/> event.</summary>
+ public class WorldBuildingListChangedEventArgs : EventArgs
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The location which changed.</summary>
+ public GameLocation Location { get; }
+
+ /// <summary>The buildings added to the location.</summary>
+ public IEnumerable<Building> Added { get; }
+
+ /// <summary>The buildings removed from the location.</summary>
+ public IEnumerable<Building> Removed { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="location">The location which changed.</param>
+ /// <param name="added">The buildings added to the location.</param>
+ /// <param name="removed">The buildings removed from the location.</param>
+ public WorldBuildingListChangedEventArgs(GameLocation location, IEnumerable<Building> added, IEnumerable<Building> removed)
+ {
+ this.Location = location;
+ this.Added = added.ToArray();
+ this.Removed = removed.ToArray();
+ }
+ }
+}
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
+{
+ /// <summary>Event arguments for a <see cref="IWorldEvents.DebrisListChanged"/> event.</summary>
+ public class WorldDebrisListChangedEventArgs : EventArgs
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The location which changed.</summary>
+ public GameLocation Location { get; }
+
+ /// <summary>The debris added to the location.</summary>
+ public IEnumerable<Debris> Added { get; }
+
+ /// <summary>The debris removed from the location.</summary>
+ public IEnumerable<Debris> Removed { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="location">The location which changed.</param>
+ /// <param name="added">The debris added to the location.</param>
+ /// <param name="removed">The debris removed from the location.</param>
+ public WorldDebrisListChangedEventArgs(GameLocation location, IEnumerable<Debris> added, IEnumerable<Debris> removed)
+ {
+ this.Location = location;
+ this.Added = added.ToArray();
+ this.Removed = removed.ToArray();
+ }
+ }
+}
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
+{
+ /// <summary>Event arguments for a <see cref="IWorldEvents.LargeTerrainFeatureListChanged"/> event.</summary>
+ public class WorldLargeTerrainFeatureListChangedEventArgs : EventArgs
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The location which changed.</summary>
+ public GameLocation Location { get; }
+
+ /// <summary>The large terrain features added to the location.</summary>
+ public IEnumerable<LargeTerrainFeature> Added { get; }
+
+ /// <summary>The large terrain features removed from the location.</summary>
+ public IEnumerable<LargeTerrainFeature> Removed { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="location">The location which changed.</param>
+ /// <param name="added">The large terrain features added to the location.</param>
+ /// <param name="removed">The large terrain features removed from the location.</param>
+ public WorldLargeTerrainFeatureListChangedEventArgs(GameLocation location, IEnumerable<LargeTerrainFeature> added, IEnumerable<LargeTerrainFeature> 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
+{
+ /// <summary>Event arguments for a <see cref="IWorldEvents.LocationListChanged"/> event.</summary>
+ public class WorldLocationListChangedEventArgs : EventArgs
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The added locations.</summary>
+ public IEnumerable<GameLocation> Added { get; }
+
+ /// <summary>The removed locations.</summary>
+ public IEnumerable<GameLocation> Removed { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="added">The added locations.</param>
+ /// <param name="removed">The removed locations.</param>
+ public WorldLocationListChangedEventArgs(IEnumerable<GameLocation> added, IEnumerable<GameLocation> removed)
+ {
+ this.Added = added.ToArray();
+ this.Removed = removed.ToArray();
+ }
+ }
+}
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
+{
+ /// <summary>Event arguments for a <see cref="IWorldEvents.NpcListChanged"/> event.</summary>
+ public class WorldNpcListChangedEventArgs : EventArgs
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The location which changed.</summary>
+ public GameLocation Location { get; }
+
+ /// <summary>The NPCs added to the location.</summary>
+ public IEnumerable<NPC> Added { get; }
+
+ /// <summary>The NPCs removed from the location.</summary>
+ public IEnumerable<NPC> Removed { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="location">The location which changed.</param>
+ /// <param name="added">The NPCs added to the location.</param>
+ /// <param name="removed">The NPCs removed from the location.</param>
+ public WorldNpcListChangedEventArgs(GameLocation location, IEnumerable<NPC> added, IEnumerable<NPC> removed)
+ {
+ this.Location = location;
+ 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
+{
+ /// <summary>Event arguments for a <see cref="IWorldEvents.ObjectListChanged"/> event.</summary>
+ public class WorldObjectListChangedEventArgs : EventArgs
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The location which changed.</summary>
+ public GameLocation Location { get; }
+
+ /// <summary>The objects added to the location.</summary>
+ public IEnumerable<KeyValuePair<Vector2, Object>> Added { get; }
+
+ /// <summary>The objects removed from the location.</summary>
+ public IEnumerable<KeyValuePair<Vector2, Object>> Removed { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="location">The location which changed.</param>
+ /// <param name="added">The objects added to the location.</param>
+ /// <param name="removed">The objects removed from the location.</param>
+ public WorldObjectListChangedEventArgs(GameLocation location, IEnumerable<KeyValuePair<Vector2, Object>> added, IEnumerable<KeyValuePair<Vector2, Object>> removed)
+ {
+ this.Location = location;
+ this.Added = added.ToArray();
+ this.Removed = removed.ToArray();
+ }
+ }
+}
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
+{
+ /// <summary>Event arguments for a <see cref="IWorldEvents.TerrainFeatureListChanged"/> event.</summary>
+ public class WorldTerrainFeatureListChangedEventArgs : EventArgs
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The location which changed.</summary>
+ public GameLocation Location { get; }
+
+ /// <summary>The terrain features added to the location.</summary>
+ public IEnumerable<KeyValuePair<Vector2, TerrainFeature>> Added { get; }
+
+ /// <summary>The terrain features removed from the location.</summary>
+ public IEnumerable<KeyValuePair<Vector2, TerrainFeature>> Removed { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="location">The location which changed.</param>
+ /// <param name="added">The terrain features added to the location.</param>
+ /// <param name="removed">The terrain features removed from the location.</param>
+ public WorldTerrainFeatureListChangedEventArgs(GameLocation location, IEnumerable<KeyValuePair<Vector2, TerrainFeature>> added, IEnumerable<KeyValuePair<Vector2, TerrainFeature>> removed)
+ {
+ this.Location = location;
+ this.Added = added.ToArray();
+ this.Removed = removed.ToArray();
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/CommandManager.cs b/src/SMAPI/Framework/CommandManager.cs
index 79a23d03..f9651ed9 100644
--- a/src/SMAPI/Framework/CommandManager.cs
+++ b/src/SMAPI/Framework/CommandManager.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Text;
namespace StardewModdingAPI.Framework
{
@@ -72,7 +73,7 @@ namespace StardewModdingAPI.Framework
if (string.IsNullOrWhiteSpace(input))
return false;
- string[] args = input.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
+ string[] args = this.ParseArgs(input);
string name = args[0];
args = args.Skip(1).ToArray();
@@ -103,6 +104,31 @@ namespace StardewModdingAPI.Framework
/*********
** Private methods
*********/
+ /// <summary>Parse a string into command arguments.</summary>
+ /// <param name="input">The string to parse.</param>
+ private string[] ParseArgs(string input)
+ {
+ bool inQuotes = false;
+ IList<string> args = new List<string>();
+ StringBuilder currentArg = new StringBuilder();
+ foreach (char ch in input)
+ {
+ if (ch == '"')
+ inQuotes = !inQuotes;
+ else if (!inQuotes && char.IsWhiteSpace(ch))
+ {
+ args.Add(currentArg.ToString());
+ currentArg.Clear();
+ }
+ else
+ currentArg.Append(ch);
+ }
+
+ args.Add(currentArg.ToString());
+
+ return args.Where(item => !string.IsNullOrWhiteSpace(item)).ToArray();
+ }
+
/// <summary>Get a normalised command name.</summary>
/// <param name="name">The command name.</param>
private string GetNormalisedName(string name)
diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs
index 1eef2afb..5c7b87de 100644
--- a/src/SMAPI/Framework/Content/AssetDataForImage.cs
+++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs
@@ -4,7 +4,7 @@ using Microsoft.Xna.Framework.Graphics;
namespace StardewModdingAPI.Framework.Content
{
- /// <summary>Encapsulates access and changes to dictionary content being read from a data file.</summary>
+ /// <summary>Encapsulates access and changes to image content being read from a data file.</summary>
internal class AssetDataForImage : AssetData<Texture2D>, IAssetDataForImage
{
/*********
@@ -29,6 +29,8 @@ namespace StardewModdingAPI.Framework.Content
public void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace)
{
// get texture
+ if (source == null)
+ throw new ArgumentNullException(nameof(source), "Can't patch from a null source texture.");
Texture2D target = this.Data;
// get areas
@@ -36,8 +38,6 @@ namespace StardewModdingAPI.Framework.Content
targetArea = targetArea ?? new Rectangle(0, 0, Math.Min(sourceArea.Value.Width, target.Width), Math.Min(sourceArea.Value.Height, target.Height));
// validate
- if (source == null)
- throw new ArgumentNullException(nameof(source), "Can't patch from a null source texture.");
if (sourceArea.Value.X < 0 || sourceArea.Value.Y < 0 || sourceArea.Value.Right > source.Width || sourceArea.Value.Bottom > source.Height)
throw new ArgumentOutOfRangeException(nameof(sourceArea), "The source area is outside the bounds of the source texture.");
if (targetArea.Value.X < 0 || targetArea.Value.Y < 0 || targetArea.Value.Right > target.Width || targetArea.Value.Bottom > target.Height)
diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs
index 533da398..a5dfac9d 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.Framework.ModLoading;
using StardewModdingAPI.Framework.Reflection;
-using StardewModdingAPI.Framework.Utilities;
+using StardewModdingAPI.Internal;
+using StardewModdingAPI.Toolkit.Utilities;
using StardewValley;
namespace StardewModdingAPI.Framework.Content
@@ -53,7 +53,7 @@ namespace StardewModdingAPI.Framework.Content
this.Cache = reflection.GetField<Dictionary<string, object>>(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<string>(path);
diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs
new file mode 100644
index 00000000..d9b2109a
--- /dev/null
+++ b/src/SMAPI/Framework/ContentCoordinator.cs
@@ -0,0 +1,315 @@
+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.ContentManagers;
+using StardewModdingAPI.Framework.Reflection;
+using StardewModdingAPI.Metadata;
+using StardewModdingAPI.Toolkit.Utilities;
+using StardewValley;
+
+namespace StardewModdingAPI.Framework
+{
+ /// <summary>The central logic for creating content managers, invalidating caches, and propagating asset changes.</summary>
+ internal class ContentCoordinator : IDisposable
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>An asset key prefix for assets from SMAPI mod folders.</summary>
+ private readonly string ManagedPrefix = "SMAPI";
+
+ /// <summary>Encapsulates monitoring and logging.</summary>
+ private readonly IMonitor Monitor;
+
+ /// <summary>Provides metadata for core game assets.</summary>
+ private readonly CoreAssetPropagator CoreAssets;
+
+ /// <summary>Simplifies access to private code.</summary>
+ private readonly Reflector Reflection;
+
+ /// <summary>The loaded content managers (including the <see cref="MainContentManager"/>).</summary>
+ private readonly IList<IContentManager> ContentManagers = new List<IContentManager>();
+
+ /// <summary>Whether the content coordinator has been disposed.</summary>
+ private bool IsDisposed;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The primary content manager used for most assets.</summary>
+ public GameContentManager MainContentManager { get; private set; }
+
+ /// <summary>The current language as a constant.</summary>
+ public LocalizedContentManager.LanguageCode Language => this.MainContentManager.Language;
+
+ /// <summary>Interceptors which provide the initial versions of matching assets.</summary>
+ public IDictionary<IModMetadata, IList<IAssetLoader>> Loaders { get; } = new Dictionary<IModMetadata, IList<IAssetLoader>>();
+
+ /// <summary>Interceptors which edit matching assets after they're loaded.</summary>
+ public IDictionary<IModMetadata, IList<IAssetEditor>> Editors { get; } = new Dictionary<IModMetadata, IList<IAssetEditor>>();
+
+ /// <summary>The absolute path to the <see cref="ContentManager.RootDirectory"/>.</summary>
+ public string FullRootDirectory { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="serviceProvider">The service provider to use to locate services.</param>
+ /// <param name="rootDirectory">The root directory to search for content.</param>
+ /// <param name="currentCulture">The current culture for which to localise content.</param>
+ /// <param name="monitor">Encapsulates monitoring and logging.</param>
+ /// <param name="reflection">Simplifies access to private code.</param>
+ 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 GameContentManager("Game1.content", serviceProvider, rootDirectory, currentCulture, this, monitor, reflection, this.OnDisposing)
+ );
+ this.CoreAssets = new CoreAssetPropagator(this.MainContentManager.AssertAndNormaliseAssetName, reflection);
+ }
+
+ /// <summary>Get a new content manager which handles reading files from the game content folder with support for interception.</summary>
+ /// <param name="name">A name for the mod manager. Not guaranteed to be unique.</param>
+ 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;
+ }
+
+ /// <summary>Get a new content manager which handles reading files from a SMAPI mod folder with support for unpacked files.</summary>
+ /// <param name="name">A name for the mod manager. Not guaranteed to be unique.</param>
+ /// <param name="rootDirectory">The root directory to search for content (or <c>null</c> for the default).</param>
+ public ModContentManager CreateModContentManager(string name, string rootDirectory)
+ {
+ 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;
+ }
+
+ /// <summary>Get the current content locale.</summary>
+ public string GetLocale()
+ {
+ return this.MainContentManager.GetLocale(LocalizedContentManager.CurrentLanguageCode);
+ }
+
+ /// <summary>Get whether this asset is mapped to a mod folder.</summary>
+ /// <param name="key">The asset key.</param>
+ public bool IsManagedAssetKey(string key)
+ {
+ return key.StartsWith(this.ManagedPrefix);
+ }
+
+ /// <summary>Parse a managed SMAPI asset key which maps to a mod folder.</summary>
+ /// <param name="key">The asset key.</param>
+ /// <param name="contentManagerID">The unique name for the content manager which should load this asset.</param>
+ /// <param name="relativePath">The relative path within the mod folder.</param>
+ /// <returns>Returns whether the asset was parsed successfully.</returns>
+ 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;
+
+ // 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;
+ }
+
+ /// <summary>Get the managed asset key prefix for a mod.</summary>
+ /// <param name="modID">The mod's unique ID.</param>
+ public string GetManagedAssetPrefix(string modID)
+ {
+ return Path.Combine(this.ManagedPrefix, modID.ToLower());
+ }
+
+ /// <summary>Get a copy of an asset from a mod folder.</summary>
+ /// <typeparam name="T">The asset type.</typeparam>
+ /// <param name="internalKey">The internal asset key.</param>
+ /// <param name="contentManagerID">The unique name for the content manager which should load this asset.</param>
+ /// <param name="relativePath">The internal SMAPI asset key.</param>
+ /// <param name="language">The language code for which to load content.</param>
+ public T LoadAndCloneManagedAsset<T>(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<T>(internalKey, language);
+ return contentManager.CloneIfPossible(data);
+ }
+
+ /// <summary>Purge assets from the cache that match one of the interceptors.</summary>
+ /// <param name="editors">The asset editors for which to purge matching assets.</param>
+ /// <param name="loaders">The asset loaders for which to purge matching assets.</param>
+ /// <returns>Returns the invalidated asset names.</returns>
+ public IEnumerable<string> 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);
+ foreach (IAssetLoader loader in loaders)
+ {
+ try
+ {
+ if ((bool)canLoadGeneric.Invoke(loader, new object[] { asset }))
+ return true;
+ }
+ catch (Exception ex)
+ {
+ this.GetModFor(loader).LogAsMod($"Mod failed when checking whether it could load asset '{asset.AssetName}'. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
+ }
+ }
+
+ // check editors
+ MethodInfo canEditGeneric = canEdit.MakeGenericMethod(asset.DataType);
+ foreach (IAssetEditor editor in editors)
+ {
+ try
+ {
+ if ((bool)canEditGeneric.Invoke(editor, new object[] { asset }))
+ return true;
+ }
+ catch (Exception ex)
+ {
+ this.GetModFor(editor).LogAsMod($"Mod failed when checking whether it could edit asset '{asset.AssetName}'. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
+ }
+ }
+
+ // asset not affected by a loader or editor
+ return false;
+ });
+ }
+
+ /// <summary>Purge matched assets from the cache.</summary>
+ /// <param name="predicate">Matches the asset keys to invalidate.</param>
+ /// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param>
+ /// <returns>Returns the invalidated asset keys.</returns>
+ public IEnumerable<string> InvalidateCache(Func<IAssetInfo, bool> predicate, bool dispose = false)
+ {
+ string locale = this.GetLocale();
+ return this.InvalidateCache((assetName, type) =>
+ {
+ IAssetInfo info = new AssetInfo(locale, assetName, type, this.MainContentManager.AssertAndNormaliseAssetName);
+ return predicate(info);
+ });
+ }
+
+ /// <summary>Purge matched assets from the cache.</summary>
+ /// <param name="predicate">Matches the asset keys to invalidate.</param>
+ /// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param>
+ /// <returns>Returns the invalidated asset names.</returns>
+ public IEnumerable<string> InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false)
+ {
+ // invalidate cache
+ HashSet<string> removedAssetNames = new HashSet<string>();
+ foreach (IContentManager 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);
+ else
+ this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace);
+
+ return removedAssetNames;
+ }
+
+ /// <summary>Dispose held resources.</summary>
+ public void Dispose()
+ {
+ if (this.IsDisposed)
+ return;
+ this.IsDisposed = true;
+
+ this.Monitor.Log("Disposing the content coordinator. Content managers will no longer be usable after this point.", LogLevel.Trace);
+ foreach (IContentManager contentManager in this.ContentManagers)
+ contentManager.Dispose();
+ this.ContentManagers.Clear();
+ this.MainContentManager = null;
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>A callback invoked when a content manager is disposed.</summary>
+ /// <param name="contentManager">The content manager being disposed.</param>
+ private void OnDisposing(IContentManager contentManager)
+ {
+ if (this.IsDisposed)
+ return;
+
+ this.ContentManagers.Remove(contentManager);
+ }
+
+ /// <summary>Get the mod which registered an asset loader.</summary>
+ /// <param name="loader">The asset loader.</param>
+ /// <exception cref="KeyNotFoundException">The given loader couldn't be matched to a mod.</exception>
+ private IModMetadata GetModFor(IAssetLoader loader)
+ {
+ foreach (var pair in this.Loaders)
+ {
+ if (pair.Value.Contains(loader))
+ return pair.Key;
+ }
+
+ throw new KeyNotFoundException("This loader isn't associated with a known mod.");
+ }
+
+ /// <summary>Get the mod which registered an asset editor.</summary>
+ /// <param name="editor">The asset editor.</param>
+ /// <exception cref="KeyNotFoundException">The given editor couldn't be matched to a mod.</exception>
+ private IModMetadata GetModFor(IAssetEditor editor)
+ {
+ foreach (var pair in this.Editors)
+ {
+ if (pair.Value.Contains(editor))
+ return pair.Key;
+ }
+
+ throw new KeyNotFoundException("This editor isn't associated with a known mod.");
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/ContentCore.cs b/src/SMAPI/Framework/ContentCore.cs
deleted file mode 100644
index 43357553..00000000
--- a/src/SMAPI/Framework/ContentCore.cs
+++ /dev/null
@@ -1,882 +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
-{
- /// <summary>A thread-safe content handler which loads assets with support for mod injection and editing.</summary>
- /// <remarks>
- /// 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 <see cref="ContentManagerShim"/> 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.
- /// </remarks>
- internal class ContentCore : IDisposable
- {
- /*********
- ** Properties
- *********/
- /// <summary>The underlying content manager.</summary>
- private readonly LocalizedContentManager Content;
-
- /// <summary>Encapsulates monitoring and logging.</summary>
- private readonly IMonitor Monitor;
-
- /// <summary>The underlying asset cache.</summary>
- private readonly ContentCache Cache;
-
-#if STARDEW_VALLEY_1_3
- /// <summary>A lookup which indicates whether the asset is localisable (i.e. the filename contains the locale), if previously loaded.</summary>
- private readonly IDictionary<string, bool> IsLocalisableLookup;
-#endif
-
- /// <summary>The locale codes used in asset keys indexed by enum value.</summary>
- private readonly IDictionary<LocalizedContentManager.LanguageCode, string> Locales;
-
- /// <summary>The language enum values indexed by locale code.</summary>
- private readonly IDictionary<string, LocalizedContentManager.LanguageCode> LanguageCodes;
-
- /// <summary>Provides metadata for core game assets.</summary>
- private readonly CoreAssetPropagator CoreAssets;
-
- /// <summary>The assets currently being intercepted by <see cref="IAssetLoader"/> instances. This is used to prevent infinite loops when a loader loads a new asset.</summary>
- private readonly ContextHash<string> AssetsBeingLoaded = new ContextHash<string>();
-
- /// <summary>A lookup of the content managers which loaded each asset.</summary>
- private readonly IDictionary<string, HashSet<ContentManager>> ContentManagersByAssetKey = new Dictionary<string, HashSet<ContentManager>>();
-
- /// <summary>The path prefix for assets in mod folders.</summary>
- private readonly string ModContentPrefix;
-
- /// <summary>A lock used to prevents concurrent changes to the cache while data is being read.</summary>
- private readonly ReaderWriterLockSlim Lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
-
-
- /*********
- ** Accessors
- *********/
- /// <summary>The current language as a constant.</summary>
- public LocalizedContentManager.LanguageCode Language => this.Content.GetCurrentLanguage();
-
- /// <summary>Interceptors which provide the initial versions of matching assets.</summary>
- public IDictionary<IModMetadata, IList<IAssetLoader>> Loaders { get; } = new Dictionary<IModMetadata, IList<IAssetLoader>>();
-
- /// <summary>Interceptors which edit matching assets after they're loaded.</summary>
- public IDictionary<IModMetadata, IList<IAssetEditor>> Editors { get; } = new Dictionary<IModMetadata, IList<IAssetEditor>>();
-
- /// <summary>The absolute path to the <see cref="ContentManager.RootDirectory"/>.</summary>
- public string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.Content.RootDirectory);
-
- /*********
- ** Public methods
- *********/
- /****
- ** Constructor
- ****/
- /// <summary>Construct an instance.</summary>
- /// <param name="serviceProvider">The service provider to use to locate services.</param>
- /// <param name="rootDirectory">The root directory to search for content.</param>
- /// <param name="currentCulture">The current culture for which to localise content.</param>
- /// <param name="languageCodeOverride">The current language code for which to localise content.</param>
- /// <param name="monitor">Encapsulates monitoring and logging.</param>
- /// <param name="reflection">Simplifies access to private code.</param>
- public ContentCore(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, string languageCodeOverride, IMonitor monitor, Reflector reflection)
- {
- // init
- this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor));
- this.Content = new LocalizedContentManager(serviceProvider, rootDirectory, currentCulture, languageCodeOverride);
- this.Cache = new ContentCache(this.Content, reflection);
- this.ModContentPrefix = this.GetAssetNameFromFilePath(Constants.ModPath);
-
- // get asset data
- this.CoreAssets = new CoreAssetPropagator(this.NormaliseAssetName, reflection);
- this.Locales = this.GetKeyLocales(reflection);
- this.LanguageCodes = this.Locales.ToDictionary(p => p.Value, p => p.Key, StringComparer.InvariantCultureIgnoreCase);
-#if STARDEW_VALLEY_1_3
- this.IsLocalisableLookup = reflection.GetField<IDictionary<string, bool>>(this.Content, "_localizedAsset").GetValue();
-#endif
- }
-
- /// <summary>Get a new content manager which defers loading to the content core.</summary>
- /// <param name="name">The content manager's name for logs (if any).</param>
- /// <param name="rootDirectory">The root directory to search for content (or <c>null</c>. for the default)</param>
- public ContentManagerShim CreateContentManager(string name, string rootDirectory = null)
- {
- return new ContentManagerShim(this, name, this.Content.ServiceProvider, rootDirectory ?? this.Content.RootDirectory, this.Content.CurrentCulture, this.Content.LanguageCodeOverride);
- }
-
- /****
- ** Asset key/name handling
- ****/
- /// <summary>Normalise path separators in a file path. For asset keys, see <see cref="NormaliseAssetName"/> instead.</summary>
- /// <param name="path">The file path to normalise.</param>
- [Pure]
- public string NormalisePathSeparators(string path)
- {
- return this.Cache.NormalisePathSeparators(path);
- }
-
- /// <summary>Normalise an asset name so it's consistent with the underlying cache.</summary>
- /// <param name="assetName">The asset key.</param>
- [Pure]
- public string NormaliseAssetName(string assetName)
- {
- return this.Cache.NormaliseKey(assetName);
- }
-
- /// <summary>Assert that the given key has a valid format.</summary>
- /// <param name="key">The asset key to check.</param>
- /// <exception cref="ArgumentException">The asset key is empty or contains invalid characters.</exception>
- [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.");
- }
-
- /// <summary>Convert an absolute file path into a appropriate asset name.</summary>
- /// <param name="absolutePath">The absolute path to the file.</param>
- 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
- ****/
- /// <summary>Get the current content locale.</summary>
- public string GetLocale()
- {
- return this.GetLocale(this.Content.GetCurrentLanguage());
- }
-
- /// <summary>The locale for a language.</summary>
- /// <param name="language">The language.</param>
- public string GetLocale(LocalizedContentManager.LanguageCode language)
- {
- return this.Locales[language];
- }
-
- /// <summary>Get whether the content manager has already loaded and cached the given asset.</summary>
- /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
- public bool IsLoaded(string assetName)
- {
- assetName = this.Cache.NormaliseKey(assetName);
- return this.WithReadLock(() => this.IsNormalisedKeyLoaded(assetName));
- }
-
- /// <summary>Get the cached asset keys.</summary>
- public IEnumerable<string> GetAssetKeys()
- {
- return this.WithReadLock(() =>
- this.Cache.Keys
- .Select(this.GetAssetName)
- .Distinct()
- );
- }
-
- /// <summary>Load an asset through the content pipeline. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary>
- /// <typeparam name="T">The expected asset type.</typeparam>
- /// <param name="assetName">The asset path relative to the content directory.</param>
- /// <param name="instance">The content manager instance for which to load the asset.</param>
- /// <param name="language">The language code for which to load content.</param>
- /// <exception cref="ArgumentException">The <paramref name="assetName"/> is empty or contains invalid characters.</exception>
- /// <exception cref="ContentLoadException">The content asset couldn't be loaded (e.g. because it doesn't exist).</exception>
- public T Load<T>(string assetName, ContentManager instance
-#if STARDEW_VALLEY_1_3
- , LocalizedContentManager.LanguageCode language
-#endif
- )
- {
- // normalise asset key
- this.AssertValidAssetKeyFormat(assetName);
- assetName = this.NormaliseAssetName(assetName);
-
- // load game content
- if (!assetName.StartsWith(this.ModContentPrefix))
-#if STARDEW_VALLEY_1_3
- return this.LoadImpl<T>(assetName, instance, language);
-#else
- return this.LoadImpl<T>(assetName, instance);
-#endif
-
- // 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))
-#if STARDEW_VALLEY_1_3
- return this.LoadImpl<T>(assetName, instance, language);
-#else
- return this.LoadImpl<T>(assetName, instance);
-#endif
-
- // 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":
-#if STARDEW_VALLEY_1_3
- return this.LoadImpl<T>(assetName, instance, language);
-#else
- return this.LoadImpl<T>(assetName, instance);
-#endif
-
- // 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);
- }
- }
-
- /// <summary>Inject an asset into the cache.</summary>
- /// <typeparam name="T">The type of asset to inject.</typeparam>
- /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
- /// <param name="value">The asset value.</param>
- /// <param name="instance">The content manager instance for which to load the asset.</param>
- public void Inject<T>(string assetName, T value, ContentManager instance)
- {
- this.WithWriteLock(() => this.InjectWithoutLock(assetName, value, instance));
- }
-
- /****
- ** Cache invalidation
- ****/
- /// <summary>Purge assets from the cache that match one of the interceptors.</summary>
- /// <param name="editors">The asset editors for which to purge matching assets.</param>
- /// <param name="loaders">The asset loaders for which to purge matching assets.</param>
- /// <returns>Returns whether any cache entries were invalidated.</returns>
- 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 }));
- });
- }
-
- /// <summary>Purge matched assets from the cache.</summary>
- /// <param name="predicate">Matches the asset keys to invalidate.</param>
- /// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param>
- /// <returns>Returns whether any cache entries were invalidated.</returns>
- public bool InvalidateCache(Func<IAssetInfo, bool> 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);
- });
- }
-
- /// <summary>Purge matched assets from the cache.</summary>
- /// <param name="predicate">Matches the asset keys to invalidate.</param>
- /// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param>
- /// <returns>Returns whether any cache entries were invalidated.</returns>
- public bool InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false)
- {
- return this.WithWriteLock(() =>
- {
- // invalidate matching keys
- HashSet<string> removeKeys = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
- HashSet<string> removeAssetNames = new HashSet<string>(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
- ****/
- /// <summary>Dispose assets for the given content manager shim.</summary>
- /// <param name="shim">The content manager whose assets to dispose.</param>
- 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[key].Any(), dispose: true);
- });
- }
-
-
- /*********
- ** Private methods
- *********/
- /****
- ** Disposal
- ****/
- /// <summary>Dispose held resources.</summary>
- 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
- ****/
- /// <summary>Get a directory or file path relative to the content root.</summary>
- /// <param name="targetPath">The target file path.</param>
- private string GetRelativePath(string targetPath)
- {
- return PathUtilities.GetRelativePath(this.FullRootDirectory, targetPath);
- }
-
- /// <summary>Get the locale codes (like <c>ja-JP</c>) used in asset keys.</summary>
- /// <param name="reflection">Simplifies access to private game code.</param>
- private IDictionary<LocalizedContentManager.LanguageCode, string> GetKeyLocales(Reflector reflection)
- {
-#if !STARDEW_VALLEY_1_3
- IReflectedField<LocalizedContentManager.LanguageCode> codeField = reflection.GetField<LocalizedContentManager.LanguageCode>(typeof(LocalizedContentManager), "_currentLangCode");
- LocalizedContentManager.LanguageCode previousCode = codeField.GetValue();
-#endif
- string previousOverride = this.Content.LanguageCodeOverride;
-
- try
- {
- // temporarily disable language override
- this.Content.LanguageCodeOverride = null;
-
- // create locale => code map
- IReflectedMethod languageCodeString = reflection
-#if STARDEW_VALLEY_1_3
- .GetMethod(this.Content, "languageCodeString");
-#else
- .GetMethod(this.Content, "languageCode");
-#endif
- IDictionary<LocalizedContentManager.LanguageCode, string> map = new Dictionary<LocalizedContentManager.LanguageCode, string>();
- foreach (LocalizedContentManager.LanguageCode code in Enum.GetValues(typeof(LocalizedContentManager.LanguageCode)))
- {
-#if STARDEW_VALLEY_1_3
- map[code] = languageCodeString.Invoke<string>(code);
-#else
- codeField.SetValue(code);
- map[code] = languageCodeString.Invoke<string>();
-#endif
- }
-
- return map;
- }
- finally
- {
- // restore previous settings
- this.Content.LanguageCodeOverride = previousOverride;
-#if !STARDEW_VALLEY_1_3
- codeField.SetValue(previousCode);
-#endif
-
- }
- }
-
- /// <summary>Get the asset name from a cache key.</summary>
- /// <param name="cacheKey">The input cache key.</param>
- private string GetAssetName(string cacheKey)
- {
- this.ParseCacheKey(cacheKey, out string assetName, out string _);
- return assetName;
- }
-
- /// <summary>Parse a cache key into its component parts.</summary>
- /// <param name="cacheKey">The input cache key.</param>
- /// <param name="assetName">The original asset name.</param>
- /// <param name="localeCode">The asset locale code (or <c>null</c> if not localised).</param>
- 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
- ****/
- /// <summary>Get whether an asset has already been loaded.</summary>
- /// <param name="normalisedAssetName">The normalised asset name.</param>
- private bool IsNormalisedKeyLoaded(string normalisedAssetName)
- {
-#if STARDEW_VALLEY_1_3
- if (!this.IsLocalisableLookup.TryGetValue(normalisedAssetName, out bool localisable))
- return false;
-
- return localisable
- ? this.Cache.ContainsKey($"{normalisedAssetName}.{this.Locales[this.Content.GetCurrentLanguage()]}")
- : this.Cache.ContainsKey(normalisedAssetName);
-#else
- return
- this.Cache.ContainsKey(normalisedAssetName)
- || this.Cache.ContainsKey($"{normalisedAssetName}.{this.Locales[this.Content.GetCurrentLanguage()]}"); // translated asset
-#endif
- }
-
- /// <summary>Track that a content manager loaded an asset.</summary>
- /// <param name="key">The asset key that was loaded.</param>
- /// <param name="manager">The content manager that loaded the asset.</param>
- private void TrackAssetLoader(string key, ContentManager manager)
- {
- if (!this.ContentManagersByAssetKey.TryGetValue(key, out HashSet<ContentManager> hash))
- hash = this.ContentManagersByAssetKey[key] = new HashSet<ContentManager>();
- hash.Add(manager);
- }
-
- /****
- ** Content loading
- ****/
- /// <summary>Load an asset name without heuristics to support mod content.</summary>
- /// <typeparam name="T">The type of asset to load.</typeparam>
- /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
- /// <param name="instance">The content manager instance for which to load the asset.</param>
- /// <param name="language">The language code for which to load content.</param>
- private T LoadImpl<T>(string assetName, ContentManager instance
-#if STARDEW_VALLEY_1_3
- , LocalizedContentManager.LanguageCode language
-#endif
- )
- {
- return this.WithWriteLock(() =>
- {
- // skip if already loaded
- if (this.IsNormalisedKeyLoaded(assetName))
- {
- this.TrackAssetLoader(assetName, instance);
- return this.Content
-
-#if STARDEW_VALLEY_1_3
- .Load<T>(assetName, language);
-#else
- .Load<T>(assetName);
-#endif
- }
-
- // 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
-#if STARDEW_VALLEY_1_3
- .Load<T>(assetName, language);
-#else
- .Load<T>(assetName);
-#endif
- }
- else
- {
- data = this.AssetsBeingLoaded.Track(assetName, () =>
- {
- string locale =
-#if STARDEW_VALLEY_1_3
- this.GetLocale(language);
-#else
- this.GetLocale();
-#endif
- IAssetInfo info = new AssetInfo(locale, assetName, typeof(T), this.NormaliseAssetName);
- IAssetData asset = this.ApplyLoader<T>(info)
-#if STARDEW_VALLEY_1_3
- ?? new AssetDataForObject(info, this.Content.Load<T>(assetName, language), this.NormaliseAssetName);
-#else
- ?? new AssetDataForObject(info, this.Content.Load<T>(assetName), this.NormaliseAssetName);
-#endif
- asset = this.ApplyEditors<T>(info, asset);
- return (T)asset.Data;
- });
- }
-
- // update cache & return data
- this.InjectWithoutLock(assetName, data, instance);
- return data;
- });
- }
-
- /// <summary>Inject an asset into the cache without acquiring a write lock. This should only be called from within a write lock.</summary>
- /// <typeparam name="T">The type of asset to inject.</typeparam>
- /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
- /// <param name="value">The asset value.</param>
- /// <param name="instance">The content manager instance for which to load the asset.</param>
- private void InjectWithoutLock<T>(string assetName, T value, ContentManager instance)
- {
- assetName = this.NormaliseAssetName(assetName);
- this.Cache[assetName] = value;
- this.TrackAssetLoader(assetName, instance);
- }
-
- /// <summary>Get a file from the mod folder.</summary>
- /// <param name="path">The asset path relative to the content folder.</param>
- 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;
- }
-
- /// <summary>Load the initial asset from the registered <see cref="Loaders"/>.</summary>
- /// <param name="info">The basic asset metadata.</param>
- /// <returns>Returns the loaded asset metadata, or <c>null</c> if no loader matched.</returns>
- private IAssetData ApplyLoader<T>(IAssetInfo info)
- {
- // find matching loaders
- var loaders = this.GetInterceptors(this.Loaders)
- .Where(entry =>
- {
- try
- {
- return entry.Value.CanLoad<T>(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<T>(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);
- }
-
- /// <summary>Apply any <see cref="Editors"/> to a loaded asset.</summary>
- /// <typeparam name="T">The asset type.</typeparam>
- /// <param name="info">The basic asset metadata.</param>
- /// <param name="asset">The loaded asset.</param>
- private IAssetData ApplyEditors<T>(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<T>(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<T>(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;
- }
-
- /// <summary>Get all registered interceptors from a list.</summary>
- private IEnumerable<KeyValuePair<IModMetadata, T>> GetInterceptors<T>(IDictionary<IModMetadata, IList<T>> entries)
- {
- foreach (var entry in entries)
- {
- IModMetadata mod = entry.Key;
- IList<T> interceptors = entry.Value;
-
- // registered editors
- foreach (T interceptor in interceptors)
- yield return new KeyValuePair<IModMetadata, T>(mod, interceptor);
- }
- }
-
- /// <summary>Premultiply a texture's alpha values to avoid transparency issues in the game. This is only possible if the game isn't currently drawing.</summary>
- /// <param name="texture">The texture to premultiply.</param>
- /// <returns>Returns a premultiplied texture.</returns>
- /// <remarks>Based on <a href="https://gist.github.com/Layoric/6255384">code by Layoric</a>.</remarks>
- 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
- ****/
- /// <summary>Acquire a read lock which prevents concurrent writes to the cache while it's open.</summary>
- /// <typeparam name="T">The action's return value.</typeparam>
- /// <param name="action">The action to perform.</param>
- private T WithReadLock<T>(Func<T> action)
- {
- try
- {
- this.Lock.EnterReadLock();
- return action();
- }
- finally
- {
- this.Lock.ExitReadLock();
- }
- }
-
- /// <summary>Acquire a write lock which prevents concurrent reads or writes to the cache while it's open.</summary>
- /// <param name="action">The action to perform.</param>
- private void WithWriteLock(Action action)
- {
- try
- {
- this.Lock.EnterWriteLock();
- action();
- }
- finally
- {
- this.Lock.ExitWriteLock();
- }
- }
-
- /// <summary>Acquire a write lock which prevents concurrent reads or writes to the cache while it's open.</summary>
- /// <typeparam name="T">The action's return value.</typeparam>
- /// <param name="action">The action to perform.</param>
- private T WithWriteLock<T>(Func<T> 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 8f88fc2d..00000000
--- a/src/SMAPI/Framework/ContentManagerShim.cs
+++ /dev/null
@@ -1,100 +0,0 @@
-using System;
-using System.Globalization;
-using StardewValley;
-
-namespace StardewModdingAPI.Framework
-{
- /// <summary>A minimal content manager which defers to SMAPI's core content logic.</summary>
- internal class ContentManagerShim : LocalizedContentManager
- {
- /*********
- ** Properties
- *********/
- /// <summary>SMAPI's core content logic.</summary>
- private readonly ContentCore ContentCore;
-
-
- /*********
- ** Accessors
- *********/
- /// <summary>The content manager's name for logs (if any).</summary>
- public string Name { get; }
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Construct an instance.</summary>
- /// <param name="contentCore">SMAPI's core content logic.</param>
- /// <param name="name">The content manager's name for logs (if any).</param>
- /// <param name="serviceProvider">The service provider to use to locate services.</param>
- /// <param name="rootDirectory">The root directory to search for content.</param>
- /// <param name="currentCulture">The current culture for which to localise content.</param>
- /// <param name="languageCodeOverride">The current language code for which to localise content.</param>
- public ContentManagerShim(ContentCore contentCore, string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, string languageCodeOverride)
- : base(serviceProvider, rootDirectory, currentCulture, languageCodeOverride)
- {
- this.ContentCore = contentCore;
- this.Name = name;
- }
-
- /// <summary>Load an asset that has been processed by the content pipeline.</summary>
- /// <typeparam name="T">The type of asset to load.</typeparam>
- /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
- public override T Load<T>(string assetName)
- {
-#if STARDEW_VALLEY_1_3
- return this.Load<T>(assetName, LocalizedContentManager.CurrentLanguageCode);
-#else
- return this.ContentCore.Load<T>(assetName, this);
-#endif
- }
-
-#if STARDEW_VALLEY_1_3
- /// <summary>Load an asset that has been processed by the content pipeline.</summary>
- /// <typeparam name="T">The type of asset to load.</typeparam>
- /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
- /// <param name="language">The language code for which to load content.</param>
- public override T Load<T>(string assetName, LanguageCode language)
- {
- return this.ContentCore.Load<T>(assetName, this, language);
- }
-
- /// <summary>Load the base asset without localisation.</summary>
- /// <typeparam name="T">The type of asset to load.</typeparam>
- /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
- public override T LoadBase<T>(string assetName)
- {
- return this.Load<T>(assetName, LanguageCode.en);
- }
-#endif
-
- /// <summary>Inject an asset into the cache.</summary>
- /// <typeparam name="T">The type of asset to inject.</typeparam>
- /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
- /// <param name="value">The asset value.</param>
- public void Inject<T>(string assetName, T value)
- {
- this.ContentCore.Inject<T>(assetName, value, this);
- }
-
-#if STARDEW_VALLEY_1_3
- /// <summary>Create a new content manager for temporary use.</summary>
- public override LocalizedContentManager CreateTemporary()
- {
- return this.ContentCore.CreateContentManager("(temporary)");
- }
-#endif
-
-
- /*********
- ** Protected methods
- *********/
- /// <summary>Dispose held resources.</summary>
- /// <param name="disposing">Whether the content manager is disposing (rather than finalising).</param>
- protected override void Dispose(bool disposing)
- {
- this.ContentCore.DisposeFor(this);
- }
- }
-}
diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
new file mode 100644
index 00000000..18aae05b
--- /dev/null
+++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
@@ -0,0 +1,297 @@
+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 Microsoft.Xna.Framework.Graphics;
+using StardewModdingAPI.Framework.Content;
+using StardewModdingAPI.Framework.Exceptions;
+using StardewModdingAPI.Framework.Reflection;
+using StardewValley;
+
+namespace StardewModdingAPI.Framework.ContentManagers
+{
+ /// <summary>A content manager which handles reading files from a SMAPI mod folder with support for unpacked files.</summary>
+ internal abstract class BaseContentManager : LocalizedContentManager, IContentManager
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The central coordinator which manages content managers.</summary>
+ protected readonly ContentCoordinator Coordinator;
+
+ /// <summary>The underlying asset cache.</summary>
+ protected readonly ContentCache Cache;
+
+ /// <summary>Encapsulates monitoring and logging.</summary>
+ protected readonly IMonitor Monitor;
+
+ /// <summary>Whether the content coordinator has been disposed.</summary>
+ private bool IsDisposed;
+
+ /// <summary>The language enum values indexed by locale code.</summary>
+ private readonly IDictionary<string, LanguageCode> LanguageCodes;
+
+ /// <summary>A callback to invoke when the content manager is being disposed.</summary>
+ private readonly Action<BaseContentManager> OnDisposing;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>A name for the mod manager. Not guaranteed to be unique.</summary>
+ public string Name { get; }
+
+ /// <summary>The current language as a constant.</summary>
+ public LanguageCode Language => this.GetCurrentLanguage();
+
+ /// <summary>The absolute path to the <see cref="ContentManager.RootDirectory"/>.</summary>
+ public string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory);
+
+ /// <summary>Whether this content manager is for a mod folder.</summary>
+ public bool IsModContentManager { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="name">A name for the mod manager. Not guaranteed to be unique.</param>
+ /// <param name="serviceProvider">The service provider to use to locate services.</param>
+ /// <param name="rootDirectory">The root directory to search for content.</param>
+ /// <param name="currentCulture">The current culture for which to localise content.</param>
+ /// <param name="coordinator">The central coordinator which manages content managers.</param>
+ /// <param name="monitor">Encapsulates monitoring and logging.</param>
+ /// <param name="reflection">Simplifies access to private code.</param>
+ /// <param name="onDisposing">A callback to invoke when the content manager is being disposed.</param>
+ /// <param name="isModFolder">Whether this content manager is for a mod folder.</param>
+ protected BaseContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action<BaseContentManager> 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);
+ }
+
+ /// <summary>Load an asset that has been processed by the content pipeline.</summary>
+ /// <typeparam name="T">The type of asset to load.</typeparam>
+ /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
+ public override T Load<T>(string assetName)
+ {
+ return this.Load<T>(assetName, LocalizedContentManager.CurrentLanguageCode);
+ }
+
+ /// <summary>Load the base asset without localisation.</summary>
+ /// <typeparam name="T">The type of asset to load.</typeparam>
+ /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
+ public override T LoadBase<T>(string assetName)
+ {
+ return this.Load<T>(assetName, LanguageCode.en);
+ }
+
+ /// <summary>Inject an asset into the cache.</summary>
+ /// <typeparam name="T">The type of asset to inject.</typeparam>
+ /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
+ /// <param name="value">The asset value.</param>
+ public void Inject<T>(string assetName, T value)
+ {
+ assetName = this.AssertAndNormaliseAssetName(assetName);
+ this.Cache[assetName] = value;
+
+ }
+
+ /// <summary>Get a copy of the given asset if supported.</summary>
+ /// <typeparam name="T">The asset type.</typeparam>
+ /// <param name="asset">The asset to clone.</param>
+ public T CloneIfPossible<T>(T asset)
+ {
+ switch (asset 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<string, string> source:
+ return (T)(object)new Dictionary<string, string>(source);
+
+ case Dictionary<int, string> source:
+ return (T)(object)new Dictionary<int, string>(source);
+
+ default:
+ return asset;
+ }
+ }
+
+ /// <summary>Normalise path separators in a file path. For asset keys, see <see cref="AssertAndNormaliseAssetName"/> instead.</summary>
+ /// <param name="path">The file path to normalise.</param>
+ [Pure]
+ public string NormalisePathSeparators(string path)
+ {
+ return this.Cache.NormalisePathSeparators(path);
+ }
+
+ /// <summary>Assert that the given key has a valid format and return a normalised form consistent with the underlying cache.</summary>
+ /// <param name="assetName">The asset key to check.</param>
+ /// <exception cref="SContentLoadException">The asset key is empty or contains invalid characters.</exception>
+ [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
+ ****/
+ /// <summary>Get the current content locale.</summary>
+ public string GetLocale()
+ {
+ return this.GetLocale(this.GetCurrentLanguage());
+ }
+
+ /// <summary>The locale for a language.</summary>
+ /// <param name="language">The language.</param>
+ public string GetLocale(LanguageCode language)
+ {
+ return this.LanguageCodeString(language);
+ }
+
+ /// <summary>Get whether the content manager has already loaded and cached the given asset.</summary>
+ /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
+ public bool IsLoaded(string assetName)
+ {
+ assetName = this.Cache.NormaliseKey(assetName);
+ return this.IsNormalisedKeyLoaded(assetName);
+ }
+
+ /// <summary>Get the cached asset keys.</summary>
+ public IEnumerable<string> GetAssetKeys()
+ {
+ return this.Cache.Keys
+ .Select(this.GetAssetName)
+ .Distinct();
+ }
+
+ /****
+ ** Cache invalidation
+ ****/
+ /// <summary>Purge matched assets from the cache.</summary>
+ /// <param name="predicate">Matches the asset keys to invalidate.</param>
+ /// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param>
+ /// <returns>Returns the number of invalidated assets.</returns>
+ public IEnumerable<string> InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false)
+ {
+ HashSet<string> removeAssetNames = new HashSet<string>(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;
+ }
+
+ /// <summary>Dispose held resources.</summary>
+ /// <param name="isDisposing">Whether the content manager is being disposed (rather than finalized).</param>
+ protected override void Dispose(bool isDisposing)
+ {
+ if (this.IsDisposed)
+ return;
+ this.IsDisposed = true;
+
+ this.OnDisposing(this);
+ base.Dispose(isDisposing);
+ }
+
+ /// <inheritdoc />
+ 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
+ *********/
+ /// <summary>Get the locale codes (like <c>ja-JP</c>) used in asset keys.</summary>
+ private IDictionary<LanguageCode, string> GetKeyLocales()
+ {
+ // create locale => code map
+ IDictionary<LanguageCode, string> map = new Dictionary<LanguageCode, string>();
+ foreach (LanguageCode code in Enum.GetValues(typeof(LanguageCode)))
+ map[code] = this.GetLocale(code);
+
+ return map;
+ }
+
+ /// <summary>Get the asset name from a cache key.</summary>
+ /// <param name="cacheKey">The input cache key.</param>
+ private string GetAssetName(string cacheKey)
+ {
+ this.ParseCacheKey(cacheKey, out string assetName, out string _);
+ return assetName;
+ }
+
+ /// <summary>Parse a cache key into its component parts.</summary>
+ /// <param name="cacheKey">The input cache key.</param>
+ /// <param name="assetName">The original asset name.</param>
+ /// <param name="localeCode">The asset locale code (or <c>null</c> if not localised).</param>
+ 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;
+ }
+
+ /// <summary>Get whether an asset has already been loaded.</summary>
+ /// <param name="normalisedAssetName">The normalised asset name.</param>
+ 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..a53840bc
--- /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
+{
+ /// <summary>A content manager which handles reading files from the game content folder with support for interception.</summary>
+ internal class GameContentManager : BaseContentManager
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The assets currently being intercepted by <see cref="IAssetLoader"/> instances. This is used to prevent infinite loops when a loader loads a new asset.</summary>
+ private readonly ContextHash<string> AssetsBeingLoaded = new ContextHash<string>();
+
+ /// <summary>Interceptors which provide the initial versions of matching assets.</summary>
+ private IDictionary<IModMetadata, IList<IAssetLoader>> Loaders => this.Coordinator.Loaders;
+
+ /// <summary>Interceptors which edit matching assets after they're loaded.</summary>
+ private IDictionary<IModMetadata, IList<IAssetEditor>> Editors => this.Coordinator.Editors;
+
+ /// <summary>A lookup which indicates whether the asset is localisable (i.e. the filename contains the locale), if previously loaded.</summary>
+ private readonly IDictionary<string, bool> IsLocalisableLookup;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="name">A name for the mod manager. Not guaranteed to be unique.</param>
+ /// <param name="serviceProvider">The service provider to use to locate services.</param>
+ /// <param name="rootDirectory">The root directory to search for content.</param>
+ /// <param name="currentCulture">The current culture for which to localise content.</param>
+ /// <param name="coordinator">The central coordinator which manages content managers.</param>
+ /// <param name="monitor">Encapsulates monitoring and logging.</param>
+ /// <param name="reflection">Simplifies access to private code.</param>
+ /// <param name="onDisposing">A callback to invoke when the content manager is being disposed.</param>
+ public GameContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action<BaseContentManager> onDisposing)
+ : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isModFolder: false)
+ {
+ this.IsLocalisableLookup = reflection.GetField<IDictionary<string, bool>>(this, "_localizedAsset").GetValue();
+ }
+
+ /// <summary>Load an asset that has been processed by the content pipeline.</summary>
+ /// <typeparam name="T">The type of asset to load.</typeparam>
+ /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
+ /// <param name="language">The language code for which to load content.</param>
+ public override T Load<T>(string assetName, LanguageCode language)
+ {
+ assetName = this.AssertAndNormaliseAssetName(assetName);
+
+ // get from cache
+ if (this.IsLoaded(assetName))
+ return base.Load<T>(assetName, language);
+
+ // get managed asset
+ if (this.Coordinator.TryParseManagedAssetKey(assetName, out string contentManagerID, out string relativePath))
+ {
+ T managedAsset = this.Coordinator.LoadAndCloneManagedAsset<T>(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<T>(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<T>(info)
+ ?? new AssetDataForObject(info, base.Load<T>(assetName, language), this.AssertAndNormaliseAssetName);
+ asset = this.ApplyEditors<T>(info, asset);
+ return (T)asset.Data;
+ });
+ }
+
+ // update cache & return data
+ this.Inject(assetName, data);
+ return data;
+ }
+
+ /// <summary>Create a new content manager for temporary use.</summary>
+ public override LocalizedContentManager CreateTemporary()
+ {
+ return this.Coordinator.CreateGameContentManager("(temporary)");
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get whether an asset has already been loaded.</summary>
+ /// <param name="normalisedAssetName">The normalised asset name.</param>
+ 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;
+ }
+
+ /// <summary>Load the initial asset from the registered <see cref="Loaders"/>.</summary>
+ /// <param name="info">The basic asset metadata.</param>
+ /// <returns>Returns the loaded asset metadata, or <c>null</c> if no loader matched.</returns>
+ private IAssetData ApplyLoader<T>(IAssetInfo info)
+ {
+ // find matching loaders
+ var loaders = this.GetInterceptors(this.Loaders)
+ .Where(entry =>
+ {
+ try
+ {
+ return entry.Value.CanLoad<T>(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 = this.CloneIfPossible(loader.Load<T>(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);
+ }
+
+ /// <summary>Apply any <see cref="Editors"/> to a loaded asset.</summary>
+ /// <typeparam name="T">The asset type.</typeparam>
+ /// <param name="info">The basic asset metadata.</param>
+ /// <param name="asset">The loaded asset.</param>
+ private IAssetData ApplyEditors<T>(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<T>(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<T>(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;
+ }
+
+ /// <summary>Get all registered interceptors from a list.</summary>
+ private IEnumerable<KeyValuePair<IModMetadata, T>> GetInterceptors<T>(IDictionary<IModMetadata, IList<T>> entries)
+ {
+ foreach (var entry in entries)
+ {
+ IModMetadata mod = entry.Key;
+ IList<T> interceptors = entry.Value;
+
+ // registered editors
+ foreach (T interceptor in interceptors)
+ yield return new KeyValuePair<IModMetadata, T>(mod, interceptor);
+ }
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/ContentManagers/IContentManager.cs b/src/SMAPI/Framework/ContentManagers/IContentManager.cs
new file mode 100644
index 00000000..1eb8b0ac
--- /dev/null
+++ b/src/SMAPI/Framework/ContentManagers/IContentManager.cs
@@ -0,0 +1,86 @@
+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
+{
+ /// <summary>A content manager which handles reading files.</summary>
+ internal interface IContentManager : IDisposable
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>A name for the mod manager. Not guaranteed to be unique.</summary>
+ string Name { get; }
+
+ /// <summary>The current language as a constant.</summary>
+ LocalizedContentManager.LanguageCode Language { get; }
+
+ /// <summary>The absolute path to the <see cref="ContentManager.RootDirectory"/>.</summary>
+ string FullRootDirectory { get; }
+
+ /// <summary>Whether this content manager is for a mod folder.</summary>
+ bool IsModContentManager { get; }
+
+
+ /*********
+ ** Methods
+ *********/
+ /// <summary>Load an asset that has been processed by the content pipeline.</summary>
+ /// <typeparam name="T">The type of asset to load.</typeparam>
+ /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
+ T Load<T>(string assetName);
+
+ /// <summary>Load an asset that has been processed by the content pipeline.</summary>
+ /// <typeparam name="T">The type of asset to load.</typeparam>
+ /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
+ /// <param name="language">The language code for which to load content.</param>
+ T Load<T>(string assetName, LocalizedContentManager.LanguageCode language);
+
+ /// <summary>Inject an asset into the cache.</summary>
+ /// <typeparam name="T">The type of asset to inject.</typeparam>
+ /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
+ /// <param name="value">The asset value.</param>
+ void Inject<T>(string assetName, T value);
+
+ /// <summary>Get a copy of the given asset if supported.</summary>
+ /// <typeparam name="T">The asset type.</typeparam>
+ /// <param name="asset">The asset to clone.</param>
+ T CloneIfPossible<T>(T asset);
+
+ /// <summary>Normalise path separators in a file path. For asset keys, see <see cref="AssertAndNormaliseAssetName"/> instead.</summary>
+ /// <param name="path">The file path to normalise.</param>
+ [Pure]
+ string NormalisePathSeparators(string path);
+
+ /// <summary>Assert that the given key has a valid format and return a normalised form consistent with the underlying cache.</summary>
+ /// <param name="assetName">The asset key to check.</param>
+ /// <exception cref="SContentLoadException">The asset key is empty or contains invalid characters.</exception>
+ [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")]
+ string AssertAndNormaliseAssetName(string assetName);
+
+ /// <summary>Get the current content locale.</summary>
+ string GetLocale();
+
+ /// <summary>The locale for a language.</summary>
+ /// <param name="language">The language.</param>
+ string GetLocale(LocalizedContentManager.LanguageCode language);
+
+ /// <summary>Get whether the content manager has already loaded and cached the given asset.</summary>
+ /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
+ bool IsLoaded(string assetName);
+
+ /// <summary>Get the cached asset keys.</summary>
+ IEnumerable<string> GetAssetKeys();
+
+ /// <summary>Purge matched assets from the cache.</summary>
+ /// <param name="predicate">Matches the asset keys to invalidate.</param>
+ /// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param>
+ /// <returns>Returns the number of invalidated assets.</returns>
+ IEnumerable<string> InvalidateCache(Func<string, Type, bool> 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
+{
+ /// <summary>A content manager which handles reading files from a SMAPI mod folder with support for unpacked files.</summary>
+ internal class ModContentManager : BaseContentManager
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="name">A name for the mod manager. Not guaranteed to be unique.</param>
+ /// <param name="serviceProvider">The service provider to use to locate services.</param>
+ /// <param name="rootDirectory">The root directory to search for content.</param>
+ /// <param name="currentCulture">The current culture for which to localise content.</param>
+ /// <param name="coordinator">The central coordinator which manages content managers.</param>
+ /// <param name="monitor">Encapsulates monitoring and logging.</param>
+ /// <param name="reflection">Simplifies access to private code.</param>
+ /// <param name="onDisposing">A callback to invoke when the content manager is being disposed.</param>
+ public ModContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action<BaseContentManager> onDisposing)
+ : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isModFolder: true) { }
+
+ /// <summary>Load an asset that has been processed by the content pipeline.</summary>
+ /// <typeparam name="T">The type of asset to load.</typeparam>
+ /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
+ /// <param name="language">The language code for which to load content.</param>
+ public override T Load<T>(string assetName, LanguageCode language)
+ {
+ assetName = this.AssertAndNormaliseAssetName(assetName);
+
+ // get from cache
+ if (this.IsLoaded(assetName))
+ return base.Load<T>(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<T>(assetName, contentManagerID, relativePath, language);
+ this.Inject(assetName, data);
+ return data;
+ }
+
+ return this.LoadManagedAsset<T>(assetName, contentManagerID, relativePath, language);
+ }
+
+ throw new NotSupportedException("Can't load content folder asset from a mod content manager.");
+ }
+
+ /// <summary>Create a new content manager for temporary use.</summary>
+ public override LocalizedContentManager CreateTemporary()
+ {
+ throw new NotSupportedException("Can't create a temporary mod content manager.");
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get whether an asset has already been loaded.</summary>
+ /// <param name="normalisedAssetName">The normalised asset name.</param>
+ protected override bool IsNormalisedKeyLoaded(string normalisedAssetName)
+ {
+ return this.Cache.ContainsKey(normalisedAssetName);
+ }
+
+ /// <summary>Load a managed mod asset.</summary>
+ /// <typeparam name="T">The type of asset to load.</typeparam>
+ /// <param name="internalKey">The internal asset key.</param>
+ /// <param name="contentManagerID">The unique name for the content manager which should load this asset.</param>
+ /// <param name="relativePath">The relative path within the mod folder.</param>
+ /// <param name="language">The language code for which to load content.</param>
+ private T LoadManagedAsset<T>(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<T>(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);
+ }
+ }
+
+ /// <summary>Get a file from the mod folder.</summary>
+ /// <param name="path">The asset path relative to the content folder.</param>
+ 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;
+ }
+
+ /// <summary>Premultiply a texture's alpha values to avoid transparency issues in the game. This is only possible if the game isn't currently drawing.</summary>
+ /// <param name="texture">The texture to premultiply.</param>
+ /// <returns>Returns a premultiplied texture.</returns>
+ /// <remarks>Based on <a href="https://gist.github.com/Layoric/6255384">code by Layoric</a>.</remarks>
+ 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/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs
index 071fb872..4a4adb90 100644
--- a/src/SMAPI/Framework/ContentPack.cs
+++ b/src/SMAPI/Framework/ContentPack.cs
@@ -2,8 +2,8 @@ using System;
using System.IO;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
-using StardewModdingAPI.Framework.Serialisation;
-using StardewModdingAPI.Framework.Utilities;
+using StardewModdingAPI.Toolkit.Serialisation;
+using StardewModdingAPI.Toolkit.Utilities;
using xTile;
namespace StardewModdingAPI.Framework
diff --git a/src/SMAPI/Framework/CursorPosition.cs b/src/SMAPI/Framework/CursorPosition.cs
index db02b3d1..079917f2 100644
--- a/src/SMAPI/Framework/CursorPosition.cs
+++ b/src/SMAPI/Framework/CursorPosition.cs
@@ -8,6 +8,9 @@ namespace StardewModdingAPI.Framework
/*********
** Accessors
*********/
+ /// <summary>The pixel position relative to the top-left corner of the in-game map.</summary>
+ public Vector2 AbsolutePixels { get; }
+
/// <summary>The pixel position relative to the top-left corner of the visible screen.</summary>
public Vector2 ScreenPixels { get; }
@@ -22,14 +25,23 @@ namespace StardewModdingAPI.Framework
** Public methods
*********/
/// <summary>Construct an instance.</summary>
+ /// <param name="absolutePixels">The pixel position relative to the top-left corner of the in-game map.</param>
/// <param name="screenPixels">The pixel position relative to the top-left corner of the visible screen.</param>
/// <param name="tile">The tile position relative to the top-left corner of the map.</param>
/// <param name="grabTile">The tile position that the game considers under the cursor for purposes of clicking actions.</param>
- public CursorPosition(Vector2 screenPixels, Vector2 tile, Vector2 grabTile)
+ public CursorPosition(Vector2 absolutePixels, Vector2 screenPixels, Vector2 tile, Vector2 grabTile)
{
+ this.AbsolutePixels = absolutePixels;
this.ScreenPixels = screenPixels;
this.Tile = tile;
this.GrabTile = grabTile;
}
+
+ /// <summary>Get whether the current object is equal to another object of the same type.</summary>
+ /// <param name="other">An object to compare with this object.</param>
+ public bool Equals(ICursorPosition other)
+ {
+ return other != null && this.AbsolutePixels == other.AbsolutePixels;
+ }
}
}
diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs
index d7c89a76..168ddde0 100644
--- a/src/SMAPI/Framework/Events/EventManager.cs
+++ b/src/SMAPI/Framework/Events/EventManager.cs
@@ -9,7 +9,62 @@ namespace StardewModdingAPI.Framework.Events
internal class EventManager
{
/*********
- ** Properties
+ ** Events (new)
+ *********/
+ /****
+ ** Game loop
+ ****/
+ /// <summary>Raised after the game is launched, right before the first update tick.</summary>
+ public readonly ManagedEvent<GameLoopLaunchedEventArgs> GameLoop_Launched;
+
+ /// <summary>Raised before the game performs its overall update tick (≈60 times per second).</summary>
+ public readonly ManagedEvent<GameLoopUpdatingEventArgs> GameLoop_Updating;
+
+ /// <summary>Raised after the game performs its overall update tick (≈60 times per second).</summary>
+ public readonly ManagedEvent<GameLoopUpdatedEventArgs> GameLoop_Updated;
+
+ /****
+ ** Input
+ ****/
+ /// <summary>Raised after the player presses a button on the keyboard, controller, or mouse.</summary>
+ public readonly ManagedEvent<InputButtonPressedEventArgs> Input_ButtonPressed;
+
+ /// <summary>Raised after the player released a button on the keyboard, controller, or mouse.</summary>
+ public readonly ManagedEvent<InputButtonReleasedEventArgs> Input_ButtonReleased;
+
+ /// <summary>Raised after the player moves the in-game cursor.</summary>
+ public readonly ManagedEvent<InputCursorMovedEventArgs> Input_CursorMoved;
+
+ /// <summary>Raised after the player scrolls the mouse wheel.</summary>
+ public readonly ManagedEvent<InputMouseWheelScrolledEventArgs> Input_MouseWheelScrolled;
+
+ /****
+ ** World
+ ****/
+ /// <summary>Raised after a game location is added or removed.</summary>
+ public readonly ManagedEvent<WorldLocationListChangedEventArgs> World_LocationListChanged;
+
+ /// <summary>Raised after buildings are added or removed in a location.</summary>
+ public readonly ManagedEvent<WorldBuildingListChangedEventArgs> World_BuildingListChanged;
+
+ /// <summary>Raised after debris are added or removed in a location.</summary>
+ public readonly ManagedEvent<WorldDebrisListChangedEventArgs> World_DebrisListChanged;
+
+ /// <summary>Raised after large terrain features (like bushes) are added or removed in a location.</summary>
+ public readonly ManagedEvent<WorldLargeTerrainFeatureListChangedEventArgs> World_LargeTerrainFeatureListChanged;
+
+ /// <summary>Raised after NPCs are added or removed in a location.</summary>
+ public readonly ManagedEvent<WorldNpcListChangedEventArgs> World_NpcListChanged;
+
+ /// <summary>Raised after objects are added or removed in a location.</summary>
+ public readonly ManagedEvent<WorldObjectListChangedEventArgs> World_ObjectListChanged;
+
+ /// <summary>Raised after terrain features (like floors and trees) are added or removed in a location.</summary>
+ public readonly ManagedEvent<WorldTerrainFeatureListChangedEventArgs> World_TerrainFeatureListChanged;
+
+
+ /*********
+ ** Events (old)
*********/
/****
** ContentEvents
@@ -21,28 +76,28 @@ namespace StardewModdingAPI.Framework.Events
** ControlEvents
****/
/// <summary>Raised when the <see cref="KeyboardState"/> changes. That happens when the player presses or releases a key.</summary>
- public readonly ManagedEvent<EventArgsKeyboardStateChanged> Control_KeyboardChanged;
+ public readonly ManagedEvent<EventArgsKeyboardStateChanged> Legacy_Control_KeyboardChanged;
- /// <summary>Raised when the player presses a keyboard key.</summary>
- public readonly ManagedEvent<EventArgsKeyPressed> Control_KeyPressed;
+ /// <summary>Raised after the player presses a keyboard key.</summary>
+ public readonly ManagedEvent<EventArgsKeyPressed> Legacy_Control_KeyPressed;
- /// <summary>Raised when the player releases a keyboard key.</summary>
- public readonly ManagedEvent<EventArgsKeyPressed> Control_KeyReleased;
+ /// <summary>Raised after the player releases a keyboard key.</summary>
+ public readonly ManagedEvent<EventArgsKeyPressed> Legacy_Control_KeyReleased;
/// <summary>Raised when the <see cref="MouseState"/> changes. That happens when the player moves the mouse, scrolls the mouse wheel, or presses/releases a button.</summary>
- public readonly ManagedEvent<EventArgsMouseStateChanged> Control_MouseChanged;
+ public readonly ManagedEvent<EventArgsMouseStateChanged> Legacy_Control_MouseChanged;
/// <summary>The player pressed a controller button. This event isn't raised for trigger buttons.</summary>
- public readonly ManagedEvent<EventArgsControllerButtonPressed> Control_ControllerButtonPressed;
+ public readonly ManagedEvent<EventArgsControllerButtonPressed> Legacy_Control_ControllerButtonPressed;
/// <summary>The player released a controller button. This event isn't raised for trigger buttons.</summary>
- public readonly ManagedEvent<EventArgsControllerButtonReleased> Control_ControllerButtonReleased;
+ public readonly ManagedEvent<EventArgsControllerButtonReleased> Legacy_Control_ControllerButtonReleased;
/// <summary>The player pressed a controller trigger button.</summary>
- public readonly ManagedEvent<EventArgsControllerTriggerPressed> Control_ControllerTriggerPressed;
+ public readonly ManagedEvent<EventArgsControllerTriggerPressed> Legacy_Control_ControllerTriggerPressed;
/// <summary>The player released a controller trigger button.</summary>
- public readonly ManagedEvent<EventArgsControllerTriggerReleased> Control_ControllerTriggerReleased;
+ public readonly ManagedEvent<EventArgsControllerTriggerReleased> Legacy_Control_ControllerTriggerReleased;
/****
** GameEvents
@@ -98,23 +153,23 @@ namespace StardewModdingAPI.Framework.Events
/****
** InputEvents
****/
- /// <summary>Raised when the player presses a button on the keyboard, controller, or mouse.</summary>
- public readonly ManagedEvent<EventArgsInput> Input_ButtonPressed;
+ /// <summary>Raised after the player presses a button on the keyboard, controller, or mouse.</summary>
+ public readonly ManagedEvent<EventArgsInput> Legacy_Input_ButtonPressed;
- /// <summary>Raised when the player releases a keyboard key on the keyboard, controller, or mouse.</summary>
- public readonly ManagedEvent<EventArgsInput> Input_ButtonReleased;
+ /// <summary>Raised after the player releases a keyboard key on the keyboard, controller, or mouse.</summary>
+ public readonly ManagedEvent<EventArgsInput> Legacy_Input_ButtonReleased;
/****
** LocationEvents
****/
- /// <summary>Raised after the player warps to a new location.</summary>
- public readonly ManagedEvent<EventArgsCurrentLocationChanged> Location_CurrentLocationChanged;
-
/// <summary>Raised after a game location is added or removed.</summary>
- public readonly ManagedEvent<EventArgsGameLocationsChanged> Location_LocationsChanged;
+ public readonly ManagedEvent<EventArgsLocationsChanged> Legacy_Location_LocationsChanged;
- /// <summary>Raised after the list of objects in the current location changes (e.g. an object is added or removed).</summary>
- public readonly ManagedEvent<EventArgsLocationObjectsChanged> Location_LocationObjectsChanged;
+ /// <summary>Raised after buildings are added or removed in a location.</summary>
+ public readonly ManagedEvent<EventArgsLocationBuildingsChanged> Legacy_Location_BuildingsChanged;
+
+ /// <summary>Raised after objects are added or removed in a location.</summary>
+ public readonly ManagedEvent<EventArgsLocationObjectsChanged> Legacy_Location_ObjectsChanged;
/****
** MenuEvents
@@ -126,6 +181,21 @@ namespace StardewModdingAPI.Framework.Events
public readonly ManagedEvent<EventArgsClickableMenuClosed> Menu_Closed;
/****
+ ** MultiplayerEvents
+ ****/
+ /// <summary>Raised before the game syncs changes from other players.</summary>
+ public readonly ManagedEvent Multiplayer_BeforeMainSync;
+
+ /// <summary>Raised after the game syncs changes from other players.</summary>
+ public readonly ManagedEvent Multiplayer_AfterMainSync;
+
+ /// <summary>Raised before the game broadcasts changes to other players.</summary>
+ public readonly ManagedEvent Multiplayer_BeforeMainBroadcast;
+
+ /// <summary>Raised after the game broadcasts changes to other players.</summary>
+ public readonly ManagedEvent Multiplayer_AfterMainBroadcast;
+
+ /****
** MineEvents
****/
/// <summary>Raised after the player warps to a new level of the mine.</summary>
@@ -140,6 +210,10 @@ namespace StardewModdingAPI.Framework.Events
/// <summary> 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.</summary>
public readonly ManagedEvent<EventArgsLevelUp> Player_LeveledUp;
+ /// <summary>Raised after the player warps to a new location.</summary>
+ public readonly ManagedEvent<EventArgsPlayerWarped> Player_Warped;
+
+
/****
** SaveEvents
****/
@@ -189,17 +263,35 @@ namespace StardewModdingAPI.Framework.Events
ManagedEvent<TEventArgs> ManageEventOf<TEventArgs>(string typeName, string eventName) => new ManagedEvent<TEventArgs>($"{typeName}.{eventName}", monitor, modRegistry);
ManagedEvent ManageEvent(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry);
- // init events
+ // init events (new)
+ this.GameLoop_Launched = ManageEventOf<GameLoopLaunchedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Launched));
+ this.GameLoop_Updating = ManageEventOf<GameLoopUpdatingEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Updating));
+ this.GameLoop_Updated = ManageEventOf<GameLoopUpdatedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Updated));
+
+ this.Input_ButtonPressed = ManageEventOf<InputButtonPressedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed));
+ this.Input_ButtonReleased = ManageEventOf<InputButtonReleasedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased));
+ this.Input_CursorMoved = ManageEventOf<InputCursorMovedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved));
+ this.Input_MouseWheelScrolled = ManageEventOf<InputMouseWheelScrolledEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled));
+
+ this.World_BuildingListChanged = ManageEventOf<WorldBuildingListChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.LocationListChanged));
+ this.World_DebrisListChanged = ManageEventOf<WorldDebrisListChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.DebrisListChanged));
+ this.World_LargeTerrainFeatureListChanged = ManageEventOf<WorldLargeTerrainFeatureListChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.LargeTerrainFeatureListChanged));
+ this.World_LocationListChanged = ManageEventOf<WorldLocationListChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.BuildingListChanged));
+ this.World_NpcListChanged = ManageEventOf<WorldNpcListChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.NpcListChanged));
+ this.World_ObjectListChanged = ManageEventOf<WorldObjectListChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.ObjectListChanged));
+ this.World_TerrainFeatureListChanged = ManageEventOf<WorldTerrainFeatureListChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.TerrainFeatureListChanged));
+
+ // init events (old)
this.Content_LocaleChanged = ManageEventOf<EventArgsValueChanged<string>>(nameof(ContentEvents), nameof(ContentEvents.AfterLocaleChanged));
- this.Control_ControllerButtonPressed = ManageEventOf<EventArgsControllerButtonPressed>(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonPressed));
- this.Control_ControllerButtonReleased = ManageEventOf<EventArgsControllerButtonReleased>(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonReleased));
- this.Control_ControllerTriggerPressed = ManageEventOf<EventArgsControllerTriggerPressed>(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerPressed));
- this.Control_ControllerTriggerReleased = ManageEventOf<EventArgsControllerTriggerReleased>(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerReleased));
- this.Control_KeyboardChanged = ManageEventOf<EventArgsKeyboardStateChanged>(nameof(ControlEvents), nameof(ControlEvents.KeyboardChanged));
- this.Control_KeyPressed = ManageEventOf<EventArgsKeyPressed>(nameof(ControlEvents), nameof(ControlEvents.KeyPressed));
- this.Control_KeyReleased = ManageEventOf<EventArgsKeyPressed>(nameof(ControlEvents), nameof(ControlEvents.KeyReleased));
- this.Control_MouseChanged = ManageEventOf<EventArgsMouseStateChanged>(nameof(ControlEvents), nameof(ControlEvents.MouseChanged));
+ this.Legacy_Control_ControllerButtonPressed = ManageEventOf<EventArgsControllerButtonPressed>(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonPressed));
+ this.Legacy_Control_ControllerButtonReleased = ManageEventOf<EventArgsControllerButtonReleased>(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonReleased));
+ this.Legacy_Control_ControllerTriggerPressed = ManageEventOf<EventArgsControllerTriggerPressed>(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerPressed));
+ this.Legacy_Control_ControllerTriggerReleased = ManageEventOf<EventArgsControllerTriggerReleased>(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerReleased));
+ this.Legacy_Control_KeyboardChanged = ManageEventOf<EventArgsKeyboardStateChanged>(nameof(ControlEvents), nameof(ControlEvents.KeyboardChanged));
+ this.Legacy_Control_KeyPressed = ManageEventOf<EventArgsKeyPressed>(nameof(ControlEvents), nameof(ControlEvents.KeyPressed));
+ this.Legacy_Control_KeyReleased = ManageEventOf<EventArgsKeyPressed>(nameof(ControlEvents), nameof(ControlEvents.KeyReleased));
+ this.Legacy_Control_MouseChanged = ManageEventOf<EventArgsMouseStateChanged>(nameof(ControlEvents), nameof(ControlEvents.MouseChanged));
this.Game_FirstUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.FirstUpdateTick));
this.Game_UpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.UpdateTick));
@@ -218,20 +310,26 @@ 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<EventArgsInput>(nameof(InputEvents), nameof(InputEvents.ButtonPressed));
- this.Input_ButtonReleased = ManageEventOf<EventArgsInput>(nameof(InputEvents), nameof(InputEvents.ButtonReleased));
+ this.Legacy_Input_ButtonPressed = ManageEventOf<EventArgsInput>(nameof(InputEvents), nameof(InputEvents.ButtonPressed));
+ this.Legacy_Input_ButtonReleased = ManageEventOf<EventArgsInput>(nameof(InputEvents), nameof(InputEvents.ButtonReleased));
- this.Location_CurrentLocationChanged = ManageEventOf<EventArgsCurrentLocationChanged>(nameof(LocationEvents), nameof(LocationEvents.CurrentLocationChanged));
- this.Location_LocationsChanged = ManageEventOf<EventArgsGameLocationsChanged>(nameof(LocationEvents), nameof(LocationEvents.LocationsChanged));
- this.Location_LocationObjectsChanged = ManageEventOf<EventArgsLocationObjectsChanged>(nameof(LocationEvents), nameof(LocationEvents.LocationObjectsChanged));
+ this.Legacy_Location_LocationsChanged = ManageEventOf<EventArgsLocationsChanged>(nameof(LocationEvents), nameof(LocationEvents.LocationsChanged));
+ this.Legacy_Location_BuildingsChanged = ManageEventOf<EventArgsLocationBuildingsChanged>(nameof(LocationEvents), nameof(LocationEvents.BuildingsChanged));
+ this.Legacy_Location_ObjectsChanged = ManageEventOf<EventArgsLocationObjectsChanged>(nameof(LocationEvents), nameof(LocationEvents.ObjectsChanged));
this.Menu_Changed = ManageEventOf<EventArgsClickableMenuChanged>(nameof(MenuEvents), nameof(MenuEvents.MenuChanged));
this.Menu_Closed = ManageEventOf<EventArgsClickableMenuClosed>(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<EventArgsMineLevelChanged>(nameof(MineEvents), nameof(MineEvents.MineLevelChanged));
this.Player_InventoryChanged = ManageEventOf<EventArgsInventoryChanged>(nameof(PlayerEvents), nameof(PlayerEvents.InventoryChanged));
this.Player_LeveledUp = ManageEventOf<EventArgsLevelUp>(nameof(PlayerEvents), nameof(PlayerEvents.LeveledUp));
+ this.Player_Warped = ManageEventOf<EventArgsPlayerWarped>(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/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
@@ -28,8 +28,16 @@ namespace StardewModdingAPI.Framework.Events
/// <param name="handler">The event handler.</param>
public void Add(EventHandler<TEventArgs> handler)
{
+ this.Add(handler, this.ModRegistry.GetFromStack());
+ }
+
+ /// <summary>Add an event handler.</summary>
+ /// <param name="handler">The event handler.</param>
+ /// <param name="mod">The mod which added the event handler.</param>
+ public void Add(EventHandler<TEventArgs> handler, IModMetadata mod)
+ {
this.Event += handler;
- this.AddTracking(handler, this.Event?.GetInvocationList().Cast<EventHandler<TEventArgs>>());
+ this.AddTracking(mod, handler, this.Event?.GetInvocationList().Cast<EventHandler<TEventArgs>>());
}
/// <summary>Remove an event handler.</summary>
@@ -85,8 +93,16 @@ namespace StardewModdingAPI.Framework.Events
/// <param name="handler">The event handler.</param>
public void Add(EventHandler handler)
{
+ this.Add(handler, this.ModRegistry.GetFromStack());
+ }
+
+ /// <summary>Add an event handler.</summary>
+ /// <param name="handler">The event handler.</param>
+ /// <param name="mod">The mod which added the event handler.</param>
+ public void Add(EventHandler handler, IModMetadata mod)
+ {
this.Event += handler;
- this.AddTracking(handler, this.Event?.GetInvocationList().Cast<EventHandler>());
+ this.AddTracking(mod, handler, this.Event?.GetInvocationList().Cast<EventHandler>());
}
/// <summary>Remove an event handler.</summary>
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;
/// <summary>The mod registry with which to identify mods.</summary>
- private readonly ModRegistry ModRegistry;
+ protected readonly ModRegistry ModRegistry;
/// <summary>The display names for the mods which added each delegate.</summary>
private readonly IDictionary<TEventHandler, IModMetadata> SourceMods = new Dictionary<TEventHandler, IModMetadata>();
@@ -50,11 +50,12 @@ namespace StardewModdingAPI.Framework.Events
}
/// <summary>Track an event handler.</summary>
+ /// <param name="mod">The mod which added the handler.</param>
/// <param name="handler">The event handler.</param>
/// <param name="invocationList">The updated event invocation list.</param>
- protected void AddTracking(TEventHandler handler, IEnumerable<TEventHandler> invocationList)
+ protected void AddTracking(IModMetadata mod, TEventHandler handler, IEnumerable<TEventHandler> 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<TEventHandler> 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..9e474457
--- /dev/null
+++ b/src/SMAPI/Framework/Events/ModEvents.cs
@@ -0,0 +1,34 @@
+using StardewModdingAPI.Events;
+
+namespace StardewModdingAPI.Framework.Events
+{
+ /// <summary>Manages access to events raised by SMAPI.</summary>
+ internal class ModEvents : IModEvents
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>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 <see cref="IModEvents.Input"/> if possible.</summary>
+ public IGameLoopEvents GameLoop { get; }
+
+ /// <summary>Events raised when the player provides input using a controller, keyboard, or mouse.</summary>
+ public IInputEvents Input { get; }
+
+ /// <summary>Events raised when something changes in the world.</summary>
+ public IWorldEvents World { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="mod">The mod which uses this instance.</param>
+ /// <param name="eventManager">The underlying event manager.</param>
+ 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/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
+{
+ /// <summary>An internal base class for event API classes.</summary>
+ internal abstract class ModEventsBase
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The underlying event manager.</summary>
+ protected readonly EventManager EventManager;
+
+ /// <summary>The mod which uses this instance.</summary>
+ protected readonly IModMetadata Mod;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="mod">The mod which uses this instance.</param>
+ /// <param name="eventManager">The underlying event manager.</param>
+ internal ModEventsBase(IModMetadata mod, EventManager eventManager)
+ {
+ this.Mod = mod;
+ this.EventManager = eventManager;
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs
new file mode 100644
index 00000000..379a4e96
--- /dev/null
+++ b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs
@@ -0,0 +1,43 @@
+using System;
+using StardewModdingAPI.Events;
+
+namespace StardewModdingAPI.Framework.Events
+{
+ /// <summary>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 <see cref="IInputEvents"/> if possible.</summary>
+ internal class ModGameLoopEvents : ModEventsBase, IGameLoopEvents
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Raised after the game is launched, right before the first update tick.</summary>
+ public event EventHandler<GameLoopLaunchedEventArgs> Launched
+ {
+ add => this.EventManager.GameLoop_Launched.Add(value);
+ remove => this.EventManager.GameLoop_Launched.Remove(value);
+ }
+
+ /// <summary>Raised before the game performs its overall update tick (≈60 times per second).</summary>
+ public event EventHandler<GameLoopUpdatingEventArgs> Updating
+ {
+ add => this.EventManager.GameLoop_Updating.Add(value);
+ remove => this.EventManager.GameLoop_Updating.Remove(value);
+ }
+
+ /// <summary>Raised after the game performs its overall update tick (≈60 times per second).</summary>
+ public event EventHandler<GameLoopUpdatedEventArgs> Updated
+ {
+ add => this.EventManager.GameLoop_Updated.Add(value);
+ remove => this.EventManager.GameLoop_Updated.Remove(value);
+ }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="mod">The mod which uses this instance.</param>
+ /// <param name="eventManager">The underlying event manager.</param>
+ internal ModGameLoopEvents(IModMetadata mod, EventManager eventManager)
+ : base(mod, eventManager) { }
+ }
+}
diff --git a/src/SMAPI/Framework/Events/ModInputEvents.cs b/src/SMAPI/Framework/Events/ModInputEvents.cs
new file mode 100644
index 00000000..feca34f3
--- /dev/null
+++ b/src/SMAPI/Framework/Events/ModInputEvents.cs
@@ -0,0 +1,50 @@
+using System;
+using StardewModdingAPI.Events;
+
+namespace StardewModdingAPI.Framework.Events
+{
+ /// <summary>Events raised when the player provides input using a controller, keyboard, or mouse.</summary>
+ internal class ModInputEvents : ModEventsBase, IInputEvents
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Raised after the player presses a button on the keyboard, controller, or mouse.</summary>
+ public event EventHandler<InputButtonPressedEventArgs> ButtonPressed
+ {
+ add => this.EventManager.Input_ButtonPressed.Add(value);
+ remove => this.EventManager.Input_ButtonPressed.Remove(value);
+ }
+
+ /// <summary>Raised after the player releases a button on the keyboard, controller, or mouse.</summary>
+ public event EventHandler<InputButtonReleasedEventArgs> ButtonReleased
+ {
+ add => this.EventManager.Input_ButtonReleased.Add(value);
+ remove => this.EventManager.Input_ButtonReleased.Remove(value);
+ }
+
+ /// <summary>Raised after the player moves the in-game cursor.</summary>
+ public event EventHandler<InputCursorMovedEventArgs> CursorMoved
+ {
+ add => this.EventManager.Input_CursorMoved.Add(value);
+ remove => this.EventManager.Input_CursorMoved.Remove(value);
+ }
+
+ /// <summary>Raised after the player scrolls the mouse wheel.</summary>
+ public event EventHandler<InputMouseWheelScrolledEventArgs> MouseWheelScrolled
+ {
+ add => this.EventManager.Input_MouseWheelScrolled.Add(value);
+ remove => this.EventManager.Input_MouseWheelScrolled.Remove(value);
+ }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="mod">The mod which uses this instance.</param>
+ /// <param name="eventManager">The underlying event manager.</param>
+ internal ModInputEvents(IModMetadata mod, EventManager eventManager)
+ : base(mod, eventManager) { }
+ }
+}
diff --git a/src/SMAPI/Framework/Events/ModWorldEvents.cs b/src/SMAPI/Framework/Events/ModWorldEvents.cs
new file mode 100644
index 00000000..dc9c0f4c
--- /dev/null
+++ b/src/SMAPI/Framework/Events/ModWorldEvents.cs
@@ -0,0 +1,71 @@
+using System;
+using StardewModdingAPI.Events;
+
+namespace StardewModdingAPI.Framework.Events
+{
+ /// <summary>Events raised when something changes in the world.</summary>
+ internal class ModWorldEvents : ModEventsBase, IWorldEvents
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Raised after a game location is added or removed.</summary>
+ public event EventHandler<WorldLocationListChangedEventArgs> LocationListChanged
+ {
+ add => this.EventManager.World_LocationListChanged.Add(value, this.Mod);
+ remove => this.EventManager.World_LocationListChanged.Remove(value);
+ }
+
+ /// <summary>Raised after buildings are added or removed in a location.</summary>
+ public event EventHandler<WorldBuildingListChangedEventArgs> BuildingListChanged
+ {
+ add => this.EventManager.World_BuildingListChanged.Add(value, this.Mod);
+ remove => this.EventManager.World_BuildingListChanged.Remove(value);
+ }
+
+ /// <summary>Raised after debris are added or removed in a location.</summary>
+ public event EventHandler<WorldDebrisListChangedEventArgs> DebrisListChanged
+ {
+ add => this.EventManager.World_DebrisListChanged.Add(value, this.Mod);
+ remove => this.EventManager.World_DebrisListChanged.Remove(value);
+ }
+
+ /// <summary>Raised after large terrain features (like bushes) are added or removed in a location.</summary>
+ public event EventHandler<WorldLargeTerrainFeatureListChangedEventArgs> LargeTerrainFeatureListChanged
+ {
+ add => this.EventManager.World_LargeTerrainFeatureListChanged.Add(value, this.Mod);
+ remove => this.EventManager.World_LargeTerrainFeatureListChanged.Remove(value);
+ }
+
+ /// <summary>Raised after NPCs are added or removed in a location.</summary>
+ public event EventHandler<WorldNpcListChangedEventArgs> NpcListChanged
+ {
+ add => this.EventManager.World_NpcListChanged.Add(value);
+ remove => this.EventManager.World_NpcListChanged.Remove(value);
+ }
+
+ /// <summary>Raised after objects are added or removed in a location.</summary>
+ public event EventHandler<WorldObjectListChangedEventArgs> ObjectListChanged
+ {
+ add => this.EventManager.World_ObjectListChanged.Add(value);
+ remove => this.EventManager.World_ObjectListChanged.Remove(value);
+ }
+
+ /// <summary>Raised after terrain features (like floors and trees) are added or removed in a location.</summary>
+ public event EventHandler<WorldTerrainFeatureListChangedEventArgs> TerrainFeatureListChanged
+ {
+ add => this.EventManager.World_TerrainFeatureListChanged.Add(value);
+ remove => this.EventManager.World_TerrainFeatureListChanged.Remove(value);
+ }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="mod">The mod which uses this instance.</param>
+ /// <param name="eventManager">The underlying event manager.</param>
+ internal ModWorldEvents(IModMetadata mod, EventManager eventManager)
+ : base(mod, eventManager) { }
+ }
+}
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
-{
- /// <summary>A format exception which provides a user-facing error message.</summary>
- internal class SParseException : FormatException
- {
- /*********
- ** Public methods
- *********/
- /// <summary>Construct an instance.</summary>
- /// <param name="message">The error message.</param>
- /// <param name="ex">The underlying exception, if any.</param>
- public SParseException(string message, Exception ex = null)
- : base(message, ex) { }
- }
-}
diff --git a/src/SMAPI/Framework/GameVersion.cs b/src/SMAPI/Framework/GameVersion.cs
index e5022212..261de374 100644
--- a/src/SMAPI/Framework/GameVersion.cs
+++ b/src/SMAPI/Framework/GameVersion.cs
@@ -49,11 +49,6 @@ namespace StardewModdingAPI.Framework
/// <param name="gameVersion">The game version string.</param>
private static string GetSemanticVersionString(string gameVersion)
{
-#if STARDEW_VALLEY_1_3
- if(gameVersion.StartsWith("1.3.0."))
- return new SemanticVersion(1, 3, 0, "alpha." + gameVersion.Substring("1.3.0.".Length)).ToString();
-#endif
-
return GameVersion.VersionMap.TryGetValue(gameVersion, out string semanticVersion)
? semanticVersion
: gameVersion;
@@ -63,11 +58,6 @@ namespace StardewModdingAPI.Framework
/// <param name="semanticVersion">The semantic version string.</param>
private static string GetGameVersionString(string semanticVersion)
{
- #if STARDEW_VALLEY_1_3
- if(semanticVersion.StartsWith("1.3-alpha."))
- return "1.3.0." + semanticVersion.Substring("1.3-alpha.".Length);
- #endif
-
foreach (var mapping in GameVersion.VersionMap)
{
if (mapping.Value.Equals(semanticVersion, StringComparison.InvariantCultureIgnoreCase))
diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs
index d1e8eb7d..2145105b 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.Toolkit.Framework.Clients.WebApi;
+using StardewModdingAPI.Toolkit.Framework.ModData;
namespace StardewModdingAPI.Framework
{
@@ -19,11 +20,14 @@ namespace StardewModdingAPI.Framework
IManifest Manifest { get; }
/// <summary>Metadata about the mod from SMAPI's internal data (if any).</summary>
- ParsedModDataRecord DataRecord { get; }
+ ModDataRecordVersionedFields DataRecord { get; }
/// <summary>The metadata resolution status.</summary>
ModMetadataStatus Status { get; }
+ /// <summary>Indicates non-error issues with the mod.</summary>
+ ModWarning Warnings { get; }
+
/// <summary>The reason the metadata is invalid, if any.</summary>
string Error { get; }
@@ -42,6 +46,9 @@ namespace StardewModdingAPI.Framework
/// <summary>Whether the mod is a content pack.</summary>
bool IsContentPack { get; }
+ /// <summary>The update-check metadata for this mod (if any).</summary>
+ ModEntryModel UpdateCheckData { get; }
+
/*********
** Public methods
@@ -52,6 +59,10 @@ namespace StardewModdingAPI.Framework
/// <returns>Return the instance for chaining.</returns>
IModMetadata SetStatus(ModMetadataStatus status, string error = null);
+ /// <summary>Set a warning flag for the mod.</summary>
+ /// <param name="warning">The warning to set.</param>
+ IModMetadata SetWarning(ModWarning warning);
+
/// <summary>Set the mod instance.</summary>
/// <param name="mod">The mod instance to set.</param>
IModMetadata SetMod(IMod mod);
@@ -64,5 +75,18 @@ namespace StardewModdingAPI.Framework
/// <summary>Set the mod-provided API instance.</summary>
/// <param name="api">The mod-provided API.</param>
IModMetadata SetApi(object api);
+
+ /// <summary>Set the update-check metadata for this mod.</summary>
+ /// <param name="data">The update-check metadata.</param>
+ IModMetadata SetUpdateData(ModEntryModel data);
+
+ /// <summary>Whether the mod manifest was loaded (regardless of whether the mod itself was loaded).</summary>
+ bool HasManifest();
+
+ /// <summary>Whether the mod has an ID (regardless of whether the ID is valid or the mod itself was loaded).</summary>
+ bool HasID();
+
+ /// <summary>Whether the mod has at least one update key set.</summary>
+ bool HasUpdateKeys();
}
}
diff --git a/src/SMAPI/Framework/Input/GamePadStateBuilder.cs b/src/SMAPI/Framework/Input/GamePadStateBuilder.cs
new file mode 100644
index 00000000..33557385
--- /dev/null
+++ b/src/SMAPI/Framework/Input/GamePadStateBuilder.cs
@@ -0,0 +1,162 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+
+namespace StardewModdingAPI.Framework.Input
+{
+ /// <summary>An abstraction for manipulating controller state.</summary>
+ internal class GamePadStateBuilder
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The current button states.</summary>
+ private readonly IDictionary<SButton, ButtonState> ButtonStates;
+
+ /// <summary>The left trigger value.</summary>
+ private float LeftTrigger;
+
+ /// <summary>The right trigger value.</summary>
+ private float RightTrigger;
+
+ /// <summary>The left thumbstick position.</summary>
+ private Vector2 LeftStickPos;
+
+ /// <summary>The left thumbstick position.</summary>
+ private Vector2 RightStickPos;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="state">The initial controller state.</param>
+ public GamePadStateBuilder(GamePadState state)
+ {
+ this.ButtonStates = new Dictionary<SButton, ButtonState>
+ {
+ [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;
+ }
+
+ /// <summary>Mark all matching buttons unpressed.</summary>
+ /// <param name="buttons">The buttons.</param>
+ public void SuppressButtons(IEnumerable<SButton> buttons)
+ {
+ foreach (SButton button in buttons)
+ this.SuppressButton(button);
+ }
+
+ /// <summary>Mark a button unpressed.</summary>
+ /// <param name="button">The button.</param>
+ 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;
+ }
+ }
+
+ /// <summary>Construct an equivalent gamepad state.</summary>
+ public GamePadState ToGamePadState()
+ {
+ return new GamePadState(
+ leftThumbStick: this.LeftStickPos,
+ rightThumbStick: this.RightStickPos,
+ leftTrigger: this.LeftTrigger,
+ rightTrigger: this.RightTrigger,
+ buttons: this.GetBitmask(this.GetPressedButtons()) // MonoDevelop requires one bitmask here; don't specify multiple values
+ );
+ }
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get all pressed buttons.</summary>
+ private IEnumerable<Buttons> GetPressedButtons()
+ {
+ foreach (var pair in this.ButtonStates)
+ {
+ if (pair.Value == ButtonState.Pressed && pair.Key.TryGetController(out Buttons button))
+ yield return button;
+ }
+ }
+
+ /// <summary>Get a bitmask representing the given buttons.</summary>
+ /// <param name="buttons">The buttons to represent.</param>
+ private Buttons GetBitmask(IEnumerable<Buttons> buttons)
+ {
+ Buttons flag = 0;
+ foreach (Buttons button in buttons)
+ flag |= button;
+ return flag;
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/Input/InputState.cs b/src/SMAPI/Framework/Input/InputState.cs
deleted file mode 100644
index 8b0108ae..00000000
--- a/src/SMAPI/Framework/Input/InputState.cs
+++ /dev/null
@@ -1,163 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-using Microsoft.Xna.Framework;
-using Microsoft.Xna.Framework.Input;
-using StardewValley;
-
-namespace StardewModdingAPI.Framework.Input
-{
- /// <summary>A summary of input changes during an update frame.</summary>
- internal class InputState
- {
- /*********
- ** Accessors
- *********/
- /// <summary>The underlying controller state.</summary>
- public GamePadState ControllerState { get; }
-
- /// <summary>The underlying keyboard state.</summary>
- public KeyboardState KeyboardState { get; }
-
- /// <summary>The underlying mouse state.</summary>
- public MouseState MouseState { get; }
-
- /// <summary>The mouse position on the screen adjusted for the zoom level.</summary>
- public Point MousePosition { get; }
-
- /// <summary>The buttons which were pressed, held, or released.</summary>
- public IDictionary<SButton, InputStatus> ActiveButtons { get; } = new Dictionary<SButton, InputStatus>();
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Construct an empty instance.</summary>
- public InputState() { }
-
- /// <summary>Construct an instance.</summary>
- /// <param name="previousState">The previous input state.</param>
- /// <param name="controllerState">The current controller state.</param>
- /// <param name="keyboardState">The current keyboard state.</param>
- /// <param name="mouseState">The current mouse state.</param>
- 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 = InputState.GetPressedButtons(keyboardState, mouseState, controllerState).ToArray();
- foreach (SButton button in down)
- this.ActiveButtons[button] = this.GetStatus(previousState.GetStatus(button), isDown: true);
- foreach (KeyValuePair<SButton, InputStatus> prev in previousState.ActiveButtons)
- {
- if (prev.Value.IsDown() && !this.ActiveButtons.ContainsKey(prev.Key))
- this.ActiveButtons[prev.Key] = InputStatus.Released;
- }
- }
-
- /// <summary>Get the status of a button.</summary>
- /// <param name="button">The button to check.</param>
- public InputStatus GetStatus(SButton button)
- {
- return this.ActiveButtons.TryGetValue(button, out InputStatus status) ? status : InputStatus.None;
- }
-
- /// <summary>Get whether a given button was pressed or held.</summary>
- /// <param name="button">The button to check.</param>
- public bool IsDown(SButton button)
- {
- return this.GetStatus(button).IsDown();
- }
-
- /// <summary>Get the current input state.</summary>
- /// <param name="previousState">The previous input state.</param>
- 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
- *********/
- /// <summary>Get the status of a button.</summary>
- /// <param name="oldStatus">The previous button status.</param>
- /// <param name="isDown">Whether the button is currently down.</param>
- public InputStatus GetStatus(InputStatus oldStatus, bool isDown)
- {
- if (isDown && oldStatus.IsDown())
- return InputStatus.Held;
- if (isDown)
- return InputStatus.Pressed;
- return InputStatus.Released;
- }
-
- /// <summary>Get the buttons pressed in the given stats.</summary>
- /// <param name="keyboard">The keyboard state.</param>
- /// <param name="mouse">The mouse state.</param>
- /// <param name="controller">The controller state.</param>
- private static IEnumerable<SButton> 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)
- {
- 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.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;
- 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;
- if (controller.Triggers.Left > 0.2f)
- yield return SButton.LeftTrigger;
- if (controller.Triggers.Right > 0.2f)
- yield return SButton.RightTrigger;
- }
- }
- }
-}
diff --git a/src/SMAPI/Framework/Input/SInputState.cs b/src/SMAPI/Framework/Input/SInputState.cs
new file mode 100644
index 00000000..0228db0d
--- /dev/null
+++ b/src/SMAPI/Framework/Input/SInputState.cs
@@ -0,0 +1,382 @@
+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
+{
+ /// <summary>Manages the game's input state.</summary>
+ internal sealed class SInputState : InputState
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The maximum amount of direction to ignore for the left thumbstick.</summary>
+ private const float LeftThumbstickDeadZone = 0.2f;
+
+ /// <summary>The cursor position on the screen adjusted for the zoom level.</summary>
+ private CursorPosition CursorPositionImpl;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The controller state as of the last update.</summary>
+ public GamePadState RealController { get; private set; }
+
+ /// <summary>The keyboard state as of the last update.</summary>
+ public KeyboardState RealKeyboard { get; private set; }
+
+ /// <summary>The mouse state as of the last update.</summary>
+ public MouseState RealMouse { get; private set; }
+
+ /// <summary>A derivative of <see cref="RealController"/> which suppresses the buttons in <see cref="SuppressButtons"/>.</summary>
+ public GamePadState SuppressedController { get; private set; }
+
+ /// <summary>A derivative of <see cref="RealKeyboard"/> which suppresses the buttons in <see cref="SuppressButtons"/>.</summary>
+ public KeyboardState SuppressedKeyboard { get; private set; }
+
+ /// <summary>A derivative of <see cref="RealMouse"/> which suppresses the buttons in <see cref="SuppressButtons"/>.</summary>
+ public MouseState SuppressedMouse { get; private set; }
+
+ /// <summary>The cursor position on the screen adjusted for the zoom level.</summary>
+ public ICursorPosition CursorPosition => this.CursorPositionImpl;
+
+ /// <summary>The buttons which were pressed, held, or released.</summary>
+ public IDictionary<SButton, InputStatus> ActiveButtons { get; private set; } = new Dictionary<SButton, InputStatus>();
+
+ /// <summary>The buttons to suppress when the game next handles input. Each button is suppressed until it's released.</summary>
+ public HashSet<SButton> SuppressButtons { get; } = new HashSet<SButton>();
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Get a copy of the current state.</summary>
+ public SInputState Clone()
+ {
+ return new SInputState
+ {
+ ActiveButtons = this.ActiveButtons,
+ RealController = this.RealController,
+ RealKeyboard = this.RealKeyboard,
+ RealMouse = this.RealMouse,
+ CursorPositionImpl = this.CursorPositionImpl
+ };
+ }
+
+ /// <summary>This method is called by the game, and does nothing since SMAPI will already have updated by that point.</summary>
+ [Obsolete("This method should only be called by the game itself.")]
+ public override void Update() { }
+
+ /// <summary>Update the current button statuses for the given tick.</summary>
+ public void TrueUpdate()
+ {
+ try
+ {
+ // get new states
+ GamePadState realController = GamePad.GetState(PlayerIndex.One);
+ KeyboardState realKeyboard = Keyboard.GetState();
+ MouseState realMouse = Mouse.GetState();
+ var activeButtons = this.DeriveStatuses(this.ActiveButtons, realKeyboard, realMouse, realController);
+ Vector2 cursorAbsolutePos = new Vector2(realMouse.X + Game1.viewport.X, realMouse.Y + Game1.viewport.Y);
+
+ // update real states
+ this.ActiveButtons = activeButtons;
+ this.RealController = realController;
+ this.RealKeyboard = realKeyboard;
+ this.RealMouse = realMouse;
+ if (this.CursorPositionImpl?.AbsolutePixels != cursorAbsolutePos)
+ this.CursorPositionImpl = this.GetCursorPosition(realMouse, cursorAbsolutePos);
+
+ // update suppressed states
+ this.SuppressButtons.RemoveWhere(p => !this.GetStatus(activeButtons, p).IsDown());
+ this.UpdateSuppression();
+ }
+ catch (InvalidOperationException)
+ {
+ // GetState() may crash for some players if window doesn't have focus but game1.IsActive == true
+ }
+ }
+
+ /// <summary>Apply input suppression to current input.</summary>
+ public void UpdateSuppression()
+ {
+ GamePadState suppressedController = this.RealController;
+ KeyboardState suppressedKeyboard = this.RealKeyboard;
+ MouseState suppressedMouse = this.RealMouse;
+
+ this.SuppressGivenStates(this.ActiveButtons, ref suppressedKeyboard, ref suppressedMouse, ref suppressedController);
+
+ this.SuppressedController = suppressedController;
+ this.SuppressedKeyboard = suppressedKeyboard;
+ this.SuppressedMouse = suppressedMouse;
+ }
+
+ /// <summary>Get the gamepad state visible to the game.</summary>
+ [Obsolete("This method should only be called by the game itself.")]
+ public override GamePadState GetGamePadState()
+ {
+ return this.ShouldSuppressNow()
+ ? this.SuppressedController
+ : this.RealController;
+ }
+
+ /// <summary>Get the keyboard state visible to the game.</summary>
+ [Obsolete("This method should only be called by the game itself.")]
+ public override KeyboardState GetKeyboardState()
+ {
+ return this.ShouldSuppressNow()
+ ? this.SuppressedKeyboard
+ : this.RealKeyboard;
+ }
+
+ /// <summary>Get the keyboard state visible to the game.</summary>
+ [Obsolete("This method should only be called by the game itself.")]
+ public override MouseState GetMouseState()
+ {
+ return this.ShouldSuppressNow()
+ ? this.SuppressedMouse
+ : this.RealMouse;
+ }
+
+ /// <summary>Get whether a given button was pressed or held.</summary>
+ /// <param name="button">The button to check.</param>
+ public bool IsDown(SButton button)
+ {
+ return this.GetStatus(this.ActiveButtons, button).IsDown();
+ }
+
+ /// <summary>Get whether any of the given buttons were pressed or held.</summary>
+ /// <param name="buttons">The buttons to check.</param>
+ public bool IsAnyDown(InputButton[] buttons)
+ {
+ return buttons.Any(button => this.IsDown(button.ToSButton()));
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get the current cursor position.</summary>
+ /// <param name="mouseState">The current mouse state.</param>
+ /// <param name="absolutePixels">The absolute pixel position relative to the map.</param>
+ private CursorPosition GetCursorPosition(MouseState mouseState, Vector2 absolutePixels)
+ {
+ Vector2 rawPixels = new Vector2(mouseState.X, mouseState.Y);
+ Vector2 screenPixels = rawPixels * new Vector2((float)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(absolutePixels, screenPixels, tile, grabTile);
+ }
+
+ /// <summary>Whether input should be suppressed in the current context.</summary>
+ private bool ShouldSuppressNow()
+ {
+ return Game1.chatBox == null || !Game1.chatBox.isActive();
+ }
+
+ /// <summary>Apply input suppression to the given input states.</summary>
+ /// <param name="activeButtons">The current button states to check.</param>
+ /// <param name="keyboardState">The game's keyboard state for the current tick.</param>
+ /// <param name="mouseState">The game's mouse state for the current tick.</param>
+ /// <param name="gamePadState">The game's controller state for the current tick.</param>
+ private void SuppressGivenStates(IDictionary<SButton, InputStatus> activeButtons, ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState)
+ {
+ if (this.SuppressButtons.Count == 0)
+ return;
+
+ // gather info
+ HashSet<Keys> suppressKeys = new HashSet<Keys>();
+ HashSet<SButton> suppressButtons = new HashSet<SButton>();
+ HashSet<SButton> suppressMouse = new HashSet<SButton>();
+ foreach (SButton button in this.SuppressButtons)
+ {
+ if (button == SButton.MouseLeft || button == SButton.MouseMiddle || button == SButton.MouseRight || button == SButton.MouseX1 || button == SButton.MouseX2)
+ suppressMouse.Add(button);
+ else if (button.TryGetKeyboard(out Keys key))
+ suppressKeys.Add(key);
+ else if (gamePadState.IsConnected && button.TryGetController(out Buttons _))
+ suppressButtons.Add(button);
+ }
+
+ // suppress keyboard keys
+ if (keyboardState.GetPressedKeys().Any() && suppressKeys.Any())
+ keyboardState = new KeyboardState(keyboardState.GetPressedKeys().Except(suppressKeys).ToArray());
+
+ // suppress controller keys
+ if (gamePadState.IsConnected && suppressButtons.Any())
+ {
+ GamePadStateBuilder builder = new GamePadStateBuilder(gamePadState);
+ builder.SuppressButtons(suppressButtons);
+ gamePadState = builder.ToGamePadState();
+ }
+
+ // suppress mouse buttons
+ if (suppressMouse.Any())
+ {
+ mouseState = new MouseState(
+ x: mouseState.X,
+ y: mouseState.Y,
+ scrollWheel: mouseState.ScrollWheelValue,
+ leftButton: suppressMouse.Contains(SButton.MouseLeft) ? ButtonState.Released : mouseState.LeftButton,
+ middleButton: suppressMouse.Contains(SButton.MouseMiddle) ? ButtonState.Released : mouseState.MiddleButton,
+ rightButton: suppressMouse.Contains(SButton.MouseRight) ? ButtonState.Released : mouseState.RightButton,
+ xButton1: suppressMouse.Contains(SButton.MouseX1) ? ButtonState.Released : mouseState.XButton1,
+ xButton2: suppressMouse.Contains(SButton.MouseX2) ? ButtonState.Released : mouseState.XButton2
+ );
+ }
+ }
+
+ /// <summary>Get the status of all pressed or released buttons relative to their previous status.</summary>
+ /// <param name="previousStatuses">The previous button statuses.</param>
+ /// <param name="keyboard">The keyboard state.</param>
+ /// <param name="mouse">The mouse state.</param>
+ /// <param name="controller">The controller state.</param>
+ private IDictionary<SButton, InputStatus> DeriveStatuses(IDictionary<SButton, InputStatus> previousStatuses, KeyboardState keyboard, MouseState mouse, GamePadState controller)
+ {
+ IDictionary<SButton, InputStatus> activeButtons = new Dictionary<SButton, InputStatus>();
+
+ // 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<SButton, InputStatus> prev in previousStatuses)
+ {
+ if (prev.Value.IsDown() && !activeButtons.ContainsKey(prev.Key))
+ activeButtons[prev.Key] = InputStatus.Released;
+ }
+
+ return activeButtons;
+ }
+
+ /// <summary>Get the status of a button relative to its previous status.</summary>
+ /// <param name="oldStatus">The previous button status.</param>
+ /// <param name="isDown">Whether the button is currently down.</param>
+ private InputStatus DeriveStatus(InputStatus oldStatus, bool isDown)
+ {
+ if (isDown && oldStatus.IsDown())
+ return InputStatus.Held;
+ if (isDown)
+ return InputStatus.Pressed;
+ return InputStatus.Released;
+ }
+
+ /// <summary>Get the status of a button.</summary>
+ /// <param name="activeButtons">The current button states to check.</param>
+ /// <param name="button">The button to check.</param>
+ private InputStatus GetStatus(IDictionary<SButton, InputStatus> activeButtons, SButton button)
+ {
+ return activeButtons.TryGetValue(button, out InputStatus status) ? status : InputStatus.None;
+ }
+
+ /// <summary>Get the buttons pressed in the given stats.</summary>
+ /// <param name="keyboard">The keyboard state.</param>
+ /// <param name="mouse">The mouse state.</param>
+ /// <param name="controller">The controller state.</param>
+ /// <remarks>Thumbstick direction logic derived from <see cref="ButtonCollection"/>.</remarks>
+ private IEnumerable<SButton> 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;
+ }
+ }
+ }
+
+ /// <summary>Get whether the right thumbstick should be considered outside the dead zone.</summary>
+ /// <param name="direction">The right thumbstick value.</param>
+ private bool IsRightThumbstickOutsideDeadZone(Vector2 direction)
+ {
+ return direction.Length() > 0.9f;
+ }
+ }
+}
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<bool>(Game1.spriteBatch, fieldName).GetValue();
}
-
- /****
- ** Json.NET
- ****/
- /// <summary>Get a JSON field value from a case-insensitive field name. This will check for an exact match first, then search without case sensitivity.</summary>
- /// <typeparam name="T">The value type.</typeparam>
- /// <param name="obj">The JSON object to search.</param>
- /// <param name="fieldName">The field name.</param>
- public static T ValueIgnoreCase<T>(this JObject obj, string fieldName)
- {
- JToken token = obj.GetValue(fieldName, StringComparison.InvariantCultureIgnoreCase);
- return token != null
- ? token.Value<T>()
- : 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
-{
- /// <summary>An implementation of <see cref="ISemanticVersion"/> that hamdles the legacy <see cref="IManifest"/> version format.</summary>
- internal class LegacyManifestVersion : SemanticVersion
- {
- /*********
- ** Public methods
- *********/
- /// <summary>Construct an instance.</summary>
- /// <param name="majorVersion">The major version incremented for major API changes.</param>
- /// <param name="minorVersion">The minor version incremented for backwards-compatible changes.</param>
- /// <param name="patchVersion">The patch version for backwards-compatible bug fixes.</param>
- /// <param name="build">An optional build tag.</param>
- [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/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
*********/
- /// <summary>Whether the current console supports color formatting.</summary>
- public bool SupportsColor { get; }
-
/// <summary>The event raised when a message is written to the console directly.</summary>
public event Action<string> 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();
}
/// <summary>Get an exclusive lock and write to the console output without interception.</summary>
@@ -61,26 +55,5 @@ namespace StardewModdingAPI.Framework.Logging
Console.SetOut(this.Output.Out);
this.Output.Dispose();
}
-
-
- /*********
- ** private methods
- *********/
- /// <summary>Test whether the current console supports color formatting.</summary>
- private bool TestColorSupport()
- {
- try
- {
- this.ExclusiveWriteWithoutInterception(() =>
- {
- Console.ForegroundColor = Console.ForegroundColor;
- });
- return true;
- }
- catch (Exception)
- {
- return false; // Mono bug
- }
- }
}
}
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
-{
- /// <summary>A versioned mod metadata field.</summary>
- internal class ModDataField
- {
- /*********
- ** Accessors
- *********/
- /// <summary>The field key.</summary>
- public ModDataFieldKey Key { get; }
-
- /// <summary>The field value.</summary>
- public string Value { get; }
-
- /// <summary>Whether this field should only be applied if it's not already set.</summary>
- public bool IsDefault { get; }
-
- /// <summary>The lowest version in the range, or <c>null</c> for all past versions.</summary>
- public ISemanticVersion LowerVersion { get; }
-
- /// <summary>The highest version in the range, or <c>null</c> for all future versions.</summary>
- public ISemanticVersion UpperVersion { get; }
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Construct an instance.</summary>
- /// <param name="key">The field key.</param>
- /// <param name="value">The field value.</param>
- /// <param name="isDefault">Whether this field should only be applied if it's not already set.</param>
- /// <param name="lowerVersion">The lowest version in the range, or <c>null</c> for all past versions.</param>
- /// <param name="upperVersion">The highest version in the range, or <c>null</c> for all future versions.</param>
- 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;
- }
-
- /// <summary>Get whether this data field applies for the given manifest.</summary>
- /// <param name="manifest">The mod manifest.</param>
- 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
- *********/
- /// <summary>Get whether a manifest field has a meaningful value for the purposes of enforcing <see cref="IsDefault"/>.</summary>
- /// <param name="manifest">The mod manifest.</param>
- /// <param name="key">The field key matching <see cref="ModDataFieldKey"/>.</param>
- 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
-{
- /// <summary>The valid field keys.</summary>
- public enum ModDataFieldKey
- {
- /// <summary>A manifest update key.</summary>
- UpdateKey,
-
- /// <summary>An alternative URL the player can check for an updated version.</summary>
- AlternativeUrl,
-
- /// <summary>The mod's predefined compatibility status.</summary>
- Status,
-
- /// <summary>A reason phrase for the <see cref="Status"/>, or <c>null</c> to use the default reason.</summary>
- 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
-{
- /// <summary>Raw mod metadata from SMAPI's internal mod list.</summary>
- internal class ModDataRecord
- {
- /*********
- ** Properties
- *********/
- /// <summary>This field stores properties that aren't mapped to another field before they're parsed into <see cref="Fields"/>.</summary>
- [JsonExtensionData]
- private IDictionary<string, JToken> ExtensionData;
-
-
- /*********
- ** Accessors
- *********/
- /// <summary>The mod's current unique ID.</summary>
- public string ID { get; set; }
-
- /// <summary>The former mod IDs (if any).</summary>
- /// <remarks>
- /// 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
- /// <c>|</c> 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.
- /// </remarks>
- public string FormerIDs { get; set; }
-
- /// <summary>Maps local versions to a semantic version for update checks.</summary>
- public IDictionary<string, string> MapLocalVersions { get; set; } = new Dictionary<string, string>();
-
- /// <summary>Maps remote versions to a semantic version for update checks.</summary>
- public IDictionary<string, string> MapRemoteVersions { get; set; } = new Dictionary<string, string>();
-
- /// <summary>The versioned field data.</summary>
- /// <remarks>
- /// This maps field names to values. This should be accessed via <see cref="GetFields"/>.
- /// Format notes:
- /// - Each key consists of a field name prefixed with any combination of version range
- /// and <c>Default</c>, separated by pipes (whitespace trimmed). For example, <c>Name</c>
- /// will always override the name, <c>Default | Name</c> will only override a blank
- /// name, and <c>~1.1 | Default | Name</c> will override blank names up to version 1.1.
- /// - The version format is <c>min~max</c> (where either side can be blank for unbounded), or
- /// a single version number.
- /// - The field name itself corresponds to a <see cref="ModDataFieldKey"/> value.
- /// </remarks>
- public IDictionary<string, string> Fields { get; set; } = new Dictionary<string, string>();
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Get a parsed representation of the <see cref="Fields"/>.</summary>
- public IEnumerable<ModDataField> GetFields()
- {
- foreach (KeyValuePair<string, string> 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);
- }
- }
-
- /// <summary>Get a semantic local version for update checks.</summary>
- /// <param name="version">The remote version to normalise.</param>
- public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version)
- {
- return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version.ToString(), out string newVersion)
- ? new SemanticVersion(newVersion)
- : version;
- }
-
- /// <summary>Get a semantic remote version for update checks.</summary>
- /// <param name="version">The remote version to normalise.</param>
- 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
- *********/
- /// <summary>The method invoked after JSON deserialisation.</summary>
- /// <param name="context">The deserialisation context.</param>
- [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 3fd68440..00000000
--- a/src/SMAPI/Framework/ModData/ModDatabase.cs
+++ /dev/null
@@ -1,206 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using Newtonsoft.Json;
-
-namespace StardewModdingAPI.Framework.ModData
-{
- /// <summary>Handles access to SMAPI's internal mod metadata list.</summary>
- internal class ModDatabase
- {
- /*********
- ** Properties
- *********/
- /// <summary>The underlying mod data records indexed by default display name.</summary>
- private readonly IDictionary<string, ModDataRecord> Records;
-
- /// <summary>Get an update URL for an update key (if valid).</summary>
- private readonly Func<string, string> GetUpdateUrl;
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Construct an empty instance.</summary>
- public ModDatabase()
- : this(new Dictionary<string, ModDataRecord>(), key => null) { }
-
- /// <summary>Construct an instance.</summary>
- /// <param name="records">The underlying mod data records indexed by default display name.</param>
- /// <param name="getUpdateUrl">Get an update URL for an update key (if valid).</param>
- public ModDatabase(IDictionary<string, ModDataRecord> records, Func<string, string> getUpdateUrl)
- {
- this.Records = records;
- this.GetUpdateUrl = getUpdateUrl;
- }
-
- /// <summary>Get a parsed representation of the <see cref="ModDataRecord.Fields"/> which match a given manifest.</summary>
- /// <param name="manifest">The manifest to match.</param>
- public ParsedModDataRecord GetParsed(IManifest manifest)
- {
- // get raw record
- if (!this.TryGetRaw(manifest, 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;
- }
-
- /// <summary>Get the display name for a given mod ID (if available).</summary>
- /// <param name="id">The unique mod ID.</param>
- public string GetDisplayNameFor(string id)
- {
- return this.TryGetRaw(id, out string displayName, out ModDataRecord _)
- ? displayName
- : null;
- }
-
- /// <summary>Get the mod page URL for a mod (if available).</summary>
- /// <param name="id">The unique mod ID.</param>
- 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
- *********/
- /// <summary>Get a raw data record.</summary>
- /// <param name="id">The mod ID to match.</param>
- /// <param name="displayName">The mod's default display name.</param>
- /// <param name="record">The raw mod record.</param>
- private bool TryGetRaw(string id, out string displayName, out ModDataRecord record)
- {
- foreach (var entry in this.Records)
- {
- if (entry.Value.ID != null && entry.Value.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase))
- {
- displayName = entry.Key;
- record = entry.Value;
- return true;
- }
- }
-
- displayName = null;
- record = null;
- return false;
- }
- /// <summary>Get a raw data record.</summary>
- /// <param name="manifest">The mod manifest whose fields to match.</param>
- /// <param name="displayName">The mod's default display name.</param>
- /// <param name="record">The raw mod record.</param>
- private bool TryGetRaw(IManifest manifest, out string displayName, out ModDataRecord record)
- {
- if (manifest != null)
- {
- foreach (var entry in this.Records)
- {
- displayName = entry.Key;
- record = entry.Value;
-
- // try main ID
- if (record.ID != null && record.ID.Equals(manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase))
- return true;
-
- // try former IDs
- if (record.FormerIDs != null)
- {
- foreach (string part in record.FormerIDs.Split('|'))
- {
- // packed field snapshot
- if (part.StartsWith("{"))
- {
- FieldSnapshot snapshot = JsonConvert.DeserializeObject<FieldSnapshot>(part);
- bool isMatch =
- (snapshot.ID == null || snapshot.ID.Equals(manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase))
- && (snapshot.EntryDll == null || snapshot.EntryDll.Equals(manifest.EntryDll, StringComparison.InvariantCultureIgnoreCase))
- && (
- snapshot.Author == null
- || snapshot.Author.Equals(manifest.Author, StringComparison.InvariantCultureIgnoreCase)
- || (manifest.ExtraFields != null && manifest.ExtraFields.ContainsKey("Authour") && snapshot.Author.Equals(manifest.ExtraFields["Authour"].ToString(), StringComparison.InvariantCultureIgnoreCase))
- )
- && (snapshot.Name == null || snapshot.Name.Equals(manifest.Name, StringComparison.InvariantCultureIgnoreCase));
-
- if (isMatch)
- return true;
- }
-
- // plain ID
- else if (part.Equals(manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase))
- return true;
- }
- }
- }
- }
-
- displayName = null;
- record = null;
- return false;
- }
-
-
- /*********
- ** Private models
- *********/
- /// <summary>A unique set of fields which identifies the mod.</summary>
- [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local", Justification = "Used via JSON deserialisation.")]
- [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local", Justification = "Used via JSON deserialisation.")]
- private class FieldSnapshot
- {
- /*********
- ** Accessors
- *********/
- /// <summary>The unique mod ID (or <c>null</c> to ignore it).</summary>
- public string ID { get; set; }
-
- /// <summary>The entry DLL (or <c>null</c> to ignore it).</summary>
- public string EntryDll { get; set; }
-
- /// <summary>The mod name (or <c>null</c> to ignore it).</summary>
- public string Name { get; set; }
-
- /// <summary>The author name (or <c>null</c> to ignore it).</summary>
- public string Author { get; set; }
- }
- }
-}
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
-{
- /// <summary>Indicates how SMAPI should treat a mod.</summary>
- internal enum ModStatus
- {
- /// <summary>Don't override the status.</summary>
- None,
-
- /// <summary>The mod is obsolete and shouldn't be used, regardless of version.</summary>
- Obsolete,
-
- /// <summary>Assume the mod is not compatible, even if SMAPI doesn't detect any incompatible code.</summary>
- AssumeBroken,
-
- /// <summary>Assume the mod is compatible, even if SMAPI detects incompatible code.</summary>
- AssumeCompatible
- }
-}
diff --git a/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs b/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs
deleted file mode 100644
index deb12bdc..00000000
--- a/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-namespace StardewModdingAPI.Framework.ModData
-{
- /// <summary>A parsed representation of the fields from a <see cref="ModDataRecord"/> for a specific manifest.</summary>
- internal class ParsedModDataRecord
- {
- /*********
- ** Accessors
- *********/
- /// <summary>The underlying data record.</summary>
- public ModDataRecord DataRecord { get; set; }
-
- /// <summary>The default mod name to display when the name isn't available (e.g. during dependency checks).</summary>
- public string DisplayName { get; set; }
-
- /// <summary>The update key to apply.</summary>
- public string UpdateKey { get; set; }
-
- /// <summary>The alternative URL the player can check for an updated version.</summary>
- public string AlternativeUrl { get; set; }
-
- /// <summary>The predefined compatibility status.</summary>
- public ModStatus Status { get; set; } = ModStatus.None;
-
- /// <summary>A reason phrase for the <see cref="Status"/>, or <c>null</c> to use the default reason.</summary>
- public string StatusReasonPhrase { get; set; }
-
- /// <summary>The upper version for which the <see cref="Status"/> applies (if any).</summary>
- public ISemanticVersion StatusUpperVersion { get; set; }
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Get a semantic local version for update checks.</summary>
- /// <param name="version">The remote version to normalise.</param>
- public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version)
- {
- return this.DataRecord.GetLocalVersionForUpdateChecks(version);
- }
-
- /// <summary>Get a semantic remote version for update checks.</summary>
- /// <param name="version">The remote version to normalise.</param>
- public string GetRemoteVersionForUpdateChecks(string version)
- {
- return this.DataRecord.GetRemoteVersionForUpdateChecks(version);
- }
- }
-}
diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
index c7d4c39e..671dc21e 100644
--- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
@@ -7,8 +7,9 @@ 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 StardewModdingAPI.Toolkit.Utilities;
using StardewValley;
using xTile;
using xTile.Format;
@@ -23,10 +24,13 @@ namespace StardewModdingAPI.Framework.ModHelpers
** Properties
*********/
/// <summary>SMAPI's core content logic.</summary>
- private readonly ContentCore ContentCore;
+ private readonly ContentCoordinator ContentCore;
- /// <summary>The content manager for this mod.</summary>
- private readonly ContentManagerShim ContentManager;
+ /// <summary>A content manager for this mod which manages files from the game's Content folder.</summary>
+ private readonly IContentManager GameContentManager;
+
+ /// <summary>A content manager for this mod which manages files from the mod's folder.</summary>
+ private readonly IContentManager ModContentManager;
/// <summary>The absolute path to the mod folder.</summary>
private readonly string ModFolderPath;
@@ -42,10 +46,10 @@ namespace StardewModdingAPI.Framework.ModHelpers
** Accessors
*********/
/// <summary>The game's current locale code (like <c>pt-BR</c>).</summary>
- public string CurrentLocale => this.ContentCore.GetLocale();
+ public string CurrentLocale => this.GameContentManager.GetLocale();
/// <summary>The game's current locale as an enum value.</summary>
- public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.ContentCore.Language;
+ public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.GameContentManager.Language;
/// <summary>The observable implementation of <see cref="AssetEditors"/>.</summary>
internal ObservableCollection<IAssetEditor> ObservableAssetEditors { get; } = new ObservableCollection<IAssetEditor>();
@@ -65,16 +69,16 @@ namespace StardewModdingAPI.Framework.ModHelpers
*********/
/// <summary>Construct an instance.</summary>
/// <param name="contentCore">SMAPI's core content logic.</param>
- /// <param name="contentManager">The content manager for this mod.</param>
/// <param name="modFolderPath">The absolute path to the mod folder.</param>
/// <param name="modID">The unique ID of the relevant mod.</param>
/// <param name="modName">The friendly mod name for use in errors.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
- public ContentHelper(ContentCore contentCore, ContentManagerShim 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.ContentManager.Load<T>(key);
+ return this.GameContentManager.Load<T>(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.ContentCore.GetAssetNameFromFilePath(file.FullName);
+ string internalKey = this.GetInternalModAssetKey(file);
// try cache
- if (this.ContentCore.IsLoaded(assetName))
- return this.ContentManager.Load<T>(assetName);
+ if (this.ModContentManager.IsLoaded(internalKey))
+ return this.ModContentManager.Load<T>(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<T>(assetName);
+ return this.ModContentManager.Load<T>(internalKey);
default:
throw GetContentError($"unknown content source '{source}'.");
@@ -146,7 +148,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
[Pure]
public string NormaliseAssetName(string assetName)
{
- return this.ContentCore.NormaliseAssetName(assetName);
+ return this.ModContentManager.AssertAndNormaliseAssetName(assetName);
}
/// <summary>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.</summary>
@@ -158,11 +160,11 @@ namespace StardewModdingAPI.Framework.ModHelpers
switch (source)
{
case ContentSource.GameContent:
- return this.ContentCore.NormaliseAssetName(key);
+ return this.GameContentManager.AssertAndNormaliseAssetName(key);
case ContentSource.ModFolder:
FileInfo file = this.GetModFile(key);
- return this.ContentCore.NormaliseAssetName(this.ContentCore.GetAssetNameFromFilePath(file.FullName));
+ return this.GetInternalModAssetKey(file);
default:
throw new NotSupportedException($"Unknown content source '{source}'.");
@@ -177,7 +179,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();
}
/// <summary>Remove all assets of the given type from the cache so they're reloaded on the next request. <b>This can be a very expensive operation and should only be used in very specific cases.</b> This will reload core game assets if needed, but references to the former assets will still show the previous content.</summary>
@@ -186,7 +188,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
public bool InvalidateCache<T>()
{
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();
}
/// <summary>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.</summary>
@@ -195,7 +197,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
public bool InvalidateCache(Func<IAssetInfo, bool> 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();
}
/*********
@@ -205,16 +207,24 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <param name="key">The asset key to check.</param>
/// <exception cref="ArgumentException">The asset key is empty or contains invalid characters.</exception>
[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.ContentCore.AssertValidAssetKeyFormat(key);
+ this.ModContentManager.AssertAndNormaliseAssetName(key);
if (Path.IsPathRooted(key))
throw new ArgumentException("The asset key must not be an absolute path.");
}
+ /// <summary>Get the internal key in the content cache for a mod asset.</summary>
+ /// <param name="modFile">The asset file.</param>
+ private string GetInternalModAssetKey(FileInfo modFile)
+ {
+ string relativePath = PathUtilities.GetRelativePath(this.ModFolderPath, modFile.FullName);
+ return Path.Combine(this.ModContentManager.Name, relativePath);
+ }
+
/// <summary>Fix custom map tilesheet paths so they can be found by the content manager.</summary>
/// <param name="map">The map whose tilesheets to fix.</param>
- /// <param name="mapKey">The map asset key within the mod folder.</param>
+ /// <param name="relativeMapPath">The relative map path within the mod folder.</param>
/// <exception cref="ContentLoadException">A map tilesheet couldn't be resolved.</exception>
/// <remarks>
/// The game's logic for tilesheets in <see cref="Game1.setGraphicsForSeason"/> 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.
/// </remarks>
- private void FixCustomTilesheetPaths(Map map, string mapKey)
+ private void FixCustomTilesheetPaths(Map map, string relativeMapPath)
{
// get map info
if (!map.TileSheets.Any())
return;
- mapKey = this.ContentCore.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)
@@ -251,7 +261,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 +351,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.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.ContentCore.FullRootDirectory, key);
+ string path = Path.Combine(this.GameContentManager.FullRootDirectory, key);
if (!path.EndsWith(".xnb"))
path += ".xnb";
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
+{
+ /// <summary>Provides an API for checking and changing input state.</summary>
+ internal class InputHelper : BaseHelper, IInputHelper
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Manages the game's input state.</summary>
+ private readonly SInputState InputState;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="modID">The unique ID of the relevant mod.</param>
+ /// <param name="inputState">Manages the game's input state.</param>
+ public InputHelper(string modID, SInputState inputState)
+ : base(modID)
+ {
+ this.InputState = inputState;
+ }
+
+ /// <summary>Get the current cursor position.</summary>
+ public ICursorPosition GetCursorPosition()
+ {
+ return this.InputState.CursorPosition;
+ }
+
+ /// <summary>Get whether a button is currently pressed.</summary>
+ /// <param name="button">The button.</param>
+ public bool IsDown(SButton button)
+ {
+ return this.InputState.IsDown(button);
+ }
+
+ /// <summary>Get whether a button is currently suppressed, so the game won't see it.</summary>
+ /// <param name="button">The button.</param>
+ public bool IsSuppressed(SButton button)
+ {
+ return this.InputState.SuppressButtons.Contains(button);
+ }
+
+ /// <summary>Prevent the game from handling a button press. This doesn't prevent other mods from receiving the event.</summary>
+ /// <param name="button">The button to suppress.</param>
+ 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 b5758d21..d9498e83 100644
--- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs
@@ -2,9 +2,11 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using StardewModdingAPI.Framework.Models;
-using StardewModdingAPI.Framework.Serialisation;
-using StardewModdingAPI.Framework.Utilities;
+using StardewModdingAPI.Events;
+using StardewModdingAPI.Framework.Input;
+using StardewModdingAPI.Toolkit.Serialisation;
+using StardewModdingAPI.Toolkit.Serialisation.Models;
+using StardewModdingAPI.Toolkit.Utilities;
namespace StardewModdingAPI.Framework.ModHelpers
{
@@ -33,9 +35,15 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>The full path to the mod's folder.</summary>
public string DirectoryPath { get; }
+ /// <summary>Manages access to events raised by SMAPI, which let your mod react when something happens in the game.</summary>
+ public IModEvents Events { get; }
+
/// <summary>An API for loading content assets.</summary>
public IContentHelper Content { get; }
+ /// <summary>An API for checking and changing input state.</summary>
+ public IInputHelper Input { get; }
+
/// <summary>An API for accessing private game code.</summary>
public IReflectionHelper Reflection { get; }
@@ -45,6 +53,9 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>An API for managing console commands.</summary>
public ICommandHelper ConsoleCommands { get; }
+ /// <summary>Provides multiplayer utilities.</summary>
+ public IMultiplayerHelper Multiplayer { get; }
+
/// <summary>An API for reading translations stored in the mod's <c>i18n</c> folder, with one file per locale (like <c>en.json</c>) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like <c>pt-BR.json</c> &lt; <c>pt.json</c> &lt; <c>default.json</c>).</summary>
public ITranslationHelper Translation { get; }
@@ -56,17 +67,20 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <param name="modID">The mod's unique ID.</param>
/// <param name="modDirectory">The full path to the mod's folder.</param>
/// <param name="jsonHelper">Encapsulate SMAPI's JSON parsing.</param>
+ /// <param name="inputState">Manages the game's input state.</param>
+ /// <param name="events">Manages access to events raised by SMAPI.</param>
/// <param name="contentHelper">An API for loading content assets.</param>
/// <param name="commandHelper">An API for managing console commands.</param>
/// <param name="modRegistry">an API for fetching metadata about loaded mods.</param>
/// <param name="reflectionHelper">An API for accessing private game code.</param>
+ /// <param name="multiplayer">Provides multiplayer utilities.</param>
/// <param name="translationHelper">An API for reading translations stored in the mod's <c>i18n</c> folder.</param>
/// <param name="contentPacks">The content packs loaded for this mod.</param>
/// <param name="createContentPack">Create a transitional content pack.</param>
/// <param name="deprecationManager">Manages deprecation warnings.</param>
/// <exception cref="ArgumentNullException">An argument is null or empty.</exception>
/// <exception cref="InvalidOperationException">The <paramref name="modDirectory"/> path does not exist on disk.</exception>
- public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, IContentHelper contentHelper, ICommandHelper commandHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, ITranslationHelper translationHelper, IEnumerable<IContentPack> contentPacks, Func<string, IManifest, IContentPack> 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<IContentPack> contentPacks, Func<string, IManifest, IContentPack> createContentPack, DeprecationManager deprecationManager)
: base(modID)
{
// validate directory
@@ -79,13 +93,16 @@ 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));
+ this.Multiplayer = multiplayer ?? throw new ArgumentNullException(nameof(multiplayer));
this.Translation = translationHelper ?? throw new ArgumentNullException(nameof(translationHelper));
this.ContentPacks = contentPacks.ToArray();
this.CreateContentPack = createContentPack;
this.DeprecationManager = deprecationManager;
+ this.Events = events;
}
/****
@@ -152,26 +169,24 @@ 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
- 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: this.ModID
+ );
// create content pack
return this.CreateContentPack(directoryPath, manifest);
diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs
index e579a830..008a80f5 100644
--- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs
@@ -82,12 +82,12 @@ namespace StardewModdingAPI.Framework.ModHelpers
}
if (!typeof(TInterface).IsInterface)
{
- this.Monitor.Log("Tried to map a mod-provided API to a class; must be a public interface.", LogLevel.Error);
+ this.Monitor.Log($"Tried to map a mod-provided API to class '{typeof(TInterface).FullName}'; must be a public interface.", LogLevel.Error);
return null;
}
if (!typeof(TInterface).IsPublic)
{
- this.Monitor.Log("Tried to map a mod-provided API to a non-public interface; must be a public interface.", LogLevel.Error);
+ this.Monitor.Log($"Tried to map a mod-provided API to non-public interface '{typeof(TInterface).FullName}'; must be a public interface.", LogLevel.Error);
return null;
}
diff --git a/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs b/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs
new file mode 100644
index 00000000..c449a51b
--- /dev/null
+++ b/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs
@@ -0,0 +1,40 @@
+using System.Collections.Generic;
+using StardewValley;
+
+namespace StardewModdingAPI.Framework.ModHelpers
+{
+ /// <summary>Provides multiplayer utilities.</summary>
+ internal class MultiplayerHelper : BaseHelper, IMultiplayerHelper
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>SMAPI's core multiplayer utility.</summary>
+ private readonly SMultiplayer Multiplayer;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="modID">The unique ID of the relevant mod.</param>
+ /// <param name="multiplayer">SMAPI's core multiplayer utility.</param>
+ public MultiplayerHelper(string modID, SMultiplayer multiplayer)
+ : base(modID)
+ {
+ this.Multiplayer = multiplayer;
+ }
+
+ /// <summary>Get the locations which are being actively synced from the host.</summary>
+ public IEnumerable<GameLocation> GetActiveLocations()
+ {
+ return this.Multiplayer.activeLocations();
+ }
+
+ /// <summary>Get a new multiplayer ID.</summary>
+ public long GetNewID()
+ {
+ return this.Multiplayer.getNewID();
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs
index e5bf47f6..648d6742 100644
--- a/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs
@@ -107,122 +107,6 @@ namespace StardewModdingAPI.Framework.ModHelpers
);
}
-#if !STARDEW_VALLEY_1_3
- /****
- ** Obsolete
- ****/
- /// <summary>Get a private instance field.</summary>
- /// <typeparam name="TValue">The field type.</typeparam>
- /// <param name="obj">The object which has the field.</param>
- /// <param name="name">The field name.</param>
- /// <param name="required">Whether to throw an exception if the private field is not found.</param>
- /// <returns>Returns the field wrapper, or <c>null</c> if the field doesn't exist and <paramref name="required"/> is <c>false</c>.</returns>
- [Obsolete]
- public IPrivateField<TValue> GetPrivateField<TValue>(object obj, string name, bool required = true)
- {
- this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
- return (IPrivateField<TValue>)this.GetField<TValue>(obj, name, required);
- }
-
- /// <summary>Get a private static field.</summary>
- /// <typeparam name="TValue">The field type.</typeparam>
- /// <param name="type">The type which has the field.</param>
- /// <param name="name">The field name.</param>
- /// <param name="required">Whether to throw an exception if the private field is not found.</param>
- [Obsolete]
- public IPrivateField<TValue> GetPrivateField<TValue>(Type type, string name, bool required = true)
- {
- this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
- return (IPrivateField<TValue>)this.GetField<TValue>(type, name, required);
- }
-
- /// <summary>Get a private instance property.</summary>
- /// <typeparam name="TValue">The property type.</typeparam>
- /// <param name="obj">The object which has the property.</param>
- /// <param name="name">The property name.</param>
- /// <param name="required">Whether to throw an exception if the private property is not found.</param>
- [Obsolete]
- public IPrivateProperty<TValue> GetPrivateProperty<TValue>(object obj, string name, bool required = true)
- {
- this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
- return (IPrivateProperty<TValue>)this.GetProperty<TValue>(obj, name, required);
- }
-
- /// <summary>Get a private static property.</summary>
- /// <typeparam name="TValue">The property type.</typeparam>
- /// <param name="type">The type which has the property.</param>
- /// <param name="name">The property name.</param>
- /// <param name="required">Whether to throw an exception if the private property is not found.</param>
- [Obsolete]
- public IPrivateProperty<TValue> GetPrivateProperty<TValue>(Type type, string name, bool required = true)
- {
- this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
- return (IPrivateProperty<TValue>)this.GetProperty<TValue>(type, name, required);
- }
-
- /// <summary>Get the value of a private instance field.</summary>
- /// <typeparam name="TValue">The field type.</typeparam>
- /// <param name="obj">The object which has the field.</param>
- /// <param name="name">The field name.</param>
- /// <param name="required">Whether to throw an exception if the private field is not found.</param>
- /// <returns>Returns the field value, or the default value for <typeparamref name="TValue"/> if the field wasn't found and <paramref name="required"/> is false.</returns>
- /// <remarks>
- /// This is a shortcut for <see cref="GetPrivateField{TValue}(object,string,bool)"/> followed by <see cref="IPrivateField{TValue}.GetValue"/>.
- /// When <paramref name="required" /> is false, this will return the default value if reflection fails. If you need to check whether the field exists, use <see cref="GetPrivateField{TValue}(object,string,bool)" /> instead.
- /// </remarks>
- [Obsolete]
- public TValue GetPrivateValue<TValue>(object obj, string name, bool required = true)
- {
- this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
- IPrivateField<TValue> field = (IPrivateField<TValue>)this.GetField<TValue>(obj, name, required);
- return field != null
- ? field.GetValue()
- : default(TValue);
- }
-
- /// <summary>Get the value of a private static field.</summary>
- /// <typeparam name="TValue">The field type.</typeparam>
- /// <param name="type">The type which has the field.</param>
- /// <param name="name">The field name.</param>
- /// <param name="required">Whether to throw an exception if the private field is not found.</param>
- /// <returns>Returns the field value, or the default value for <typeparamref name="TValue"/> if the field wasn't found and <paramref name="required"/> is false.</returns>
- /// <remarks>
- /// This is a shortcut for <see cref="GetPrivateField{TValue}(Type,string,bool)"/> followed by <see cref="IPrivateField{TValue}.GetValue"/>.
- /// When <paramref name="required" /> is false, this will return the default value if reflection fails. If you need to check whether the field exists, use <see cref="GetPrivateField{TValue}(Type,string,bool)" /> instead.
- /// </remarks>
- [Obsolete]
- public TValue GetPrivateValue<TValue>(Type type, string name, bool required = true)
- {
- this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
- IPrivateField<TValue> field = (IPrivateField<TValue>)this.GetField<TValue>(type, name, required);
- return field != null
- ? field.GetValue()
- : default(TValue);
- }
-
- /// <summary>Get a private instance method.</summary>
- /// <param name="obj">The object which has the method.</param>
- /// <param name="name">The field name.</param>
- /// <param name="required">Whether to throw an exception if the private field is not found.</param>
- [Obsolete]
- public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true)
- {
- this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
- return (IPrivateMethod)this.GetMethod(obj, name, required);
- }
-
- /// <summary>Get a private static method.</summary>
- /// <param name="type">The type which has the method.</param>
- /// <param name="name">The field name.</param>
- /// <param name="required">Whether to throw an exception if the private field is not found.</param>
- [Obsolete]
- public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true)
- {
- this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
- return (IPrivateMethod)this.GetMethod(type, name, required);
- }
-#endif
-
/*********
** Private methods
diff --git a/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs b/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs
index d85a9a28..91c9e192 100644
--- a/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs
+++ b/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs
@@ -10,7 +10,7 @@ namespace StardewModdingAPI.Framework.ModLoading
** Properties
*********/
/// <summary>The known assemblies.</summary>
- private readonly IDictionary<string, AssemblyDefinition> Loaded = new Dictionary<string, AssemblyDefinition>();
+ private readonly IDictionary<string, AssemblyDefinition> Lookup = new Dictionary<string, AssemblyDefinition>();
/*********
@@ -22,8 +22,9 @@ namespace StardewModdingAPI.Framework.ModLoading
{
foreach (AssemblyDefinition assembly in assemblies)
{
- this.Loaded[assembly.Name.Name] = assembly;
- this.Loaded[assembly.Name.FullName] = assembly;
+ this.RegisterAssembly(assembly);
+ this.Lookup[assembly.Name.Name] = assembly;
+ this.Lookup[assembly.Name.FullName] = assembly;
}
}
@@ -36,15 +37,6 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <param name="parameters">The assembly reader parameters.</param>
public override AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters) => this.ResolveName(name.Name) ?? base.Resolve(name, parameters);
- /// <summary>Resolve an assembly reference.</summary>
- /// <param name="fullName">The assembly full name (including version, etc).</param>
- public override AssemblyDefinition Resolve(string fullName) => this.ResolveName(fullName) ?? base.Resolve(fullName);
-
- /// <summary>Resolve an assembly reference.</summary>
- /// <param name="fullName">The assembly full name (including version, etc).</param>
- /// <param name="parameters">The assembly reader parameters.</param>
- public override AssemblyDefinition Resolve(string fullName, ReaderParameters parameters) => this.ResolveName(fullName) ?? base.Resolve(fullName, parameters);
-
/*********
** Private methods
@@ -53,8 +45,8 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <param name="name">The assembly's short or full name.</param>
private AssemblyDefinition ResolveName(string name)
{
- return this.Loaded.ContainsKey(name)
- ? this.Loaded[name]
+ return this.Lookup.TryGetValue(name, out AssemblyDefinition match)
+ ? match
: null;
}
}
diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
index a60f63da..37b1a378 100644
--- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
+++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
@@ -6,12 +6,13 @@ using System.Reflection;
using Mono.Cecil;
using Mono.Cecil.Cil;
using StardewModdingAPI.Framework.Exceptions;
+using StardewModdingAPI.Internal;
using StardewModdingAPI.Metadata;
namespace StardewModdingAPI.Framework.ModLoading
{
/// <summary>Preprocesses and loads mod assemblies.</summary>
- internal class AssemblyLoader
+ internal class AssemblyLoader : IDisposable
{
/*********
** Properties
@@ -19,9 +20,6 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <summary>Encapsulates monitoring and logging.</summary>
private readonly IMonitor Monitor;
- /// <summary>Whether to enable developer mode logging.</summary>
- private readonly bool IsDeveloperMode;
-
/// <summary>Metadata for mapping assemblies to the current platform.</summary>
private readonly PlatformAssemblyMap AssemblyMap;
@@ -31,6 +29,9 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <summary>A minimal assembly definition resolver which resolves references to known loaded assemblies.</summary>
private readonly AssemblyDefinitionResolver AssemblyDefinitionResolver;
+ /// <summary>The objects to dispose as part of this instance.</summary>
+ private readonly HashSet<IDisposable> Disposables = new HashSet<IDisposable>();
+
/*********
** Public methods
@@ -38,13 +39,12 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <summary>Construct an instance.</summary>
/// <param name="targetPlatform">The current game platform.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
- /// <param name="isDeveloperMode">Whether to enable developer mode logging.</param>
- 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());
+ this.AssemblyDefinitionResolver.AddSearchDirectory(Constants.ExecutionPath);
// generate type => assembly lookup for types which should be rewritten
this.TypeAssemblies = new Dictionary<string, Assembly>();
@@ -98,13 +98,26 @@ namespace StardewModdingAPI.Framework.ModLoading
continue;
// rewrite assembly
- bool changed = this.RewriteAssembly(mod, assembly.Definition, assumeCompatible, loggedMessages, logPrefix: " ");
+ bool changed = this.RewriteAssembly(mod, assembly.Definition, assumeCompatible, loggedMessages, logPrefix: " ");
+
+ // detect broken assembly reference
+ foreach (AssemblyNameReference reference in assembly.Definition.MainModule.AssemblyReferences)
+ {
+ if (!reference.Name.StartsWith("System.") && !this.IsAssemblyLoaded(reference))
+ {
+ this.Monitor.LogOnce(loggedMessages, $" Broken code in {assembly.File.Name}: reference to missing assembly '{reference.FullName}'.");
+ if (!assumeCompatible)
+ throw new IncompatibleInstructionException($"assembly reference to {reference.FullName}", $"Found a reference to missing assembly '{reference.FullName}' while loading assembly {assembly.File.Name}.");
+ mod.SetWarning(ModWarning.BrokenCodeLoaded);
+ break;
+ }
+ }
// load assembly
if (changed)
{
if (!oneAssembly)
- this.Monitor.Log($" Loading {assembly.File.Name} (rewritten in memory)...", LogLevel.Trace);
+ this.Monitor.Log($" Loading {assembly.File.Name} (rewritten in memory)...", LogLevel.Trace);
using (MemoryStream outStream = new MemoryStream())
{
assembly.Definition.Write(outStream);
@@ -115,7 +128,7 @@ namespace StardewModdingAPI.Framework.ModLoading
else
{
if (!oneAssembly)
- this.Monitor.Log($" Loading {assembly.File.Name}...", LogLevel.Trace);
+ this.Monitor.Log($" Loading {assembly.File.Name}...", LogLevel.Trace);
lastAssembly = Assembly.UnsafeLoadFrom(assembly.File.FullName);
}
@@ -127,6 +140,20 @@ namespace StardewModdingAPI.Framework.ModLoading
return lastAssembly;
}
+ /// <summary>Get whether an assembly is loaded.</summary>
+ /// <param name="reference">The assembly name reference.</param>
+ public bool IsAssemblyLoaded(AssemblyNameReference reference)
+ {
+ try
+ {
+ return this.AssemblyDefinitionResolver.Resolve(reference) != null;
+ }
+ catch (AssemblyResolutionException)
+ {
+ return false;
+ }
+ }
+
/// <summary>Resolve an assembly by its name.</summary>
/// <param name="name">The assembly name.</param>
/// <remarks>
@@ -135,7 +162,7 @@ namespace StardewModdingAPI.Framework.ModLoading
/// assemblies (especially with Mono). Since this is meant to be called on <see cref="AppDomain.AssemblyResolve"/>,
/// the implicit assumption is that loading the exact assembly failed.
/// </remarks>
- public Assembly ResolveAssembly(string name)
+ public static Assembly ResolveAssembly(string name)
{
string shortName = name.Split(new[] { ',' }, 2).First(); // get simple name (without version and culture)
return AppDomain.CurrentDomain
@@ -143,10 +170,26 @@ namespace StardewModdingAPI.Framework.ModLoading
.FirstOrDefault(p => p.GetName().Name == shortName);
}
+ /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
+ public void Dispose()
+ {
+ foreach (IDisposable instance in this.Disposables)
+ instance.Dispose();
+ }
+
/*********
** Private methods
*********/
+ /// <summary>Track an object for disposal as part of the assembly loader.</summary>
+ /// <typeparam name="T">The instance type.</typeparam>
+ /// <param name="instance">The disposable instance.</param>
+ private T TrackForDisposal<T>(T instance) where T : IDisposable
+ {
+ this.Disposables.Add(instance);
+ return instance;
+ }
+
/****
** Assembly parsing
****/
@@ -165,9 +208,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))
@@ -284,33 +326,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, $"{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, $"{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, $"{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, $"{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/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs
index b5e45742..cf5a3175 100644
--- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs
+++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs
@@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.Linq;
-using System.Text.RegularExpressions;
using Mono.Cecil;
using Mono.Cecil.Cil;
@@ -16,9 +15,6 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
/// <summary>The assembly names to which to heuristically detect broken references.</summary>
private readonly HashSet<string> ValidateReferencesToAssemblies;
- /// <summary>A pattern matching type name substrings to strip for display.</summary>
- private readonly Regex StripTypeNamePattern = new Regex(@"`\d+(?=<)", RegexOptions.Compiled);
-
/*********
** Accessors
@@ -59,21 +55,15 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction);
if (fieldRef != null && this.ShouldValidate(fieldRef.DeclaringType))
{
- // can't compare generic type parameters between definition and reference
- if (fieldRef.FieldType.IsGenericInstance || fieldRef.FieldType.IsGenericParameter)
- return InstructionHandleResult.None;
-
// get target field
FieldDefinition targetField = fieldRef.DeclaringType.Resolve()?.Fields.FirstOrDefault(p => p.Name == fieldRef.Name);
if (targetField == null)
return InstructionHandleResult.None;
// validate return type
- string actualReturnTypeID = this.GetComparableTypeID(targetField.FieldType);
- string expectedReturnTypeID = this.GetComparableTypeID(fieldRef.FieldType);
- if (actualReturnTypeID != expectedReturnTypeID)
+ if (!RewriteHelper.LooksLikeSameType(fieldRef.FieldType, targetField.FieldType))
{
- this.NounPhrase = $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (field returns {this.GetFriendlyTypeName(targetField.FieldType, actualReturnTypeID)}, not {this.GetFriendlyTypeName(fieldRef.FieldType, expectedReturnTypeID)})";
+ this.NounPhrase = $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (field returns {this.GetFriendlyTypeName(targetField.FieldType)}, not {this.GetFriendlyTypeName(fieldRef.FieldType)})";
return InstructionHandleResult.NotCompatible;
}
}
@@ -82,10 +72,6 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
MethodReference methodReference = RewriteHelper.AsMethodReference(instruction);
if (methodReference != null && this.ShouldValidate(methodReference.DeclaringType))
{
- // can't compare generic type parameters between definition and reference
- if (methodReference.ReturnType.IsGenericInstance || methodReference.ReturnType.IsGenericParameter)
- return InstructionHandleResult.None;
-
// get potential targets
MethodDefinition[] candidateMethods = methodReference.DeclaringType.Resolve()?.Methods.Where(found => found.Name == methodReference.Name).ToArray();
if (candidateMethods == null || !candidateMethods.Any())
@@ -99,10 +85,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
return InstructionHandleResult.NotCompatible;
}
- string expectedReturnType = this.GetComparableTypeID(methodDef.ReturnType);
- if (candidateMethods.All(method => this.GetComparableTypeID(method.ReturnType) != expectedReturnType))
+ if (candidateMethods.All(method => !RewriteHelper.LooksLikeSameType(method.ReturnType, methodDef.ReturnType)))
{
- this.NounPhrase = $"reference to {methodDef.DeclaringType.FullName}.{methodDef.Name} (no such method returns {this.GetFriendlyTypeName(methodDef.ReturnType, expectedReturnType)})";
+ this.NounPhrase = $"reference to {methodDef.DeclaringType.FullName}.{methodDef.Name} (no such method returns {this.GetFriendlyTypeName(methodDef.ReturnType)})";
return InstructionHandleResult.NotCompatible;
}
}
@@ -121,17 +106,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
return type != null && this.ValidateReferencesToAssemblies.Contains(type.Scope.Name);
}
- /// <summary>Get a unique string representation of a type.</summary>
- /// <param name="type">The type reference.</param>
- private string GetComparableTypeID(TypeReference type)
- {
- return this.StripTypeNamePattern.Replace(type.FullName, "");
- }
-
/// <summary>Get a shorter type name for display.</summary>
/// <param name="type">The type reference.</param>
- /// <param name="typeID">The comparable type ID from <see cref="GetComparableTypeID"/>.</param>
- private string GetFriendlyTypeName(TypeReference type, string typeID)
+ private string GetFriendlyTypeName(TypeReference type)
{
// most common built-in types
switch (type.FullName)
@@ -148,10 +125,10 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
foreach (string @namespace in new[] { "Microsoft.Xna.Framework", "Netcode", "System", "System.Collections.Generic" })
{
if (type.Namespace == @namespace)
- return typeID.Substring(@namespace.Length + 1);
+ return type.Name;
}
- return typeID;
+ return type.FullName;
}
}
}
diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs
index f5e33313..b95dd79c 100644
--- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs
+++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs
@@ -67,12 +67,15 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
MethodReference methodRef = RewriteHelper.AsMethodReference(instruction);
if (methodRef != null && this.ShouldValidate(methodRef.DeclaringType) && !this.IsUnsupported(methodRef))
{
- MethodDefinition target = methodRef.DeclaringType.Resolve()?.Methods.FirstOrDefault(p => p.Name == methodRef.Name);
+ MethodDefinition target = methodRef.Resolve();
if (target == null)
{
- this.NounPhrase = this.IsProperty(methodRef)
- ? $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name.Substring(4)} (no such property)"
- : $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name} (no such method)";
+ if (this.IsProperty(methodRef))
+ this.NounPhrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name.Substring(4)} (no such property)";
+ else if (methodRef.Name == ".ctor")
+ this.NounPhrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name} (no matching constructor)";
+ else
+ this.NounPhrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name} (no such method)";
return InstructionHandleResult.NotCompatible;
}
}
diff --git a/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs
index 45349def..79045241 100644
--- a/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs
+++ b/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs
@@ -1,3 +1,4 @@
+using System;
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
@@ -16,6 +17,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
/// <summary>The result to return for matching instructions.</summary>
private readonly InstructionHandleResult Result;
+ /// <summary>A lambda which overrides a matched type.</summary>
+ protected readonly Func<TypeReference, bool> ShouldIgnore;
+
/*********
** Accessors
@@ -30,11 +34,13 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
/// <summary>Construct an instance.</summary>
/// <param name="fullTypeName">The full type name to match.</param>
/// <param name="result">The result to return for matching instructions.</param>
- public TypeFinder(string fullTypeName, InstructionHandleResult result)
+ /// <param name="shouldIgnore">A lambda which overrides a matched type.</param>
+ public TypeFinder(string fullTypeName, InstructionHandleResult result, Func<TypeReference, bool> shouldIgnore = null)
{
this.FullTypeName = fullTypeName;
this.Result = result;
this.NounPhrase = $"{fullTypeName} type";
+ this.ShouldIgnore = shouldIgnore ?? (p => false);
}
/// <summary>Perform the predefined logic for a method if applicable.</summary>
@@ -113,7 +119,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
protected bool IsMatch(TypeReference type)
{
// root type
- if (type.FullName == this.FullTypeName)
+ if (type.FullName == this.FullTypeName && !this.ShouldIgnore(type))
return true;
// generic arguments
diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs
index 1a0f9994..585debb4 100644
--- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs
+++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs
@@ -1,5 +1,7 @@
using System;
-using StardewModdingAPI.Framework.ModData;
+using System.Linq;
+using StardewModdingAPI.Toolkit.Framework.Clients.WebApi;
+using StardewModdingAPI.Toolkit.Framework.ModData;
namespace StardewModdingAPI.Framework.ModLoading
{
@@ -19,11 +21,14 @@ namespace StardewModdingAPI.Framework.ModLoading
public IManifest Manifest { get; }
/// <summary>Metadata about the mod from SMAPI's internal data (if any).</summary>
- public ParsedModDataRecord DataRecord { get; }
+ public ModDataRecordVersionedFields DataRecord { get; }
/// <summary>The metadata resolution status.</summary>
public ModMetadataStatus Status { get; private set; }
+ /// <summary>Indicates non-error issues with the mod.</summary>
+ public ModWarning Warnings { get; private set; }
+
/// <summary>The reason the metadata is invalid, if any.</summary>
public string Error { get; private set; }
@@ -39,6 +44,9 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <summary>The mod-provided API (if any).</summary>
public object Api { get; private set; }
+ /// <summary>The update-check metadata for this mod (if any).</summary>
+ public ModEntryModel UpdateCheckData { get; private set; }
+
/// <summary>Whether the mod is a content pack.</summary>
public bool IsContentPack => this.Manifest?.ContentPackFor != null;
@@ -51,7 +59,7 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <param name="directoryPath">The mod's full directory path.</param>
/// <param name="manifest">The mod manifest.</param>
/// <param name="dataRecord">Metadata about the mod from SMAPI's internal data (if any).</param>
- public ModMetadata(string displayName, string directoryPath, IManifest manifest, ParsedModDataRecord dataRecord)
+ public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModDataRecordVersionedFields dataRecord)
{
this.DisplayName = displayName;
this.DirectoryPath = directoryPath;
@@ -70,6 +78,14 @@ namespace StardewModdingAPI.Framework.ModLoading
return this;
}
+ /// <summary>Set a warning flag for the mod.</summary>
+ /// <param name="warning">The warning to set.</param>
+ public IModMetadata SetWarning(ModWarning warning)
+ {
+ this.Warnings |= warning;
+ return this;
+ }
+
/// <summary>Set the mod instance.</summary>
/// <param name="mod">The mod instance to set.</param>
public IModMetadata SetMod(IMod mod)
@@ -102,5 +118,36 @@ namespace StardewModdingAPI.Framework.ModLoading
this.Api = api;
return this;
}
+
+ /// <summary>Set the update-check metadata for this mod.</summary>
+ /// <param name="data">The update-check metadata.</param>
+ public IModMetadata SetUpdateData(ModEntryModel data)
+ {
+ this.UpdateCheckData = data;
+ return this;
+ }
+
+ /// <summary>Whether the mod manifest was loaded (regardless of whether the mod itself was loaded).</summary>
+ public bool HasManifest()
+ {
+ return this.Manifest != null;
+ }
+
+ /// <summary>Whether the mod has an ID (regardless of whether the ID is valid or the mod itself was loaded).</summary>
+ public bool HasID()
+ {
+ return
+ this.HasManifest()
+ && !string.IsNullOrWhiteSpace(this.Manifest.UniqueID);
+ }
+
+ /// <summary>Whether the mod has at least one update key set.</summary>
+ public bool HasUpdateKeys()
+ {
+ return
+ this.HasManifest()
+ && this.Manifest.UpdateKeys != null
+ && this.Manifest.UpdateKeys.Any(key => !string.IsNullOrWhiteSpace(key));
+ }
}
}
diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs
index a9896278..9ac95fd4 100644
--- a/src/SMAPI/Framework/ModLoading/ModResolver.cs
+++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs
@@ -2,11 +2,12 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using StardewModdingAPI.Framework.Exceptions;
-using StardewModdingAPI.Framework.ModData;
-using StardewModdingAPI.Framework.Models;
-using StardewModdingAPI.Framework.Serialisation;
-using StardewModdingAPI.Framework.Utilities;
+using System.Text.RegularExpressions;
+using StardewModdingAPI.Toolkit;
+using StardewModdingAPI.Toolkit.Framework.ModData;
+using StardewModdingAPI.Toolkit.Framework.ModScanning;
+using StardewModdingAPI.Toolkit.Serialisation.Models;
+using StardewModdingAPI.Toolkit.Utilities;
namespace StardewModdingAPI.Framework.ModLoading
{
@@ -17,46 +18,25 @@ namespace StardewModdingAPI.Framework.ModLoading
** Public methods
*********/
/// <summary>Get manifest metadata for each folder in the given root path.</summary>
+ /// <param name="toolkit">The mod toolkit.</param>
/// <param name="rootPath">The root path to search for mods.</param>
- /// <param name="jsonHelper">The JSON helper with which to read manifests.</param>
/// <param name="modDatabase">Handles access to SMAPI's internal mod metadata list.</param>
/// <returns>Returns the manifests by relative folder.</returns>
- public IEnumerable<IModMetadata> ReadManifests(string rootPath, JsonHelper jsonHelper, ModDatabase modDatabase)
+ public IEnumerable<IModMetadata> ReadManifests(ModToolkit toolkit, string rootPath, ModDatabase modDatabase)
{
- foreach (DirectoryInfo modDir in this.GetModFolders(rootPath))
+ foreach (ModFolder folder in toolkit.GetModFolders(rootPath))
{
- // read file
- Manifest manifest = null;
- string path = Path.Combine(modDir.FullName, "manifest.json");
- string error = null;
- try
- {
- manifest = jsonHelper.ReadJsonFile<Manifest>(path);
- if (manifest == null)
- {
- error = File.Exists(path)
- ? "its manifest is invalid."
- : "it doesn't have a manifest.";
- }
- }
- catch (SParseException ex)
- {
- error = $"parsing its manifest failed: {ex.Message}";
- }
- catch (Exception ex)
- {
- error = $"parsing its manifest failed:\n{ex.GetLogSummary()}";
- }
+ Manifest manifest = folder.Manifest;
// parse internal data record (if any)
- ParsedModDataRecord dataRecord = modDatabase.GetParsed(manifest);
+ ModDataRecordVersionedFields dataRecord = modDatabase.Get(manifest?.UniqueID)?.GetVersionedFields(manifest);
// get display name
string displayName = manifest?.Name;
if (string.IsNullOrWhiteSpace(displayName))
displayName = dataRecord?.DisplayName;
if (string.IsNullOrWhiteSpace(displayName))
- displayName = PathUtilities.GetRelativePath(rootPath, modDir.FullName);
+ displayName = PathUtilities.GetRelativePath(rootPath, folder.ActualDirectory?.FullName ?? folder.SearchDirectory.FullName);
// apply defaults
if (manifest != null && dataRecord != null)
@@ -66,10 +46,10 @@ namespace StardewModdingAPI.Framework.ModLoading
}
// build metadata
- ModMetadataStatus status = error == null
+ ModMetadataStatus status = folder.ManifestParseError == null
? ModMetadataStatus.Found
: ModMetadataStatus.Failed;
- yield return new ModMetadata(displayName, modDir.FullName, manifest, dataRecord).SetStatus(status, error);
+ yield return new ModMetadata(displayName, folder.ActualDirectory?.FullName, manifest, dataRecord).SetStatus(status, folder.ManifestParseError);
}
}
@@ -98,7 +78,7 @@ namespace StardewModdingAPI.Framework.ModLoading
case ModStatus.AssumeBroken:
{
// get reason
- string reasonPhrase = mod.DataRecord.StatusReasonPhrase ?? "it's outdated";
+ string reasonPhrase = mod.DataRecord.StatusReasonPhrase ?? "it's no longer compatible";
// get update URLs
List<string> updateUrls = new List<string>();
@@ -194,8 +174,15 @@ namespace StardewModdingAPI.Framework.ModLoading
missingFields.Add(nameof(IManifest.UniqueID));
if (missingFields.Any())
+ {
mod.SetStatus(ModMetadataStatus.Failed, $"its manifest is missing required fields ({string.Join(", ", missingFields)}).");
+ continue;
+ }
}
+
+ // validate ID format
+ if (Regex.IsMatch(mod.Manifest.UniqueID, "[^a-z0-9_.-]", RegexOptions.IgnoreCase))
+ mod.SetStatus(ModMetadataStatus.Failed, "its manifest specifies an invalid ID (IDs must only contain letters, numbers, underscores, periods, or hyphens).");
}
// validate IDs are unique
@@ -292,7 +279,7 @@ namespace StardewModdingAPI.Framework.ModLoading
string[] failedModNames = (
from entry in dependencies
where entry.IsRequired && entry.Mod == null
- let displayName = modDatabase.GetDisplayNameFor(entry.ID) ?? entry.ID
+ let displayName = modDatabase.Get(entry.ID)?.DisplayName ?? entry.ID
let modUrl = modDatabase.GetModPageUrlFor(entry.ID)
orderby displayName
select modUrl != null
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
+{
+ /// <summary>Indicates a detected non-error mod issue.</summary>
+ [Flags]
+ internal enum ModWarning
+ {
+ /// <summary>No issues detected.</summary>
+ None = 0,
+
+ /// <summary>SMAPI detected incompatible code in the mod, but was configured to load it anyway.</summary>
+ BrokenCodeLoaded = 1,
+
+ /// <summary>The mod affects the save serializer in a way that may make saves unloadable without the mod.</summary>
+ ChangesSaveSerialiser = 2,
+
+ /// <summary>The mod patches the game in a way that may impact stability.</summary>
+ PatchesGame = 4,
+
+ /// <summary>The mod uses the <c>dynamic</c> keyword which won't work on Linux/Mac.</summary>
+ UsesDynamic = 8,
+
+ /// <summary>The mod references <see cref="SpecialisedEvents.UnvalidatedUpdateTick"/> which may impact stability.</summary>
+ UsesUnvalidatedUpdateTick = 16,
+
+ /// <summary>The mod has no update keys set.</summary>
+ NoUpdateKeys = 32
+ }
+}
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
-{
- /// <summary>The game's platform version.</summary>
- internal enum Platform
- {
- /// <summary>The Linux/Mac version of the game.</summary>
- Mono,
-
- /// <summary>The Windows version of the game.</summary>
- Windows
- }
-}
diff --git a/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs b/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs
index 463f45e8..01460dce 100644
--- a/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs
+++ b/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs
@@ -1,12 +1,14 @@
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Mono.Cecil;
+using StardewModdingAPI.Internal;
namespace StardewModdingAPI.Framework.ModLoading
{
/// <summary>Metadata for mapping assemblies to the current <see cref="Platform"/>.</summary>
- internal class PlatformAssemblyMap
+ internal class PlatformAssemblyMap : IDisposable
{
/*********
** Accessors
@@ -49,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 }));
+ }
+
+ /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
+ public void Dispose()
+ {
+ foreach (ModuleDefinition module in this.TargetModules.Values)
+ module.Dispose();
}
}
}
diff --git a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs
index 56a60a72..9ff43d45 100644
--- a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs
+++ b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs
@@ -10,6 +10,13 @@ namespace StardewModdingAPI.Framework.ModLoading
internal static class RewriteHelper
{
/*********
+ ** Properties
+ *********/
+ /// <summary>The comparer which heuristically compares type definitions.</summary>
+ private static readonly TypeReferenceComparer TypeDefinitionComparer = new TypeReferenceComparer();
+
+
+ /*********
** Public methods
*********/
/// <summary>Get the field reference from an instruction if it matches.</summary>
@@ -25,7 +32,7 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <param name="instruction">The IL instruction.</param>
public static MethodReference AsMethodReference(Instruction instruction)
{
- return instruction.OpCode == OpCodes.Call || instruction.OpCode == OpCodes.Callvirt
+ return instruction.OpCode == OpCodes.Call || instruction.OpCode == OpCodes.Callvirt || instruction.OpCode == OpCodes.Newobj
? (MethodReference)instruction.Operand
: null;
}
@@ -59,6 +66,15 @@ namespace StardewModdingAPI.Framework.ModLoading
return true;
}
+ /// <summary>Determine whether two type IDs look like the same type, accounting for placeholder values such as !0.</summary>
+ /// <param name="typeA">The type ID to compare.</param>
+ /// <param name="typeB">The other type ID to compare.</param>
+ /// <returns>true if the type IDs look like the same type, false if not.</returns>
+ public static bool LooksLikeSameType(TypeReference typeA, TypeReference typeB)
+ {
+ return RewriteHelper.TypeDefinitionComparer.Equals(typeA, typeB);
+ }
+
/// <summary>Get whether a method definition matches the signature expected by a method reference.</summary>
/// <param name="definition">The method definition.</param>
/// <param name="reference">The method reference.</param>
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..62e15731 100644
--- a/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs
+++ b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs
@@ -24,8 +24,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
/// <summary>Construct an instance.</summary>
/// <param name="fromTypeFullName">The full type name to which to find references.</param>
/// <param name="toType">The new type to reference.</param>
- public TypeReferenceRewriter(string fromTypeFullName, Type toType)
- : base(fromTypeFullName, InstructionHandleResult.None)
+ /// <param name="shouldIgnore">A lambda which overrides a matched type.</param>
+ public TypeReferenceRewriter(string fromTypeFullName, Type toType, Func<TypeReference, bool> shouldIgnore = null)
+ : base(fromTypeFullName, InstructionHandleResult.None, shouldIgnore)
{
this.FromTypeName = fromTypeFullName;
this.ToType = toType;
@@ -43,7 +44,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
// return type
if (this.IsMatch(method.ReturnType))
{
- method.ReturnType = this.RewriteIfNeeded(module, method.ReturnType);
+ this.RewriteIfNeeded(module, method.ReturnType, newType => method.ReturnType = newType);
rewritten = true;
}
@@ -52,7 +53,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
{
if (this.IsMatch(parameter.ParameterType))
{
- parameter.ParameterType = this.RewriteIfNeeded(module, parameter.ParameterType);
+ this.RewriteIfNeeded(module, parameter.ParameterType, newType => parameter.ParameterType = newType);
rewritten = true;
}
}
@@ -63,9 +64,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
var parameter = method.GenericParameters[i];
if (this.IsMatch(parameter))
{
- TypeReference newType = this.RewriteIfNeeded(module, parameter);
- if (newType != parameter)
- method.GenericParameters[i] = new GenericParameter(parameter.Name, newType);
+ this.RewriteIfNeeded(module, parameter, newType => method.GenericParameters[i] = new GenericParameter(parameter.Name, newType));
rewritten = true;
}
}
@@ -75,7 +74,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
{
if (this.IsMatch(variable.VariableType))
{
- variable.VariableType = this.RewriteIfNeeded(module, variable.VariableType);
+ this.RewriteIfNeeded(module, variable.VariableType, newType => variable.VariableType = newType);
rewritten = true;
}
}
@@ -93,34 +92,30 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
/// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged)
{
- if (!this.IsMatch(instruction) && !instruction.ToString().Contains(this.FromTypeName))
+ if (!this.IsMatch(instruction))
return InstructionHandleResult.None;
// field reference
FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction);
if (fieldRef != null)
{
- fieldRef.DeclaringType = this.RewriteIfNeeded(module, fieldRef.DeclaringType);
- fieldRef.FieldType = this.RewriteIfNeeded(module, fieldRef.FieldType);
+ this.RewriteIfNeeded(module, fieldRef.DeclaringType, newType => fieldRef.DeclaringType = newType);
+ this.RewriteIfNeeded(module, fieldRef.FieldType, newType => fieldRef.FieldType = newType);
}
// method reference
MethodReference methodRef = RewriteHelper.AsMethodReference(instruction);
if (methodRef != null)
{
- methodRef.DeclaringType = this.RewriteIfNeeded(module, methodRef.DeclaringType);
- methodRef.ReturnType = this.RewriteIfNeeded(module, methodRef.ReturnType);
+ this.RewriteIfNeeded(module, methodRef.DeclaringType, newType => methodRef.DeclaringType = newType);
+ this.RewriteIfNeeded(module, methodRef.ReturnType, newType => methodRef.ReturnType = newType);
foreach (var parameter in methodRef.Parameters)
- parameter.ParameterType = this.RewriteIfNeeded(module, parameter.ParameterType);
+ this.RewriteIfNeeded(module, parameter.ParameterType, newType => parameter.ParameterType = newType);
}
// type reference
if (instruction.Operand is TypeReference typeRef)
- {
- TypeReference newRef = this.RewriteIfNeeded(module, typeRef);
- if (typeRef != newRef)
- cil.Replace(instruction, cil.Create(instruction.OpCode, newRef));
- }
+ this.RewriteIfNeeded(module, typeRef, newType => cil.Replace(instruction, cil.Create(instruction.OpCode, newType)));
return InstructionHandleResult.Rewritten;
}
@@ -128,27 +123,30 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
/*********
** Private methods
*********/
- /// <summary>Get the adjusted type reference if it matches, else the same value.</summary>
+ /// <summary>Change a type reference if needed.</summary>
/// <param name="module">The assembly module containing the instruction.</param>
/// <param name="type">The type to replace if it matches.</param>
- private TypeReference RewriteIfNeeded(ModuleDefinition module, TypeReference type)
+ /// <param name="set">Assign the new type reference.</param>
+ private void RewriteIfNeeded(ModuleDefinition module, TypeReference type, Action<TypeReference> set)
{
- // root type
+ // current type
if (type.FullName == this.FromTypeName)
- return module.Import(this.ToType);
+ {
+ if (!this.ShouldIgnore(type))
+ set(module.ImportReference(this.ToType));
+ return;
+ }
- // generic arguments
+ // recurse into generic arguments
if (type is GenericInstanceType genericType)
{
for (int i = 0; i < genericType.GenericArguments.Count; i++)
- genericType.GenericArguments[i] = this.RewriteIfNeeded(module, genericType.GenericArguments[i]);
+ this.RewriteIfNeeded(module, genericType.GenericArguments[i], typeRef => genericType.GenericArguments[i] = typeRef);
}
- // generic parameters (e.g. constraints)
+ // recurse into generic parameters (e.g. constraints)
for (int i = 0; i < type.GenericParameters.Count; i++)
- type.GenericParameters[i] = new GenericParameter(this.RewriteIfNeeded(module, type.GenericParameters[i]));
-
- return type;
+ this.RewriteIfNeeded(module, type.GenericParameters[i], typeRef => type.GenericParameters[i] = new GenericParameter(typeRef));
}
}
}
diff --git a/src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs b/src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs
new file mode 100644
index 00000000..f7497789
--- /dev/null
+++ b/src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs
@@ -0,0 +1,201 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Mono.Cecil;
+
+namespace StardewModdingAPI.Framework.ModLoading
+{
+ /// <summary>Performs heuristic equality checks for <see cref="TypeReference"/> instances.</summary>
+ /// <remarks>
+ /// This implementation compares <see cref="TypeReference"/> instances to see if they likely
+ /// refer to the same type. While the implementation is obvious for types like <c>System.Bool</c>,
+ /// this class mainly exists to handle cases like <c>System.Collections.Generic.Dictionary`2&lt;!0,Netcode.NetRoot`1&lt;!1&gt;&gt;</c>
+ /// and <c>System.Collections.Generic.Dictionary`2&lt;TKey,Netcode.NetRoot`1&lt;TValue&gt;&gt;</c>
+ /// which are compatible, but not directly comparable. It does this by splitting each type name
+ /// into its component token types, and performing placeholder substitution (e.g. <c>!0</c> to
+ /// <c>TKey</c> in the above example). If all components are equal after substitution, and the
+ /// tokens can all be mapped to the same generic type, the types are considered equal.
+ /// </remarks>
+ internal class TypeReferenceComparer : IEqualityComparer<TypeReference>
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Get whether the specified objects are equal.</summary>
+ /// <param name="a">The first object to compare.</param>
+ /// <param name="b">The second object to compare.</param>
+ public bool Equals(TypeReference a, TypeReference b)
+ {
+ if (a == null || b == null)
+ return a == b;
+
+ return
+ a == b
+ || a.FullName == b.FullName
+ || this.HeuristicallyEquals(a, b);
+ }
+
+ /// <summary>Get a hash code for the specified object.</summary>
+ /// <param name="obj">The object for which a hash code is to be returned.</param>
+ /// <exception cref="T:System.ArgumentNullException">The object type is a reference type and <paramref name="obj" /> is null.</exception>
+ public int GetHashCode(TypeReference obj)
+ {
+ return obj.GetHashCode();
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get whether two types are heuristically equal based on generic type token substitution.</summary>
+ /// <param name="typeA">The first type to compare.</param>
+ /// <param name="typeB">The second type to compare.</param>
+ private bool HeuristicallyEquals(TypeReference typeA, TypeReference typeB)
+ {
+ bool HeuristicallyEquals(string typeNameA, string typeNameB, IDictionary<string, string> tokenMap)
+ {
+ // analyse type names
+ bool hasTokensA = typeNameA.Contains("!");
+ bool hasTokensB = typeNameB.Contains("!");
+ bool isTokenA = hasTokensA && typeNameA[0] == '!';
+ bool isTokenB = hasTokensB && typeNameB[0] == '!';
+
+ // validate
+ if (!hasTokensA && !hasTokensB)
+ return typeNameA == typeNameB; // no substitution needed
+ if (hasTokensA && hasTokensB)
+ throw new InvalidOperationException("Can't compare two type names when both contain generic type tokens.");
+
+ // perform substitution if applicable
+ if (isTokenA)
+ typeNameA = this.MapPlaceholder(placeholder: typeNameA, type: typeNameB, map: tokenMap);
+ if (isTokenB)
+ typeNameB = this.MapPlaceholder(placeholder: typeNameB, type: typeNameA, map: tokenMap);
+
+ // compare inner tokens
+ string[] symbolsA = this.GetTypeSymbols(typeNameA).ToArray();
+ string[] symbolsB = this.GetTypeSymbols(typeNameB).ToArray();
+ if (symbolsA.Length != symbolsB.Length)
+ return false;
+
+ for (int i = 0; i < symbolsA.Length; i++)
+ {
+ if (!HeuristicallyEquals(symbolsA[i], symbolsB[i], tokenMap))
+ return false;
+ }
+
+ return true;
+ }
+
+ return HeuristicallyEquals(typeA.FullName, typeB.FullName, new Dictionary<string, string>());
+ }
+
+ /// <summary>Map a generic type placeholder (like <c>!0</c>) to its actual type.</summary>
+ /// <param name="placeholder">The token placeholder.</param>
+ /// <param name="type">The actual type.</param>
+ /// <param name="map">The map of token to map substitutions.</param>
+ /// <returns>Returns the previously-mapped type if applicable, else the <paramref name="type"/>.</returns>
+ private string MapPlaceholder(string placeholder, string type, IDictionary<string, string> map)
+ {
+ if (map.TryGetValue(placeholder, out string result))
+ return result;
+
+ map[placeholder] = type;
+ return type;
+ }
+
+ /// <summary>Get the top-level type symbols in a type name (e.g. <code>List</code> and <code>NetRef&lt;T&gt;</code> in <code>List&lt;NetRef&lt;T&gt;&gt;</code>)</summary>
+ /// <param name="typeName">The full type name.</param>
+ private IEnumerable<string> GetTypeSymbols(string typeName)
+ {
+ int openGenerics = 0;
+
+ Queue<char> queue = new Queue<char>(typeName);
+ string symbol = "";
+ while (queue.Any())
+ {
+ char ch = queue.Dequeue();
+ switch (ch)
+ {
+ // skip `1 generic type identifiers
+ case '`':
+ while (int.TryParse(queue.Peek().ToString(), out int _))
+ queue.Dequeue();
+ break;
+
+ // start generic args
+ case '<':
+ switch (openGenerics)
+ {
+ // start new generic symbol
+ case 0:
+ yield return symbol;
+ symbol = "";
+ openGenerics++;
+ break;
+
+ // continue accumulating nested type symbol
+ default:
+ symbol += ch;
+ openGenerics++;
+ break;
+ }
+ break;
+
+ // generic args delimiter
+ case ',':
+ switch (openGenerics)
+ {
+ // invalid
+ case 0:
+ throw new InvalidOperationException($"Encountered unexpected comma in type name: {typeName}.");
+
+ // start next generic symbol
+ case 1:
+ yield return symbol;
+ symbol = "";
+ break;
+
+ // continue accumulating nested type symbol
+ default:
+ symbol += ch;
+ break;
+ }
+ break;
+
+
+ // end generic args
+ case '>':
+ switch (openGenerics)
+ {
+ // invalid
+ case 0:
+ throw new InvalidOperationException($"Encountered unexpected closing generic in type name: {typeName}.");
+
+ // end generic symbol
+ case 1:
+ yield return symbol;
+ symbol = "";
+ openGenerics--;
+ break;
+
+ // continue accumulating nested type symbol
+ default:
+ symbol += ch;
+ openGenerics--;
+ break;
+ }
+ break;
+
+ // continue symbol
+ default:
+ symbol += ch;
+ break;
+ }
+ }
+
+ if (symbol != "")
+ yield return symbol;
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/Models/Manifest.cs b/src/SMAPI/Framework/Models/Manifest.cs
deleted file mode 100644
index f5867cf3..00000000
--- a/src/SMAPI/Framework/Models/Manifest.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-using System.Collections.Generic;
-using Newtonsoft.Json;
-using StardewModdingAPI.Framework.Serialisation.SmapiConverters;
-
-namespace StardewModdingAPI.Framework.Models
-{
- /// <summary>A manifest which describes a mod for SMAPI.</summary>
- internal class Manifest : IManifest
- {
- /*********
- ** Accessors
- *********/
- /// <summary>The mod name.</summary>
- public string Name { get; set; }
-
- /// <summary>A brief description of the mod.</summary>
- public string Description { get; set; }
-
- /// <summary>The mod author's name.</summary>
- public string Author { get; set; }
-
- /// <summary>The mod version.</summary>
- public ISemanticVersion Version { get; set; }
-
- /// <summary>The minimum SMAPI version required by this mod, if any.</summary>
- public ISemanticVersion MinimumApiVersion { get; set; }
-
- /// <summary>The name of the DLL in the directory that has the <see cref="IMod.Entry"/> method. Mutually exclusive with <see cref="ContentPackFor"/>.</summary>
- public string EntryDll { get; set; }
-
- /// <summary>The mod which will read this as a content pack. Mutually exclusive with <see cref="IManifest.EntryDll"/>.</summary>
- [JsonConverter(typeof(ManifestContentPackForConverter))]
- public IManifestContentPackFor ContentPackFor { get; set; }
-
- /// <summary>The other mods that must be loaded before this mod.</summary>
- [JsonConverter(typeof(ManifestDependencyArrayConverter))]
- public IManifestDependency[] Dependencies { get; set; }
-
- /// <summary>The namespaced mod IDs to query for updates (like <c>Nexus:541</c>).</summary>
- public string[] UpdateKeys { get; set; }
-
- /// <summary>The unique mod ID.</summary>
- public string UniqueID { get; set; }
-
- /// <summary>Any manifest fields which didn't match a valid field.</summary>
- [JsonExtensionData]
- public IDictionary<string, object> ExtraFields { get; set; }
- }
-}
diff --git a/src/SMAPI/Framework/Models/ManifestContentPackFor.cs b/src/SMAPI/Framework/Models/ManifestContentPackFor.cs
deleted file mode 100644
index 7836bbcc..00000000
--- a/src/SMAPI/Framework/Models/ManifestContentPackFor.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-namespace StardewModdingAPI.Framework.Models
-{
- /// <summary>Indicates which mod can read the content pack represented by the containing manifest.</summary>
- internal class ManifestContentPackFor : IManifestContentPackFor
- {
- /*********
- ** Accessors
- *********/
- /// <summary>The unique ID of the mod which can read this content pack.</summary>
- public string UniqueID { get; set; }
-
- /// <summary>The minimum required version (if any).</summary>
- public ISemanticVersion MinimumVersion { get; set; }
- }
-}
diff --git a/src/SMAPI/Framework/Models/ManifestDependency.cs b/src/SMAPI/Framework/Models/ManifestDependency.cs
deleted file mode 100644
index 97f0775a..00000000
--- a/src/SMAPI/Framework/Models/ManifestDependency.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-namespace StardewModdingAPI.Framework.Models
-{
- /// <summary>A mod dependency listed in a mod manifest.</summary>
- internal class ManifestDependency : IManifestDependency
- {
- /*********
- ** Accessors
- *********/
- /// <summary>The unique mod ID to require.</summary>
- public string UniqueID { get; set; }
-
- /// <summary>The minimum required version (if any).</summary>
- public ISemanticVersion MinimumVersion { get; set; }
-
- /// <summary>Whether the dependency must be installed to use the mod.</summary>
- public bool IsRequired { get; set; }
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Construct an instance.</summary>
- /// <param name="uniqueID">The unique mod ID to require.</param>
- /// <param name="minimumVersion">The minimum required version (if any).</param>
- /// <param name="required">Whether the dependency must be installed to use the mod.</param>
- 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/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
+{
+ /// <summary>Metadata exported to the mod folder.</summary>
+ internal class ModFolderExport
+ {
+ /// <summary>When the export was generated.</summary>
+ public string Exported { get; set; }
+
+ /// <summary>The absolute path of the mod folder.</summary>
+ public string ModFolderPath { get; set; }
+
+ /// <summary>The game version which last loaded the mods.</summary>
+ public string GameVersion { get; set; }
+
+ /// <summary>The SMAPI version which last loaded the mods.</summary>
+ public string ApiVersion { get; set; }
+
+ /// <summary>The detected mods.</summary>
+ public IModMetadata[] Mods { get; set; }
+ }
+}
diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs
index 17169714..15671af4 100644
--- a/src/SMAPI/Framework/Models/SConfig.cs
+++ b/src/SMAPI/Framework/Models/SConfig.cs
@@ -1,5 +1,4 @@
-using System.Collections.Generic;
-using StardewModdingAPI.Framework.ModData;
+using StardewModdingAPI.Internal.ConsoleWriting;
namespace StardewModdingAPI.Framework.Models
{
@@ -15,6 +14,9 @@ namespace StardewModdingAPI.Framework.Models
/// <summary>Whether to check for newer versions of SMAPI and mods on startup.</summary>
public bool CheckForUpdates { get; set; }
+ /// <summary>Whether to show beta versions as valid updates.</summary>
+ public bool UseBetaChannel { get; set; } = Constants.ApiVersion.IsPrerelease();
+
/// <summary>SMAPI's GitHub project name, used to perform update checks.</summary>
public string GitHubProjectName { get; set; }
@@ -24,7 +26,13 @@ namespace StardewModdingAPI.Framework.Models
/// <summary>Whether SMAPI should log more information about the game context.</summary>
public bool VerboseLogging { get; set; }
- /// <summary>Extra metadata about mods.</summary>
- public IDictionary<string, ModDataRecord> ModData { get; set; }
+ /// <summary>Whether to generate a file in the mods folder with detailed metadata about the detected mods.</summary>
+ public bool DumpMetadata { get; set; }
+
+ /// <summary>The console color scheme to use.</summary>
+ public MonitorColorScheme ColorScheme { get; set; }
+
+ /// <summary>The mod IDs SMAPI should ignore when performing update checks or validating update keys.</summary>
+ public string[] SuppressUpdateChecks { get; set; }
}
}
diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs
index bf338386..2812a9cc 100644
--- a/src/SMAPI/Framework/Monitor.cs
+++ b/src/SMAPI/Framework/Monitor.cs
@@ -1,8 +1,8 @@
using System;
-using System.Collections.Generic;
using System.Linq;
using System.Threading;
using StardewModdingAPI.Framework.Logging;
+using StardewModdingAPI.Internal.ConsoleWriting;
namespace StardewModdingAPI.Framework
{
@@ -15,8 +15,11 @@ namespace StardewModdingAPI.Framework
/// <summary>The name of the module which logs messages using this instance.</summary>
private readonly string Source;
+ /// <summary>Handles writing color-coded text to the console.</summary>
+ private readonly ColorfulConsoleWriter ConsoleWriter;
+
/// <summary>Manages access to the console output.</summary>
- private readonly ConsoleInterceptionManager ConsoleManager;
+ private readonly ConsoleInterceptionManager ConsoleInterceptor;
/// <summary>The log file to which to write messages.</summary>
private readonly LogFileManager LogFile;
@@ -24,9 +27,6 @@ namespace StardewModdingAPI.Framework
/// <summary>The maximum length of the <see cref="LogLevel"/> values.</summary>
private static readonly int MaxLevelLength = (from level in Enum.GetValues(typeof(LogLevel)).Cast<LogLevel>() select level.ToString().Length).Max();
- /// <summary>The console text color for each log level.</summary>
- private static readonly IDictionary<LogLevel, ConsoleColor> Colors = Monitor.GetConsoleColorScheme();
-
/// <summary>Propagates notification that SMAPI should exit.</summary>
private readonly CancellationTokenSource ExitTokenSource;
@@ -46,19 +46,17 @@ namespace StardewModdingAPI.Framework
/// <summary>Whether to write anything to the console. This should be disabled if no console is available.</summary>
internal bool WriteToConsole { get; set; } = true;
- /// <summary>Whether to write anything to the log file. This should almost always be enabled.</summary>
- internal bool WriteToFile { get; set; } = true;
-
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="source">The name of the module which logs messages using this instance.</param>
- /// <param name="consoleManager">Manages access to the console output.</param>
+ /// <param name="consoleInterceptor">Intercepts access to the console output.</param>
/// <param name="logFile">The log file to which to write messages.</param>
/// <param name="exitTokenSource">Propagates notification that SMAPI should exit.</param>
- public Monitor(string source, ConsoleInterceptionManager consoleManager, LogFileManager logFile, CancellationTokenSource exitTokenSource)
+ /// <param name="colorScheme">The console color scheme to use.</param>
+ public Monitor(string source, ConsoleInterceptionManager consoleInterceptor, LogFileManager logFile, CancellationTokenSource exitTokenSource, MonitorColorScheme colorScheme)
{
// validate
if (string.IsNullOrWhiteSpace(source))
@@ -67,7 +65,8 @@ namespace StardewModdingAPI.Framework
// initialise
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;
}
@@ -76,7 +75,7 @@ namespace StardewModdingAPI.Framework
/// <param name="level">The log severity level.</param>
public void Log(string message, LogLevel level = LogLevel.Debug)
{
- this.LogImpl(this.Source, message, level, Monitor.Colors[level]);
+ this.LogImpl(this.Source, message, (ConsoleLogLevel)level);
}
/// <summary>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.</summary>
@@ -91,9 +90,17 @@ namespace StardewModdingAPI.Framework
internal void Newline()
{
if (this.WriteToConsole)
- this.ConsoleManager.ExclusiveWriteWithoutInterception(Console.WriteLine);
- if (this.WriteToFile)
- this.LogFile.WriteLine("");
+ this.ConsoleInterceptor.ExclusiveWriteWithoutInterception(Console.WriteLine);
+ this.LogFile.WriteLine("");
+ }
+
+ /// <summary>Log console input from the user.</summary>
+ /// <param name="input">The user input to log.</param>
+ 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, (ConsoleLogLevel)LogLevel.Info);
+ this.LogFile.WriteLine($"{prefix} $>{input}");
}
@@ -104,91 +111,40 @@ namespace StardewModdingAPI.Framework
/// <param name="message">The message to log.</param>
private void LogFatal(string message)
{
- this.LogImpl(this.Source, message, LogLevel.Error, ConsoleColor.White, background: ConsoleColor.Red);
+ this.LogImpl(this.Source, message, ConsoleLogLevel.Critical);
}
/// <summary>Write a message line to the log.</summary>
/// <param name="source">The name of the mod logging the message.</param>
/// <param name="message">The message to log.</param>
/// <param name="level">The log level.</param>
- /// <param name="color">The console foreground color.</param>
- /// <param name="background">The console background color (or <c>null</c> to leave it as-is).</param>
- 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 levelStr = level.ToString().ToUpper().PadRight(Monitor.MaxLevelLength);
-
- string fullMessage = $"[{DateTime.Now:HH:mm:ss} {levelStr} {source}] {message}";
+ string prefix = this.GenerateMessagePrefix(source, level);
+ string fullMessage = $"{prefix} {message}";
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);
});
}
// write to log file
- if (this.WriteToFile)
- this.LogFile.WriteLine(fullMessage);
+ this.LogFile.WriteLine(fullMessage);
}
- /// <summary>Get the color scheme to use for the current console.</summary>
- private static IDictionary<LogLevel, ConsoleColor> GetConsoleColorScheme()
- {
- // scheme for dark console background
- if (Monitor.IsDark(Console.BackgroundColor))
- {
- return new Dictionary<LogLevel, ConsoleColor>
- {
- [LogLevel.Trace] = ConsoleColor.DarkGray,
- [LogLevel.Debug] = ConsoleColor.DarkGray,
- [LogLevel.Info] = ConsoleColor.White,
- [LogLevel.Warn] = ConsoleColor.Yellow,
- [LogLevel.Error] = ConsoleColor.Red,
- [LogLevel.Alert] = ConsoleColor.Magenta
- };
- }
-
- // scheme for light console background
- return new Dictionary<LogLevel, ConsoleColor>
- {
- [LogLevel.Trace] = ConsoleColor.DarkGray,
- [LogLevel.Debug] = ConsoleColor.DarkGray,
- [LogLevel.Info] = ConsoleColor.Black,
- [LogLevel.Warn] = ConsoleColor.DarkYellow,
- [LogLevel.Error] = ConsoleColor.Red,
- [LogLevel.Alert] = ConsoleColor.DarkMagenta
- };
- }
-
- /// <summary>Get whether a console color should be considered dark, which is subjectively defined as 'white looks better than black on this text'.</summary>
- /// <param name="color">The color to check.</param>
- private static bool IsDark(ConsoleColor color)
+ /// <summary>Generate a message prefix for the current time.</summary>
+ /// <param name="source">The name of the mod logging the message.</param>
+ /// <param name="level">The log level.</param>
+ private string GenerateMessagePrefix(string source, ConsoleLogLevel level)
{
- switch (color)
- {
- case ConsoleColor.Black:
- case ConsoleColor.Blue:
- case ConsoleColor.DarkBlue:
- case ConsoleColor.DarkRed:
- case ConsoleColor.Red:
- return true;
-
- default:
- return false;
- }
+ string levelStr = level.ToString().ToUpper().PadRight(Monitor.MaxLevelLength);
+ return $"[{DateTime.Now:HH:mm:ss} {levelStr} {source}]";
}
}
}
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
+{
+ /// <summary>Encapsulates applying Harmony patches to the game.</summary>
+ internal class GamePatcher
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>Encapsulates monitoring and logging.</summary>
+ private readonly IMonitor Monitor;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="monitor">Encapsulates monitoring and logging.</param>
+ public GamePatcher(IMonitor monitor)
+ {
+ this.Monitor = monitor;
+ }
+
+ /// <summary>Apply all loaded patches to the game.</summary>
+ /// <param name="patches">The patches to apply.</param>
+ 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
+{
+ /// <summary>A Harmony patch to apply.</summary>
+ internal interface IHarmonyPatch
+ {
+ /// <summary>A unique name for this patch.</summary>
+ string Name { get; }
+
+ /// <summary>Apply the Harmony patch.</summary>
+ /// <param name="harmony">The Harmony instance.</param>
+ void Apply(HarmonyInstance harmony);
+ }
+}
diff --git a/src/SMAPI/Framework/Reflection/ReflectedField.cs b/src/SMAPI/Framework/Reflection/ReflectedField.cs
index fb420dc5..09638b1d 100644
--- a/src/SMAPI/Framework/Reflection/ReflectedField.cs
+++ b/src/SMAPI/Framework/Reflection/ReflectedField.cs
@@ -6,9 +6,6 @@ namespace StardewModdingAPI.Framework.Reflection
/// <summary>A field obtained through reflection.</summary>
/// <typeparam name="TValue">The field value type.</typeparam>
internal class ReflectedField<TValue> : IReflectedField<TValue>
-#if !STARDEW_VALLEY_1_3
- , IPrivateField<TValue>
-#endif
{
/*********
** Properties
diff --git a/src/SMAPI/Framework/Reflection/ReflectedMethod.cs b/src/SMAPI/Framework/Reflection/ReflectedMethod.cs
index 803bc316..7d9072a0 100644
--- a/src/SMAPI/Framework/Reflection/ReflectedMethod.cs
+++ b/src/SMAPI/Framework/Reflection/ReflectedMethod.cs
@@ -5,9 +5,6 @@ namespace StardewModdingAPI.Framework.Reflection
{
/// <summary>A method obtained through reflection.</summary>
internal class ReflectedMethod : IReflectedMethod
-#if !STARDEW_VALLEY_1_3
- , IPrivateMethod
-#endif
{
/*********
** Properties
diff --git a/src/SMAPI/Framework/Reflection/ReflectedProperty.cs b/src/SMAPI/Framework/Reflection/ReflectedProperty.cs
index 4f9d4e19..d59b71ac 100644
--- a/src/SMAPI/Framework/Reflection/ReflectedProperty.cs
+++ b/src/SMAPI/Framework/Reflection/ReflectedProperty.cs
@@ -6,9 +6,6 @@ namespace StardewModdingAPI.Framework.Reflection
/// <summary>A property obtained through reflection.</summary>
/// <typeparam name="TValue">The property value type.</typeparam>
internal class ReflectedProperty<TValue> : IReflectedProperty<TValue>
-#if !STARDEW_VALLEY_1_3
- , IPrivateProperty<TValue>
-#endif
{
/*********
** Properties
diff --git a/src/SMAPI/Framework/RewriteFacades/SpriteBatchMethods.cs b/src/SMAPI/Framework/RewriteFacades/SpriteBatchMethods.cs
new file mode 100644
index 00000000..26b22315
--- /dev/null
+++ b/src/SMAPI/Framework/RewriteFacades/SpriteBatchMethods.cs
@@ -0,0 +1,61 @@
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+#pragma warning disable 1591 // missing documentation
+namespace StardewModdingAPI.Framework.RewriteFacades
+{
+ /// <summary>Provides <see cref="SpriteBatch"/> method signatures that can be injected into mod code for compatibility between Linux/Mac or Windows.</summary>
+ /// <remarks>This is public to support SMAPI rewriting and should not be referenced directly by mods.</remarks>
+ public class SpriteBatchMethods : SpriteBatch
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ 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/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs
index c6e9aa92..05fedc3d 100644
--- a/src/SMAPI/Framework/SGame.cs
+++ b/src/SMAPI/Framework/SGame.cs
@@ -1,7 +1,7 @@
using System;
-using System.Collections;
+using System.Collections.Concurrent;
using System.Collections.Generic;
-using System.Diagnostics;
+using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
@@ -10,25 +10,23 @@ using System.Threading.Tasks;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
-#if STARDEW_VALLEY_1_3
using Netcode;
-#endif
using StardewModdingAPI.Events;
using StardewModdingAPI.Framework.Events;
using StardewModdingAPI.Framework.Input;
using StardewModdingAPI.Framework.Reflection;
+using StardewModdingAPI.Framework.StateTracking;
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;
-#if !STARDEW_VALLEY_1_3
using xTile.Layers;
-#else
-using SFarmer = StardewValley.Farmer;
-#endif
+using Object = StardewValley.Object;
namespace StardewModdingAPI.Framework
{
@@ -65,10 +63,10 @@ namespace StardewModdingAPI.Framework
/// <summary>The number of ticks until SMAPI should notify mods that the game has loaded.</summary>
/// <remarks>Skipping a few frames ensures the game finishes initialising the world before mods try to change it.</remarks>
- private int AfterLoadTimer = 5;
+ private readonly Countdown AfterLoadTimer = new Countdown(5);
- /// <summary>Whether the game is returning to the menu.</summary>
- private bool IsExitingToTitle;
+ /// <summary>Whether the after-load events were raised for this session.</summary>
+ private bool RaisedAfterLoadEvent;
/// <summary>Whether the game is saving and SMAPI has already raised <see cref="SaveEvents.BeforeSave"/>.</summary>
private bool IsBetweenSaveEvents;
@@ -76,114 +74,56 @@ namespace StardewModdingAPI.Framework
/// <summary>Whether the game is creating the save file and SMAPI has already raised <see cref="SaveEvents.BeforeCreate"/>.</summary>
private bool IsBetweenCreateEvents;
- /****
- ** Game state
- ****/
- /// <summary>The player input as of the previous tick.</summary>
- private InputState PreviousInput = new InputState();
-
- /// <summary>The window size value at last check.</summary>
- private Point PreviousWindowSize;
-
- /// <summary>The save ID at last check.</summary>
- private ulong PreviousSaveID;
-
- /// <summary>A hash of <see cref="Game1.locations"/> at last check.</summary>
- private int PreviousGameLocations;
-
- /// <summary>A hash of the current location's <see cref="GameLocation.objects"/> at last check.</summary>
- private int PreviousLocationObjects;
-
- /// <summary>The player's inventory at last check.</summary>
- private IDictionary<Item, int> PreviousItems;
-
- /// <summary>The player's combat skill level at last check.</summary>
- private int PreviousCombatLevel;
-
- /// <summary>The player's farming skill level at last check.</summary>
- private int PreviousFarmingLevel;
-
- /// <summary>The player's fishing skill level at last check.</summary>
- private int PreviousFishingLevel;
-
- /// <summary>The player's foraging skill level at last check.</summary>
- private int PreviousForagingLevel;
-
- /// <summary>The player's mining skill level at last check.</summary>
- private int PreviousMiningLevel;
-
- /// <summary>The player's luck skill level at last check.</summary>
- private int PreviousLuckLevel;
-
- /// <summary>The player's location at last check.</summary>
- private GameLocation PreviousGameLocation;
-
- /// <summary>The active game menu at last check.</summary>
- private IClickableMenu PreviousActiveMenu;
+ /// <summary>A callback to invoke after the game finishes initialising.</summary>
+ private readonly Action OnGameInitialised;
- /// <summary>The mine level at last check.</summary>
- private int PreviousMineLevel;
+ /// <summary>A callback to invoke when the game exits.</summary>
+ private readonly Action OnGameExiting;
- /// <summary>The time of day (in 24-hour military format) at last check.</summary>
- private int PreviousTime;
+ /// <summary>Simplifies access to private game code.</summary>
+ private readonly Reflector Reflection;
- /// <summary>The previous content locale.</summary>
- private LocalizedContentManager.LanguageCode? PreviousLocale;
+ /****
+ ** Game state
+ ****/
+ /// <summary>Monitors the entire game state for changes.</summary>
+ private WatcherCore Watchers;
/// <summary>An index incremented on every tick and reset every 60th tick (0–59).</summary>
private int CurrentUpdateTick;
- /// <summary>Whether this is the very first update tick since the game started.</summary>
- private bool FirstUpdate;
+ /// <summary>Whether post-game-startup initialisation has been performed.</summary>
+ private bool IsInitialised;
-#if !STARDEW_VALLEY_1_3
- /// <summary>The current game instance.</summary>
- private static SGame Instance;
-#endif
+ /// <summary>The number of update ticks which have already executed.</summary>
+ private uint TicksElapsed;
- /// <summary>A callback to invoke after the game finishes initialising.</summary>
- private readonly Action OnGameInitialised;
-
- /****
- ** Private wrappers
- ****/
- /// <summary>Simplifies access to private game code.</summary>
- private static Reflector Reflection;
-
-#if !STARDEW_VALLEY_1_3
- // ReSharper disable ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming
- /// <summary>Used to access private fields and methods.</summary>
- private static List<float> _fpsList => SGame.Reflection.GetField<List<float>>(typeof(Game1), nameof(_fpsList)).GetValue();
- private static Stopwatch _fpsStopwatch => SGame.Reflection.GetField<Stopwatch>(typeof(Game1), nameof(SGame._fpsStopwatch)).GetValue();
- private static float _fps
- {
- set => SGame.Reflection.GetField<float>(typeof(Game1), nameof(_fps)).SetValue(value);
- }
- private static Task _newDayTask => SGame.Reflection.GetField<Task>(typeof(Game1), nameof(_newDayTask)).GetValue();
- private Color bgColor => SGame.Reflection.GetField<Color>(this, nameof(this.bgColor)).GetValue();
- public RenderTarget2D screenWrapper => SGame.Reflection.GetProperty<RenderTarget2D>(this, "screen").GetValue(); // deliberately renamed to avoid an infinite loop
- public BlendState lightingBlend => SGame.Reflection.GetField<BlendState>(this, nameof(this.lightingBlend)).GetValue();
- private readonly Action drawFarmBuildings = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawFarmBuildings)).Invoke();
- private readonly Action drawHUD = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawHUD)).Invoke();
- private readonly Action drawDialogueBox = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawDialogueBox)).Invoke();
- private readonly Action renderScreenBuffer = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(renderScreenBuffer)).Invoke();
- // ReSharper restore ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming
-#endif
-
-#if STARDEW_VALLEY_1_3
- private static StringBuilder _debugStringBuilder => SGame.Reflection.GetField<StringBuilder>(typeof(Game1), nameof(_debugStringBuilder)).GetValue();
-#endif
+ /// <summary>Whether the next content manager requested by the game will be for <see cref="Game1.content"/>.</summary>
+ private bool NextContentManagerIsMain;
/*********
** Accessors
*********/
/// <summary>SMAPI's content manager.</summary>
- public ContentCore ContentCore { get; private set; }
+ public ContentCoordinator ContentCore { get; private set; }
+
+ /// <summary>Manages console commands.</summary>
+ public CommandManager CommandManager { get; } = new CommandManager();
+
+ /// <summary>Manages input visible to the game.</summary>
+ public SInputState Input => (SInputState)Game1.input;
+
+ /// <summary>The game's core multiplayer utility.</summary>
+ public SMultiplayer Multiplayer => (SMultiplayer)Game1.multiplayer;
/// <summary>Whether SMAPI should log more information about the game context.</summary>
public bool VerboseLogging { get; set; }
+ /// <summary>A list of queued commands to execute.</summary>
+ /// <remarks>This property must be threadsafe, since it's accessed from a separate console input thread.</remarks>
+ public ConcurrentQueue<string> CommandQueue { get; } = new ConcurrentQueue<string>();
+
/*********
** Protected methods
@@ -193,29 +133,50 @@ namespace StardewModdingAPI.Framework
/// <param name="reflection">Simplifies access to private game code.</param>
/// <param name="eventManager">Manages SMAPI events for mods.</param>
/// <param name="onGameInitialised">A callback to invoke after the game finishes initialising.</param>
- internal SGame(IMonitor monitor, Reflector reflection, EventManager eventManager, Action onGameInitialised)
+ /// <param name="onGameExiting">A callback to invoke when the game exits.</param>
+ internal SGame(IMonitor monitor, Reflector reflection, EventManager eventManager, Action onGameInitialised, Action onGameExiting)
{
- // initialise
+ // 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;
+
+ // init SMAPI
this.Monitor = monitor;
this.Events = eventManager;
- this.FirstUpdate = true;
-#if !STARDEW_VALLEY_1_3
- SGame.Instance = this;
-#endif
- SGame.Reflection = reflection;
+ this.Reflection = reflection;
this.OnGameInitialised = onGameInitialised;
- 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, null, this.Monitor, reflection);
+ this.OnGameExiting = onGameExiting;
+ Game1.input = new SInputState();
+ Game1.multiplayer = new SMultiplayer(monitor, eventManager);
- // set XNA option required by Stardew Valley
- Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef;
+ // init observables
+ Game1.locations = new ObservableCollection<GameLocation>();
+ }
+
+ /// <summary>Initialise just before the game's first update tick.</summary>
+ private void InitialiseAfterGameStarted()
+ {
+ // set initial state
+ this.Input.TrueUpdate();
+
+ // init watchers
+ this.Watchers = new WatcherCore(this.Input);
+
+ // raise callback
+ this.OnGameInitialised();
+ }
-#if !STARDEW_VALLEY_1_3
- // replace already-created content managers
- this.Monitor?.Log("Overriding content manager...", LogLevel.Trace);
- this.Content = this.ContentCore.CreateContentManager("SGame.Content");
- reflection.GetField<LocalizedContentManager>(typeof(Game1), "_temporaryContent").SetValue(this.ContentCore.CreateContentManager("Game1._temporaryContent")); // regenerate value with new content manager
-#endif
+ /// <summary>Perform cleanup logic when the game exits.</summary>
+ /// <param name="sender">The event sender.</param>
+ /// <param name="args">The event args.</param>
+ /// <remarks>This overrides the logic in <see cref="Game1.exitEvent"/> to let SMAPI clean up before exit.</remarks>
+ protected override void OnExiting(object sender, EventArgs args)
+ {
+ Game1.multiplayer.Disconnect();
+ this.OnGameExiting?.Invoke();
}
/****
@@ -226,14 +187,25 @@ namespace StardewModdingAPI.Framework
/// <param name="rootDirectory">The root directory to search for content.</param>
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, null, 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.CreateGameContentManager("Game1._temporaryContent");
}
- return this.ContentCore.CreateContentManager("(generated)", rootDirectory);
+
+ // Game1.content initialising from LoadContent
+ if (this.NextContentManagerIsMain)
+ {
+ this.NextContentManagerIsMain = false;
+ return this.ContentCore.MainContentManager;
+ }
+
+ // any other content manager
+ return this.ContentCore.CreateGameContentManager("(generated)");
}
/// <summary>The method called when the game is updating its state. This happens roughly 60 times per second.</summary>
@@ -243,45 +215,83 @@ namespace StardewModdingAPI.Framework
try
{
/*********
- ** Skip conditions
+ ** Special cases
*********/
- // SMAPI exiting, stop processing game updates
+ // Perform first-tick initialisation.
+ if (!this.IsInitialised)
+ {
+ this.IsInitialised = true;
+ this.InitialiseAfterGameStarted();
+ }
+
+ // Abort if SMAPI is exiting.
if (this.Monitor.IsExiting)
{
this.Monitor.Log("SMAPI shutting down: aborting update.", LogLevel.Trace);
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.
+ // Run async tasks synchronously to avoid issues due to mod events triggering
+ // concurrently with game code.
+ if (Game1.currentLoader != null)
+ {
+ this.Monitor.Log("Game loader synchronising...", LogLevel.Trace);
+ while (Game1.currentLoader?.MoveNext() == true)
+ ;
+ Game1.currentLoader = null;
+ this.Monitor.Log("Game loader done.", LogLevel.Trace);
+ }
+ if (Game1._newDayTask?.Status == TaskStatus.Created)
+ {
+ this.Monitor.Log("New day task synchronising...", LogLevel.Trace);
+ Game1._newDayTask.RunSynchronously();
+ this.Monitor.Log("New day task done.", LogLevel.Trace);
+ }
+
+ // 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 STARDEW_VALLEY_1_3
- if (Game1._newDayTask != null)
-#else
- if (SGame._newDayTask != null)
-#endif
+ 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)
+ /*********
+ ** Execute commands
+ *********/
+ while (this.CommandQueue.TryDequeue(out string rawInput))
{
- base.Update(gameTime);
- this.Events.Specialised_UnvalidatedUpdateTick.Raise();
- return;
+ try
+ {
+ if (!this.CommandManager.Trigger(rawInput))
+ this.Monitor.Log("Unknown command; type 'help' for a list of available commands.", LogLevel.Error);
+ }
+ catch (Exception ex)
+ {
+ this.Monitor.Log($"The handler registered for that command failed:\n{ex.GetLogSummary()}", LogLevel.Error);
+ }
}
/*********
+ ** Update input
+ *********/
+ // This should *always* run, even when suppressing mod events, since the game uses
+ // this too. For example, doing this after mod event suppression would prevent the
+ // user from doing anything on the overnight shipping screen.
+ SInputState previousInputState = this.Input.Clone();
+ SInputState inputState = this.Input;
+ if (this.IsActive)
+ inputState.TrueUpdate();
+
+ /*********
** Save events + suppress events during save
*********/
// While the game is writing to the save file in the background, mods can unexpectedly
@@ -329,61 +339,63 @@ namespace StardewModdingAPI.Framework
}
/*********
- ** Notify SMAPI that game is initialised
+ ** Update context
*********/
- if (this.FirstUpdate)
- this.OnGameInitialised();
+ bool wasWorldReady = Context.IsWorldReady;
+ if ((Context.IsWorldReady && !Context.IsSaveLoaded) || Game1.exitToTitle)
+ this.MarkWorldNotReady();
+ else if (Context.IsSaveLoaded && this.AfterLoadTimer.Current > 0 && Game1.currentLocation != null)
+ {
+ if (Game1.dayOfMonth != 0) // wait until new-game intro finishes (world not fully initialised yet)
+ this.AfterLoadTimer.Decrement();
+ Context.IsWorldReady = this.AfterLoadTimer.Current == 0;
+ }
/*********
- ** Locale changed events
+ ** Update watchers
*********/
- if (this.PreviousLocale != LocalizedContentManager.CurrentLanguageCode)
- {
- var oldValue = this.PreviousLocale;
- var newValue = LocalizedContentManager.CurrentLanguageCode;
-
- this.Monitor.Log($"Context: locale set to {newValue}.", LogLevel.Trace);
-
- if (oldValue != null)
- this.Events.Content_LocaleChanged.Raise(new EventArgsValueChanged<string>(oldValue.ToString(), newValue.ToString()));
-
- this.PreviousLocale = newValue;
- }
+ this.Watchers.Update();
/*********
- ** After load events
+ ** Locale changed events
*********/
- if (Context.IsSaveLoaded && !SaveGame.IsProcessing /*still loading save*/ && this.AfterLoadTimer >= 0)
+ if (this.Watchers.LocaleWatcher.IsChanged)
{
- if (Game1.dayOfMonth != 0) // wait until new-game intro finishes (world not fully initialised yet)
- this.AfterLoadTimer--;
+ var was = this.Watchers.LocaleWatcher.PreviousValue;
+ var now = this.Watchers.LocaleWatcher.CurrentValue;
- 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.Monitor.Log($"Context: locale set to {now}.", LogLevel.Trace);
+ this.Events.Content_LocaleChanged.Raise(new EventArgsValueChanged<string>(was.ToString(), now.ToString()));
- this.Events.Save_AfterLoad.Raise();
- this.Events.Time_AfterDayStarted.Raise();
- }
+ this.Watchers.LocaleWatcher.Reset();
}
/*********
- ** Exit to title events
+ ** Load / return-to-title events
*********/
- // before exit to title
- if (Game1.exitToTitle)
- this.IsExitingToTitle = true;
-
- // after exit to title
- if (Context.IsWorldReady && this.IsExitingToTitle && Game1.activeClickableMenu is TitleMenu)
+ if (wasWorldReady && !Context.IsWorldReady)
{
this.Monitor.Log("Context: returned to title", LogLevel.Trace);
-
- this.IsExitingToTitle = false;
- this.CleanupAfterReturnToTitle();
this.Events.Save_AfterReturnToTitle.Raise();
}
+ else if (!this.RaisedAfterLoadEvent && Context.IsWorldReady)
+ {
+ // print context
+ string context = $"Context: loaded saved game '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.";
+ if (Context.IsMultiplayer)
+ {
+ int onlineCount = Game1.getOnlineFarmers().Count();
+ context += $" {(Context.IsMainPlayer ? "Main player" : "Farmhand")} with {onlineCount} {(onlineCount == 1 ? "player" : "players")} online.";
+ }
+ else
+ context += " Single-player.";
+ this.Monitor.Log(context, LogLevel.Trace);
+
+ // raise events
+ this.RaisedAfterLoadEvent = true;
+ this.Events.Save_AfterLoad.Raise();
+ this.Events.Time_AfterDayStarted.Raise();
+ }
/*********
** Window events
@@ -392,123 +404,124 @@ 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.Watchers.WindowSizeWatcher.IsChanged)
{
- Point size = new Point(Game1.viewport.Width, Game1.viewport.Height);
+ if (this.VerboseLogging)
+ this.Monitor.Log($"Events: window size changed to {this.Watchers.WindowSizeWatcher.CurrentValue}.", LogLevel.Trace);
this.Events.Graphics_Resize.Raise();
- this.PreviousWindowSize = size;
+ this.Watchers.WindowSizeWatcher.Reset();
}
/*********
** Input events (if window has focus)
*********/
- if (Game1.game1.IsActive)
+ if (this.IsActive)
{
- // get input state
- InputState inputState;
- try
+ // raise events
+ bool isChatInput = Game1.IsChatting || (Context.IsMultiplayer && Context.IsWorldReady && Game1.activeClickableMenu == null && Game1.currentMinigame == null && inputState.IsAnyDown(Game1.options.chatButton));
+ if (!isChatInput)
{
- 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;
- }
+ ICursorPosition cursor = this.Input.CursorPosition;
- // get cursor position
- ICursorPosition cursor;
- {
- // 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);
- }
+ // raise cursor moved event
+ if (this.Watchers.CursorWatcher.IsChanged)
+ {
+ ICursorPosition was = this.Watchers.CursorWatcher.PreviousValue;
+ ICursorPosition now = this.Watchers.CursorWatcher.CurrentValue;
+ this.Watchers.CursorWatcher.Reset();
- // raise input events
- foreach (var pair in inputState.ActiveButtons)
- {
- SButton button = pair.Key;
- InputStatus status = pair.Value;
+ this.Events.Input_CursorMoved.Raise(new InputCursorMovedEventArgs(was, now));
+ }
- if (status == InputStatus.Pressed)
+ // raise mouse wheel scrolled
+ if (this.Watchers.MouseWheelScrollWatcher.IsChanged)
{
- this.Events.Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton()));
+ int was = this.Watchers.MouseWheelScrollWatcher.PreviousValue;
+ int now = this.Watchers.MouseWheelScrollWatcher.CurrentValue;
+ this.Watchers.MouseWheelScrollWatcher.Reset();
- // legacy events
- if (button.TryGetKeyboard(out Keys key))
- {
- if (key != Keys.None)
- this.Events.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.ControllerState.Triggers.Left : inputState.ControllerState.Triggers.Right));
- else
- this.Events.Control_ControllerButtonPressed.Raise(new EventArgsControllerButtonPressed(PlayerIndex.One, controllerButton));
- }
+ if (this.VerboseLogging)
+ this.Monitor.Log($"Events: mouse wheel scrolled to {now}.", LogLevel.Trace);
+ this.Events.Input_MouseWheelScrolled.Raise(new InputMouseWheelScrolledEventArgs(cursor, was, now));
}
- else if (status == InputStatus.Released)
+
+ // raise input button events
+ foreach (var pair in inputState.ActiveButtons)
{
- this.Events.Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton()));
+ SButton button = pair.Key;
+ InputStatus status = pair.Value;
- // legacy events
- if (button.TryGetKeyboard(out Keys key))
+ if (status == InputStatus.Pressed)
{
- if (key != Keys.None)
- this.Events.Control_KeyReleased.Raise(new EventArgsKeyPressed(key));
+ if (this.VerboseLogging)
+ this.Monitor.Log($"Events: button {button} pressed.", LogLevel.Trace);
+
+ this.Events.Input_ButtonPressed.Raise(new InputButtonPressedEventArgs(button, cursor, inputState));
+ 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.Legacy_Control_KeyPressed.Raise(new EventArgsKeyPressed(key));
+ }
+ else if (button.TryGetController(out Buttons controllerButton))
+ {
+ if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger)
+ 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.Legacy_Control_ControllerButtonPressed.Raise(new EventArgsControllerButtonPressed(PlayerIndex.One, controllerButton));
+ }
}
- else if (button.TryGetController(out Buttons controllerButton))
+ else if (status == InputStatus.Released)
{
- 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));
- else
- this.Events.Control_ControllerButtonReleased.Raise(new EventArgsControllerButtonReleased(PlayerIndex.One, controllerButton));
+ if (this.VerboseLogging)
+ this.Monitor.Log($"Events: button {button} released.", LogLevel.Trace);
+
+ this.Events.Input_ButtonReleased.Raise(new InputButtonReleasedEventArgs(button, cursor, inputState));
+ 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.Legacy_Control_KeyReleased.Raise(new EventArgsKeyPressed(key));
+ }
+ else if (button.TryGetController(out Buttons controllerButton))
+ {
+ if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger)
+ 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.Legacy_Control_ControllerButtonReleased.Raise(new EventArgsControllerButtonReleased(PlayerIndex.One, controllerButton));
+ }
}
}
- }
-
- // 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));
- // track state
- this.PreviousInput = inputState;
+ // raise legacy state-changed events
+ 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, new Point((int)previousInputState.CursorPosition.ScreenPixels.X, (int)previousInputState.CursorPosition.ScreenPixels.Y), new Point((int)inputState.CursorPosition.ScreenPixels.X, (int)inputState.CursorPosition.ScreenPixels.Y)));
+ }
}
/*********
** Menu events
*********/
- if (Game1.activeClickableMenu != this.PreviousActiveMenu)
+ if (this.Watchers.ActiveMenuWatcher.IsChanged)
{
- IClickableMenu previousMenu = this.PreviousActiveMenu;
- IClickableMenu newMenu = Game1.activeClickableMenu;
+ 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
- // log context
if (this.VerboseLogging)
- {
- if (previousMenu == null)
- this.Monitor.Log($"Context: opened menu {newMenu?.GetType().FullName ?? "(none)"}.", LogLevel.Trace);
- else if (newMenu == null)
- this.Monitor.Log($"Context: closed menu {previousMenu.GetType().FullName}.", LogLevel.Trace);
- else
- this.Monitor.Log($"Context: changed menu from {previousMenu.GetType().FullName} to {newMenu.GetType().FullName}.", 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));
-
- // 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;
+ this.Events.Menu_Closed.Raise(new EventArgsClickableMenuClosed(was));
}
/*********
@@ -516,97 +529,190 @@ namespace StardewModdingAPI.Framework
*********/
if (Context.IsWorldReady)
{
- // raise current location changed
- // ReSharper disable once PossibleUnintendedReferenceComparison
- if (Game1.currentLocation != this.PreviousGameLocation)
+ bool raiseWorldEvents = !this.Watchers.SaveIdWatcher.IsChanged; // don't report changes from unloaded => loaded
+
+ // raise location changes
+ if (this.Watchers.LocationsWatcher.IsChanged)
{
- 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));
+ // location list changes
+ if (this.Watchers.LocationsWatcher.IsLocationListChanged)
+ {
+ GameLocation[] added = this.Watchers.LocationsWatcher.Added.ToArray();
+ GameLocation[] removed = this.Watchers.LocationsWatcher.Removed.ToArray();
+ this.Watchers.LocationsWatcher.ResetLocationList();
+
+ if (this.VerboseLogging)
+ {
+ 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);
+ }
+
+ this.Events.World_LocationListChanged.Raise(new WorldLocationListChangedEventArgs(added, removed));
+ this.Events.Legacy_Location_LocationsChanged.Raise(new EventArgsLocationsChanged(added, removed));
+ }
+
+ // raise location contents changed
+ if (raiseWorldEvents)
+ {
+ foreach (LocationTracker watcher in this.Watchers.LocationsWatcher.Locations)
+ {
+ // buildings changed
+ if (watcher.BuildingsWatcher.IsChanged)
+ {
+ GameLocation location = watcher.Location;
+ 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.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)
+ {
+ 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)
+ {
+ 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<Vector2, Object>[] added = watcher.ObjectsWatcher.Added.ToArray();
+ KeyValuePair<Vector2, Object>[] removed = watcher.ObjectsWatcher.Removed.ToArray();
+ watcher.ObjectsWatcher.Reset();
+
+ this.Events.World_ObjectListChanged.Raise(new WorldObjectListChangedEventArgs(location, added, removed));
+ this.Events.Legacy_Location_ObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, added, removed));
+ }
+
+ // terrain features changed
+ if (watcher.TerrainFeaturesWatcher.IsChanged)
+ {
+ GameLocation location = watcher.Location;
+ KeyValuePair<Vector2, TerrainFeature>[] added = watcher.TerrainFeaturesWatcher.Added.ToArray();
+ KeyValuePair<Vector2, TerrainFeature>[] removed = watcher.TerrainFeaturesWatcher.Removed.ToArray();
+ watcher.TerrainFeaturesWatcher.Reset();
+
+ this.Events.World_TerrainFeatureListChanged.Raise(new WorldTerrainFeatureListChangedEventArgs(location, added, removed));
+ }
+ }
+ }
+ else
+ this.Watchers.LocationsWatcher.Reset();
}
- // raise location list changed
- if (this.GetHash(Game1.locations) != this.PreviousGameLocations)
- this.Events.Location_LocationsChanged.Raise(new EventArgsGameLocationsChanged(Game1.locations));
+ // raise time changed
+ if (raiseWorldEvents && this.Watchers.TimeWatcher.IsChanged)
+ {
+ int was = this.Watchers.TimeWatcher.PreviousValue;
+ int now = this.Watchers.TimeWatcher.CurrentValue;
+ this.Watchers.TimeWatcher.Reset();
+
+ if (this.VerboseLogging)
+ this.Monitor.Log($"Events: time changed from {was} to {now}.", LogLevel.Trace);
- // raise events that shouldn't be triggered on initial load
- if (Game1.uniqueIDForThisGame == this.PreviousSaveID)
+ this.Events.Time_TimeOfDayChanged.Raise(new EventArgsIntChanged(was, now));
+ }
+ else
+ this.Watchers.TimeWatcher.Reset();
+
+ // raise player events
+ if (raiseWorldEvents)
{
+ PlayerTracker curPlayer = this.Watchers.CurrentPlayerTracker;
+
+ // 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
- 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<EventArgsLevelUp.LevelType, IValueWatcher<int>> pair in curPlayer.GetChangedSkills())
+ {
+ if (this.VerboseLogging)
+ this.Monitor.Log($"Events: player skill '{pair.Key}' changed from {pair.Value.PreviousValue} to {pair.Value.CurrentValue}.", LogLevel.Trace);
+ 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(
-#if STARDEW_VALLEY_1_3
- Game1.currentLocation.objects.FieldDict
-#else
- Game1.currentLocation.objects
-#endif
- ));
-
- // raise time changed
- if (Game1.timeOfDay != this.PreviousTime)
- this.Events.Time_TimeOfDayChanged.Raise(new EventArgsIntChanged(this.PreviousTime, Game1.timeOfDay));
+ {
+ if (this.VerboseLogging)
+ this.Monitor.Log("Events: player inventory changed.", LogLevel.Trace);
+ this.Events.Player_InventoryChanged.Raise(new EventArgsInventoryChanged(Game1.player.Items, changedItems.ToList()));
+ }
// 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))
+ {
+ if (this.VerboseLogging)
+ this.Monitor.Log($"Context: mine level changed to {mineLevel}.", LogLevel.Trace);
+ 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;
+ this.Watchers.CurrentPlayerTracker?.Reset();
}
+ // update save ID watcher
+ this.Watchers.SaveIdWatcher.Reset();
+
/*********
** Game update
*********/
+ 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();
@@ -662,7 +768,7 @@ namespace StardewModdingAPI.Framework
// recover sprite batch
try
{
- if (Game1.spriteBatch.IsOpen(SGame.Reflection))
+ if (Game1.spriteBatch.IsOpen(this.Reflection))
{
this.Monitor.Log("Recovering sprite batch from error...", LogLevel.Trace);
Game1.spriteBatch.End();
@@ -686,7 +792,8 @@ 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")]
-#if STARDEW_VALLEY_1_3
+ [SuppressMessage("SMAPI.CommonErrors", "AvoidNetField", Justification = "copied from game code as-is")]
+ [SuppressMessage("SMAPI.CommonErrors", "AvoidImplicitNetFieldCast", Justification = "copied from game code as-is")]
private void DrawImpl(GameTime gameTime)
{
if (Game1.debugMode)
@@ -770,6 +877,7 @@ namespace StardewModdingAPI.Framework
}
this.RaisePostRender();
Game1.spriteBatch.End();
+ this.drawOverlays(Game1.spriteBatch);
if ((double)Game1.options.zoomLevel != 1.0)
{
this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null);
@@ -778,9 +886,8 @@ namespace StardewModdingAPI.Framework
Game1.spriteBatch.Draw((Texture2D)this.screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screen.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f);
Game1.spriteBatch.End();
}
- this.drawOverlays(Game1.spriteBatch);
}
- else if ((int)Game1.gameMode == 11)
+ else if (Game1.gameMode == (byte)11)
{
Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3685"), new Vector2(16f, 16f), Color.HotPink);
@@ -795,9 +902,10 @@ namespace StardewModdingAPI.Framework
if (Game1.globalFade && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause))
{
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
- Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * ((int)Game1.gameMode == 0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha));
+ Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * (Game1.gameMode == (byte)0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha));
Game1.spriteBatch.End();
}
+ this.drawOverlays(Game1.spriteBatch);
this.RaisePostRender(needsNewBatch: true);
if ((double)Game1.options.zoomLevel != 1.0)
{
@@ -807,7 +915,6 @@ namespace StardewModdingAPI.Framework
Game1.spriteBatch.Draw((Texture2D)this.screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screen.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f);
Game1.spriteBatch.End();
}
- this.drawOverlays(Game1.spriteBatch);
}
else if (Game1.showingEndOfNightStuff)
{
@@ -828,694 +935,46 @@ namespace StardewModdingAPI.Framework
}
this.RaisePostRender();
Game1.spriteBatch.End();
- if ((double)Game1.options.zoomLevel != 1.0)
- {
- this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null);
- this.GraphicsDevice.Clear(this.bgColor);
- Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone);
- Game1.spriteBatch.Draw((Texture2D)this.screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screen.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f);
- Game1.spriteBatch.End();
- }
this.drawOverlays(Game1.spriteBatch);
- }
- else
- {
- int num1;
- switch (Game1.gameMode)
- {
- case 3:
- num1 = Game1.currentLocation == null ? 1 : 0;
- break;
- case 6:
- num1 = 1;
- break;
- default:
- num1 = 0;
- break;
- }
- if (num1 != 0)
- {
- Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
- string str1 = "";
- for (int index = 0; (double)index < gameTime.TotalGameTime.TotalMilliseconds % 999.0 / 333.0; ++index)
- str1 += ".";
- string str2 = Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3688");
- string s = str2 + str1;
- string str3 = str2 + "... ";
- int widthOfString = SpriteText.getWidthOfString(str3);
- int height = 64;
- int x = 64;
- int y = Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - height;
- SpriteText.drawString(Game1.spriteBatch, s, x, y, 999999, widthOfString, height, 1f, 0.88f, false, 0, str3, -1);
- Game1.spriteBatch.End();
- if ((double)Game1.options.zoomLevel != 1.0)
- {
- this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null);
- this.GraphicsDevice.Clear(this.bgColor);
- Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone);
- Game1.spriteBatch.Draw((Texture2D)this.screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screen.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f);
- Game1.spriteBatch.End();
- }
- this.drawOverlays(Game1.spriteBatch);
- //base.Draw(gameTime);
- }
- else
- {
- Viewport viewport1;
- if ((int)Game1.gameMode == 0)
- {
- Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
- }
- else
- {
- Microsoft.Xna.Framework.Rectangle bounds;
- if (Game1.drawLighting)
- {
- this.GraphicsDevice.SetRenderTarget(Game1.lightmap);
- this.GraphicsDevice.Clear(Color.White * 0.0f);
- Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
- Game1.spriteBatch.Draw(Game1.staminaRect, Game1.lightmap.Bounds, Game1.currentLocation.Name.StartsWith("UndergroundMine") ? Game1.mine.getLightingColor(gameTime) : (Game1.ambientLight.Equals(Color.White) || Game1.isRaining && (bool)((NetFieldBase<bool, NetBool>)Game1.currentLocation.isOutdoors) ? Game1.outdoorLight : Game1.ambientLight));
- for (int index = 0; index < Game1.currentLightSources.Count; ++index)
- {
- if (Utility.isOnScreen((Vector2)((NetFieldBase<Vector2, NetVector2>)Game1.currentLightSources.ElementAt<LightSource>(index).position), (int)((double)(float)((NetFieldBase<float, NetFloat>)Game1.currentLightSources.ElementAt<LightSource>(index).radius) * 64.0 * 4.0)))
- {
- SpriteBatch spriteBatch = Game1.spriteBatch;
- Texture2D lightTexture = Game1.currentLightSources.ElementAt<LightSource>(index).lightTexture;
- Vector2 position = Game1.GlobalToLocal(Game1.viewport, (Vector2)((NetFieldBase<Vector2, NetVector2>)Game1.currentLightSources.ElementAt<LightSource>(index).position)) / (float)(Game1.options.lightingQuality / 2);
- Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.currentLightSources.ElementAt<LightSource>(index).lightTexture.Bounds);
- Color color = (Color)((NetFieldBase<Color, NetColor>)Game1.currentLightSources.ElementAt<LightSource>(index).color);
- double num2 = 0.0;
- bounds = Game1.currentLightSources.ElementAt<LightSource>(index).lightTexture.Bounds;
- double x = (double)bounds.Center.X;
- bounds = Game1.currentLightSources.ElementAt<LightSource>(index).lightTexture.Bounds;
- double y = (double)bounds.Center.Y;
- Vector2 origin = new Vector2((float)x, (float)y);
- double num3 = (double)(float)((NetFieldBase<float, NetFloat>)Game1.currentLightSources.ElementAt<LightSource>(index).radius) / (double)(Game1.options.lightingQuality / 2);
- int num4 = 0;
- double num5 = 0.899999976158142;
- spriteBatch.Draw(lightTexture, position, sourceRectangle, color, (float)num2, origin, (float)num3, (SpriteEffects)num4, (float)num5);
- }
- }
- Game1.spriteBatch.End();
- this.GraphicsDevice.SetRenderTarget((double)Game1.options.zoomLevel == 1.0 ? (RenderTarget2D)null : this.screen);
- }
- if (Game1.bloomDay && Game1.bloom != null)
- Game1.bloom.BeginDraw();
- this.GraphicsDevice.Clear(this.bgColor);
- Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
- this.Events.Graphics_OnPreRenderEvent.Raise();
- if (Game1.background != null)
- Game1.background.draw(Game1.spriteBatch);
- Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch);
- Game1.currentLocation.Map.GetLayer("Back").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4);
- Game1.currentLocation.drawWater(Game1.spriteBatch);
- if (Game1.CurrentEvent == null)
- {
- foreach (NPC character in Game1.currentLocation.characters)
- {
- if (!(bool)((NetFieldBase<bool, NetBool>)character.swimming) && !character.HideShadow && !character.IsInvisible && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation()))
- {
- SpriteBatch spriteBatch = Game1.spriteBatch;
- Texture2D shadowTexture = Game1.shadowTexture;
- Vector2 local = Game1.GlobalToLocal(Game1.viewport, character.Position + new Vector2((float)(character.Sprite.SpriteWidth * 4) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : 12))));
- Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds);
- Color white = Color.White;
- double num2 = 0.0;
- bounds = Game1.shadowTexture.Bounds;
- double x = (double)bounds.Center.X;
- bounds = Game1.shadowTexture.Bounds;
- double y = (double)bounds.Center.Y;
- Vector2 origin = new Vector2((float)x, (float)y);
- double num3 = (4.0 + (double)character.yJumpOffset / 40.0) * (double)(float)((NetFieldBase<float, NetFloat>)character.scale);
- int num4 = 0;
- double num5 = (double)Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 9.99999997475243E-07;
- spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num2, origin, (float)num3, (SpriteEffects)num4, (float)num5);
- }
- }
- }
- else
- {
- foreach (NPC actor in Game1.CurrentEvent.actors)
- {
- if (!(bool)((NetFieldBase<bool, NetBool>)actor.swimming) && !actor.HideShadow && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation()))
- {
- SpriteBatch spriteBatch = Game1.spriteBatch;
- Texture2D shadowTexture = Game1.shadowTexture;
- Vector2 local = Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : (actor.Sprite.SpriteHeight <= 16 ? -4 : 12)))));
- Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds);
- Color white = Color.White;
- double num2 = 0.0;
- bounds = Game1.shadowTexture.Bounds;
- double x = (double)bounds.Center.X;
- bounds = Game1.shadowTexture.Bounds;
- double y = (double)bounds.Center.Y;
- Vector2 origin = new Vector2((float)x, (float)y);
- double num3 = (4.0 + (double)actor.yJumpOffset / 40.0) * (double)(float)((NetFieldBase<float, NetFloat>)actor.scale);
- int num4 = 0;
- double num5 = (double)Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 9.99999997475243E-07;
- spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num2, origin, (float)num3, (SpriteEffects)num4, (float)num5);
- }
- }
- }
- foreach (SFarmer farmer in Game1.currentLocation.getFarmers())
- {
- if (!(bool)((NetFieldBase<bool, NetBool>)farmer.swimming) && !farmer.isRidingHorse() && (Game1.currentLocation == null || !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmer.getTileLocation())))
- {
- SpriteBatch spriteBatch = Game1.spriteBatch;
- Texture2D shadowTexture = Game1.shadowTexture;
- Vector2 local = Game1.GlobalToLocal(farmer.Position + new Vector2(32f, 24f));
- Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds);
- Color white = Color.White;
- double num2 = 0.0;
- Microsoft.Xna.Framework.Rectangle bounds2 = Game1.shadowTexture.Bounds;
- double x = (double)bounds2.Center.X;
- bounds2 = Game1.shadowTexture.Bounds;
- double y = (double)bounds2.Center.Y;
- Vector2 origin = new Vector2((float)x, (float)y);
- double num3 = 4.0 - (!farmer.running && !farmer.UsingTool || farmer.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmer.FarmerSprite.CurrentFrame]) * 0.5);
- int num4 = 0;
- double num5 = 0.0;
- spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num2, origin, (float)num3, (SpriteEffects)num4, (float)num5);
- }
- }
- Game1.currentLocation.Map.GetLayer("Buildings").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4);
- Game1.mapDisplayDevice.EndScene();
- Game1.spriteBatch.End();
- Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
- if (Game1.CurrentEvent == null)
- {
- foreach (NPC character in Game1.currentLocation.characters)
- {
- if (!(bool)((NetFieldBase<bool, NetBool>)character.swimming) && !character.HideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation()))
- Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, character.Position + new Vector2((float)(character.Sprite.SpriteWidth * 4) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)character.yJumpOffset / 40.0) * (float)((NetFieldBase<float, NetFloat>)character.scale), SpriteEffects.None, Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 1E-06f);
- }
- }
- else
- {
- foreach (NPC actor in Game1.CurrentEvent.actors)
- {
- if (!(bool)((NetFieldBase<bool, NetBool>)actor.swimming) && !actor.HideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation()))
- Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)actor.yJumpOffset / 40.0) * (float)((NetFieldBase<float, NetFloat>)actor.scale), SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f);
- }
- }
- foreach (SFarmer farmer in Game1.currentLocation.getFarmers())
- {
- if (!(bool)((NetFieldBase<bool, NetBool>)farmer.swimming) && !farmer.isRidingHorse() && (Game1.currentLocation != null && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmer.getTileLocation())))
- {
- SpriteBatch spriteBatch = Game1.spriteBatch;
- Texture2D shadowTexture = Game1.shadowTexture;
- Vector2 local = Game1.GlobalToLocal(farmer.Position + new Vector2(32f, 24f));
- Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds);
- Color white = Color.White;
- double num2 = 0.0;
- Microsoft.Xna.Framework.Rectangle bounds2 = Game1.shadowTexture.Bounds;
- double x = (double)bounds2.Center.X;
- bounds2 = Game1.shadowTexture.Bounds;
- double y = (double)bounds2.Center.Y;
- Vector2 origin = new Vector2((float)x, (float)y);
- double num3 = 4.0 - (!farmer.running && !farmer.UsingTool || farmer.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmer.FarmerSprite.CurrentFrame]) * 0.5);
- int num4 = 0;
- double num5 = 0.0;
- spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num2, origin, (float)num3, (SpriteEffects)num4, (float)num5);
- }
- }
- if ((Game1.eventUp || Game1.killScreen) && (!Game1.killScreen && Game1.currentLocation.currentEvent != null))
- Game1.currentLocation.currentEvent.draw(Game1.spriteBatch);
- if (Game1.player.currentUpgrade != null && Game1.player.currentUpgrade.daysLeftTillUpgradeDone <= 3 && Game1.currentLocation.Name.Equals("Farm"))
- Game1.spriteBatch.Draw(Game1.player.currentUpgrade.workerTexture, Game1.GlobalToLocal(Game1.viewport, Game1.player.currentUpgrade.positionOfCarpenter), new Microsoft.Xna.Framework.Rectangle?(Game1.player.currentUpgrade.getSourceRectangle()), Color.White, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, (float)(((double)Game1.player.currentUpgrade.positionOfCarpenter.Y + 48.0) / 10000.0));
- Game1.currentLocation.draw(Game1.spriteBatch);
- if (!Game1.eventUp || Game1.currentLocation.currentEvent == null || Game1.currentLocation.currentEvent.messageToScreen == null)
- ;
- if (Game1.player.ActiveObject == null && ((Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && (!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool)))
- Game1.drawTool(Game1.player);
- if (Game1.currentLocation.Name.Equals("Farm"))
- this.drawFarmBuildings();
- if (Game1.tvStation >= 0)
- Game1.spriteBatch.Draw(Game1.tvStationTexture, Game1.GlobalToLocal(Game1.viewport, new Vector2(400f, 160f)), new Microsoft.Xna.Framework.Rectangle?(new Microsoft.Xna.Framework.Rectangle(Game1.tvStation * 24, 0, 24, 15)), Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, 1E-08f);
- if (Game1.panMode)
- {
- Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((int)Math.Floor((double)(Game1.getOldMouseX() + Game1.viewport.X) / 64.0) * 64 - Game1.viewport.X, (int)Math.Floor((double)(Game1.getOldMouseY() + Game1.viewport.Y) / 64.0) * 64 - Game1.viewport.Y, 64, 64), Color.Lime * 0.75f);
- foreach (Warp warp in (NetList<Warp, NetRef<Warp>>)Game1.currentLocation.warps)
- Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(warp.X * 64 - Game1.viewport.X, warp.Y * 64 - Game1.viewport.Y, 64, 64), Color.Red * 0.75f);
- }
- Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch);
- Game1.currentLocation.Map.GetLayer("Front").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4);
- Game1.mapDisplayDevice.EndScene();
- Game1.currentLocation.drawAboveFrontLayer(Game1.spriteBatch);
- Game1.spriteBatch.End();
- Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
- if (Game1.displayFarmer && Game1.player.ActiveObject != null && ((bool)((NetFieldBase<bool, NetBool>)Game1.player.ActiveObject.bigCraftable) && this.checkBigCraftableBoundariesForFrontLayer()) && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null)
- Game1.drawPlayerHeldObject(Game1.player);
- else if (Game1.displayFarmer && Game1.player.ActiveObject != null && (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.Position.X, (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && !Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.Position.X, (int)Game1.player.Position.Y - 38), Game1.viewport.Size).TileIndexProperties.ContainsKey("FrontAlways") || Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.GetBoundingBox().Right, (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && !Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.GetBoundingBox().Right, (int)Game1.player.Position.Y - 38), Game1.viewport.Size).TileIndexProperties.ContainsKey("FrontAlways")))
- Game1.drawPlayerHeldObject(Game1.player);
- 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)
- {
- Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch);
- Game1.currentLocation.Map.GetLayer("AlwaysFront").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4);
- Game1.mapDisplayDevice.EndScene();
- }
- if ((double)Game1.toolHold > 400.0 && Game1.player.CurrentTool.UpgradeLevel >= 1 && Game1.player.canReleaseTool)
- {
- Color color = Color.White;
- switch ((int)((double)Game1.toolHold / 600.0) + 2)
- {
- case 1:
- color = Tool.copperColor;
- break;
- case 2:
- color = Tool.steelColor;
- break;
- case 3:
- color = Tool.goldColor;
- break;
- case 4:
- color = Tool.iridiumColor;
- break;
- }
- Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X - 2, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : 64) - 2, (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607) + 4, 12), Color.Black);
- Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : 64), (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607), 8), color);
- }
- if (Game1.isDebrisWeather && Game1.currentLocation.IsOutdoors && (!(bool)((NetFieldBase<bool, NetBool>)Game1.currentLocation.ignoreDebrisWeather) && !Game1.currentLocation.Name.Equals("Desert")) && Game1.viewport.X > -10)
- {
- foreach (WeatherDebris weatherDebris in Game1.debrisWeather)
- weatherDebris.draw(Game1.spriteBatch);
- }
- if (Game1.farmEvent != null)
- Game1.farmEvent.draw(Game1.spriteBatch);
- if ((double)Game1.currentLocation.LightLevel > 0.0 && Game1.timeOfDay < 2000)
- Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * Game1.currentLocation.LightLevel);
- if (Game1.screenGlow)
- Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Game1.screenGlowColor * Game1.screenGlowAlpha);
- Game1.currentLocation.drawAboveAlwaysFrontLayer(Game1.spriteBatch);
- if (Game1.player.CurrentTool != null && Game1.player.CurrentTool is FishingRod && ((Game1.player.CurrentTool as FishingRod).isTimingCast || (double)(Game1.player.CurrentTool as FishingRod).castingChosenCountdown > 0.0 || (Game1.player.CurrentTool as FishingRod).fishCaught || (Game1.player.CurrentTool as FishingRod).showingTreasure))
- Game1.player.CurrentTool.draw(Game1.spriteBatch);
- if (Game1.isRaining && Game1.currentLocation.IsOutdoors && (!Game1.currentLocation.Name.Equals("Desert") && !(Game1.currentLocation is Summit)) && (!Game1.eventUp || Game1.currentLocation.isTileOnMap(new Vector2((float)(Game1.viewport.X / 64), (float)(Game1.viewport.Y / 64)))))
- {
- for (int index = 0; index < Game1.rainDrops.Length; ++index)
- Game1.spriteBatch.Draw(Game1.rainTexture, Game1.rainDrops[index].position, new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.rainTexture, Game1.rainDrops[index].frame, -1, -1)), Color.White);
- }
- Game1.spriteBatch.End();
- Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
- if (Game1.eventUp && Game1.currentLocation.currentEvent != null)
- {
- foreach (NPC actor in Game1.currentLocation.currentEvent.actors)
- {
- if (actor.isEmoting)
- {
- Vector2 localPosition = actor.getLocalPosition(Game1.viewport);
- localPosition.Y -= 140f;
- if (actor.Age == 2)
- localPosition.Y += 32f;
- else if (actor.Gender == 1)
- localPosition.Y += 10f;
- Game1.spriteBatch.Draw(Game1.emoteSpriteSheet, localPosition, new Microsoft.Xna.Framework.Rectangle?(new Microsoft.Xna.Framework.Rectangle(actor.CurrentEmoteIndex * 16 % Game1.emoteSpriteSheet.Width, actor.CurrentEmoteIndex * 16 / Game1.emoteSpriteSheet.Width * 16, 16, 16)), Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, (float)actor.getStandingY() / 10000f);
- }
- }
- }
- Game1.spriteBatch.End();
- if (Game1.drawLighting)
- {
- Game1.spriteBatch.Begin(SpriteSortMode.Deferred, this.lightingBlend, SamplerState.LinearClamp, (DepthStencilState)null, (RasterizerState)null);
- Game1.spriteBatch.Draw((Texture2D)Game1.lightmap, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(Game1.lightmap.Bounds), Color.White, 0.0f, Vector2.Zero, (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 1f);
- if (Game1.isRaining && (bool)((NetFieldBase<bool, NetBool>)Game1.currentLocation.isOutdoors) && !(Game1.currentLocation is Desert))
- Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.OrangeRed * 0.45f);
- Game1.spriteBatch.End();
- }
- Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
- if (Game1.drawGrid)
- {
- int num2 = -Game1.viewport.X % 64;
- float num3 = (float)(-Game1.viewport.Y % 64);
- int num4 = num2;
- while (true)
- {
- int num5 = num4;
- viewport1 = Game1.graphics.GraphicsDevice.Viewport;
- int width1 = viewport1.Width;
- if (num5 < width1)
- {
- SpriteBatch spriteBatch = Game1.spriteBatch;
- Texture2D staminaRect = Game1.staminaRect;
- int x = num4;
- int y = (int)num3;
- int width2 = 1;
- viewport1 = Game1.graphics.GraphicsDevice.Viewport;
- int height = viewport1.Height;
- Microsoft.Xna.Framework.Rectangle destinationRectangle = new Microsoft.Xna.Framework.Rectangle(x, y, width2, height);
- Color color = Color.Red * 0.5f;
- spriteBatch.Draw(staminaRect, destinationRectangle, color);
- num4 += 64;
- }
- else
- break;
- }
- float num6 = num3;
- while (true)
- {
- double num5 = (double)num6;
- viewport1 = Game1.graphics.GraphicsDevice.Viewport;
- double height1 = (double)viewport1.Height;
- if (num5 < height1)
- {
- SpriteBatch spriteBatch = Game1.spriteBatch;
- Texture2D staminaRect = Game1.staminaRect;
- int x = num2;
- int y = (int)num6;
- viewport1 = Game1.graphics.GraphicsDevice.Viewport;
- int width = viewport1.Width;
- int height2 = 1;
- Microsoft.Xna.Framework.Rectangle destinationRectangle = new Microsoft.Xna.Framework.Rectangle(x, y, width, height2);
- Color color = Color.Red * 0.5f;
- spriteBatch.Draw(staminaRect, destinationRectangle, color);
- num6 += 64f;
- }
- else
- break;
- }
- }
- if ((uint)Game1.currentBillboard > 0U)
- this.drawBillboard();
- if ((Game1.displayHUD || Game1.eventUp) && (Game1.currentBillboard == 0 && (int)Game1.gameMode == 3) && (!Game1.freezeControls && !Game1.panMode) && !Game1.HostPaused)
- {
- this.Events.Graphics_OnPreRenderHudEvent.Raise();
- this.drawHUD();
- this.Events.Graphics_OnPostRenderHudEvent.Raise();
- }
- else if (Game1.activeClickableMenu == null && Game1.farmEvent == null)
- Game1.spriteBatch.Draw(Game1.mouseCursors, new Vector2((float)Game1.getOldMouseX(), (float)Game1.getOldMouseY()), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.mouseCursors, 0, 16, 16)), Color.White, 0.0f, Vector2.Zero, (float)(4.0 + (double)Game1.dialogueButtonScale / 150.0), SpriteEffects.None, 1f);
- if (Game1.hudMessages.Count > 0 && (!Game1.eventUp || Game1.isFestival()))
- {
- for (int i = Game1.hudMessages.Count - 1; i >= 0; --i)
- Game1.hudMessages[i].draw(Game1.spriteBatch, i);
- }
- }
- if (Game1.farmEvent != null)
- Game1.farmEvent.draw(Game1.spriteBatch);
- if (Game1.dialogueUp && !Game1.nameSelectUp && !Game1.messagePause && (Game1.activeClickableMenu == null || !(Game1.activeClickableMenu is DialogueBox)))
- this.drawDialogueBox();
- if (Game1.progressBar)
- {
- Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, Game1.dialogueWidth, 32), Color.LightGray);
- Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle((Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, (int)((double)Game1.pauseAccumulator / (double)Game1.pauseTime * (double)Game1.dialogueWidth), 32), Color.DimGray);
- }
- if (Game1.eventUp && (Game1.currentLocation != null && Game1.currentLocation.currentEvent != null))
- Game1.currentLocation.currentEvent.drawAfterMap(Game1.spriteBatch);
- if (Game1.isRaining && (Game1.currentLocation != null && (bool)((NetFieldBase<bool, NetBool>)Game1.currentLocation.isOutdoors) && !(Game1.currentLocation is Desert)))
- {
- SpriteBatch spriteBatch = Game1.spriteBatch;
- Texture2D staminaRect = Game1.staminaRect;
- viewport1 = Game1.graphics.GraphicsDevice.Viewport;
- Microsoft.Xna.Framework.Rectangle bounds = viewport1.Bounds;
- Color color = Color.Blue * 0.2f;
- spriteBatch.Draw(staminaRect, bounds, color);
- }
- if ((Game1.fadeToBlack || Game1.globalFade) && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause))
- {
- SpriteBatch spriteBatch = Game1.spriteBatch;
- Texture2D fadeToBlackRect = Game1.fadeToBlackRect;
- viewport1 = Game1.graphics.GraphicsDevice.Viewport;
- Microsoft.Xna.Framework.Rectangle bounds = viewport1.Bounds;
- Color color = Color.Black * ((int)Game1.gameMode == 0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha);
- spriteBatch.Draw(fadeToBlackRect, bounds, color);
- }
- else if ((double)Game1.flashAlpha > 0.0)
- {
- if (Game1.options.screenFlash)
- {
- SpriteBatch spriteBatch = Game1.spriteBatch;
- Texture2D fadeToBlackRect = Game1.fadeToBlackRect;
- viewport1 = Game1.graphics.GraphicsDevice.Viewport;
- Microsoft.Xna.Framework.Rectangle bounds = viewport1.Bounds;
- Color color = Color.White * Math.Min(1f, Game1.flashAlpha);
- spriteBatch.Draw(fadeToBlackRect, bounds, color);
- }
- Game1.flashAlpha -= 0.1f;
- }
- if ((Game1.messagePause || Game1.globalFade) && Game1.dialogueUp)
- this.drawDialogueBox();
- foreach (TemporaryAnimatedSprite overlayTempSprite in Game1.screenOverlayTempSprites)
- overlayTempSprite.draw(Game1.spriteBatch, true, 0, 0, 1f);
- if (Game1.debugMode)
- {
- StringBuilder debugStringBuilder = SGame._debugStringBuilder;
- debugStringBuilder.Clear();
- if (Game1.panMode)
- {
- debugStringBuilder.Append((Game1.getOldMouseX() + Game1.viewport.X) / 64);
- debugStringBuilder.Append(",");
- debugStringBuilder.Append((Game1.getOldMouseY() + Game1.viewport.Y) / 64);
- }
- else
- {
- debugStringBuilder.Append("player: ");
- debugStringBuilder.Append(Game1.player.getStandingX() / 64);
- debugStringBuilder.Append(", ");
- debugStringBuilder.Append(Game1.player.getStandingY() / 64);
- }
- debugStringBuilder.Append(" mouseTransparency: ");
- debugStringBuilder.Append(Game1.mouseCursorTransparency);
- debugStringBuilder.Append(" mousePosition: ");
- debugStringBuilder.Append(Game1.getMouseX());
- debugStringBuilder.Append(",");
- debugStringBuilder.Append(Game1.getMouseY());
- debugStringBuilder.Append(Environment.NewLine);
- debugStringBuilder.Append("debugOutput: ");
- debugStringBuilder.Append(Game1.debugOutput);
- Game1.spriteBatch.DrawString(Game1.smallFont, debugStringBuilder, new Vector2((float)this.GraphicsDevice.Viewport.GetTitleSafeArea().X, (float)(this.GraphicsDevice.Viewport.GetTitleSafeArea().Y + Game1.smallFont.LineSpacing * 8)), Color.Red, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f);
- }
- if (Game1.showKeyHelp)
- Game1.spriteBatch.DrawString(Game1.smallFont, Game1.keyHelpString, new Vector2(64f, (float)(Game1.viewport.Height - 64 - (Game1.dialogueUp ? 192 + (Game1.isQuestion ? Game1.questionChoices.Count * 64 : 0) : 0)) - Game1.smallFont.MeasureString(Game1.keyHelpString).Y), Color.LightGray, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f);
- if (Game1.activeClickableMenu != null)
- {
- try
- {
- this.Events.Graphics_OnPreRenderGuiEvent.Raise();
- Game1.activeClickableMenu.draw(Game1.spriteBatch);
- this.Events.Graphics_OnPostRenderGuiEvent.Raise();
- }
- catch (Exception ex)
- {
- this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error);
- Game1.activeClickableMenu.exitThisMenu();
- }
- }
- else if (Game1.farmEvent != null)
- Game1.farmEvent.drawAboveEverything(Game1.spriteBatch);
- if (Game1.HostPaused)
- {
- string s = Game1.content.LoadString("Strings\\StringsFromCSFiles:DayTimeMoneyBox.cs.10378");
- SpriteText.drawStringWithScrollBackground(Game1.spriteBatch, s, 96, 32, "", 1f, -1);
- }
- this.RaisePostRender();
- Game1.spriteBatch.End();
- this.drawOverlays(Game1.spriteBatch);
- this.renderScreenBuffer();
- //base.Draw(gameTime);
- }
- }
- }
- }
- }
-#else
- private void DrawImpl(GameTime gameTime)
- {
- if (Game1.debugMode)
- {
- if (SGame._fpsStopwatch.IsRunning)
- {
- float totalSeconds = (float)SGame._fpsStopwatch.Elapsed.TotalSeconds;
- SGame._fpsList.Add(totalSeconds);
- while (SGame._fpsList.Count >= 120)
- SGame._fpsList.RemoveAt(0);
- float num = 0.0f;
- foreach (float fps in SGame._fpsList)
- num += fps;
- SGame._fps = (float)(1.0 / ((double)num / (double)SGame._fpsList.Count));
- }
- SGame._fpsStopwatch.Restart();
- }
- else
- {
- if (SGame._fpsStopwatch.IsRunning)
- SGame._fpsStopwatch.Reset();
- SGame._fps = 0.0f;
- SGame._fpsList.Clear();
- }
- if (SGame._newDayTask != null)
- {
- this.GraphicsDevice.Clear(this.bgColor);
- //base.Draw(gameTime);
- }
- else
- {
- if ((double)Game1.options.zoomLevel != 1.0)
- this.GraphicsDevice.SetRenderTarget(this.screenWrapper);
- if (this.IsSaving)
- {
- this.GraphicsDevice.Clear(this.bgColor);
- IClickableMenu activeClickableMenu = Game1.activeClickableMenu;
- if (activeClickableMenu != null)
- {
- Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
- try
- {
- this.Events.Graphics_OnPreRenderGuiEvent.Raise();
- activeClickableMenu.draw(Game1.spriteBatch);
- this.Events.Graphics_OnPostRenderGuiEvent.Raise();
- }
- catch (Exception ex)
- {
- this.Monitor.Log($"The {activeClickableMenu.GetType().FullName} menu crashed while drawing itself during save. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error);
- activeClickableMenu.exitThisMenu();
- }
- this.RaisePostRender();
- Game1.spriteBatch.End();
- }
- //base.Draw(gameTime);
- this.renderScreenBuffer();
- }
- else
- {
- this.GraphicsDevice.Clear(this.bgColor);
- if (Game1.activeClickableMenu != null && Game1.options.showMenuBackground && Game1.activeClickableMenu.showWithoutTransparencyIfOptionIsSet())
- {
- Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
- try
- {
- Game1.activeClickableMenu.drawBackground(Game1.spriteBatch);
- this.Events.Graphics_OnPreRenderGuiEvent.Raise();
- Game1.activeClickableMenu.draw(Game1.spriteBatch);
- this.Events.Graphics_OnPostRenderGuiEvent.Raise();
- }
- catch (Exception ex)
- {
- this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error);
- Game1.activeClickableMenu.exitThisMenu();
- }
- this.RaisePostRender();
- Game1.spriteBatch.End();
- if ((double)Game1.options.zoomLevel != 1.0)
- {
- this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null);
- this.GraphicsDevice.Clear(this.bgColor);
- Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone);
- Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f);
- Game1.spriteBatch.End();
- }
- if (Game1.overlayMenu != null)
- {
- Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
- Game1.overlayMenu.draw(Game1.spriteBatch);
- Game1.spriteBatch.End();
- }
- }
- else if ((int)Game1.gameMode == 11)
- {
- Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
- Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3685"), new Vector2(16f, 16f), Color.HotPink);
- Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3686"), new Vector2(16f, 32f), new Color(0, (int)byte.MaxValue, 0));
- Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.parseText(Game1.errorMessage, Game1.dialogueFont, Game1.graphics.GraphicsDevice.Viewport.Width), new Vector2(16f, 48f), Color.White);
- this.RaisePostRender();
- Game1.spriteBatch.End();
- }
- else if (Game1.currentMinigame != null)
- {
- Game1.currentMinigame.draw(Game1.spriteBatch);
- if (Game1.globalFade && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause))
- {
- Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
- Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * ((int)Game1.gameMode == 0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha));
- Game1.spriteBatch.End();
- }
- this.RaisePostRender(needsNewBatch: true);
- if ((double)Game1.options.zoomLevel != 1.0)
- {
- this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null);
- this.GraphicsDevice.Clear(this.bgColor);
- Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone);
- Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f);
- Game1.spriteBatch.End();
- }
- if (Game1.overlayMenu != null)
- {
- Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
- Game1.overlayMenu.draw(Game1.spriteBatch);
- Game1.spriteBatch.End();
- }
- }
- else if (Game1.showingEndOfNightStuff)
- {
- Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
- if (Game1.activeClickableMenu != null)
- {
- try
- {
- this.Events.Graphics_OnPreRenderGuiEvent.Raise();
- Game1.activeClickableMenu.draw(Game1.spriteBatch);
- this.Events.Graphics_OnPostRenderGuiEvent.Raise();
- }
- catch (Exception ex)
- {
- this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself during end-of-night-stuff. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error);
- Game1.activeClickableMenu.exitThisMenu();
- }
- }
- this.RaisePostRender();
- Game1.spriteBatch.End();
if ((double)Game1.options.zoomLevel != 1.0)
{
this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null);
this.GraphicsDevice.Clear(this.bgColor);
Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone);
- Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f);
- Game1.spriteBatch.End();
- }
- if (Game1.overlayMenu != null)
- {
- Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
- Game1.overlayMenu.draw(Game1.spriteBatch);
+ Game1.spriteBatch.Draw((Texture2D)this.screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screen.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f);
Game1.spriteBatch.End();
}
}
- else if ((int)Game1.gameMode == 6)
+ else if (Game1.gameMode == (byte)6 || Game1.gameMode == (byte)3 && Game1.currentLocation == null)
{
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
string str1 = "";
for (int index = 0; (double)index < gameTime.TotalGameTime.TotalMilliseconds % 999.0 / 333.0; ++index)
str1 += ".";
string str2 = Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3688");
- string str3 = str1;
- string s = str2 + str3;
- string str4 = "...";
- string str5 = str2 + str4;
- int widthOfString = SpriteText.getWidthOfString(str5);
+ string s = str2 + str1;
+ string str3 = str2 + "... ";
+ int widthOfString = SpriteText.getWidthOfString(str3);
int height = 64;
int x = 64;
- int y = Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - height;
- SpriteText.drawString(Game1.spriteBatch, s, x, y, 999999, widthOfString, height, 1f, 0.88f, false, 0, str5, -1);
- this.RaisePostRender();
+ int y = Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - height;
+ SpriteText.drawString(Game1.spriteBatch, s, x, y, 999999, widthOfString, height, 1f, 0.88f, false, 0, str3, -1);
Game1.spriteBatch.End();
+ this.drawOverlays(Game1.spriteBatch);
if ((double)Game1.options.zoomLevel != 1.0)
{
this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null);
this.GraphicsDevice.Clear(this.bgColor);
Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone);
- Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f);
- Game1.spriteBatch.End();
- }
- if (Game1.overlayMenu != null)
- {
- Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
- Game1.overlayMenu.draw(Game1.spriteBatch);
+ Game1.spriteBatch.Draw((Texture2D)this.screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screen.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f);
Game1.spriteBatch.End();
}
+ //base.Draw(gameTime);
}
else
{
Microsoft.Xna.Framework.Rectangle rectangle;
- if ((int)Game1.gameMode == 0)
+ if (Game1.gameMode == (byte)0)
{
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
}
@@ -1526,14 +985,14 @@ namespace StardewModdingAPI.Framework
this.GraphicsDevice.SetRenderTarget(Game1.lightmap);
this.GraphicsDevice.Clear(Color.White * 0.0f);
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
- Game1.spriteBatch.Draw(Game1.staminaRect, Game1.lightmap.Bounds, Game1.currentLocation.name.Equals("UndergroundMine") ? Game1.mine.getLightingColor(gameTime) : (Game1.ambientLight.Equals(Color.White) || Game1.isRaining && Game1.currentLocation.isOutdoors ? Game1.outdoorLight : Game1.ambientLight));
+ Game1.spriteBatch.Draw(Game1.staminaRect, Game1.lightmap.Bounds, Game1.currentLocation.Name.StartsWith("UndergroundMine") ? Game1.mine.getLightingColor(gameTime) : (Game1.ambientLight.Equals(Color.White) || Game1.isRaining && (bool)((NetFieldBase<bool, NetBool>)Game1.currentLocation.isOutdoors) ? Game1.outdoorLight : Game1.ambientLight));
for (int index = 0; index < Game1.currentLightSources.Count; ++index)
{
- if (Utility.isOnScreen(Game1.currentLightSources.ElementAt<LightSource>(index).position, (int)((double)Game1.currentLightSources.ElementAt<LightSource>(index).radius * (double)Game1.tileSize * 4.0)))
- Game1.spriteBatch.Draw(Game1.currentLightSources.ElementAt<LightSource>(index).lightTexture, Game1.GlobalToLocal(Game1.viewport, Game1.currentLightSources.ElementAt<LightSource>(index).position) / (float)(Game1.options.lightingQuality / 2), new Microsoft.Xna.Framework.Rectangle?(Game1.currentLightSources.ElementAt<LightSource>(index).lightTexture.Bounds), Game1.currentLightSources.ElementAt<LightSource>(index).color, 0.0f, new Vector2((float)Game1.currentLightSources.ElementAt<LightSource>(index).lightTexture.Bounds.Center.X, (float)Game1.currentLightSources.ElementAt<LightSource>(index).lightTexture.Bounds.Center.Y), Game1.currentLightSources.ElementAt<LightSource>(index).radius / (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 0.9f);
+ if (Utility.isOnScreen((Vector2)((NetFieldBase<Vector2, NetVector2>)Game1.currentLightSources.ElementAt<LightSource>(index).position), (int)((double)(float)((NetFieldBase<float, NetFloat>)Game1.currentLightSources.ElementAt<LightSource>(index).radius) * 64.0 * 4.0)))
+ Game1.spriteBatch.Draw(Game1.currentLightSources.ElementAt<LightSource>(index).lightTexture, Game1.GlobalToLocal(Game1.viewport, (Vector2)((NetFieldBase<Vector2, NetVector2>)Game1.currentLightSources.ElementAt<LightSource>(index).position)) / (float)(Game1.options.lightingQuality / 2), new Microsoft.Xna.Framework.Rectangle?(Game1.currentLightSources.ElementAt<LightSource>(index).lightTexture.Bounds), (Color)((NetFieldBase<Color, NetColor>)Game1.currentLightSources.ElementAt<LightSource>(index).color), 0.0f, new Vector2((float)Game1.currentLightSources.ElementAt<LightSource>(index).lightTexture.Bounds.Center.X, (float)Game1.currentLightSources.ElementAt<LightSource>(index).lightTexture.Bounds.Center.Y), (float)((NetFieldBase<float, NetFloat>)Game1.currentLightSources.ElementAt<LightSource>(index).radius) / (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 0.9f);
}
Game1.spriteBatch.End();
- this.GraphicsDevice.SetRenderTarget((double)Game1.options.zoomLevel == 1.0 ? (RenderTarget2D)null : this.screenWrapper);
+ this.GraphicsDevice.SetRenderTarget((double)Game1.options.zoomLevel == 1.0 ? (RenderTarget2D)null : this.screen);
}
if (Game1.bloomDay && Game1.bloom != null)
Game1.bloom.BeginDraw();
@@ -1543,117 +1002,105 @@ namespace StardewModdingAPI.Framework
if (Game1.background != null)
Game1.background.draw(Game1.spriteBatch);
Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch);
- Game1.currentLocation.Map.GetLayer("Back").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom);
+ Game1.currentLocation.Map.GetLayer("Back").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4);
Game1.currentLocation.drawWater(Game1.spriteBatch);
- if (Game1.CurrentEvent == null)
+ IEnumerable<Farmer> source = Game1.currentLocation.farmers;
+ if (Game1.currentLocation.currentEvent != null && !Game1.currentLocation.currentEvent.isFestival && Game1.currentLocation.currentEvent.farmerActors.Count > 0)
+ source = (IEnumerable<Farmer>)Game1.currentLocation.currentEvent.farmerActors;
+ IEnumerable<Farmer> farmers = source.Where<Farmer>((Func<Farmer, bool>)(farmer =>
{
- foreach (NPC character in Game1.currentLocation.characters)
+ if (!farmer.IsLocalPlayer)
+ return !(bool)((NetFieldBase<bool, NetBool>)farmer.hidden);
+ return true;
+ }));
+ if (!Game1.currentLocation.shouldHideCharacters())
+ {
+ if (Game1.CurrentEvent == null)
{
- if (!character.swimming && !character.hideShadow && (!character.isInvisible && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation())))
- Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, character.position + new Vector2((float)(character.sprite.spriteWidth * Game1.pixelZoom) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : Game1.pixelZoom * 3)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), ((float)Game1.pixelZoom + (float)character.yJumpOffset / 40f) * character.scale, SpriteEffects.None, Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 1E-06f);
+ foreach (NPC character in Game1.currentLocation.characters)
+ {
+ if (!(bool)((NetFieldBase<bool, NetBool>)character.swimming) && !character.HideShadow && (!character.IsInvisible && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation())))
+ Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, character.Position + new Vector2((float)(character.Sprite.SpriteWidth * 4) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)character.yJumpOffset / 40.0) * (float)((NetFieldBase<float, NetFloat>)character.scale), SpriteEffects.None, Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 1E-06f);
+ }
}
- }
- else
- {
- foreach (NPC actor in Game1.CurrentEvent.actors)
+ else
{
- if (!actor.swimming && !actor.hideShadow && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation()))
- Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.position + new Vector2((float)(actor.sprite.spriteWidth * Game1.pixelZoom) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : (actor.sprite.spriteHeight <= 16 ? -Game1.pixelZoom : Game1.pixelZoom * 3))))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), ((float)Game1.pixelZoom + (float)actor.yJumpOffset / 40f) * actor.scale, SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f);
+ foreach (NPC actor in Game1.CurrentEvent.actors)
+ {
+ if (!(bool)((NetFieldBase<bool, NetBool>)actor.swimming) && !actor.HideShadow && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation()))
+ Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : (actor.Sprite.SpriteHeight <= 16 ? -4 : 12))))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)actor.yJumpOffset / 40.0) * (float)((NetFieldBase<float, NetFloat>)actor.scale), SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f);
+ }
}
- }
- Microsoft.Xna.Framework.Rectangle bounds;
- if (Game1.displayFarmer && !Game1.player.swimming && (!Game1.player.isRidingHorse() && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(Game1.player.getTileLocation())))
- {
- SpriteBatch spriteBatch = Game1.spriteBatch;
- Texture2D shadowTexture = Game1.shadowTexture;
- Vector2 local = Game1.GlobalToLocal(Game1.player.position + new Vector2(32f, 24f));
- Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds);
- Color white = Color.White;
- double num1 = 0.0;
- double x = (double)Game1.shadowTexture.Bounds.Center.X;
- bounds = Game1.shadowTexture.Bounds;
- double y = (double)bounds.Center.Y;
- Vector2 origin = new Vector2((float)x, (float)y);
- double num2 = 4.0 - (!Game1.player.running && !Game1.player.usingTool || Game1.player.FarmerSprite.indexInCurrentAnimation <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[Game1.player.FarmerSprite.CurrentFrame]) * 0.5);
- int num3 = 0;
- double num4 = 0.0;
- spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4);
- }
- Game1.currentLocation.Map.GetLayer("Buildings").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom);
- Game1.mapDisplayDevice.EndScene();
- Game1.spriteBatch.End();
- Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
- if (Game1.CurrentEvent == null)
- {
- foreach (NPC character in Game1.currentLocation.characters)
+ foreach (Farmer farmer in farmers)
{
- if (!character.swimming && !character.hideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation()))
+ if (!(bool)((NetFieldBase<bool, NetBool>)farmer.swimming) && !farmer.isRidingHorse() && (Game1.currentLocation == null || !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmer.getTileLocation())))
{
SpriteBatch spriteBatch = Game1.spriteBatch;
Texture2D shadowTexture = Game1.shadowTexture;
- Vector2 local = Game1.GlobalToLocal(Game1.viewport, character.position + new Vector2((float)(character.sprite.spriteWidth * Game1.pixelZoom) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : Game1.pixelZoom * 3))));
+ Vector2 local = Game1.GlobalToLocal(farmer.Position + new Vector2(32f, 24f));
Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds);
Color white = Color.White;
double num1 = 0.0;
- bounds = Game1.shadowTexture.Bounds;
+ Microsoft.Xna.Framework.Rectangle bounds = Game1.shadowTexture.Bounds;
double x = (double)bounds.Center.X;
bounds = Game1.shadowTexture.Bounds;
double y = (double)bounds.Center.Y;
Vector2 origin = new Vector2((float)x, (float)y);
- double num2 = ((double)Game1.pixelZoom + (double)character.yJumpOffset / 40.0) * (double)character.scale;
+ double num2 = 4.0 - (!farmer.running && !farmer.UsingTool || farmer.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmer.FarmerSprite.CurrentFrame]) * 0.5);
int num3 = 0;
- double num4 = (double)Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 9.99999997475243E-07;
+ double num4 = 0.0;
spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4);
}
}
}
- else
+ Game1.currentLocation.Map.GetLayer("Buildings").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4);
+ Game1.mapDisplayDevice.EndScene();
+ Game1.spriteBatch.End();
+ Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
+ if (!Game1.currentLocation.shouldHideCharacters())
{
- foreach (NPC actor in Game1.CurrentEvent.actors)
+ if (Game1.CurrentEvent == null)
+ {
+ foreach (NPC character in Game1.currentLocation.characters)
+ {
+ if (!(bool)((NetFieldBase<bool, NetBool>)character.swimming) && !character.HideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation()))
+ Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, character.Position + new Vector2((float)(character.Sprite.SpriteWidth * 4) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)character.yJumpOffset / 40.0) * (float)((NetFieldBase<float, NetFloat>)character.scale), SpriteEffects.None, Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 1E-06f);
+ }
+ }
+ else
+ {
+ foreach (NPC actor in Game1.CurrentEvent.actors)
+ {
+ if (!(bool)((NetFieldBase<bool, NetBool>)actor.swimming) && !actor.HideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation()))
+ Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)actor.yJumpOffset / 40.0) * (float)((NetFieldBase<float, NetFloat>)actor.scale), SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f);
+ }
+ }
+ foreach (Farmer farmer in farmers)
{
- if (!actor.swimming && !actor.hideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation()))
+ if (!(bool)((NetFieldBase<bool, NetBool>)farmer.swimming) && !farmer.isRidingHorse() && (Game1.currentLocation != null && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmer.getTileLocation())))
{
SpriteBatch spriteBatch = Game1.spriteBatch;
Texture2D shadowTexture = Game1.shadowTexture;
- Vector2 local = Game1.GlobalToLocal(Game1.viewport, actor.position + new Vector2((float)(actor.sprite.spriteWidth * Game1.pixelZoom) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : Game1.pixelZoom * 3))));
+ Vector2 local = Game1.GlobalToLocal(farmer.Position + new Vector2(32f, 24f));
Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds);
Color white = Color.White;
double num1 = 0.0;
- bounds = Game1.shadowTexture.Bounds;
+ Microsoft.Xna.Framework.Rectangle bounds = Game1.shadowTexture.Bounds;
double x = (double)bounds.Center.X;
bounds = Game1.shadowTexture.Bounds;
double y = (double)bounds.Center.Y;
Vector2 origin = new Vector2((float)x, (float)y);
- double num2 = ((double)Game1.pixelZoom + (double)actor.yJumpOffset / 40.0) * (double)actor.scale;
+ double num2 = 4.0 - (!farmer.running && !farmer.UsingTool || farmer.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmer.FarmerSprite.CurrentFrame]) * 0.5);
int num3 = 0;
- double num4 = (double)Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 9.99999997475243E-07;
+ double num4 = 0.0;
spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4);
}
}
}
- if (Game1.displayFarmer && !Game1.player.swimming && (!Game1.player.isRidingHorse() && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(Game1.player.getTileLocation())))
- {
- SpriteBatch spriteBatch = Game1.spriteBatch;
- Texture2D shadowTexture = Game1.shadowTexture;
- Vector2 local = Game1.GlobalToLocal(Game1.player.position + new Vector2(32f, 24f));
- Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds);
- Color white = Color.White;
- double num1 = 0.0;
- double x = (double)Game1.shadowTexture.Bounds.Center.X;
- rectangle = Game1.shadowTexture.Bounds;
- double y = (double)rectangle.Center.Y;
- Vector2 origin = new Vector2((float)x, (float)y);
- double num2 = 4.0 - (!Game1.player.running && !Game1.player.usingTool || Game1.player.FarmerSprite.indexInCurrentAnimation <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[Game1.player.FarmerSprite.CurrentFrame]) * 0.5);
- int num3 = 0;
- double num4 = (double)Math.Max(0.0001f, (float)((double)Game1.player.getStandingY() / 10000.0 + 0.000110000000859145)) - 9.99999974737875E-05;
- spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4);
- }
- if (Game1.displayFarmer)
- Game1.player.draw(Game1.spriteBatch);
if ((Game1.eventUp || Game1.killScreen) && (!Game1.killScreen && Game1.currentLocation.currentEvent != null))
Game1.currentLocation.currentEvent.draw(Game1.spriteBatch);
if (Game1.player.currentUpgrade != null && Game1.player.currentUpgrade.daysLeftTillUpgradeDone <= 3 && Game1.currentLocation.Name.Equals("Farm"))
- Game1.spriteBatch.Draw(Game1.player.currentUpgrade.workerTexture, Game1.GlobalToLocal(Game1.viewport, Game1.player.currentUpgrade.positionOfCarpenter), new Microsoft.Xna.Framework.Rectangle?(Game1.player.currentUpgrade.getSourceRectangle()), Color.White, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, (float)(((double)Game1.player.currentUpgrade.positionOfCarpenter.Y + (double)(Game1.tileSize * 3 / 4)) / 10000.0));
+ Game1.spriteBatch.Draw(Game1.player.currentUpgrade.workerTexture, Game1.GlobalToLocal(Game1.viewport, Game1.player.currentUpgrade.positionOfCarpenter), new Microsoft.Xna.Framework.Rectangle?(Game1.player.currentUpgrade.getSourceRectangle()), Color.White, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, (float)(((double)Game1.player.currentUpgrade.positionOfCarpenter.Y + 48.0) / 10000.0));
Game1.currentLocation.draw(Game1.spriteBatch);
if (Game1.eventUp && Game1.currentLocation.currentEvent != null)
{
@@ -1664,60 +1111,50 @@ namespace StardewModdingAPI.Framework
if (Game1.currentLocation.Name.Equals("Farm"))
this.drawFarmBuildings();
if (Game1.tvStation >= 0)
- Game1.spriteBatch.Draw(Game1.tvStationTexture, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(6 * Game1.tileSize + Game1.tileSize / 4), (float)(2 * Game1.tileSize + Game1.tileSize / 2))), new Microsoft.Xna.Framework.Rectangle?(new Microsoft.Xna.Framework.Rectangle(Game1.tvStation * 24, 0, 24, 15)), Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, 1E-08f);
+ Game1.spriteBatch.Draw(Game1.tvStationTexture, Game1.GlobalToLocal(Game1.viewport, new Vector2(400f, 160f)), new Microsoft.Xna.Framework.Rectangle?(new Microsoft.Xna.Framework.Rectangle(Game1.tvStation * 24, 0, 24, 15)), Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, 1E-08f);
if (Game1.panMode)
{
- Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((int)Math.Floor((double)(Game1.getOldMouseX() + Game1.viewport.X) / (double)Game1.tileSize) * Game1.tileSize - Game1.viewport.X, (int)Math.Floor((double)(Game1.getOldMouseY() + Game1.viewport.Y) / (double)Game1.tileSize) * Game1.tileSize - Game1.viewport.Y, Game1.tileSize, Game1.tileSize), Color.Lime * 0.75f);
- foreach (Warp warp in Game1.currentLocation.warps)
- Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(warp.X * Game1.tileSize - Game1.viewport.X, warp.Y * Game1.tileSize - Game1.viewport.Y, Game1.tileSize, Game1.tileSize), Color.Red * 0.75f);
+ Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((int)Math.Floor((double)(Game1.getOldMouseX() + Game1.viewport.X) / 64.0) * 64 - Game1.viewport.X, (int)Math.Floor((double)(Game1.getOldMouseY() + Game1.viewport.Y) / 64.0) * 64 - Game1.viewport.Y, 64, 64), Color.Lime * 0.75f);
+ foreach (Warp warp in (NetList<Warp, NetRef<Warp>>)Game1.currentLocation.warps)
+ Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(warp.X * 64 - Game1.viewport.X, warp.Y * 64 - Game1.viewport.Y, 64, 64), Color.Red * 0.75f);
}
Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch);
- Game1.currentLocation.Map.GetLayer("Front").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom);
+ Game1.currentLocation.Map.GetLayer("Front").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4);
Game1.mapDisplayDevice.EndScene();
Game1.currentLocation.drawAboveFrontLayer(Game1.spriteBatch);
Game1.spriteBatch.End();
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
- if (Game1.currentLocation.Name.Equals("Farm") && Game1.stats.SeedsSown >= 200U)
- {
- Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(3 * Game1.tileSize + Game1.tileSize / 4), (float)(Game1.tileSize + Game1.tileSize / 3))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White);
- Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(4 * Game1.tileSize + Game1.tileSize), (float)(2 * Game1.tileSize + Game1.tileSize))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White);
- Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(5 * Game1.tileSize), (float)(2 * Game1.tileSize))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White);
- Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(3 * Game1.tileSize + Game1.tileSize / 2), (float)(3 * Game1.tileSize))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White);
- Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(5 * Game1.tileSize - Game1.tileSize / 4), (float)Game1.tileSize)), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White);
- Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(4 * Game1.tileSize), (float)(3 * Game1.tileSize + Game1.tileSize / 6))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White);
- Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(4 * Game1.tileSize + Game1.tileSize / 5), (float)(2 * Game1.tileSize + Game1.tileSize / 3))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White);
- }
- if (Game1.displayFarmer && Game1.player.ActiveObject != null && (Game1.player.ActiveObject.bigCraftable && this.checkBigCraftableBoundariesForFrontLayer()) && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null)
+ if (Game1.displayFarmer && Game1.player.ActiveObject != null && ((bool)((NetFieldBase<bool, NetBool>)Game1.player.ActiveObject.bigCraftable) && this.checkBigCraftableBoundariesForFrontLayer()) && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null)
Game1.drawPlayerHeldObject(Game1.player);
else if (Game1.displayFarmer && Game1.player.ActiveObject != null)
{
- if (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.position.X, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5), Game1.viewport.Size) == null || Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.position.X, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5), Game1.viewport.Size).TileIndexProperties.ContainsKey("FrontAlways"))
+ if (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.Position.X, (int)Game1.player.Position.Y - 38), Game1.viewport.Size) == null || Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.Position.X, (int)Game1.player.Position.Y - 38), Game1.viewport.Size).TileIndexProperties.ContainsKey("FrontAlways"))
{
Layer layer1 = Game1.currentLocation.Map.GetLayer("Front");
rectangle = Game1.player.GetBoundingBox();
- Location mapDisplayLocation1 = new Location(rectangle.Right, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5);
+ Location mapDisplayLocation1 = new Location(rectangle.Right, (int)Game1.player.Position.Y - 38);
Size size1 = Game1.viewport.Size;
if (layer1.PickTile(mapDisplayLocation1, size1) != null)
{
Layer layer2 = Game1.currentLocation.Map.GetLayer("Front");
rectangle = Game1.player.GetBoundingBox();
- Location mapDisplayLocation2 = new Location(rectangle.Right, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5);
+ Location mapDisplayLocation2 = new Location(rectangle.Right, (int)Game1.player.Position.Y - 38);
Size size2 = Game1.viewport.Size;
if (layer2.PickTile(mapDisplayLocation2, size2).TileIndexProperties.ContainsKey("FrontAlways"))
- goto label_127;
+ goto label_139;
}
else
- goto label_127;
+ goto label_139;
}
Game1.drawPlayerHeldObject(Game1.player);
}
- label_127:
- 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 - Game1.tileSize * 3 / 5), Game1.viewport.Size) != null && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null)))
+ label_139:
+ 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)
{
Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch);
- Game1.currentLocation.Map.GetLayer("AlwaysFront").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom);
+ Game1.currentLocation.Map.GetLayer("AlwaysFront").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4);
Game1.mapDisplayDevice.EndScene();
}
if ((double)Game1.toolHold > 400.0 && Game1.player.CurrentTool.UpgradeLevel >= 1 && Game1.player.canReleaseTool)
@@ -1738,10 +1175,10 @@ namespace StardewModdingAPI.Framework
color = Tool.iridiumColor;
break;
}
- Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X - 2, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : Game1.tileSize) - 2, (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607) + 4, Game1.tileSize / 8 + 4), Color.Black);
- Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : Game1.tileSize), (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607), Game1.tileSize / 8), color);
+ Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X - 2, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : 64) - 2, (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607) + 4, 12), Color.Black);
+ Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : 64), (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607), 8), color);
}
- if (Game1.isDebrisWeather && Game1.currentLocation.IsOutdoors && (!Game1.currentLocation.ignoreDebrisWeather && !Game1.currentLocation.Name.Equals("Desert")) && Game1.viewport.X > -10)
+ if (Game1.isDebrisWeather && Game1.currentLocation.IsOutdoors && (!(bool)((NetFieldBase<bool, NetBool>)Game1.currentLocation.ignoreDebrisWeather) && !Game1.currentLocation.Name.Equals("Desert")) && Game1.viewport.X > -10)
{
foreach (WeatherDebris weatherDebris in Game1.debrisWeather)
weatherDebris.draw(Game1.spriteBatch);
@@ -1755,13 +1192,12 @@ namespace StardewModdingAPI.Framework
Game1.currentLocation.drawAboveAlwaysFrontLayer(Game1.spriteBatch);
if (Game1.player.CurrentTool != null && Game1.player.CurrentTool is FishingRod && ((Game1.player.CurrentTool as FishingRod).isTimingCast || (double)(Game1.player.CurrentTool as FishingRod).castingChosenCountdown > 0.0 || ((Game1.player.CurrentTool as FishingRod).fishCaught || (Game1.player.CurrentTool as FishingRod).showingTreasure)))
Game1.player.CurrentTool.draw(Game1.spriteBatch);
- if (Game1.isRaining && Game1.currentLocation.IsOutdoors && (!Game1.currentLocation.Name.Equals("Desert") && !(Game1.currentLocation is Summit)) && (!Game1.eventUp || Game1.currentLocation.isTileOnMap(new Vector2((float)(Game1.viewport.X / Game1.tileSize), (float)(Game1.viewport.Y / Game1.tileSize)))))
+ if (Game1.isRaining && Game1.currentLocation.IsOutdoors && (!Game1.currentLocation.Name.Equals("Desert") && !(Game1.currentLocation is Summit)) && (!Game1.eventUp || Game1.currentLocation.isTileOnMap(new Vector2((float)(Game1.viewport.X / 64), (float)(Game1.viewport.Y / 64)))))
{
for (int index = 0; index < Game1.rainDrops.Length; ++index)
Game1.spriteBatch.Draw(Game1.rainTexture, Game1.rainDrops[index].position, new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.rainTexture, Game1.rainDrops[index].frame, -1, -1)), Color.White);
}
Game1.spriteBatch.End();
- //base.Draw(gameTime);
Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
if (Game1.eventUp && Game1.currentLocation.currentEvent != null)
{
@@ -1770,12 +1206,12 @@ namespace StardewModdingAPI.Framework
if (actor.isEmoting)
{
Vector2 localPosition = actor.getLocalPosition(Game1.viewport);
- localPosition.Y -= (float)(Game1.tileSize * 2 + Game1.pixelZoom * 3);
- if (actor.age == 2)
- localPosition.Y += (float)(Game1.tileSize / 2);
- else if (actor.gender == 1)
- localPosition.Y += (float)(Game1.tileSize / 6);
- Game1.spriteBatch.Draw(Game1.emoteSpriteSheet, localPosition, new Microsoft.Xna.Framework.Rectangle?(new Microsoft.Xna.Framework.Rectangle(actor.CurrentEmoteIndex * (Game1.tileSize / 4) % Game1.emoteSpriteSheet.Width, actor.CurrentEmoteIndex * (Game1.tileSize / 4) / Game1.emoteSpriteSheet.Width * (Game1.tileSize / 4), Game1.tileSize / 4, Game1.tileSize / 4)), Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, (float)actor.getStandingY() / 10000f);
+ localPosition.Y -= 140f;
+ if (actor.Age == 2)
+ localPosition.Y += 32f;
+ else if (actor.Gender == 1)
+ localPosition.Y += 10f;
+ Game1.spriteBatch.Draw(Game1.emoteSpriteSheet, localPosition, new Microsoft.Xna.Framework.Rectangle?(new Microsoft.Xna.Framework.Rectangle(actor.CurrentEmoteIndex * 16 % Game1.emoteSpriteSheet.Width, actor.CurrentEmoteIndex * 16 / Game1.emoteSpriteSheet.Width * 16, 16, 16)), Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, (float)actor.getStandingY() / 10000f);
}
}
}
@@ -1784,31 +1220,31 @@ namespace StardewModdingAPI.Framework
{
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, this.lightingBlend, SamplerState.LinearClamp, (DepthStencilState)null, (RasterizerState)null);
Game1.spriteBatch.Draw((Texture2D)Game1.lightmap, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(Game1.lightmap.Bounds), Color.White, 0.0f, Vector2.Zero, (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 1f);
- if (Game1.isRaining && Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert))
+ if (Game1.isRaining && (bool)((NetFieldBase<bool, NetBool>)Game1.currentLocation.isOutdoors) && !(Game1.currentLocation is Desert))
Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.OrangeRed * 0.45f);
Game1.spriteBatch.End();
}
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
if (Game1.drawGrid)
{
- int x1 = -Game1.viewport.X % Game1.tileSize;
- float num1 = (float)(-Game1.viewport.Y % Game1.tileSize);
+ int x1 = -Game1.viewport.X % 64;
+ float num1 = (float)(-Game1.viewport.Y % 64);
int x2 = x1;
while (x2 < Game1.graphics.GraphicsDevice.Viewport.Width)
{
Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(x2, (int)num1, 1, Game1.graphics.GraphicsDevice.Viewport.Height), Color.Red * 0.5f);
- x2 += Game1.tileSize;
+ x2 += 64;
}
float num2 = num1;
while ((double)num2 < (double)Game1.graphics.GraphicsDevice.Viewport.Height)
{
Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(x1, (int)num2, Game1.graphics.GraphicsDevice.Viewport.Width, 1), Color.Red * 0.5f);
- num2 += (float)Game1.tileSize;
+ num2 += 64f;
}
}
if (Game1.currentBillboard != 0)
this.drawBillboard();
- if ((Game1.displayHUD || Game1.eventUp) && (Game1.currentBillboard == 0 && (int)Game1.gameMode == 3) && (!Game1.freezeControls && !Game1.panMode))
+ if ((Game1.displayHUD || Game1.eventUp) && (Game1.currentBillboard == 0 && Game1.gameMode == (byte)3) && (!Game1.freezeControls && !Game1.panMode && !Game1.HostPaused))
{
this.Events.Graphics_OnPreRenderHudEvent.Raise();
this.drawHUD();
@@ -1826,120 +1262,75 @@ namespace StardewModdingAPI.Framework
Game1.farmEvent.draw(Game1.spriteBatch);
if (Game1.dialogueUp && !Game1.nameSelectUp && !Game1.messagePause && (Game1.activeClickableMenu == null || !(Game1.activeClickableMenu is DialogueBox)))
this.drawDialogueBox();
- Viewport viewport;
if (Game1.progressBar)
{
SpriteBatch spriteBatch1 = Game1.spriteBatch;
Texture2D fadeToBlackRect = Game1.fadeToBlackRect;
- int x1 = (Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Width - Game1.dialogueWidth) / 2;
- rectangle = Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea;
- int y1 = rectangle.Bottom - Game1.tileSize * 2;
+ int x1 = (Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2;
+ rectangle = Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea();
+ int y1 = rectangle.Bottom - 128;
int dialogueWidth = Game1.dialogueWidth;
- int height1 = Game1.tileSize / 2;
+ int height1 = 32;
Microsoft.Xna.Framework.Rectangle destinationRectangle1 = new Microsoft.Xna.Framework.Rectangle(x1, y1, dialogueWidth, height1);
Color lightGray = Color.LightGray;
spriteBatch1.Draw(fadeToBlackRect, destinationRectangle1, lightGray);
SpriteBatch spriteBatch2 = Game1.spriteBatch;
Texture2D staminaRect = Game1.staminaRect;
- viewport = Game1.graphics.GraphicsDevice.Viewport;
- int x2 = (viewport.TitleSafeArea.Width - Game1.dialogueWidth) / 2;
- viewport = Game1.graphics.GraphicsDevice.Viewport;
- rectangle = viewport.TitleSafeArea;
- int y2 = rectangle.Bottom - Game1.tileSize * 2;
+ int x2 = (Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2;
+ rectangle = Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea();
+ int y2 = rectangle.Bottom - 128;
int width = (int)((double)Game1.pauseAccumulator / (double)Game1.pauseTime * (double)Game1.dialogueWidth);
- int height2 = Game1.tileSize / 2;
+ int height2 = 32;
Microsoft.Xna.Framework.Rectangle destinationRectangle2 = new Microsoft.Xna.Framework.Rectangle(x2, y2, width, height2);
Color dimGray = Color.DimGray;
spriteBatch2.Draw(staminaRect, destinationRectangle2, dimGray);
}
if (Game1.eventUp && Game1.currentLocation != null && Game1.currentLocation.currentEvent != null)
Game1.currentLocation.currentEvent.drawAfterMap(Game1.spriteBatch);
- if (Game1.isRaining && Game1.currentLocation != null && (Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert)))
- {
- SpriteBatch spriteBatch = Game1.spriteBatch;
- Texture2D staminaRect = Game1.staminaRect;
- viewport = Game1.graphics.GraphicsDevice.Viewport;
- Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds;
- Color color = Color.Blue * 0.2f;
- spriteBatch.Draw(staminaRect, bounds, color);
- }
+ if (Game1.isRaining && Game1.currentLocation != null && ((bool)((NetFieldBase<bool, NetBool>)Game1.currentLocation.isOutdoors) && !(Game1.currentLocation is Desert)))
+ Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Blue * 0.2f);
if ((Game1.fadeToBlack || Game1.globalFade) && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause))
- {
- SpriteBatch spriteBatch = Game1.spriteBatch;
- Texture2D fadeToBlackRect = Game1.fadeToBlackRect;
- viewport = Game1.graphics.GraphicsDevice.Viewport;
- Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds;
- Color color = Color.Black * ((int)Game1.gameMode == 0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha);
- spriteBatch.Draw(fadeToBlackRect, bounds, color);
- }
+ Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * (Game1.gameMode == (byte)0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha));
else if ((double)Game1.flashAlpha > 0.0)
{
if (Game1.options.screenFlash)
- {
- SpriteBatch spriteBatch = Game1.spriteBatch;
- Texture2D fadeToBlackRect = Game1.fadeToBlackRect;
- viewport = Game1.graphics.GraphicsDevice.Viewport;
- Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds;
- Color color = Color.White * Math.Min(1f, Game1.flashAlpha);
- spriteBatch.Draw(fadeToBlackRect, bounds, color);
- }
+ Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.White * Math.Min(1f, Game1.flashAlpha));
Game1.flashAlpha -= 0.1f;
}
if ((Game1.messagePause || Game1.globalFade) && Game1.dialogueUp)
this.drawDialogueBox();
foreach (TemporaryAnimatedSprite overlayTempSprite in Game1.screenOverlayTempSprites)
- overlayTempSprite.draw(Game1.spriteBatch, true, 0, 0);
+ overlayTempSprite.draw(Game1.spriteBatch, true, 0, 0, 1f);
if (Game1.debugMode)
{
- SpriteBatch spriteBatch = Game1.spriteBatch;
- SpriteFont smallFont = Game1.smallFont;
- object[] objArray = new object[10];
- int index1 = 0;
- string str1;
- if (!Game1.panMode)
- str1 = "player: " + (object)(Game1.player.getStandingX() / Game1.tileSize) + ", " + (object)(Game1.player.getStandingY() / Game1.tileSize);
+ StringBuilder debugStringBuilder = Game1._debugStringBuilder;
+ debugStringBuilder.Clear();
+ if (Game1.panMode)
+ {
+ debugStringBuilder.Append((Game1.getOldMouseX() + Game1.viewport.X) / 64);
+ debugStringBuilder.Append(",");
+ debugStringBuilder.Append((Game1.getOldMouseY() + Game1.viewport.Y) / 64);
+ }
else
- str1 = ((Game1.getOldMouseX() + Game1.viewport.X) / Game1.tileSize).ToString() + "," + (object)((Game1.getOldMouseY() + Game1.viewport.Y) / Game1.tileSize);
- objArray[index1] = (object)str1;
- int index2 = 1;
- string str2 = " mouseTransparency: ";
- objArray[index2] = (object)str2;
- int index3 = 2;
- float cursorTransparency = Game1.mouseCursorTransparency;
- objArray[index3] = (object)cursorTransparency;
- int index4 = 3;
- string str3 = " mousePosition: ";
- objArray[index4] = (object)str3;
- int index5 = 4;
- int mouseX = Game1.getMouseX();
- objArray[index5] = (object)mouseX;
- int index6 = 5;
- string str4 = ",";
- objArray[index6] = (object)str4;
- int index7 = 6;
- int mouseY = Game1.getMouseY();
- objArray[index7] = (object)mouseY;
- int index8 = 7;
- string newLine = Environment.NewLine;
- objArray[index8] = (object)newLine;
- int index9 = 8;
- string str5 = "debugOutput: ";
- objArray[index9] = (object)str5;
- int index10 = 9;
- string debugOutput = Game1.debugOutput;
- objArray[index10] = (object)debugOutput;
- string text = string.Concat(objArray);
- Vector2 position = new Vector2((float)this.GraphicsDevice.Viewport.TitleSafeArea.X, (float)this.GraphicsDevice.Viewport.TitleSafeArea.Y);
- Color red = Color.Red;
- double num1 = 0.0;
- Vector2 zero = Vector2.Zero;
- double num2 = 1.0;
- int num3 = 0;
- double num4 = 0.99999988079071;
- spriteBatch.DrawString(smallFont, text, position, red, (float)num1, zero, (float)num2, (SpriteEffects)num3, (float)num4);
+ {
+ debugStringBuilder.Append("player: ");
+ debugStringBuilder.Append(Game1.player.getStandingX() / 64);
+ debugStringBuilder.Append(", ");
+ debugStringBuilder.Append(Game1.player.getStandingY() / 64);
+ }
+ debugStringBuilder.Append(" mouseTransparency: ");
+ debugStringBuilder.Append(Game1.mouseCursorTransparency);
+ debugStringBuilder.Append(" mousePosition: ");
+ debugStringBuilder.Append(Game1.getMouseX());
+ debugStringBuilder.Append(",");
+ debugStringBuilder.Append(Game1.getMouseY());
+ debugStringBuilder.Append(Environment.NewLine);
+ debugStringBuilder.Append("debugOutput: ");
+ debugStringBuilder.Append(Game1.debugOutput);
+ Game1.spriteBatch.DrawString(Game1.smallFont, debugStringBuilder, new Vector2((float)this.GraphicsDevice.Viewport.GetTitleSafeArea().X, (float)(this.GraphicsDevice.Viewport.GetTitleSafeArea().Y + Game1.smallFont.LineSpacing * 8)), Color.Red, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f);
}
if (Game1.showKeyHelp)
- Game1.spriteBatch.DrawString(Game1.smallFont, Game1.keyHelpString, new Vector2((float)Game1.tileSize, (float)(Game1.viewport.Height - Game1.tileSize - (Game1.dialogueUp ? Game1.tileSize * 3 + (Game1.isQuestion ? Game1.questionChoices.Count * Game1.tileSize : 0) : 0)) - Game1.smallFont.MeasureString(Game1.keyHelpString).Y), Color.LightGray, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f);
+ Game1.spriteBatch.DrawString(Game1.smallFont, Game1.keyHelpString, new Vector2(64f, (float)(Game1.viewport.Height - 64 - (Game1.dialogueUp ? 192 + (Game1.isQuestion ? Game1.questionChoices.Count * 64 : 0) : 0)) - Game1.smallFont.MeasureString(Game1.keyHelpString).Y), Color.LightGray, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f);
if (Game1.activeClickableMenu != null)
{
try
@@ -1956,76 +1347,30 @@ namespace StardewModdingAPI.Framework
}
else if (Game1.farmEvent != null)
Game1.farmEvent.drawAboveEverything(Game1.spriteBatch);
-
- this.RaisePostRender();
- Game1.spriteBatch.End();
- if (Game1.overlayMenu != null)
+ if (Game1.HostPaused)
{
- Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
- Game1.overlayMenu.draw(Game1.spriteBatch);
- Game1.spriteBatch.End();
+ string s = Game1.content.LoadString("Strings\\StringsFromCSFiles:DayTimeMoneyBox.cs.10378");
+ SpriteText.drawStringWithScrollBackground(Game1.spriteBatch, s, 96, 32, "", 1f, -1);
}
+ this.RaisePostRender();
+ Game1.spriteBatch.End();
+ this.drawOverlays(Game1.spriteBatch);
this.renderScreenBuffer();
+ //base.Draw(gameTime);
}
}
}
}
-#endif
/****
** Methods
****/
- /// <summary>Perform any cleanup needed when the player unloads a save and returns to the title screen.</summary>
- private void CleanupAfterReturnToTitle()
+ /// <summary>Perform any cleanup needed when a save is unloaded.</summary>
+ private void MarkWorldNotReady()
{
Context.IsWorldReady = false;
- this.AfterLoadTimer = 5;
- this.PreviousSaveID = 0;
- }
-
-
-
- /// <summary>Get the player inventory changes between two states.</summary>
- /// <param name="current">The player's current inventory.</param>
- /// <param name="previous">The player's previous inventory.</param>
- private IEnumerable<ItemStackChange> GetInventoryChanges(IEnumerable<Item> current, IDictionary<Item, int> 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 };
- }
- }
- }
-
- /// <summary>Get a hash value for an enumeration.</summary>
- /// <param name="enumerable">The enumeration of items to hash.</param>
- private int GetHash(IEnumerable enumerable)
- {
- int hash = 0;
- foreach (object v in enumerable)
- hash ^= v.GetHashCode();
- return hash;
+ this.AfterLoadTimer.Reset();
+ this.RaisedAfterLoadEvent = false;
}
/// <summary>Raise the <see cref="GraphicsEvents.OnPostRenderEvent"/> if there are any listeners.</summary>
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
+{
+ /// <summary>SMAPI's implementation of the game's core multiplayer logic.</summary>
+ internal class SMultiplayer : Multiplayer
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>Encapsulates monitoring and logging.</summary>
+ private readonly IMonitor Monitor;
+
+ /// <summary>Manages SMAPI events.</summary>
+ private readonly EventManager EventManager;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="monitor">Encapsulates monitoring and logging.</param>
+ /// <param name="eventManager">Manages SMAPI events.</param>
+ public SMultiplayer(IMonitor monitor, EventManager eventManager)
+ {
+ this.Monitor = monitor;
+ this.EventManager = eventManager;
+ }
+
+ /// <summary>Handle sync messages from other players and perform other initial sync logic.</summary>
+ public override void UpdateEarly()
+ {
+ this.EventManager.Multiplayer_BeforeMainSync.Raise();
+ base.UpdateEarly();
+ this.EventManager.Multiplayer_AfterMainSync.Raise();
+ }
+
+ /// <summary>Broadcast sync messages to other players and perform other final sync logic.</summary>
+ 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/Framework/Serialisation/CrossplatformConverters/ColorConverter.cs b/src/SMAPI/Framework/Serialisation/ColorConverter.cs
index f1b2f04f..c27065bf 100644
--- a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/ColorConverter.cs
+++ b/src/SMAPI/Framework/Serialisation/ColorConverter.cs
@@ -1,9 +1,10 @@
using System;
using Microsoft.Xna.Framework;
using Newtonsoft.Json.Linq;
-using StardewModdingAPI.Framework.Exceptions;
+using StardewModdingAPI.Toolkit.Serialisation;
+using StardewModdingAPI.Toolkit.Serialisation.Converters;
-namespace StardewModdingAPI.Framework.Serialisation.CrossplatformConverters
+namespace StardewModdingAPI.Framework.Serialisation
{
/// <summary>Handles deserialisation of <see cref="Color"/> for crossplatform compatibility.</summary>
/// <remarks>
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
-{
- /// <summary>Encapsulates SMAPI's JSON file parsing.</summary>
- internal class JsonHelper
- {
- /*********
- ** Accessors
- *********/
- /// <summary>The JSON settings to use when serialising and deserialising files.</summary>
- private readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings
- {
- Formatting = Formatting.Indented,
- ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection<T> values are duplicated each time the config is loaded
- Converters = new List<JsonConverter>
- {
- // SMAPI types
- new SemanticVersionConverter(),
-
- // enums
- new StringEnumConverter<Buttons>(),
- new StringEnumConverter<Keys>(),
- new StringEnumConverter<SButton>(),
-
- // crossplatform compatibility
- new ColorConverter(),
- new PointConverter(),
- new RectangleConverter()
- }
- };
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Read a JSON file.</summary>
- /// <typeparam name="TModel">The model type.</typeparam>
- /// <param name="fullPath">The absolete file path.</param>
- /// <returns>Returns the deserialised model, or <c>null</c> if the file doesn't exist or is empty.</returns>
- /// <exception cref="InvalidOperationException">The given path is empty or invalid.</exception>
- public TModel ReadJsonFile<TModel>(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<TModel>(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);
- }
- }
-
- /// <summary>Save to a JSON file.</summary>
- /// <typeparam name="TModel">The model type.</typeparam>
- /// <param name="fullPath">The absolete file path.</param>
- /// <param name="model">The model to save.</param>
- /// <exception cref="InvalidOperationException">The given path is empty or invalid.</exception>
- public void WriteJsonFile<TModel>(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
- *********/
- /// <summary>Deserialize JSON text if possible.</summary>
- /// <typeparam name="TModel">The model type.</typeparam>
- /// <param name="json">The raw JSON text.</param>
- private TModel Deserialise<TModel>(string json)
- {
- try
- {
- return JsonConvert.DeserializeObject<TModel>(json, this.JsonSettings);
- }
- catch (JsonReaderException)
- {
- // try replacing curly quotes
- if (json.Contains("“") || json.Contains("”"))
- {
- try
- {
- return JsonConvert.DeserializeObject<TModel>(json.Replace('“', '"').Replace('”', '"'), this.JsonSettings);
- }
- catch { /* rethrow original error */ }
- }
-
- throw;
- }
- }
- }
-}
diff --git a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/PointConverter.cs b/src/SMAPI/Framework/Serialisation/PointConverter.cs
index 434b7ea5..fbc857d2 100644
--- a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/PointConverter.cs
+++ b/src/SMAPI/Framework/Serialisation/PointConverter.cs
@@ -1,9 +1,10 @@
using System;
using Microsoft.Xna.Framework;
using Newtonsoft.Json.Linq;
-using StardewModdingAPI.Framework.Exceptions;
+using StardewModdingAPI.Toolkit.Serialisation;
+using StardewModdingAPI.Toolkit.Serialisation.Converters;
-namespace StardewModdingAPI.Framework.Serialisation.CrossplatformConverters
+namespace StardewModdingAPI.Framework.Serialisation
{
/// <summary>Handles deserialisation of <see cref="PointConverter"/> for crossplatform compatibility.</summary>
/// <remarks>
diff --git a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/RectangleConverter.cs b/src/SMAPI/Framework/Serialisation/RectangleConverter.cs
index 62bc8637..4f55cc32 100644
--- a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/RectangleConverter.cs
+++ b/src/SMAPI/Framework/Serialisation/RectangleConverter.cs
@@ -2,9 +2,10 @@ using System;
using System.Text.RegularExpressions;
using Microsoft.Xna.Framework;
using Newtonsoft.Json.Linq;
-using StardewModdingAPI.Framework.Exceptions;
+using StardewModdingAPI.Toolkit.Serialisation;
+using StardewModdingAPI.Toolkit.Serialisation.Converters;
-namespace StardewModdingAPI.Framework.Serialisation.CrossplatformConverters
+namespace StardewModdingAPI.Framework.Serialisation
{
/// <summary>Handles deserialisation of <see cref="Rectangle"/> for crossplatform compatibility.</summary>
/// <remarks>
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
-{
- /// <summary>The base implementation for simplified converters which deserialise <typeparamref name="T"/> without overriding serialisation.</summary>
- /// <typeparam name="T">The type to deserialise.</typeparam>
- internal abstract class SimpleReadOnlyConverter<T> : JsonConverter
- {
- /*********
- ** Accessors
- *********/
- /// <summary>Whether this converter can write JSON.</summary>
- public override bool CanWrite => false;
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Get whether this instance can convert the specified object type.</summary>
- /// <param name="objectType">The object type.</param>
- public override bool CanConvert(Type objectType)
- {
- return objectType == typeof(T);
- }
-
- /// <summary>Writes the JSON representation of the object.</summary>
- /// <param name="writer">The JSON writer.</param>
- /// <param name="value">The value.</param>
- /// <param name="serializer">The calling serializer.</param>
- public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
- {
- throw new InvalidOperationException("This converter does not write JSON.");
- }
-
- /// <summary>Reads the JSON representation of the object.</summary>
- /// <param name="reader">The JSON reader.</param>
- /// <param name="objectType">The object type.</param>
- /// <param name="existingValue">The object being read.</param>
- /// <param name="serializer">The calling serializer.</param>
- 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<string>(), path);
- default:
- throw new SParseException($"Can't parse {typeof(T).Name} from {reader.TokenType} node (path: {reader.Path}).");
- }
- }
-
-
- /*********
- ** Protected methods
- *********/
- /// <summary>Read a JSON object.</summary>
- /// <param name="obj">The JSON object to read.</param>
- /// <param name="path">The path to the current JSON node.</param>
- protected virtual T ReadObject(JObject obj, string path)
- {
- throw new SParseException($"Can't parse {typeof(T).Name} from object node (path: {path}).");
- }
-
- /// <summary>Read a JSON string.</summary>
- /// <param name="str">The JSON string value.</param>
- /// <param name="path">The path to the current JSON node.</param>
- 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
-{
- /// <summary>Handles deserialisation of <see cref="IManifestContentPackFor"/> arrays.</summary>
- internal class ManifestContentPackForConverter : JsonConverter
- {
- /*********
- ** Accessors
- *********/
- /// <summary>Whether this converter can write JSON.</summary>
- public override bool CanWrite => false;
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Get whether this instance can convert the specified object type.</summary>
- /// <param name="objectType">The object type.</param>
- public override bool CanConvert(Type objectType)
- {
- return objectType == typeof(IManifestContentPackFor[]);
- }
-
-
- /*********
- ** Protected methods
- *********/
- /// <summary>Read the JSON representation of the object.</summary>
- /// <param name="reader">The JSON reader.</param>
- /// <param name="objectType">The object type.</param>
- /// <param name="existingValue">The object being read.</param>
- /// <param name="serializer">The calling serializer.</param>
- public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
- {
- return serializer.Deserialize<ManifestContentPackFor>(reader);
- }
-
- /// <summary>Writes the JSON representation of the object.</summary>
- /// <param name="writer">The JSON writer.</param>
- /// <param name="value">The value.</param>
- /// <param name="serializer">The calling serializer.</param>
- 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
-{
- /// <summary>Handles deserialisation of <see cref="IManifestDependency"/> arrays.</summary>
- internal class ManifestDependencyArrayConverter : JsonConverter
- {
- /*********
- ** Accessors
- *********/
- /// <summary>Whether this converter can write JSON.</summary>
- public override bool CanWrite => false;
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Get whether this instance can convert the specified object type.</summary>
- /// <param name="objectType">The object type.</param>
- public override bool CanConvert(Type objectType)
- {
- return objectType == typeof(IManifestDependency[]);
- }
-
-
- /*********
- ** Protected methods
- *********/
- /// <summary>Read the JSON representation of the object.</summary>
- /// <param name="reader">The JSON reader.</param>
- /// <param name="objectType">The object type.</param>
- /// <param name="existingValue">The object being read.</param>
- /// <param name="serializer">The calling serializer.</param>
- public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
- {
- List<IManifestDependency> result = new List<IManifestDependency>();
- foreach (JObject obj in JArray.Load(reader).Children<JObject>())
- {
- string uniqueID = obj.ValueIgnoreCase<string>(nameof(IManifestDependency.UniqueID));
- string minVersion = obj.ValueIgnoreCase<string>(nameof(IManifestDependency.MinimumVersion));
- bool required = obj.ValueIgnoreCase<bool?>(nameof(IManifestDependency.IsRequired)) ?? true;
- result.Add(new ManifestDependency(uniqueID, minVersion, required));
- }
- return result.ToArray();
- }
-
- /// <summary>Writes the JSON representation of the object.</summary>
- /// <param name="writer">The JSON writer.</param>
- /// <param name="value">The value.</param>
- /// <param name="serializer">The calling serializer.</param>
- 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
-{
- /// <summary>Handles deserialisation of <see cref="SemanticVersion"/>.</summary>
- internal class SemanticVersionConverter : SimpleReadOnlyConverter<ISemanticVersion>
- {
- /*********
- ** Protected methods
- *********/
- /// <summary>Read a JSON object.</summary>
- /// <param name="obj">The JSON object to read.</param>
- /// <param name="path">The path to the current JSON node.</param>
- protected override ISemanticVersion ReadObject(JObject obj, string path)
- {
- int major = obj.ValueIgnoreCase<int>(nameof(ISemanticVersion.MajorVersion));
- int minor = obj.ValueIgnoreCase<int>(nameof(ISemanticVersion.MinorVersion));
- int patch = obj.ValueIgnoreCase<int>(nameof(ISemanticVersion.PatchVersion));
- string build = obj.ValueIgnoreCase<string>(nameof(ISemanticVersion.Build));
- return new LegacyManifestVersion(major, minor, patch, build);
- }
-
- /// <summary>Read a JSON string.</summary>
- /// <param name="str">The JSON string value.</param>
- /// <param name="path">The path to the current JSON node.</param>
- 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
-{
- /// <summary>A variant of <see cref="StringEnumConverter"/> which only converts a specified enum.</summary>
- /// <typeparam name="T">The enum type.</typeparam>
- internal class StringEnumConverter<T> : StringEnumConverter
- {
- /*********
- ** Public methods
- *********/
- /// <summary>Get whether this instance can convert the specified object type.</summary>
- /// <param name="type">The object type.</param>
- public override bool CanConvert(Type type)
- {
- return
- base.CanConvert(type)
- && (Nullable.GetUnderlyingType(type) ?? type) == typeof(T);
- }
- }
-}
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
+{
+ /// <summary>Compares instances using <see cref="IEqualityComparer{T}.Equals(T,T)"/>.</summary>
+ /// <typeparam name="T">The value type.</typeparam>
+ internal class EquatableComparer<T> : IEqualityComparer<T> where T : IEquatable<T>
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Determines whether the specified objects are equal.</summary>
+ /// <returns>true if the specified objects are equal; otherwise, false.</returns>
+ /// <param name="x">The first object to compare.</param>
+ /// <param name="y">The second object to compare.</param>
+ public bool Equals(T x, T y)
+ {
+ if (x == null)
+ return y == null;
+ return x.Equals(y);
+ }
+
+ /// <summary>Get a hash code for the specified object.</summary>
+ /// <param name="obj">The value.</param>
+ public int GetHashCode(T obj)
+ {
+ return RuntimeHelpers.GetHashCode(obj);
+ }
+ }
+}
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
+{
+ /// <summary>Compares values using their <see cref="object.Equals(object)"/> method. This should only be used when <see cref="EquatableComparer{T}"/> won't work, since this doesn't validate whether they're comparable.</summary>
+ /// <typeparam name="T">The value type.</typeparam>
+ internal class GenericEqualsComparer<T> : IEqualityComparer<T>
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Determines whether the specified objects are equal.</summary>
+ /// <returns>true if the specified objects are equal; otherwise, false.</returns>
+ /// <param name="x">The first object to compare.</param>
+ /// <param name="y">The second object to compare.</param>
+ public bool Equals(T x, T y)
+ {
+ if (x == null)
+ return y == null;
+ return x.Equals(y);
+ }
+
+ /// <summary>Get a hash code for the specified object.</summary>
+ /// <param name="obj">The value.</param>
+ 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
+{
+ /// <summary>A comparer which considers two references equal if they point to the same instance.</summary>
+ /// <typeparam name="T">The value type.</typeparam>
+ internal class ObjectReferenceComparer<T> : IEqualityComparer<T>
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Determines whether the specified objects are equal.</summary>
+ /// <returns>true if the specified objects are equal; otherwise, false.</returns>
+ /// <param name="x">The first object to compare.</param>
+ /// <param name="y">The second object to compare.</param>
+ public bool Equals(T x, T y)
+ {
+ return object.ReferenceEquals(x, y);
+ }
+
+ /// <summary>Get a hash code for the specified object.</summary>
+ /// <param name="obj">The value.</param>
+ 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
+{
+ /// <summary>The base implementation for a disposable watcher.</summary>
+ internal abstract class BaseDisposableWatcher : IDisposable
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>Whether the watcher has been disposed.</summary>
+ protected bool IsDisposed { get; private set; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Stop watching the field and release all references.</summary>
+ public virtual void Dispose()
+ {
+ this.IsDisposed = true;
+ }
+
+
+ /*********
+ ** Protected methods
+ *********/
+ /// <summary>Throw an exception if the watcher is disposed.</summary>
+ /// <exception cref="ObjectDisposedException">The watcher is disposed.</exception>
+ 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
+{
+ /// <summary>A watcher which detects changes to a value using a specified <see cref="IEqualityComparer{T}"/> instance.</summary>
+ internal class ComparableWatcher<T> : IValueWatcher<T>
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>Get the current value.</summary>
+ private readonly Func<T> GetValue;
+
+ /// <summary>The equality comparer.</summary>
+ private readonly IEqualityComparer<T> Comparer;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The field value at the last reset.</summary>
+ public T PreviousValue { get; private set; }
+
+ /// <summary>The latest value.</summary>
+ public T CurrentValue { get; private set; }
+
+ /// <summary>Whether the value changed since the last reset.</summary>
+ public bool IsChanged { get; private set; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="getValue">Get the current value.</param>
+ /// <param name="comparer">The equality comparer which indicates whether two values are the same.</param>
+ public ComparableWatcher(Func<T> getValue, IEqualityComparer<T> comparer)
+ {
+ this.GetValue = getValue;
+ this.Comparer = comparer;
+ this.PreviousValue = getValue();
+ }
+
+ /// <summary>Update the current value if needed.</summary>
+ public void Update()
+ {
+ this.CurrentValue = this.GetValue();
+ this.IsChanged = !this.Comparer.Equals(this.PreviousValue, this.CurrentValue);
+ }
+
+ /// <summary>Set the current value as the baseline.</summary>
+ public void Reset()
+ {
+ this.PreviousValue = this.CurrentValue;
+ this.IsChanged = false;
+ }
+
+ /// <summary>Release any references if needed when the field is no longer needed.</summary>
+ public void Dispose() { }
+ }
+}
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
+{
+ /// <summary>A watcher which detects changes to a Netcode collection.</summary>
+ internal class NetCollectionWatcher<TValue> : BaseDisposableWatcher, ICollectionWatcher<TValue>
+ where TValue : INetObject<INetSerializable>
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The field being watched.</summary>
+ private readonly NetCollection<TValue> Field;
+
+ /// <summary>The pairs added since the last reset.</summary>
+ private readonly List<TValue> AddedImpl = new List<TValue>();
+
+ /// <summary>The pairs demoved since the last reset.</summary>
+ private readonly List<TValue> RemovedImpl = new List<TValue>();
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Whether the collection changed since the last reset.</summary>
+ public bool IsChanged => this.AddedImpl.Count > 0 || this.RemovedImpl.Count > 0;
+
+ /// <summary>The values added since the last reset.</summary>
+ public IEnumerable<TValue> Added => this.AddedImpl;
+
+ /// <summary>The values removed since the last reset.</summary>
+ public IEnumerable<TValue> Removed => this.RemovedImpl;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="field">The field to watch.</param>
+ public NetCollectionWatcher(NetCollection<TValue> field)
+ {
+ this.Field = field;
+ field.OnValueAdded += this.OnValueAdded;
+ field.OnValueRemoved += this.OnValueRemoved;
+ }
+
+ /// <summary>Update the current value if needed.</summary>
+ public void Update()
+ {
+ this.AssertNotDisposed();
+ }
+
+ /// <summary>Set the current value as the baseline.</summary>
+ public void Reset()
+ {
+ this.AssertNotDisposed();
+
+ this.AddedImpl.Clear();
+ this.RemovedImpl.Clear();
+ }
+
+ /// <summary>Stop watching the field and release all references.</summary>
+ public override void Dispose()
+ {
+ if (!this.IsDisposed)
+ {
+ this.Field.OnValueAdded -= this.OnValueAdded;
+ this.Field.OnValueRemoved -= this.OnValueRemoved;
+ }
+
+ base.Dispose();
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>A callback invoked when an entry is added to the collection.</summary>
+ /// <param name="value">The added value.</param>
+ private void OnValueAdded(TValue value)
+ {
+ this.AddedImpl.Add(value);
+ }
+
+ /// <summary>A callback invoked when an entry is removed from the collection.</summary>
+ /// <param name="value">The added value.</param>
+ private void OnValueRemoved(TValue value)
+ {
+ this.RemovedImpl.Add(value);
+ }
+ }
+}
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
+{
+ /// <summary>A watcher which detects changes to a net dictionary field.</summary>
+ /// <typeparam name="TKey">The dictionary key type.</typeparam>
+ /// <typeparam name="TValue">The dictionary value type.</typeparam>
+ /// <typeparam name="TField">The net type equivalent to <typeparamref name="TValue"/>.</typeparam>
+ /// <typeparam name="TSerialDict">The serializable dictionary type that can store the keys and values.</typeparam>
+ /// <typeparam name="TSelf">The net field instance type.</typeparam>
+ internal class NetDictionaryWatcher<TKey, TValue, TField, TSerialDict, TSelf> : BaseDisposableWatcher, IDictionaryWatcher<TKey, TValue>
+ where TField : class, INetObject<INetSerializable>, new()
+ where TSerialDict : IDictionary<TKey, TValue>, new()
+ where TSelf : NetDictionary<TKey, TValue, TField, TSerialDict, TSelf>
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The pairs added since the last reset.</summary>
+ private readonly IDictionary<TKey, TValue> PairsAdded = new Dictionary<TKey, TValue>();
+
+ /// <summary>The pairs demoved since the last reset.</summary>
+ private readonly IDictionary<TKey, TValue> PairsRemoved = new Dictionary<TKey, TValue>();
+
+ /// <summary>The field being watched.</summary>
+ private readonly NetDictionary<TKey, TValue, TField, TSerialDict, TSelf> Field;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Whether the collection changed since the last reset.</summary>
+ public bool IsChanged => this.PairsAdded.Count > 0 || this.PairsRemoved.Count > 0;
+
+ /// <summary>The values added since the last reset.</summary>
+ public IEnumerable<KeyValuePair<TKey, TValue>> Added => this.PairsAdded;
+
+ /// <summary>The values removed since the last reset.</summary>
+ public IEnumerable<KeyValuePair<TKey, TValue>> Removed => this.PairsRemoved;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="field">The field to watch.</param>
+ public NetDictionaryWatcher(NetDictionary<TKey, TValue, TField, TSerialDict, TSelf> field)
+ {
+ this.Field = field;
+
+ field.OnValueAdded += this.OnValueAdded;
+ field.OnValueRemoved += this.OnValueRemoved;
+ }
+
+ /// <summary>Update the current value if needed.</summary>
+ public void Update()
+ {
+ this.AssertNotDisposed();
+ }
+
+ /// <summary>Set the current value as the baseline.</summary>
+ public void Reset()
+ {
+ this.AssertNotDisposed();
+
+ this.PairsAdded.Clear();
+ this.PairsRemoved.Clear();
+ }
+
+ /// <summary>Stop watching the field and release all references.</summary>
+ public override void Dispose()
+ {
+ if (!this.IsDisposed)
+ {
+ this.Field.OnValueAdded -= this.OnValueAdded;
+ this.Field.OnValueRemoved -= this.OnValueRemoved;
+ }
+ base.Dispose();
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>A callback invoked when an entry is added to the dictionary.</summary>
+ /// <param name="key">The entry key.</param>
+ /// <param name="value">The entry value.</param>
+ private void OnValueAdded(TKey key, TValue value)
+ {
+ this.PairsAdded[key] = value;
+ }
+
+ /// <summary>A callback invoked when an entry is removed from the dictionary.</summary>
+ /// <param name="key">The entry key.</param>
+ /// <param name="value">The entry value.</param>
+ 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
+{
+ /// <summary>A watcher which detects changes to a net value field.</summary>
+ internal class NetValueWatcher<T, TSelf> : BaseDisposableWatcher, IValueWatcher<T> where TSelf : NetFieldBase<T, TSelf>
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The field being watched.</summary>
+ private readonly NetFieldBase<T, TSelf> Field;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Whether the value changed since the last reset.</summary>
+ public bool IsChanged { get; private set; }
+
+ /// <summary>The field value at the last reset.</summary>
+ public T PreviousValue { get; private set; }
+
+ /// <summary>The latest value.</summary>
+ public T CurrentValue { get; private set; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="field">The field to watch.</param>
+ public NetValueWatcher(NetFieldBase<T, TSelf> field)
+ {
+ this.Field = field;
+ this.PreviousValue = field.Value;
+ this.CurrentValue = field.Value;
+
+ field.fieldChangeVisibleEvent += this.OnValueChanged;
+ field.fieldChangeEvent += this.OnValueChanged;
+ }
+
+ /// <summary>Update the current value if needed.</summary>
+ public void Update()
+ {
+ this.AssertNotDisposed();
+ }
+
+ /// <summary>Set the current value as the baseline.</summary>
+ public void Reset()
+ {
+ this.AssertNotDisposed();
+
+ this.PreviousValue = this.CurrentValue;
+ this.IsChanged = false;
+ }
+
+ /// <summary>Stop watching the field and release all references.</summary>
+ public override void Dispose()
+ {
+ if (!this.IsDisposed)
+ {
+ this.Field.fieldChangeEvent -= this.OnValueChanged;
+ this.Field.fieldChangeVisibleEvent -= this.OnValueChanged;
+ }
+ base.Dispose();
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>A callback invoked when the field's value changes.</summary>
+ /// <param name="field">The field being watched.</param>
+ /// <param name="oldValue">The old field value.</param>
+ /// <param name="newValue">The new field value.</param>
+ 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
+{
+ /// <summary>A watcher which detects changes to an observable collection.</summary>
+ internal class ObservableCollectionWatcher<TValue> : BaseDisposableWatcher, ICollectionWatcher<TValue>
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The field being watched.</summary>
+ private readonly ObservableCollection<TValue> Field;
+
+ /// <summary>The pairs added since the last reset.</summary>
+ private readonly List<TValue> AddedImpl = new List<TValue>();
+
+ /// <summary>The pairs demoved since the last reset.</summary>
+ private readonly List<TValue> RemovedImpl = new List<TValue>();
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Whether the collection changed since the last reset.</summary>
+ public bool IsChanged => this.AddedImpl.Count > 0 || this.RemovedImpl.Count > 0;
+
+ /// <summary>The values added since the last reset.</summary>
+ public IEnumerable<TValue> Added => this.AddedImpl;
+
+ /// <summary>The values removed since the last reset.</summary>
+ public IEnumerable<TValue> Removed => this.RemovedImpl;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="field">The field to watch.</param>
+ public ObservableCollectionWatcher(ObservableCollection<TValue> field)
+ {
+ this.Field = field;
+ field.CollectionChanged += this.OnCollectionChanged;
+ }
+
+ /// <summary>Update the current value if needed.</summary>
+ public void Update()
+ {
+ this.AssertNotDisposed();
+ }
+
+ /// <summary>Set the current value as the baseline.</summary>
+ public void Reset()
+ {
+ this.AssertNotDisposed();
+
+ this.AddedImpl.Clear();
+ this.RemovedImpl.Clear();
+ }
+
+ /// <summary>Stop watching the field and release all references.</summary>
+ public override void Dispose()
+ {
+ if (!this.IsDisposed)
+ this.Field.CollectionChanged -= this.OnCollectionChanged;
+ base.Dispose();
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>A callback invoked when an entry is added or removed from the collection.</summary>
+ /// <param name="sender">The event sender.</param>
+ /// <param name="e">The event arguments.</param>
+ private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ if (e.NewItems != null)
+ this.AddedImpl.AddRange(e.NewItems.Cast<TValue>());
+ if (e.OldItems != null)
+ this.RemovedImpl.AddRange(e.OldItems.Cast<TValue>());
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs
new file mode 100644
index 00000000..d7a02668
--- /dev/null
+++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using Netcode;
+using StardewModdingAPI.Framework.StateTracking.Comparers;
+
+namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
+{
+ /// <summary>Provides convenience wrappers for creating watchers.</summary>
+ internal static class WatcherFactory
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Get a watcher which compares values using their <see cref="object.Equals(object)"/> method. This method should only be used when <see cref="ForEquatable{T}"/> won't work, since this doesn't validate whether they're comparable.</summary>
+ /// <typeparam name="T">The value type.</typeparam>
+ /// <param name="getValue">Get the current value.</param>
+ public static ComparableWatcher<T> ForGenericEquality<T>(Func<T> getValue) where T : struct
+ {
+ return new ComparableWatcher<T>(getValue, new GenericEqualsComparer<T>());
+ }
+
+ /// <summary>Get a watcher for an <see cref="IEquatable{T}"/> value.</summary>
+ /// <typeparam name="T">The value type.</typeparam>
+ /// <param name="getValue">Get the current value.</param>
+ public static ComparableWatcher<T> ForEquatable<T>(Func<T> getValue) where T : IEquatable<T>
+ {
+ return new ComparableWatcher<T>(getValue, new EquatableComparer<T>());
+ }
+
+ /// <summary>Get a watcher which detects when an object reference changes.</summary>
+ /// <typeparam name="T">The value type.</typeparam>
+ /// <param name="getValue">Get the current value.</param>
+ public static ComparableWatcher<T> ForReference<T>(Func<T> getValue)
+ {
+ return new ComparableWatcher<T>(getValue, new ObjectReferenceComparer<T>());
+ }
+
+ /// <summary>Get a watcher for an observable collection.</summary>
+ /// <typeparam name="T">The value type.</typeparam>
+ /// <param name="collection">The observable collection.</param>
+ public static ObservableCollectionWatcher<T> ForObservableCollection<T>(ObservableCollection<T> collection)
+ {
+ return new ObservableCollectionWatcher<T>(collection);
+ }
+
+ /// <summary>Get a watcher for a net collection.</summary>
+ /// <typeparam name="T">The value type.</typeparam>
+ /// <typeparam name="TSelf">The net field instance type.</typeparam>
+ /// <param name="field">The net collection.</param>
+ public static NetValueWatcher<T, TSelf> ForNetValue<T, TSelf>(NetFieldBase<T, TSelf> field) where TSelf : NetFieldBase<T, TSelf>
+ {
+ return new NetValueWatcher<T, TSelf>(field);
+ }
+
+ /// <summary>Get a watcher for a net collection.</summary>
+ /// <typeparam name="T">The value type.</typeparam>
+ /// <param name="collection">The net collection.</param>
+ public static NetCollectionWatcher<T> ForNetCollection<T>(NetCollection<T> collection) where T : INetObject<INetSerializable>
+ {
+ return new NetCollectionWatcher<T>(collection);
+ }
+
+ /// <summary>Get a watcher for a net dictionary.</summary>
+ /// <typeparam name="TKey">The dictionary key type.</typeparam>
+ /// <typeparam name="TValue">The dictionary value type.</typeparam>
+ /// <typeparam name="TField">The net type equivalent to <typeparamref name="TValue"/>.</typeparam>
+ /// <typeparam name="TSerialDict">The serializable dictionary type that can store the keys and values.</typeparam>
+ /// <typeparam name="TSelf">The net field instance type.</typeparam>
+ /// <param name="field">The net field.</param>
+ public static NetDictionaryWatcher<TKey, TValue, TField, TSerialDict, TSelf> ForNetDictionary<TKey, TValue, TField, TSerialDict, TSelf>(NetDictionary<TKey, TValue, TField, TSerialDict, TSelf> field)
+ where TField : class, INetObject<INetSerializable>, new()
+ where TSerialDict : IDictionary<TKey, TValue>, new()
+ where TSelf : NetDictionary<TKey, TValue, TField, TSerialDict, TSelf>
+ {
+ return new NetDictionaryWatcher<TKey, TValue, TField, TSerialDict, TSelf>(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
+{
+ /// <summary>A watcher which tracks changes to a collection.</summary>
+ internal interface ICollectionWatcher<out TValue> : IWatcher
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The values added since the last reset.</summary>
+ IEnumerable<TValue> Added { get; }
+
+ /// <summary>The values removed since the last reset.</summary>
+ IEnumerable<TValue> 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
+{
+ /// <summary>A watcher which tracks changes to a dictionary.</summary>
+ internal interface IDictionaryWatcher<TKey, TValue> : ICollectionWatcher<KeyValuePair<TKey, TValue>> { }
+}
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
+{
+ /// <summary>A watcher which tracks changes to a value.</summary>
+ internal interface IValueWatcher<out T> : IWatcher
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The field value at the last reset.</summary>
+ T PreviousValue { get; }
+
+ /// <summary>The latest value.</summary>
+ 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
+{
+ /// <summary>A watcher which detects changes to something.</summary>
+ internal interface IWatcher : IDisposable
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Whether the value changed since the last reset.</summary>
+ bool IsChanged { get; }
+
+
+ /*********
+ ** Methods
+ *********/
+ /// <summary>Update the current value if needed.</summary>
+ void Update();
+
+ /// <summary>Set the current value as the baseline.</summary>
+ void Reset();
+ }
+}
diff --git a/src/SMAPI/Framework/StateTracking/LocationTracker.cs b/src/SMAPI/Framework/StateTracking/LocationTracker.cs
new file mode 100644
index 00000000..708c0716
--- /dev/null
+++ b/src/SMAPI/Framework/StateTracking/LocationTracker.cs
@@ -0,0 +1,103 @@
+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 StardewValley.TerrainFeatures;
+using Object = StardewValley.Object;
+
+namespace StardewModdingAPI.Framework.StateTracking
+{
+ /// <summary>Tracks changes to a location's data.</summary>
+ internal class LocationTracker : IWatcher
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The underlying watchers.</summary>
+ private readonly List<IWatcher> Watchers = new List<IWatcher>();
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Whether the value changed since the last reset.</summary>
+ public bool IsChanged => this.Watchers.Any(p => p.IsChanged);
+
+ /// <summary>The tracked location.</summary>
+ public GameLocation Location { get; }
+
+ /// <summary>Tracks added or removed buildings.</summary>
+ public ICollectionWatcher<Building> BuildingsWatcher { get; }
+
+ /// <summary>Tracks added or removed debris.</summary>
+ public ICollectionWatcher<Debris> DebrisWatcher { get; }
+
+ /// <summary>Tracks added or removed large terrain features.</summary>
+ public ICollectionWatcher<LargeTerrainFeature> LargeTerrainFeaturesWatcher { get; }
+
+ /// <summary>Tracks added or removed NPCs.</summary>
+ public ICollectionWatcher<NPC> NpcsWatcher { get; }
+
+ /// <summary>Tracks added or removed objects.</summary>
+ public IDictionaryWatcher<Vector2, Object> ObjectsWatcher { get; }
+
+ /// <summary>Tracks added or removed terrain features.</summary>
+ public IDictionaryWatcher<Vector2, TerrainFeature> TerrainFeaturesWatcher { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="location">The location to track.</param>
+ public LocationTracker(GameLocation location)
+ {
+ this.Location = location;
+
+ // init watchers
+ this.BuildingsWatcher = location is BuildableGameLocation buildableLocation
+ ? WatcherFactory.ForNetCollection(buildableLocation.buildings)
+ : (ICollectionWatcher<Building>)WatcherFactory.ForObservableCollection(new ObservableCollection<Building>());
+ this.DebrisWatcher = WatcherFactory.ForNetCollection(location.debris);
+ this.LargeTerrainFeaturesWatcher = WatcherFactory.ForNetCollection(location.largeTerrainFeatures);
+ 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.DebrisWatcher,
+ this.LargeTerrainFeaturesWatcher,
+ this.NpcsWatcher,
+ this.ObjectsWatcher,
+ this.TerrainFeaturesWatcher
+ });
+ }
+
+ /// <summary>Stop watching the player fields and release all references.</summary>
+ public void Dispose()
+ {
+ foreach (IWatcher watcher in this.Watchers)
+ watcher.Dispose();
+ }
+
+ /// <summary>Update the current value if needed.</summary>
+ public void Update()
+ {
+ foreach (IWatcher watcher in this.Watchers)
+ watcher.Update();
+ }
+
+ /// <summary>Set the current value as the baseline.</summary>
+ public void Reset()
+ {
+ foreach (IWatcher watcher in this.Watchers)
+ watcher.Reset();
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs
new file mode 100644
index 00000000..3814e534
--- /dev/null
+++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs
@@ -0,0 +1,169 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using StardewModdingAPI.Events;
+using StardewModdingAPI.Framework.StateTracking.FieldWatchers;
+using StardewValley;
+using StardewValley.Locations;
+
+namespace StardewModdingAPI.Framework.StateTracking
+{
+ /// <summary>Tracks changes to a player's data.</summary>
+ internal class PlayerTracker : IDisposable
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The player's inventory as of the last reset.</summary>
+ private IDictionary<Item, int> PreviousInventory;
+
+ /// <summary>The player's inventory change as of the last update.</summary>
+ private IDictionary<Item, int> CurrentInventory;
+
+ /// <summary>The player's last valid location.</summary>
+ private GameLocation LastValidLocation;
+
+ /// <summary>The underlying watchers.</summary>
+ private readonly List<IWatcher> Watchers = new List<IWatcher>();
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The player being tracked.</summary>
+ public Farmer Player { get; }
+
+ /// <summary>The player's current location.</summary>
+ public IValueWatcher<GameLocation> LocationWatcher { get; }
+
+ /// <summary>The player's current mine level.</summary>
+ public IValueWatcher<int> MineLevelWatcher { get; }
+
+ /// <summary>Tracks changes to the player's skill levels.</summary>
+ public IDictionary<EventArgsLevelUp.LevelType, IValueWatcher<int>> SkillWatchers { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="player">The player to track.</param>
+ public PlayerTracker(Farmer player)
+ {
+ // init player data
+ this.Player = player;
+ this.PreviousInventory = this.GetInventory();
+
+ // init trackers
+ this.LocationWatcher = WatcherFactory.ForReference(this.GetCurrentLocation);
+ this.MineLevelWatcher = WatcherFactory.ForEquatable(() => this.LastValidLocation is MineShaft mine ? mine.mineLevel : 0);
+ this.SkillWatchers = new Dictionary<EventArgsLevelUp.LevelType, IValueWatcher<int>>
+ {
+ [EventArgsLevelUp.LevelType.Combat] = WatcherFactory.ForNetValue(player.combatLevel),
+ [EventArgsLevelUp.LevelType.Farming] = WatcherFactory.ForNetValue(player.farmingLevel),
+ [EventArgsLevelUp.LevelType.Fishing] = WatcherFactory.ForNetValue(player.fishingLevel),
+ [EventArgsLevelUp.LevelType.Foraging] = WatcherFactory.ForNetValue(player.foragingLevel),
+ [EventArgsLevelUp.LevelType.Luck] = WatcherFactory.ForNetValue(player.luckLevel),
+ [EventArgsLevelUp.LevelType.Mining] = WatcherFactory.ForNetValue(player.miningLevel)
+ };
+
+ // track watchers for convenience
+ this.Watchers.AddRange(new IWatcher[]
+ {
+ this.LocationWatcher,
+ this.MineLevelWatcher
+ });
+ this.Watchers.AddRange(this.SkillWatchers.Values);
+ }
+
+ /// <summary>Update the current values if needed.</summary>
+ public void Update()
+ {
+ // update valid location
+ this.LastValidLocation = this.GetCurrentLocation();
+
+ // update watchers
+ foreach (IWatcher watcher in this.Watchers)
+ watcher.Update();
+
+ // update inventory
+ this.CurrentInventory = this.GetInventory();
+ }
+
+ /// <summary>Reset all trackers so their current values are the baseline.</summary>
+ public void Reset()
+ {
+ foreach (IWatcher watcher in this.Watchers)
+ watcher.Reset();
+
+ this.PreviousInventory = this.CurrentInventory;
+ }
+
+ /// <summary>Get the player's current location, ignoring temporary null values.</summary>
+ /// <remarks>The game will set <see cref="Character.currentLocation"/> 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.</remarks>
+ public GameLocation GetCurrentLocation()
+ {
+ return this.Player.currentLocation ?? this.LastValidLocation;
+ }
+
+ /// <summary>Get the player inventory changes between two states.</summary>
+ public IEnumerable<ItemStackChange> GetInventoryChanges()
+ {
+ IDictionary<Item, int> previous = this.PreviousInventory;
+ IDictionary<Item, int> 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 };
+ }
+ }
+
+ /// <summary>Get the player skill levels which changed.</summary>
+ public IEnumerable<KeyValuePair<EventArgsLevelUp.LevelType, IValueWatcher<int>>> GetChangedSkills()
+ {
+ return this.SkillWatchers.Where(p => p.Value.IsChanged);
+ }
+
+ /// <summary>Get the player's new location if it changed.</summary>
+ /// <param name="location">The player's current location.</param>
+ /// <returns>Returns whether it changed.</returns>
+ public bool TryGetNewLocation(out GameLocation location)
+ {
+ location = this.LocationWatcher.CurrentValue;
+ return this.LocationWatcher.IsChanged;
+ }
+
+ /// <summary>Get the player's new mine level if it changed.</summary>
+ /// <param name="mineLevel">The player's current mine level.</param>
+ /// <returns>Returns whether it changed.</returns>
+ public bool TryGetNewMineLevel(out int mineLevel)
+ {
+ mineLevel = this.MineLevelWatcher.CurrentValue;
+ return this.MineLevelWatcher.IsChanged;
+ }
+
+ /// <summary>Stop watching the player fields and release all references.</summary>
+ public void Dispose()
+ {
+ foreach (IWatcher watcher in this.Watchers)
+ watcher.Dispose();
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get the player's current inventory.</summary>
+ private IDictionary<Item, int> GetInventory()
+ {
+ return this.Player.Items
+ .Where(n => n != null)
+ .Distinct()
+ .ToDictionary(n => n, n => n.Stack);
+ }
+ }
+}
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
+{
+ /// <summary>Detects changes to the game's locations.</summary>
+ internal class WorldLocationsTracker : IWatcher
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>Tracks changes to the location list.</summary>
+ private readonly ICollectionWatcher<GameLocation> LocationListWatcher;
+
+ /// <summary>A lookup of the tracked locations.</summary>
+ private IDictionary<GameLocation, LocationTracker> LocationDict { get; } = new Dictionary<GameLocation, LocationTracker>();
+
+ /// <summary>A lookup of registered buildings and their indoor location.</summary>
+ private readonly IDictionary<Building, GameLocation> BuildingIndoors = new Dictionary<Building, GameLocation>();
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Whether locations were added or removed since the last reset.</summary>
+ public bool IsLocationListChanged => this.Added.Any() || this.Removed.Any();
+
+ /// <summary>Whether any tracked location data changed since the last reset.</summary>
+ public bool IsChanged => this.IsLocationListChanged || this.Locations.Any(p => p.IsChanged);
+
+ /// <summary>The tracked locations.</summary>
+ public IEnumerable<LocationTracker> Locations => this.LocationDict.Values;
+
+ /// <summary>The locations removed since the last update.</summary>
+ public ICollection<GameLocation> Added { get; } = new HashSet<GameLocation>();
+
+ /// <summary>The locations added since the last update.</summary>
+ public ICollection<GameLocation> Removed { get; } = new HashSet<GameLocation>();
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="locations">The game's list of locations.</param>
+ public WorldLocationsTracker(ObservableCollection<GameLocation> locations)
+ {
+ this.LocationListWatcher = WatcherFactory.ForObservableCollection(locations);
+ }
+
+ /// <summary>Update the current value if needed.</summary>
+ 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<Building, GameLocation> 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();
+ }
+
+ /// <summary>Set the current location list as the baseline.</summary>
+ public void ResetLocationList()
+ {
+ this.Removed.Clear();
+ this.Added.Clear();
+ this.LocationListWatcher.Reset();
+ }
+
+ /// <summary>Set the current value as the baseline.</summary>
+ public void Reset()
+ {
+ this.ResetLocationList();
+ foreach (IWatcher watcher in this.Locations)
+ watcher.Reset();
+ }
+
+ /// <summary>Stop watching the player fields and release all references.</summary>
+ public void Dispose()
+ {
+ this.LocationListWatcher.Dispose();
+ foreach (IWatcher watcher in this.Locations)
+ watcher.Dispose();
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /****
+ ** Enumerable wrappers
+ ****/
+ /// <summary>Add the given buildings.</summary>
+ /// <param name="buildings">The buildings to add.</param>
+ public void Add(IEnumerable<Building> buildings)
+ {
+ foreach (Building building in buildings)
+ this.Add(building);
+ }
+
+ /// <summary>Add the given locations.</summary>
+ /// <param name="locations">The locations to add.</param>
+ public void Add(IEnumerable<GameLocation> locations)
+ {
+ foreach (GameLocation location in locations)
+ this.Add(location);
+ }
+
+ /// <summary>Remove the given buildings.</summary>
+ /// <param name="buildings">The buildings to remove.</param>
+ public void Remove(IEnumerable<Building> buildings)
+ {
+ foreach (Building building in buildings)
+ this.Remove(building);
+ }
+
+ /// <summary>Remove the given locations.</summary>
+ /// <param name="locations">The locations to remove.</param>
+ public void Remove(IEnumerable<GameLocation> locations)
+ {
+ foreach (GameLocation location in locations)
+ this.Remove(location);
+ }
+
+ /****
+ ** Main add/remove logic
+ ****/
+ /// <summary>Add the given building.</summary>
+ /// <param name="building">The building to add.</param>
+ public void Add(Building building)
+ {
+ if (building == null)
+ return;
+
+ GameLocation indoors = building.indoors.Value;
+ this.BuildingIndoors[building] = indoors;
+ this.Add(indoors);
+ }
+
+ /// <summary>Add the given location.</summary>
+ /// <param name="location">The location to add.</param>
+ 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);
+ }
+
+ /// <summary>Remove the given building.</summary>
+ /// <param name="building">The building to remove.</param>
+ public void Remove(Building building)
+ {
+ if (building == null)
+ return;
+
+ this.BuildingIndoors.Remove(building);
+ this.Remove(building.indoors.Value);
+ }
+
+ /// <summary>Remove the given location.</summary>
+ /// <param name="location">The location to remove.</param>
+ 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/Framework/Utilities/PathUtilities.cs b/src/SMAPI/Framework/Utilities/PathUtilities.cs
deleted file mode 100644
index 0233d796..00000000
--- a/src/SMAPI/Framework/Utilities/PathUtilities.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-using System;
-using System.Diagnostics.Contracts;
-using System.IO;
-using System.Linq;
-
-namespace StardewModdingAPI.Framework.Utilities
-{
- /// <summary>Provides utilities for normalising file paths.</summary>
- internal static class PathUtilities
- {
- /*********
- ** Properties
- *********/
- /// <summary>The possible directory separator characters in a file path.</summary>
- private static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray();
-
- /// <summary>The preferred directory separator chaeacter in an asset key.</summary>
- private static readonly string PreferredPathSeparator = Path.DirectorySeparatorChar.ToString();
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Get the segments from a path (e.g. <c>/usr/bin/boop</c> => <c>usr</c>, <c>bin</c>, and <c>boop</c>).</summary>
- /// <param name="path">The path to split.</param>
- public static string[] GetSegments(string path)
- {
- return path.Split(PathUtilities.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries);
- }
-
- /// <summary>Normalise path separators in a file path.</summary>
- /// <param name="path">The file path to normalise.</param>
- [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;
- }
-
- /// <summary>Get a directory or file path relative to a given source path.</summary>
- /// <param name="sourceDir">The source folder path.</param>
- /// <param name="targetPath">The target folder or file path.</param>
- [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/Framework/WatcherCore.cs b/src/SMAPI/Framework/WatcherCore.cs
new file mode 100644
index 00000000..e06423b9
--- /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
+{
+ /// <summary>Monitors the entire game state for changes, virally spreading watchers into any new entities that get created.</summary>
+ internal class WatcherCore
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The underlying watchers for convenience. These are accessible individually as separate properties.</summary>
+ private readonly List<IWatcher> Watchers = new List<IWatcher>();
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Tracks changes to the window size.</summary>
+ public readonly IValueWatcher<Point> WindowSizeWatcher;
+
+ /// <summary>Tracks changes to the current player.</summary>
+ public PlayerTracker CurrentPlayerTracker;
+
+ /// <summary>Tracks changes to the time of day (in 24-hour military format).</summary>
+ public readonly IValueWatcher<int> TimeWatcher;
+
+ /// <summary>Tracks changes to the save ID.</summary>
+ public readonly IValueWatcher<ulong> SaveIdWatcher;
+
+ /// <summary>Tracks changes to the game's locations.</summary>
+ public readonly WorldLocationsTracker LocationsWatcher;
+
+ /// <summary>Tracks changes to <see cref="Game1.activeClickableMenu"/>.</summary>
+ public readonly IValueWatcher<IClickableMenu> ActiveMenuWatcher;
+
+ /// <summary>Tracks changes to the cursor position.</summary>
+ public readonly IValueWatcher<ICursorPosition> CursorWatcher;
+
+ /// <summary>Tracks changes to the mouse wheel scroll.</summary>
+ public readonly IValueWatcher<int> MouseWheelScrollWatcher;
+
+ /// <summary>Tracks changes to the content locale.</summary>
+ public readonly IValueWatcher<LocalizedContentManager.LanguageCode> LocaleWatcher;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="inputState">Manages input visible to the game.</param>
+ 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<GameLocation>)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
+ });
+ }
+
+ /// <summary>Update the watchers and adjust for added or removed entities.</summary>
+ 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();
+ }
+
+ /// <summary>Reset the current values as the baseline.</summary>
+ public void Reset()
+ {
+ foreach (IWatcher watcher in this.Watchers)
+ watcher.Reset();
+ this.CurrentPlayerTracker?.Reset();
+ this.LocationsWatcher.Reset();
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/WebApiClient.cs b/src/SMAPI/Framework/WebApiClient.cs
deleted file mode 100644
index 7f0122cf..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.Common.Models;
-
-namespace StardewModdingAPI.Framework
-{
- /// <summary>Provides methods for interacting with the SMAPI web API.</summary>
- internal class WebApiClient
- {
- /*********
- ** Properties
- *********/
- /// <summary>The base URL for the web API.</summary>
- private readonly Uri BaseUrl;
-
- /// <summary>The API version number.</summary>
- private readonly ISemanticVersion Version;
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Construct an instance.</summary>
- /// <param name="baseUrl">The base URL for the web API.</param>
- /// <param name="version">The web API version.</param>
- 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;
- }
-
- /// <summary>Get the latest SMAPI version.</summary>
- /// <param name="modKeys">The mod keys for which to fetch the latest version.</param>
- public IDictionary<string, ModInfoModel> GetModInfo(params string[] modKeys)
- {
- return this.Post<ModSearchModel, Dictionary<string, ModInfoModel>>(
- $"v{this.Version}/mods",
- new ModSearchModel(modKeys, allowInvalidVersions: true)
- );
- }
-
-
- /*********
- ** Private methods
- *********/
- /// <summary>Fetch the response from the backend API.</summary>
- /// <typeparam name="TBody">The body content type.</typeparam>
- /// <typeparam name="TResult">The expected response type.</typeparam>
- /// <param name="url">The request URL, optionally excluding the base URL.</param>
- /// <param name="content">The body content to post.</param>
- private TResult Post<TBody, TResult>(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<TResult>(response);
- }
- }
- }
-}
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
+{
+ /// <summary>The game's platform version.</summary>
+ public enum GamePlatform
+ {
+ /// <summary>The Linux version of the game.</summary>
+ Linux = Platform.Linux,
+
+ /// <summary>The Mac version of the game.</summary>
+ Mac = Platform.Mac,
+
+ /// <summary>The Windows version of the game.</summary>
+ Windows = Platform.Windows
+ }
+}
diff --git a/src/SMAPI/IAssetDataForImage.cs b/src/SMAPI/IAssetDataForImage.cs
index 4584a20e..1109194f 100644
--- a/src/SMAPI/IAssetDataForImage.cs
+++ b/src/SMAPI/IAssetDataForImage.cs
@@ -1,10 +1,10 @@
-using System;
+using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace StardewModdingAPI
{
- /// <summary>Encapsulates access and changes to dictionary content being read from a data file.</summary>
+ /// <summary>Encapsulates access and changes to image content being read from a data file.</summary>
public interface IAssetDataForImage : IAssetData<Texture2D>
{
/*********
diff --git a/src/SMAPI/ICursorPosition.cs b/src/SMAPI/ICursorPosition.cs
index ddb8eb49..21c57db0 100644
--- a/src/SMAPI/ICursorPosition.cs
+++ b/src/SMAPI/ICursorPosition.cs
@@ -1,10 +1,14 @@
+using System;
using Microsoft.Xna.Framework;
namespace StardewModdingAPI
{
/// <summary>Represents a cursor position in the different coordinate systems.</summary>
- public interface ICursorPosition
+ public interface ICursorPosition : IEquatable<ICursorPosition>
{
+ /// <summary>The pixel position relative to the top-left corner of the in-game map.</summary>
+ Vector2 AbsolutePixels { get; }
+
/// <summary>The pixel position relative to the top-left corner of the visible screen.</summary>
Vector2 ScreenPixels { get; }
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
+{
+ /// <summary>Provides an API for checking and changing input state.</summary>
+ public interface IInputHelper : IModLinked
+ {
+ /// <summary>Get the current cursor position.</summary>
+ ICursorPosition GetCursorPosition();
+
+ /// <summary>Get whether a button is currently pressed.</summary>
+ /// <param name="button">The button.</param>
+ bool IsDown(SButton button);
+
+ /// <summary>Get whether a button is currently suppressed, so the game won't see it.</summary>
+ /// <param name="button">The button.</param>
+ bool IsSuppressed(SButton button);
+
+ /// <summary>Prevent the game from handling a button press. This doesn't prevent other mods from receiving the event.</summary>
+ /// <param name="button">The button to suppress.</param>
+ void Suppress(SButton button);
+ }
+}
diff --git a/src/SMAPI/IManifest.cs b/src/SMAPI/IManifest.cs
deleted file mode 100644
index 183ac105..00000000
--- a/src/SMAPI/IManifest.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using System.Collections.Generic;
-
-namespace StardewModdingAPI
-{
- /// <summary>A manifest which describes a mod for SMAPI.</summary>
- public interface IManifest
- {
- /*********
- ** Accessors
- *********/
- /// <summary>The mod name.</summary>
- string Name { get; }
-
- /// <summary>A brief description of the mod.</summary>
- string Description { get; }
-
- /// <summary>The mod author's name.</summary>
- string Author { get; }
-
- /// <summary>The mod version.</summary>
- ISemanticVersion Version { get; }
-
- /// <summary>The minimum SMAPI version required by this mod, if any.</summary>
- ISemanticVersion MinimumApiVersion { get; }
-
- /// <summary>The unique mod ID.</summary>
- string UniqueID { get; }
-
- /// <summary>The name of the DLL in the directory that has the <see cref="IMod.Entry"/> method. Mutually exclusive with <see cref="EntryDll"/>.</summary>
- string EntryDll { get; }
-
- /// <summary>The mod which will read this as a content pack. Mutually exclusive with <see cref="EntryDll"/>.</summary>
- IManifestContentPackFor ContentPackFor { get; }
-
- /// <summary>The other mods that must be loaded before this mod.</summary>
- IManifestDependency[] Dependencies { get; }
-
- /// <summary>The namespaced mod IDs to query for updates (like <c>Nexus:541</c>).</summary>
- string[] UpdateKeys { get; set; }
-
- /// <summary>Any manifest fields which didn't match a valid field.</summary>
- IDictionary<string, object> 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
-{
- /// <summary>Indicates which mod can read the content pack represented by the containing manifest.</summary>
- public interface IManifestContentPackFor
- {
- /// <summary>The unique ID of the mod which can read this content pack.</summary>
- string UniqueID { get; }
-
- /// <summary>The minimum required version (if any).</summary>
- 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
-{
- /// <summary>A mod dependency listed in a mod manifest.</summary>
- public interface IManifestDependency
- {
- /*********
- ** Accessors
- *********/
- /// <summary>The unique mod ID to require.</summary>
- string UniqueID { get; }
-
- /// <summary>The minimum required version (if any).</summary>
- ISemanticVersion MinimumVersion { get; }
-
- /// <summary>Whether the dependency must be installed to use the mod.</summary>
- bool IsRequired { get; }
- }
-}
diff --git a/src/SMAPI/IModHelper.cs b/src/SMAPI/IModHelper.cs
index e9554fdc..d7b8c986 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,15 +13,25 @@ namespace StardewModdingAPI
/// <summary>The full path to the mod's folder.</summary>
string DirectoryPath { get; }
+ /// <summary>Manages access to events raised by SMAPI, which let your mod react when something happens in the game.</summary>
+ [Obsolete("This is an experimental interface which may change at any time. Don't depend on this for released mods.")]
+ IModEvents Events { get; }
+
/// <summary>An API for loading content assets.</summary>
IContentHelper Content { get; }
+ /// <summary>An API for checking and changing input state.</summary>
+ IInputHelper Input { get; }
+
/// <summary>Simplifies access to private game code.</summary>
IReflectionHelper Reflection { get; }
/// <summary>Metadata about loaded mods.</summary>
IModRegistry ModRegistry { get; }
+ /// <summary>Provides multiplayer utilities.</summary>
+ IMultiplayerHelper Multiplayer { get; }
+
/// <summary>An API for managing console commands.</summary>
ICommandHelper ConsoleCommands { get; }
diff --git a/src/SMAPI/IMultiplayerHelper.cs b/src/SMAPI/IMultiplayerHelper.cs
new file mode 100644
index 00000000..43a0ac95
--- /dev/null
+++ b/src/SMAPI/IMultiplayerHelper.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+using StardewValley;
+
+namespace StardewModdingAPI
+{
+ /// <summary>Provides multiplayer utilities.</summary>
+ public interface IMultiplayerHelper : IModLinked
+ {
+ /// <summary>Get a new multiplayer ID.</summary>
+ long GetNewID();
+
+ /// <summary>Get the locations which are being actively synced from the host.</summary>
+ IEnumerable<GameLocation> GetActiveLocations();
+ }
+}
diff --git a/src/SMAPI/IPrivateField.cs b/src/SMAPI/IPrivateField.cs
deleted file mode 100644
index 42bf7d2e..00000000
--- a/src/SMAPI/IPrivateField.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-#if !STARDEW_VALLEY_1_3
-using System;
-using System.Reflection;
-
-namespace StardewModdingAPI
-{
- /// <summary>A private field obtained through reflection.</summary>
- /// <typeparam name="TValue">The field value type.</typeparam>
- [Obsolete("Use " + nameof(IReflectedField<TValue>) + " instead")]
- public interface IPrivateField<TValue>
- {
- /*********
- ** Accessors
- *********/
- /// <summary>The reflection metadata.</summary>
- FieldInfo FieldInfo { get; }
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Get the field value.</summary>
- TValue GetValue();
-
- /// <summary>Set the field value.</summary>
- //// <param name="value">The value to set.</param>
- void SetValue(TValue value);
- }
-}
-#endif
diff --git a/src/SMAPI/IPrivateMethod.cs b/src/SMAPI/IPrivateMethod.cs
deleted file mode 100644
index c24db602..00000000
--- a/src/SMAPI/IPrivateMethod.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-#if !STARDEW_VALLEY_1_3
-using System;
-using System.Reflection;
-
-namespace StardewModdingAPI
-{
- /// <summary>A private method obtained through reflection.</summary>
- [Obsolete("Use " + nameof(IReflectedMethod) + " instead")]
- public interface IPrivateMethod
- {
- /*********
- ** Accessors
- *********/
- /// <summary>The reflection metadata.</summary>
- MethodInfo MethodInfo { get; }
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Invoke the method.</summary>
- /// <typeparam name="TValue">The return type.</typeparam>
- /// <param name="arguments">The method arguments to pass in.</param>
- TValue Invoke<TValue>(params object[] arguments);
-
- /// <summary>Invoke the method.</summary>
- /// <param name="arguments">The method arguments to pass in.</param>
- void Invoke(params object[] arguments);
- }
-}
-#endif
diff --git a/src/SMAPI/IPrivateProperty.cs b/src/SMAPI/IPrivateProperty.cs
deleted file mode 100644
index a1b21a69..00000000
--- a/src/SMAPI/IPrivateProperty.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-#if !STARDEW_VALLEY_1_3
-using System;
-using System.Reflection;
-
-namespace StardewModdingAPI
-{
- /// <summary>A private property obtained through reflection.</summary>
- /// <typeparam name="TValue">The property value type.</typeparam>
- [Obsolete("Use " + nameof(IPrivateProperty<TValue>) + " instead")]
- public interface IPrivateProperty<TValue>
- {
- /*********
- ** Accessors
- *********/
- /// <summary>The reflection metadata.</summary>
- PropertyInfo PropertyInfo { get; }
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Get the property value.</summary>
- TValue GetValue();
-
- /// <summary>Set the property value.</summary>
- //// <param name="value">The value to set.</param>
- void SetValue(TValue value);
- }
-}
-#endif
diff --git a/src/SMAPI/IReflectionHelper.cs b/src/SMAPI/IReflectionHelper.cs
index 60441471..a2b9eb32 100644
--- a/src/SMAPI/IReflectionHelper.cs
+++ b/src/SMAPI/IReflectionHelper.cs
@@ -47,74 +47,5 @@ namespace StardewModdingAPI
/// <param name="name">The field name.</param>
/// <param name="required">Whether to throw an exception if the field is not found.</param>
IReflectedMethod GetMethod(Type type, string name, bool required = true);
-
-#if !STARDEW_VALLEY_1_3
- /*****
- ** Obsolete
- *****/
- /// <summary>Get a private instance field.</summary>
- /// <typeparam name="TValue">The field type.</typeparam>
- /// <param name="obj">The object which has the field.</param>
- /// <param name="name">The field name.</param>
- /// <param name="required">Whether to throw an exception if the private field is not found.</param>
- [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetField) + " instead")]
- IPrivateField<TValue> GetPrivateField<TValue>(object obj, string name, bool required = true);
-
- /// <summary>Get a private static field.</summary>
- /// <typeparam name="TValue">The field type.</typeparam>
- /// <param name="type">The type which has the field.</param>
- /// <param name="name">The field name.</param>
- /// <param name="required">Whether to throw an exception if the private field is not found.</param>
- [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetField) + " instead")]
- IPrivateField<TValue> GetPrivateField<TValue>(Type type, string name, bool required = true);
-
- /// <summary>Get a private instance property.</summary>
- /// <typeparam name="TValue">The property type.</typeparam>
- /// <param name="obj">The object which has the property.</param>
- /// <param name="name">The property name.</param>
- /// <param name="required">Whether to throw an exception if the private property is not found.</param>
- [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetProperty) + " instead")]
- IPrivateProperty<TValue> GetPrivateProperty<TValue>(object obj, string name, bool required = true);
-
- /// <summary>Get a private static property.</summary>
- /// <typeparam name="TValue">The property type.</typeparam>
- /// <param name="type">The type which has the property.</param>
- /// <param name="name">The property name.</param>
- /// <param name="required">Whether to throw an exception if the private property is not found.</param>
- [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetProperty) + " instead")]
- IPrivateProperty<TValue> GetPrivateProperty<TValue>(Type type, string name, bool required = true);
-
- /// <summary>Get the value of a private instance field.</summary>
- /// <typeparam name="TValue">The field type.</typeparam>
- /// <param name="obj">The object which has the field.</param>
- /// <param name="name">The field name.</param>
- /// <param name="required">Whether to throw an exception if the private field is not found.</param>
- /// <remarks>This is a shortcut for <see cref="GetPrivateField{TValue}(object,string,bool)"/> followed by <see cref="IPrivateField{TValue}.GetValue"/>.</remarks>
- [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetField) + " or " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetProperty) + " instead")]
- TValue GetPrivateValue<TValue>(object obj, string name, bool required = true);
-
- /// <summary>Get the value of a private static field.</summary>
- /// <typeparam name="TValue">The field type.</typeparam>
- /// <param name="type">The type which has the field.</param>
- /// <param name="name">The field name.</param>
- /// <param name="required">Whether to throw an exception if the private field is not found.</param>
- /// <remarks>This is a shortcut for <see cref="GetPrivateField{TValue}(Type,string,bool)"/> followed by <see cref="IPrivateField{TValue}.GetValue"/>.</remarks>
- [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetField) + " or " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetProperty) + " instead")]
- TValue GetPrivateValue<TValue>(Type type, string name, bool required = true);
-
- /// <summary>Get a private instance method.</summary>
- /// <param name="obj">The object which has the method.</param>
- /// <param name="name">The field name.</param>
- /// <param name="required">Whether to throw an exception if the private field is not found.</param>
- [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetMethod) + " instead")]
- IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true);
-
- /// <summary>Get a private static method.</summary>
- /// <param name="type">The type which has the method.</param>
- /// <param name="name">The field name.</param>
- /// <param name="required">Whether to throw an exception if the private field is not found.</param>
- [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetMethod) + " instead")]
- IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true);
-#endif
}
}
diff --git a/src/SMAPI/ISemanticVersion.cs b/src/SMAPI/ISemanticVersion.cs
deleted file mode 100644
index 0483c97b..00000000
--- a/src/SMAPI/ISemanticVersion.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-using System;
-
-namespace StardewModdingAPI
-{
- /// <summary>A semantic version with an optional release tag.</summary>
- public interface ISemanticVersion : IComparable<ISemanticVersion>, IEquatable<ISemanticVersion>
- {
- /*********
- ** Accessors
- *********/
- /// <summary>The major version incremented for major API changes.</summary>
- int MajorVersion { get; }
-
- /// <summary>The minor version incremented for backwards-compatible changes.</summary>
- int MinorVersion { get; }
-
- /// <summary>The patch version for backwards-compatible bug fixes.</summary>
- int PatchVersion { get; }
-
- /// <summary>An optional build tag.</summary>
- string Build { get; }
-
-
- /*********
- ** Accessors
- *********/
- /// <summary>Get whether this version is older than the specified version.</summary>
- /// <param name="other">The version to compare with this instance.</param>
- bool IsOlderThan(ISemanticVersion other);
-
- /// <summary>Get whether this version is older than the specified version.</summary>
- /// <param name="other">The version to compare with this instance.</param>
- /// <exception cref="FormatException">The specified version is not a valid semantic version.</exception>
- bool IsOlderThan(string other);
-
- /// <summary>Get whether this version is newer than the specified version.</summary>
- /// <param name="other">The version to compare with this instance.</param>
- bool IsNewerThan(ISemanticVersion other);
-
- /// <summary>Get whether this version is newer than the specified version.</summary>
- /// <param name="other">The version to compare with this instance.</param>
- /// <exception cref="FormatException">The specified version is not a valid semantic version.</exception>
- bool IsNewerThan(string other);
-
- /// <summary>Get whether this version is between two specified versions (inclusively).</summary>
- /// <param name="min">The minimum version.</param>
- /// <param name="max">The maximum version.</param>
- bool IsBetween(ISemanticVersion min, ISemanticVersion max);
-
- /// <summary>Get whether this version is between two specified versions (inclusively).</summary>
- /// <param name="min">The minimum version.</param>
- /// <param name="max">The maximum version.</param>
- /// <exception cref="FormatException">One of the specified versions is not a valid semantic version.</exception>
- bool IsBetween(string min, string max);
-
- /// <summary>Get a string representation of the version.</summary>
- string ToString();
- }
-}
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
{
/// <summary>The log severity levels.</summary>
public enum LogLevel
{
/// <summary>Tracing info intended for developers.</summary>
- Trace,
+ Trace = ConsoleLogLevel.Trace,
/// <summary>Troubleshooting info that may be relevant to the player.</summary>
- Debug,
+ Debug = ConsoleLogLevel.Debug,
/// <summary>Info relevant to the player. This should be used judiciously.</summary>
- Info,
+ Info = ConsoleLogLevel.Info,
/// <summary>An issue the player should be aware of. This should be used rarely.</summary>
- Warn,
+ Warn = ConsoleLogLevel.Warn,
/// <summary>A message indicating something went wrong.</summary>
- Error,
+ Error = ConsoleLogLevel.Error,
/// <summary>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.</summary>
- Alert
+ Alert = ConsoleLogLevel.Alert
}
-} \ No newline at end of file
+}
diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs
index e54e0286..12abeb10 100644
--- a/src/SMAPI/Metadata/CoreAssetPropagator.cs
+++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs
@@ -80,17 +80,8 @@ namespace StardewModdingAPI.Metadata
** Buildings
****/
case "buildings\\houses": // Farm
-#if STARDEW_VALLEY_1_3
reflection.GetField<Texture2D>(typeof(Farm), nameof(Farm.houseTextures)).SetValue(content.Load<Texture2D>(key));
return true;
-#else
- {
- Farm farm = Game1.getFarm();
- if (farm == null)
- return false;
- return farm.houseTextures = content.Load<Texture2D>(key);
- }
-#endif
/****
** Content\Characters\Farmer
@@ -99,22 +90,14 @@ namespace StardewModdingAPI.Metadata
return FarmerRenderer.accessoriesTexture = content.Load<Texture2D>(key);
case "characters\\farmer\\farmer_base": // Farmer
- if (Game1.player == null || !Game1.player.isMale)
+ if (Game1.player == null || !Game1.player.IsMale)
return false;
-#if STARDEW_VALLEY_1_3
return Game1.player.FarmerRenderer = new FarmerRenderer(key);
-#else
- return Game1.player.FarmerRenderer = new FarmerRenderer(content.Load<Texture2D>(key));
-#endif
case "characters\\farmer\\farmer_girl_base": // Farmer
- if (Game1.player == null || Game1.player.isMale)
+ if (Game1.player == null || Game1.player.IsMale)
return false;
-#if STARDEW_VALLEY_1_3
return Game1.player.FarmerRenderer = new FarmerRenderer(key);
-#else
- return Game1.player.FarmerRenderer = new FarmerRenderer(content.Load<Texture2D>(key));
-#endif
case "characters\\farmer\\hairstyles": // Game1.loadContent
return FarmerRenderer.hairStylesTexture = content.Load<Texture2D>(key);
@@ -206,11 +189,6 @@ namespace StardewModdingAPI.Metadata
/****
** Content\Critters
****/
-#if !STARDEW_VALLEY_1_3
- case "tilesheets\\critters": // Criter.InitShared
- return Critter.critterTexture = content.Load<Texture2D>(key);
-#endif
-
case "tilesheets\\crops": // Game1.loadContent
return Game1.cropSpriteSheet = content.Load<Texture2D>(key);
@@ -265,11 +243,7 @@ namespace StardewModdingAPI.Metadata
Texture2D texture = content.Load<Texture2D>(key);
reflection.GetField<Texture2D>(titleMenu, "titleButtonsTexture").SetValue(texture);
foreach (TemporaryAnimatedSprite bird in reflection.GetField<List<TemporaryAnimatedSprite>>(titleMenu, "birds").GetValue())
-#if STARDEW_VALLEY_1_3
bird.texture = texture;
-#else
- bird.Texture = texture;
-#endif
return true;
}
return false;
@@ -284,12 +258,8 @@ namespace StardewModdingAPI.Metadata
return Game1.buffsIcons = content.Load<Texture2D>(key);
case "tilesheets\\bushes": // new Bush()
-#if STARDEW_VALLEY_1_3
reflection.GetField<Lazy<Texture2D>>(typeof(Bush), "texture").SetValue(new Lazy<Texture2D>(() => content.Load<Texture2D>(key)));
return true;
-#else
- return Bush.texture = content.Load<Texture2D>(key);
-#endif
case "tilesheets\\craftables": // Game1.loadContent
return Game1.bigCraftableSpriteSheet = content.Load<Texture2D>(key);
@@ -355,6 +325,10 @@ namespace StardewModdingAPI.Metadata
if (this.IsInFolder(key, "Portraits"))
return this.ReloadNpcPortraits(content, key);
+ // dynamic data
+ if (this.IsInFolder(key, "Characters\\schedules"))
+ return this.ReloadNpcSchedules(content, key);
+
return false;
}
@@ -363,7 +337,7 @@ namespace StardewModdingAPI.Metadata
** Private methods
*********/
/****
- ** Reload methods
+ ** Reload texture methods
****/
/// <summary>Reload the sprites for matching pets or horses.</summary>
/// <typeparam name="TAnimal">The animal type.</typeparam>
@@ -381,7 +355,7 @@ namespace StardewModdingAPI.Metadata
// update sprites
Texture2D texture = content.Load<Texture2D>(key);
foreach (TAnimal animal in animals)
- this.SetSpriteTexture(animal.sprite, texture);
+ this.SetSpriteTexture(animal.Sprite, texture);
return true;
}
@@ -402,16 +376,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)}"
- : animal.type;
- if (animal.showDifferentTextureWhenReadyForHarvest && animal.currentProduce <= 0)
+ string expectedKey = animal.age.Value < animal.ageWhenMature.Value
+ ? $"Baby{(animal.type.Value == "Duck" ? "White Chicken" : animal.type.Value)}"
+ : animal.type.Value;
+ if (animal.showDifferentTextureWhenReadyForHarvest.Value && 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;
}
@@ -427,7 +401,7 @@ namespace StardewModdingAPI.Metadata
Building[] buildings = Game1.locations
.OfType<BuildableGameLocation>()
.SelectMany(p => p.buildings)
- .Where(p => p.buildingType == type)
+ .Where(p => p.buildingType.Value == type)
.ToArray();
// reload buildings
@@ -435,11 +409,7 @@ namespace StardewModdingAPI.Metadata
{
Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(key));
foreach (Building building in buildings)
-#if STARDEW_VALLEY_1_3
building.texture = texture;
-#else
- building.texture = texture.Value;
-#endif
return true;
}
return false;
@@ -460,16 +430,16 @@ namespace StardewModdingAPI.Metadata
(
from location in this.GetLocations()
from fence in location.Objects.Values.OfType<Fence>()
- where fenceType == 1
- ? fence.isGate
- : fence.whichType == fenceType
+ where
+ fence.whichType.Value == fenceType
+ || (fence.isGate.Value && fenceType == 1) // gates are hardcoded to draw fence type 1
select fence
)
.ToArray();
// update fence textures
foreach (Fence fence in fences)
- fence.reloadSprite();
+ this.Reflection.GetField<Lazy<Texture2D>>(fence, "fenceTexture").SetValue(new Lazy<Texture2D>(fence.loadFenceTexture));
return true;
}
@@ -482,7 +452,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;
@@ -501,7 +471,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;
@@ -521,18 +491,14 @@ namespace StardewModdingAPI.Metadata
{
Tree[] trees = Game1.locations
.SelectMany(p => p.terrainFeatures.Values.OfType<Tree>())
- .Where(tree => tree.treeType == type)
+ .Where(tree => tree.treeType.Value == type)
.ToArray();
if (trees.Any())
{
Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(key));
foreach (Tree tree in trees)
-#if STARDEW_VALLEY_1_3
this.Reflection.GetField<Lazy<Texture2D>>(tree, "texture").SetValue(texture);
-#else
- this.Reflection.GetField<Texture2D>(tree, "texture").SetValue(texture.Value);
-#endif
return true;
}
@@ -540,6 +506,38 @@ namespace StardewModdingAPI.Metadata
}
/****
+ ** Reload data methods
+ ****/
+ /// <summary>Reload the schedules for matching NPCs.</summary>
+ /// <param name="content">The content manager through which to reload the asset.</param>
+ /// <param name="key">The asset key to reload.</param>
+ /// <returns>Returns whether any assets were reloaded.</returns>
+ private bool ReloadNpcSchedules(LocalizedContentManager content, string key)
+ {
+ // get NPCs
+ string name = Path.GetFileName(key);
+ NPC[] villagers = this.GetCharacters().Where(npc => npc.Name == name && npc.isVillager()).ToArray();
+ if (!villagers.Any())
+ return false;
+
+ // update schedule
+ foreach (NPC villager in villagers)
+ {
+ // reload schedule
+ villager.Schedule = villager.getSchedule(Game1.dayOfMonth);
+
+ // switch to new schedule if needed
+ int lastScheduleTime = villager.Schedule.Keys.Where(p => p <= Game1.timeOfDay).OrderByDescending(p => p).FirstOrDefault();
+ if (lastScheduleTime != 0)
+ {
+ this.Reflection.GetField<int>(villager, "scheduleTimeToTry").SetValue(this.Reflection.GetField<int>(typeof(NPC), "NO_TRY").GetValue()); // use time that's passed in to checkSchedule
+ villager.checkSchedule(lastScheduleTime);
+ }
+ }
+ return true;
+ }
+
+ /****
** Helpers
****/
/// <summary>Reload the texture for an animated sprite.</summary>
@@ -547,11 +545,7 @@ namespace StardewModdingAPI.Metadata
/// <param name="texture">The texture to set.</param>
private void SetSpriteTexture(AnimatedSprite sprite, Texture2D texture)
{
-#if STARDEW_VALLEY_1_3
this.Reflection.GetField<Texture2D>(sprite, "spriteTexture").SetValue(texture);
-#else
- sprite.Texture = texture;
-#endif
}
/// <summary>Get an NPC name from the name of their file under <c>Content/Characters</c>.</summary>
@@ -605,8 +599,9 @@ namespace StardewModdingAPI.Metadata
{
foreach (Building building in buildableLocation.buildings)
{
- if (building.indoors != null)
- yield return building.indoors;
+ GameLocation indoors = building.indoors.Value;
+ if (indoors != null)
+ yield return indoors;
}
}
}
diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs
index 4960a458..2f0c1b15 100644
--- a/src/SMAPI/Metadata/InstructionMetadata.cs
+++ b/src/SMAPI/Metadata/InstructionMetadata.cs
@@ -1,14 +1,11 @@
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.Framework.RewriteFacades;
using StardewValley;
-#if STARDEW_VALLEY_1_3
-using SObject = StardewValley.Object;
-#endif
namespace StardewModdingAPI.Metadata
{
@@ -20,7 +17,7 @@ namespace StardewModdingAPI.Metadata
*********/
/// <summary>The assembly names to which to heuristically detect broken references.</summary>
/// <remarks>The current implementation only works correctly with assemblies that should always be present.</remarks>
- private readonly string[] ValidateReferencesToAssemblies = { "StardewModdingAPI", "Stardew Valley", "StardewValley" };
+ private readonly string[] ValidateReferencesToAssemblies = { "StardewModdingAPI", "Stardew Valley", "StardewValley", "Netcode" };
/*********
@@ -37,75 +34,21 @@ namespace StardewModdingAPI.Metadata
// rewrite for crossplatform compatibility
new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchMethods), onlyIfPlatformChanged: true),
-#if !STARDEW_VALLEY_1_3
- // rewrite for Stardew Valley 1.2
- new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.activeClickableMenu)),
- new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.currentMinigame)),
- new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.gameMode)),
- new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.player)),
- new FieldReplaceRewriter(typeof(Game1), "borderFont", nameof(Game1.smallFont)),
- new FieldReplaceRewriter(typeof(Game1), "smoothFont", nameof(Game1.smallFont)),
-
- // rewrite for SMAPI 1.9
- new TypeReferenceRewriter("StardewModdingAPI.Inheritance.ItemStackChange", typeof(ItemStackChange)),
-#endif
-
// rewrite for SMAPI 2.0
new VirtualEntryCallRemover(),
+ // rewrite for SMAPI 2.6 (types moved into SMAPI.Toolkit.CoreInterfaces)
+ new TypeReferenceRewriter("StardewModdingAPI.IManifest", typeof(IManifest), shouldIgnore: type => type.Scope.Name != "StardewModdingAPI"),
+ new TypeReferenceRewriter("StardewModdingAPI.IManifestContentPackFor", typeof(IManifestContentPackFor), shouldIgnore: type => type.Scope.Name != "StardewModdingAPI"),
+ new TypeReferenceRewriter("StardewModdingAPI.IManifestDependency", typeof(IManifestDependency), shouldIgnore: type => type.Scope.Name != "StardewModdingAPI"),
+ new TypeReferenceRewriter("StardewModdingAPI.ISemanticVersion", typeof(ISemanticVersion), shouldIgnore: type => type.Scope.Name != "StardewModdingAPI"),
+
// rewrite for Stardew Valley 1.3
-#if STARDEW_VALLEY_1_3
new StaticFieldToConstantRewriter<int>(typeof(Game1), "tileSize", Game1.tileSize),
-#endif
/****
** detect incompatible code
****/
- #if !STARDEW_VALLEY_1_3
- // detect changes in Stardew Valley 1.2
- new FieldFinder("StardewValley.Item", "set_Name", InstructionHandleResult.NotCompatible),
-
- // detect APIs removed in SMAPI 1.9
- new TypeFinder("StardewModdingAPI.Advanced.ConfigFile", InstructionHandleResult.NotCompatible),
- new TypeFinder("StardewModdingAPI.Advanced.IConfigFile", InstructionHandleResult.NotCompatible),
- new TypeFinder("StardewModdingAPI.Entities.SPlayer", InstructionHandleResult.NotCompatible),
- new TypeFinder("StardewModdingAPI.Extensions", InstructionHandleResult.NotCompatible),
- new TypeFinder("StardewModdingAPI.Inheritance.SGame", InstructionHandleResult.NotCompatible),
- new TypeFinder("StardewModdingAPI.Inheritance.SObject", InstructionHandleResult.NotCompatible),
- new TypeFinder("StardewModdingAPI.LogWriter", InstructionHandleResult.NotCompatible),
- new TypeFinder("StardewModdingAPI.Manifest", InstructionHandleResult.NotCompatible),
- new TypeFinder("StardewModdingAPI.Version", InstructionHandleResult.NotCompatible),
- new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "DrawDebug", InstructionHandleResult.NotCompatible),
- new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "DrawTick", InstructionHandleResult.NotCompatible),
- new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPostRenderHudEventNoCheck", InstructionHandleResult.NotCompatible),
- new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPostRenderGuiEventNoCheck", InstructionHandleResult.NotCompatible),
- new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderHudEventNoCheck", InstructionHandleResult.NotCompatible),
- new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderGuiEventNoCheck", InstructionHandleResult.NotCompatible),
-
- // detect APIs removed in SMAPI 2.0
- new TypeFinder("StardewModdingAPI.Command", InstructionHandleResult.NotCompatible),
- new TypeFinder("StardewModdingAPI.Config", InstructionHandleResult.NotCompatible),
- new TypeFinder("StardewModdingAPI.Log", InstructionHandleResult.NotCompatible),
- new EventFinder("StardewModdingAPI.Events.GameEvents", "Initialize", InstructionHandleResult.NotCompatible),
- new EventFinder("StardewModdingAPI.Events.GameEvents", "LoadContent", InstructionHandleResult.NotCompatible),
- new EventFinder("StardewModdingAPI.Events.GameEvents", "GameLoaded", InstructionHandleResult.NotCompatible),
- new EventFinder("StardewModdingAPI.Events.PlayerEvents", "LoadedGame", InstructionHandleResult.NotCompatible),
- new EventFinder("StardewModdingAPI.Events.PlayerEvents", "FarmerChanged", InstructionHandleResult.NotCompatible),
- new EventFinder("StardewModdingAPI.Events.TimeEvents", "DayOfMonthChanged", InstructionHandleResult.NotCompatible),
- new EventFinder("StardewModdingAPI.Events.TimeEvents", "YearOfGameChanged", InstructionHandleResult.NotCompatible),
- new EventFinder("StardewModdingAPI.Events.TimeEvents", "SeasonOfYearChanged", InstructionHandleResult.NotCompatible),
- new EventFinder("StardewModdingAPI.Events.TimeEvents", "OnNewDay", InstructionHandleResult.NotCompatible),
- new TypeFinder("StardewModdingAPI.Events.EventArgsCommand", InstructionHandleResult.NotCompatible),
- new TypeFinder("StardewModdingAPI.Events.EventArgsFarmerChanged", InstructionHandleResult.NotCompatible),
- new TypeFinder("StardewModdingAPI.Events.EventArgsLoadedGameChanged", InstructionHandleResult.NotCompatible),
- new TypeFinder("StardewModdingAPI.Events.EventArgsNewDay", InstructionHandleResult.NotCompatible),
- new TypeFinder("StardewModdingAPI.Events.EventArgsStringChanged", InstructionHandleResult.NotCompatible),
- new PropertyFinder("StardewModdingAPI.Mod", "PathOnDisk", InstructionHandleResult.NotCompatible),
- new PropertyFinder("StardewModdingAPI.Mod", "BaseConfigPath", InstructionHandleResult.NotCompatible),
- new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigFolder", InstructionHandleResult.NotCompatible),
- new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigPath", InstructionHandleResult.NotCompatible),
- #endif
-
// detect broken code
new ReferenceToMissingMemberFinder(this.ValidateReferencesToAssemblies),
new ReferenceToMemberWithUnexpectedTypeFinder(this.ValidateReferencesToAssemblies),
diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs
index ff4e9a50..6012b15a 100644
--- a/src/SMAPI/Program.cs
+++ b/src/SMAPI/Program.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
@@ -7,29 +8,37 @@ 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;
#if SMAPI_FOR_WINDOWS
-using System.Management;
using System.Windows.Forms;
#endif
using Newtonsoft.Json;
-using StardewModdingAPI.Common.Models;
using StardewModdingAPI.Events;
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;
+using StardewModdingAPI.Framework.Patching;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Framework.Serialisation;
-using StardewModdingAPI.Framework.Utilities;
+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;
using StardewValley;
+using Keys = Microsoft.Xna.Framework.Input.Keys;
using Monitor = StardewModdingAPI.Framework.Monitor;
using SObject = StardewValley.Object;
+using ThreadState = System.Threading.ThreadState;
namespace StardewModdingAPI
{
@@ -54,15 +63,14 @@ namespace StardewModdingAPI
/// <summary>Simplifies access to private game code.</summary>
private readonly Reflector Reflection = new Reflector();
+ /// <summary>The SMAPI configuration settings.</summary>
+ private readonly SConfig Settings;
+
/// <summary>The underlying game instance.</summary>
private SGame GameInstance;
/// <summary>The underlying content manager.</summary>
- private ContentCore ContentCore => this.GameInstance.ContentCore;
-
- /// <summary>The SMAPI configuration settings.</summary>
- /// <remarks>This is initialised after the game starts.</remarks>
- private SConfig Settings;
+ private ContentCoordinator ContentCore => this.GameInstance.ContentCore;
/// <summary>Tracks the installed mods.</summary>
/// <remarks>This is initialised after the game starts.</remarks>
@@ -72,10 +80,6 @@ namespace StardewModdingAPI
/// <remarks>This is initialised after the game starts.</remarks>
private DeprecationManager DeprecationManager;
- /// <summary>Manages console commands.</summary>
- /// <remarks>This is initialised after the game starts.</remarks>
- private CommandManager CommandManager;
-
/// <summary>Manages SMAPI events for mods.</summary>
private readonly EventManager EventManager;
@@ -89,11 +93,15 @@ namespace StardewModdingAPI
private readonly Regex[] SuppressConsolePatterns =
{
new Regex(@"^TextBox\.Selected is now '(?:True|False)'\.$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
- new Regex(@"^(?:FRUIT )?TREE: IsClient:(?:True|False) randomOutput: \d+$", RegexOptions.Compiled | RegexOptions.CultureInvariant)
+ new Regex(@"^(?:FRUIT )?TREE: IsClient:(?:True|False) randomOutput: \d+$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
+ new Regex(@"^loadPreferences\(\); begin", RegexOptions.Compiled | RegexOptions.CultureInvariant),
+ new Regex(@"^savePreferences\(\); async=", RegexOptions.Compiled | RegexOptions.CultureInvariant),
+ new Regex(@"^Multiplayer auth success$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
+ new Regex(@"^DebugOutput: (?:added CLOUD|dismount tile|Ping|playerPos)", RegexOptions.Compiled | RegexOptions.CultureInvariant)
};
- /// <summary>Encapsulates SMAPI's JSON file parsing.</summary>
- private readonly JsonHelper JsonHelper = new JsonHelper();
+ /// <summary>The mod toolkit used for generic mod interactions.</summary>
+ private readonly ModToolkit Toolkit = new ModToolkit();
/*********
@@ -108,48 +116,57 @@ namespace StardewModdingAPI
// get flags from arguments
bool writeToConsole = !args.Contains("--no-terminal");
- // get log path from arguments
- string logPath = null;
- {
- int pathIndex = Array.LastIndexOf(args, "--log-path") + 1;
- if (pathIndex >= 1 && args.Length >= pathIndex)
- {
- logPath = args[pathIndex];
- if (!Path.IsPathRooted(logPath))
- logPath = Path.Combine(Constants.LogDir, logPath);
- }
- }
- if (string.IsNullOrWhiteSpace(logPath))
- logPath = Constants.DefaultLogPath;
-
// load SMAPI
- using (Program program = new Program(writeToConsole, logPath))
+ using (Program program = new Program(writeToConsole))
program.RunInteractively();
}
/// <summary>Construct an instance.</summary>
/// <param name="writeToConsole">Whether to output log messages to the console.</param>
- /// <param name="logPath">The full file path to which to write log messages.</param>
- public Program(bool writeToConsole, string logPath)
+ public Program(bool writeToConsole)
{
+ // init paths
+ this.VerifyPath(Constants.ModPath);
+ this.VerifyPath(Constants.LogDir);
+
+ // init log file
+ this.PurgeLogFiles();
+ string logPath = this.GetLogPath();
+
// init basics
+ this.Settings = JsonConvert.DeserializeObject<SConfig>(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, this.Settings.ColorScheme)
+ {
+ WriteToConsole = writeToConsole,
+ ShowTraceInConsole = this.Settings.DeveloperMode,
+ ShowFullStampInConsole = this.Settings.DeveloperMode
+ };
this.EventManager = new EventManager(this.Monitor, this.ModRegistry);
- // hook up events
- ContentEvents.Init(this.EventManager);
- ControlEvents.Init(this.EventManager);
- GameEvents.Init(this.EventManager);
- GraphicsEvents.Init(this.EventManager);
- InputEvents.Init(this.EventManager);
- LocationEvents.Init(this.EventManager);
- MenuEvents.Init(this.EventManager);
- MineEvents.Init(this.EventManager);
- PlayerEvents.Init(this.EventManager);
- SaveEvents.Init(this.EventManager);
- SpecialisedEvents.Init(this.EventManager);
- TimeEvents.Init(this.EventManager);
+ // init logging
+ 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);
+
+ // validate game version
+ if (Constants.GameVersion.IsOlderThan(Constants.MinimumGameVersion))
+ {
+ this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI.", LogLevel.Error);
+ this.PressAnyKeyToExit();
+ return;
+ }
+ if (Constants.MaximumGameVersion != null && Constants.GameVersion.IsNewerThan(Constants.MaximumGameVersion))
+ {
+ this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameVersion}, but this version of SMAPI is only compatible up to Stardew Valley {Constants.MaximumGameVersion}. Please check for a newer version of SMAPI: https://smapi.io.", LogLevel.Error);
+ this.PressAnyKeyToExit();
+ return;
+ }
+
+ // apply game patches
+ new GamePatcher(this.Monitor).Apply(
+ // new GameLocationPatch()
+ );
}
/// <summary>Launch SMAPI.</summary>
@@ -159,28 +176,32 @@ namespace StardewModdingAPI
// initialise SMAPI
try
{
- // init logging
- this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {this.GetFriendlyPlatformName()}", LogLevel.Info);
- this.Monitor.Log($"Mods go here: {Constants.ModPath}");
- this.Monitor.Log($"Log started at {DateTime.UtcNow:s} UTC", LogLevel.Trace);
-
- // validate paths
- this.VerifyPath(Constants.ModPath);
- this.VerifyPath(Constants.LogDir);
-
- // validate game version
- if (Constants.GameVersion.IsOlderThan(Constants.MinimumGameVersion))
- {
- this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI.", LogLevel.Error);
- this.PressAnyKeyToExit();
- return;
- }
- if (Constants.MaximumGameVersion != null && Constants.GameVersion.IsNewerThan(Constants.MaximumGameVersion))
- {
- this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameVersion}, but this version of SMAPI is only compatible up to Stardew Valley {Constants.MaximumGameVersion}. Please check for a newer version of SMAPI: https://smapi.io.", LogLevel.Error);
- this.PressAnyKeyToExit();
- return;
- }
+ // hook up events
+ ContentEvents.Init(this.EventManager);
+ ControlEvents.Init(this.EventManager);
+ GameEvents.Init(this.EventManager);
+ GraphicsEvents.Init(this.EventManager);
+ InputEvents.Init(this.EventManager);
+ 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);
+ TimeEvents.Init(this.EventManager);
+
+ // init JSON parser
+ JsonConverter[] converters = {
+ new StringEnumConverter<Buttons>(),
+ new StringEnumConverter<Keys>(),
+ new StringEnumConverter<SButton>(),
+ new ColorConverter(),
+ new PointConverter(),
+ new RectangleConverter()
+ };
+ foreach (JsonConverter converter in converters)
+ this.Toolkit.JsonHelper.JsonSettings.Converters.Add(converter);
// add error handlers
#if SMAPI_FOR_WINDOWS
@@ -189,10 +210,13 @@ namespace StardewModdingAPI
#endif
AppDomain.CurrentDomain.UnhandledException += (sender, e) => this.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error);
+ // add more leniant assembly resolvers
+ AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => AssemblyLoader.ResolveAssembly(e.Name);
+
// override game
SGame.MonitorDuringInitialisation = this.Monitor;
SGame.ReflectorDuringInitialisation = this.Reflection;
- this.GameInstance = new SGame(this.Monitor, this.Reflection, this.EventManager, this.InitialiseAfterGameStart);
+ this.GameInstance = new SGame(this.Monitor, this.Reflection, this.EventManager, this.InitialiseAfterGameStart, this.Dispose);
StardewValley.Program.gamePtr = this.GameInstance;
// add exit handler
@@ -216,10 +240,6 @@ namespace StardewModdingAPI
}).Start();
// hook into game events
-#if SMAPI_FOR_WINDOWS
- ((Form)Control.FromHandle(this.GameInstance.Window.Handle)).FormClosing += (sender, args) => this.Dispose();
-#endif
- this.GameInstance.Exiting += (sender, e) => this.Dispose();
ContentEvents.AfterLocaleChanged += (sender, e) => this.OnLocaleChanged();
// set window titles
@@ -233,11 +253,28 @@ namespace StardewModdingAPI
return;
}
+ // check update marker
+ if (File.Exists(Constants.UpdateMarker))
+ {
+ string rawUpdateFound = File.ReadAllText(Constants.UpdateMarker);
+ if (SemanticVersion.TryParse(rawUpdateFound, out ISemanticVersion updateFound))
+ {
+ if (Constants.ApiVersion.IsPrerelease() && updateFound.IsNewerThan(Constants.ApiVersion))
+ {
+ this.Monitor.Log("A new version of SMAPI was detected last time you played.", LogLevel.Error);
+ this.Monitor.Log($"You can update to {updateFound}: https://smapi.io.", LogLevel.Error);
+ this.Monitor.Log("Press any key to continue playing anyway. (This only appears when using a SMAPI beta.)", LogLevel.Info);
+ Console.ReadKey();
+ }
+ }
+ File.Delete(Constants.UpdateMarker);
+ }
+
// show details if game crashed during last session
if (File.Exists(Constants.FatalCrashMarker))
{
this.Monitor.Log("The game crashed last time you played. That can be due to bugs in the game, but if it happens repeatedly you can ask for help here: http://community.playstarbound.com/threads/108375/.", LogLevel.Error);
- this.Monitor.Log($"If you ask for help, make sure to attach this file: {Constants.FatalCrashLog}", LogLevel.Error);
+ this.Monitor.Log("If you ask for help, make sure to share your SMAPI log: https://log.smapi.io.", LogLevel.Error);
this.Monitor.Log("Press any key to delete the crash data and continue playing.", LogLevel.Info);
Console.ReadKey();
File.Delete(Constants.FatalCrashLog);
@@ -245,13 +282,19 @@ namespace StardewModdingAPI
}
// start game
- this.Monitor.Log("Starting game...", LogLevel.Trace);
+ this.Monitor.Log("Starting game...", LogLevel.Debug);
try
{
this.IsGameRunning = true;
StardewValley.Program.releaseBuild = true; // game's debug logic interferes with SMAPI opening the game window
this.GameInstance.Run();
}
+ catch (InvalidOperationException ex) when (ex.Source == "Microsoft.Xna.Framework.Xact" && ex.StackTrace.Contains("Microsoft.Xna.Framework.Audio.AudioEngine..ctor"))
+ {
+ this.Monitor.Log("The game couldn't load audio. Do you have speakers or headphones plugged in?", LogLevel.Error);
+ this.Monitor.Log($"Technical details: {ex.GetLogSummary()}", LogLevel.Trace);
+ this.PressAnyKeyToExit();
+ }
catch (Exception ex)
{
this.Monitor.Log($"The game failed unexpectedly: {ex.GetLogSummary()}", LogLevel.Error);
@@ -266,12 +309,11 @@ namespace StardewModdingAPI
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
public void Dispose()
{
- this.Monitor.Log("Disposing...", LogLevel.Trace);
-
// skip if already disposed
if (this.IsDisposed)
return;
this.IsDisposed = true;
+ this.Monitor.Log("Disposing...", LogLevel.Trace);
// dispose mod data
foreach (IModMetadata mod in this.ModRegistry.GetAll())
@@ -293,6 +335,9 @@ namespace StardewModdingAPI
this.CancellationTokenSource?.Dispose();
this.GameInstance?.Dispose();
this.LogFile?.Dispose();
+
+ // end game (moved from Game1.OnExiting to let us clean up first)
+ Process.GetCurrentProcess().Kill();
}
@@ -309,14 +354,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)
@@ -345,27 +383,21 @@ namespace StardewModdingAPI
private void InitialiseAfterGameStart()
{
// load settings
- this.Settings = JsonConvert.DeserializeObject<SConfig>(File.ReadAllText(Constants.ApiConfigPath));
this.GameInstance.VerboseLogging = this.Settings.VerboseLogging;
// load core components
this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry);
- this.CommandManager = new CommandManager();
// redirect direct console output
{
- Monitor monitor = this.GetSecondaryMonitor("Console.Out");
+ Monitor monitor = this.GetSecondaryMonitor("game");
if (monitor.WriteToConsole)
this.ConsoleManager.OnMessageIntercepted += message => this.HandleConsoleMessage(monitor, message);
}
// 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 +409,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);
+ ModToolkit toolkit = new ModToolkit();
+ ModDatabase modDatabase = toolkit.GetModDatabase(Constants.ApiMetadataPath);
// load mods
{
@@ -385,14 +418,28 @@ namespace StardewModdingAPI
ModResolver resolver = new ModResolver();
// load manifests
- IModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, this.JsonHelper, modDatabase).ToArray();
- resolver.ValidateManifests(mods, Constants.ApiVersion, Constants.GetUpdateUrl);
+ IModMetadata[] mods = resolver.ReadManifests(toolkit, Constants.ModPath, modDatabase).ToArray();
+ resolver.ValidateManifests(mods, Constants.ApiVersion, toolkit.GetUpdateUrl);
// process dependencies
mods = resolver.ProcessDependencies(mods, modDatabase).ToArray();
// load mods
- this.LoadMods(mods, this.JsonHelper, this.ContentCore, modDatabase);
+ this.LoadMods(mods, this.Toolkit.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.Toolkit.JsonHelper.WriteJsonFile(Path.Combine(Constants.LogDir, $"{Constants.LogNamePrefix}.metadata-dump.json"), export);
+ }
// check for updates
this.CheckForUpdatesAsync(mods);
@@ -430,8 +477,8 @@ namespace StardewModdingAPI
{
// prepare console
this.Monitor.Log("Type 'help' for help, or 'help <cmd>' for a command's usage", LogLevel.Info);
- this.CommandManager.Add("SMAPI", "help", "Lists command documentation.\n\nUsage: help\nLists all available commands.\n\nUsage: help <cmd>\n- cmd: The name of a command whose documentation to display.", this.HandleCommand);
- this.CommandManager.Add("SMAPI", "reload_i18n", "Reloads translation files for all mods.\n\nUsage: reload_i18n", this.HandleCommand);
+ this.GameInstance.CommandManager.Add("SMAPI", "help", "Lists command documentation.\n\nUsage: help\nLists all available commands.\n\nUsage: help <cmd>\n- cmd: The name of a command whose documentation to display.", this.HandleCommand);
+ this.GameInstance.CommandManager.Add("SMAPI", "reload_i18n", "Reloads translation files for all mods.\n\nUsage: reload_i18n", this.HandleCommand);
// start handling command line input
Thread inputThread = new Thread(() =>
@@ -443,16 +490,9 @@ namespace StardewModdingAPI
if (string.IsNullOrWhiteSpace(input))
continue;
- // parse input
- try
- {
- if (!this.CommandManager.Trigger(input))
- this.Monitor.Log("Unknown command; type 'help' for a list of available commands.", LogLevel.Error);
- }
- catch (Exception ex)
- {
- this.Monitor.Log($"The handler registered for that command failed:\n{ex.GetLogSummary()}", LogLevel.Error);
- }
+ // handle command
+ this.Monitor.LogUserInput(input);
+ this.GameInstance.CommandQueue.Enqueue(input);
}
});
inputThread.Start();
@@ -529,22 +569,36 @@ namespace StardewModdingAPI
new Thread(() =>
{
// create client
- WebApiClient client = new WebApiClient(this.Settings.WebApiBaseUrl, Constants.ApiVersion);
+ string url = this.Settings.WebApiBaseUrl;
+#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.ApiVersion);
this.Monitor.Log("Checking for updates...", LogLevel.Trace);
// check SMAPI version
+ ISemanticVersion updateFound = null;
try
{
- ModInfoModel response = client.GetModInfo($"GitHub:{this.Settings.GitHubProjectName}").Single().Value;
- if (response.Error != null)
+ ModEntryModel response = client.GetModInfo(new[] { new ModSearchEntryModel("Pathoschild.SMAPI", new[] { $"GitHub:{this.Settings.GitHubProjectName}" }) }).Single().Value;
+ ISemanticVersion latestStable = response.Main?.Version;
+ ISemanticVersion latestBeta = response.Optional?.Version;
+
+ 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))
+ {
+ updateFound = latestBeta;
+ this.Monitor.Log($"You can update SMAPI to {latestBeta}: {Constants.HomePageUrl}", LogLevel.Alert);
+ }
+ else if (this.IsValidUpdate(Constants.ApiVersion, latestStable, this.Settings.UseBetaChannel))
+ {
+ updateFound = latestStable;
+ this.Monitor.Log($"You can update SMAPI to {latestStable}: {Constants.HomePageUrl}", LogLevel.Alert);
}
- else if (this.IsValidUpdate(Constants.ApiVersion, new SemanticVersion(response.Version)))
- this.Monitor.Log($"You can update SMAPI to {response.Version}: {Constants.HomePageUrl}", LogLevel.Alert);
- else if (response.PreviewVersion != null && this.IsValidUpdate(Constants.ApiVersion, new SemanticVersion(response.PreviewVersion)))
- this.Monitor.Log($"You can update SMAPI to {response.PreviewVersion}: {Constants.HomePageUrl}", LogLevel.Alert);
else
this.Monitor.Log(" SMAPI okay.", LogLevel.Trace);
}
@@ -557,94 +611,85 @@ namespace StardewModdingAPI
);
}
+ // show update message on next launch
+ if (updateFound != null)
+ File.WriteAllText(Constants.UpdateMarker, updateFound.ToString());
+
// check mod versions
if (mods.Any())
{
try
{
- // prepare update keys
- Dictionary<string, IModMetadata[]> modsByKey =
- (
- from mod in mods
- where mod.Manifest?.UpdateKeys != null
- 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
- );
-
- // report update keys
+ HashSet<string> suppressUpdateChecks = new HashSet<string>(this.Settings.SuppressUpdateChecks, StringComparer.InvariantCultureIgnoreCase);
+
+ // prepare search model
+ List<ModSearchEntryModel> searchMods = new List<ModSearchEntryModel>();
+ foreach (IModMetadata mod in mods)
{
- IModMetadata[] modsWithoutKeys = (
- from mod in mods
- where
- mod.Manifest != null
- && (mod.Manifest.UpdateKeys == null || !mod.Manifest.UpdateKeys.Any())
- && (mod.Manifest?.UniqueID != "SMAPI.ConsoleCommands" && mod.Manifest?.UniqueID != "SMAPI.TrainerMod")
- orderby mod.DisplayName
- select mod
- ).ToArray();
-
- string message = $"Checking {modsByKey.Count} mod update keys.";
- if (modsWithoutKeys.Any())
- message += $" {modsWithoutKeys.Length} mods have no update keys: {string.Join(", ", modsWithoutKeys.Select(p => p.DisplayName))}.";
- this.Monitor.Log($" {message}", LogLevel.Trace);
+ if (!mod.HasID())
+ continue;
+
+ string[] updateKeys = mod.Manifest.UpdateKeys ?? new string[0];
+ searchMods.Add(new ModSearchEntryModel(mod.Manifest.UniqueID, updateKeys.Except(suppressUpdateChecks).ToArray()));
}
// fetch results
- 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<IModMetadata, ModInfoModel> updatesByMod = new Dictionary<IModMetadata, ModInfoModel>();
- foreach (var result in results)
- {
- IModMetadata mod = result.Mod;
- ModInfoModel remoteInfo = result.Info;
+ this.Monitor.Log($" Checking for updates to {searchMods.Count} mods...", LogLevel.Trace);
+ IDictionary<string, ModEntryModel> results = client.GetModInfo(searchMods.ToArray());
- // handle error
- if (remoteInfo.Error != null)
- {
- this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: {remoteInfo.Error}", LogLevel.Trace);
+ // extract update alerts & errors
+ var updates = new List<Tuple<IModMetadata, ISemanticVersion, string>>();
+ var errors = new StringBuilder();
+ foreach (IModMetadata mod in mods.OrderBy(p => p.DisplayName))
+ {
+ // link to update-check data
+ if (!mod.HasID() || !results.TryGetValue(mod.Manifest.UniqueID, out ModEntryModel result))
continue;
- }
+ mod.SetUpdateData(result);
- // normalise versions
- ISemanticVersion localVersion = mod.DataRecord?.GetLocalVersionForUpdateChecks(mod.Manifest.Version) ?? mod.Manifest.Version;
- if (!SemanticVersion.TryParse(mod.DataRecord?.GetRemoteVersionForUpdateChecks(remoteInfo.Version) ?? remoteInfo.Version, out ISemanticVersion remoteVersion))
+ // handle errors
+ if (result.Errors != null && result.Errors.Any())
{
- this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: Mod has invalid version {remoteInfo.Version}", LogLevel.Trace);
- continue;
+ errors.AppendLine(result.Errors.Length == 1
+ ? $" {mod.DisplayName}: {result.Errors[0]}"
+ : $" {mod.DisplayName}:\n - {string.Join("\n - ", result.Errors)}"
+ );
}
- // compare versions
- bool isUpdate = remoteVersion.IsNewerThan(localVersion);
- this.VerboseLog($" {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version}{(!localVersion.Equals(mod.Manifest.Version) ? $" [{localVersion}]" : "")} => {remoteInfo.Version}" : "okay")}.");
- if (isUpdate)
- {
- if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || remoteVersion.IsNewerThan(other.Version))
- updatesByMod[mod] = remoteInfo;
- }
+ // parse versions
+ ISemanticVersion localVersion = mod.DataRecord?.GetLocalVersionForUpdateChecks(mod.Manifest.Version) ?? mod.Manifest.Version;
+ ISemanticVersion latestVersion = mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.Main?.Version) ?? result.Main?.Version;
+ ISemanticVersion optionalVersion = mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.Optional?.Version) ?? result.Optional?.Version;
+ ISemanticVersion unofficialVersion = result.Unofficial?.Version;
+
+ // show update alerts
+ if (this.IsValidUpdate(localVersion, latestVersion, useBetaChannel: true))
+ updates.Add(Tuple.Create(mod, latestVersion, result.Main?.Url));
+ else if (this.IsValidUpdate(localVersion, optionalVersion, useBetaChannel: localVersion.IsPrerelease()))
+ updates.Add(Tuple.Create(mod, optionalVersion, result.Optional?.Url));
+ else if (this.IsValidUpdate(localVersion, unofficialVersion, useBetaChannel: mod.Status == ModMetadataStatus.Failed))
+ updates.Add(Tuple.Create(mod, unofficialVersion, result.Unofficial?.Url));
}
- // output
- if (updatesByMod.Any())
+ // show update errors
+ if (errors.Length != 0)
+ this.Monitor.Log("Got update-check errors for some mods:\n" + errors.ToString().TrimEnd(), 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.Version}: {entry.Value.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);
}
catch (Exception ex)
{
@@ -661,22 +706,13 @@ namespace StardewModdingAPI
/// <summary>Get whether a given version should be offered to the user as an update.</summary>
/// <param name="currentVersion">The current semantic version.</param>
/// <param name="newVersion">The target semantic version.</param>
- private bool IsValidUpdate(ISemanticVersion currentVersion, ISemanticVersion newVersion)
+ /// <param name="useBetaChannel">Whether the user enabled the beta channel and should be offered pre-release updates.</param>
+ private bool IsValidUpdate(ISemanticVersion currentVersion, ISemanticVersion newVersion, bool useBetaChannel)
{
- // basic eligibility
- bool isNewer = newVersion.IsNewerThan(currentVersion);
- bool isPrerelease = newVersion.Build != null;
- bool isEquallyStable = !isPrerelease || currentVersion.Build != null; // don't update stable => prerelease
- if (!isNewer || !isEquallyStable)
- return false;
- if (!isPrerelease)
- return true;
-
- // prerelease eligible if same version (excluding prerelease tag)
return
- newVersion.MajorVersion == currentVersion.MajorVersion
- && newVersion.MinorVersion == currentVersion.MinorVersion
- && newVersion.PatchVersion == currentVersion.PatchVersion;
+ newVersion != null
+ && newVersion.IsNewerThan(currentVersion)
+ && (useBetaChannel || !newVersion.IsPrerelease());
}
/// <summary>Create a directory path if it doesn't exist.</summary>
@@ -699,32 +735,35 @@ namespace StardewModdingAPI
/// <param name="jsonHelper">The JSON helper with which to read mods' JSON files.</param>
/// <param name="contentCore">The content manager to use for mod content.</param>
/// <param name="modDatabase">Handles access to SMAPI's internal mod metadata list.</param>
- 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);
+ HashSet<string> suppressUpdateChecks = new HashSet<string>(this.Settings.SuppressUpdateChecks, StringComparer.InvariantCultureIgnoreCase);
IDictionary<IModMetadata, string[]> skippedMods = new Dictionary<IModMetadata, string[]>();
void TrackSkip(IModMetadata mod, string userReasonPhrase, string devReasonPhrase = null) => skippedMods[mod] = new[] { userReasonPhrase, devReasonPhrase };
// load content packs
foreach (IModMetadata metadata in mods.Where(p => p.IsContentPack))
{
- // get basic info
- IManifest manifest = metadata.Manifest;
- this.Monitor.Log($"Loading {metadata.DisplayName} from {PathUtilities.GetRelativePath(Constants.ModPath, metadata.DirectoryPath)} (content pack)...", LogLevel.Trace);
+ this.Monitor.Log($" {metadata.DisplayName} (content pack, {PathUtilities.GetRelativePath(Constants.ModPath, metadata.DirectoryPath)})...", LogLevel.Trace);
+
+ // show warning for missing update key
+ if (metadata.HasManifest() && !metadata.HasUpdateKeys())
+ metadata.SetWarning(ModWarning.NoUpdateKeys);
// validate status
if (metadata.Status == ModMetadataStatus.Failed)
{
- this.Monitor.Log($" Failed: {metadata.Error}", LogLevel.Trace);
+ this.Monitor.Log($" Failed: {metadata.Error}", LogLevel.Trace);
TrackSkip(metadata, metadata.Error);
continue;
}
// 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);
- 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);
@@ -743,100 +782,103 @@ namespace StardewModdingAPI
StringComparer.InvariantCultureIgnoreCase
);
- // get assembly loaders
- AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor, this.Settings.DeveloperMode);
- AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name);
- InterfaceProxyFactory proxyFactory = new InterfaceProxyFactory();
-
- // load from metadata
- foreach (IModMetadata metadata in mods.Where(p => !p.IsContentPack))
+ // load mods from metadata
+ using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor))
{
- // get basic info
- IManifest manifest = metadata.Manifest;
- this.Monitor.Log(metadata.Manifest?.EntryDll != null
- ? $"Loading {metadata.DisplayName} from {PathUtilities.GetRelativePath(Constants.ModPath, metadata.DirectoryPath)}{Path.DirectorySeparatorChar}{metadata.Manifest.EntryDll}..." // don't use Path.Combine here, since EntryDLL might not be valid
- : $"Loading {metadata.DisplayName}...", LogLevel.Trace);
-
- // validate status
- if (metadata.Status == ModMetadataStatus.Failed)
+ InterfaceProxyFactory proxyFactory = new InterfaceProxyFactory();
+ foreach (IModMetadata metadata in mods.Where(p => !p.IsContentPack))
{
- this.Monitor.Log($" Failed: {metadata.Error}", LogLevel.Trace);
- TrackSkip(metadata, metadata.Error);
- continue;
- }
-
- // load mod
- string assemblyPath = metadata.Manifest?.EntryDll != null
- ? Path.Combine(metadata.DirectoryPath, metadata.Manifest.EntryDll)
- : null;
- Assembly modAssembly;
- try
- {
- modAssembly = modAssemblyLoader.Load(metadata, assemblyPath, assumeCompatible: metadata.DataRecord?.Status == ModStatus.AssumeCompatible);
- }
- catch (IncompatibleInstructionException) // details already in trace logs
- {
- string[] updateUrls = new[] { modDatabase.GetModPageUrlFor(metadata.Manifest.UniqueID), "https://smapi.io/compat" }.Where(p => p != null).ToArray();
+ // get basic info
+ IManifest manifest = metadata.Manifest;
+ this.Monitor.Log(metadata.Manifest?.EntryDll != null
+ ? $" {metadata.DisplayName} ({PathUtilities.GetRelativePath(Constants.ModPath, metadata.DirectoryPath)}{Path.DirectorySeparatorChar}{metadata.Manifest.EntryDll})..." // don't use Path.Combine here, since EntryDLL might not be valid
+ : $" {metadata.DisplayName}...", LogLevel.Trace);
+
+ // show warnings
+ if (metadata.HasManifest() && !metadata.HasUpdateKeys() && !suppressUpdateChecks.Contains(metadata.Manifest.UniqueID))
+ metadata.SetWarning(ModWarning.NoUpdateKeys);
+
+ // validate status
+ if (metadata.Status == ModMetadataStatus.Failed)
+ {
+ this.Monitor.Log($" Failed: {metadata.Error}", LogLevel.Trace);
+ TrackSkip(metadata, metadata.Error);
+ continue;
+ }
- TrackSkip(metadata, $"it's outdated. Please check for a new version at {string.Join(" or ", updateUrls)}.");
- continue;
- }
- catch (SAssemblyLoadFailedException ex)
- {
- TrackSkip(metadata, $"it DLL couldn't be loaded: {ex.Message}");
- continue;
- }
- catch (Exception ex)
- {
- TrackSkip(metadata, "its DLL couldn't be loaded.", $"Error: {ex.GetLogSummary()}");
- continue;
- }
+ // load mod
+ string assemblyPath = metadata.Manifest?.EntryDll != null
+ ? Path.Combine(metadata.DirectoryPath, metadata.Manifest.EntryDll)
+ : null;
+ Assembly modAssembly;
+ try
+ {
+ modAssembly = modAssemblyLoader.Load(metadata, assemblyPath, assumeCompatible: metadata.DataRecord?.Status == ModStatus.AssumeCompatible);
+ }
+ catch (IncompatibleInstructionException) // details already in trace logs
+ {
+ string[] updateUrls = new[] { modDatabase.GetModPageUrlFor(metadata.Manifest.UniqueID), "https://smapi.io/compat" }.Where(p => p != null).ToArray();
- // initialise mod
- try
- {
- // get content packs
- if (!contentPacksByModID.TryGetValue(manifest.UniqueID, out IContentPack[] contentPacks))
- contentPacks = new IContentPack[0];
+ TrackSkip(metadata, $"it's no longer compatible. Please check for a new version at {string.Join(" or ", updateUrls)}.");
+ continue;
+ }
+ catch (SAssemblyLoadFailedException ex)
+ {
+ TrackSkip(metadata, $"it DLL couldn't be loaded: {ex.Message}");
+ continue;
+ }
+ catch (Exception ex)
+ {
+ TrackSkip(metadata, "its DLL couldn't be loaded.", $"Error: {ex.GetLogSummary()}");
+ continue;
+ }
- // init mod helpers
- IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName);
- IModHelper modHelper;
+ // initialise mod
+ try
{
- ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager);
- ContentManagerShim contentManager = this.ContentCore.CreateContentManager($"Mods.{metadata.Manifest.UniqueID}", 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);
- ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentCore.GetLocale(), contentCore.Language);
-
- IContentPack CreateTransitionalContentPack(string packDirPath, IManifest packManifest)
- {
- IMonitor packMonitor = this.GetSecondaryMonitor(packManifest.Name);
- ContentManagerShim packContentManager = this.ContentCore.CreateContentManager($"Mods.{packManifest.UniqueID}", packDirPath);
- IContentHelper packContentHelper = new ContentHelper(contentCore, packContentManager, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor);
- return new ContentPack(packDirPath, packManifest, packContentHelper, this.JsonHelper);
- }
+ // get mod instance
+ if (!this.TryLoadModEntry(modAssembly, error => TrackSkip(metadata, error), out Mod mod))
+ continue;
- modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager);
- }
+ // get content packs
+ if (!contentPacksByModID.TryGetValue(manifest.UniqueID, out IContentPack[] contentPacks))
+ contentPacks = new IContentPack[0];
- // get mod instance
- if (!this.TryLoadModEntry(modAssembly, error => TrackSkip(metadata, error), out Mod mod))
- continue;
+ // init mod helpers
+ IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName);
+ IModHelper modHelper;
+ {
+ IModEvents events = new ModEvents(metadata, this.EventManager);
+ ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.GameInstance.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);
+ 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)
+ {
+ IMonitor packMonitor = this.GetSecondaryMonitor(packManifest.Name);
+ IContentHelper packContentHelper = new ContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor);
+ return new ContentPack(packDirPath, packManifest, packContentHelper, this.Toolkit.JsonHelper);
+ }
+
+ modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, this.GameInstance.Input, events, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager);
+ }
- // init mod
- mod.ModManifest = manifest;
- mod.Helper = modHelper;
- mod.Monitor = monitor;
+ // init mod
+ mod.ModManifest = manifest;
+ mod.Helper = modHelper;
+ mod.Monitor = monitor;
- // track mod
- metadata.SetMod(mod);
- this.ModRegistry.Add(metadata);
- }
- catch (Exception ex)
- {
- TrackSkip(metadata, $"initialisation failed:\n{ex.GetLogSummary()}");
+ // track mod
+ metadata.SetMod(mod);
+ this.ModRegistry.Add(metadata);
+ }
+ catch (Exception ex)
+ {
+ TrackSkip(metadata, $"initialisation failed:\n{ex.GetLogSummary()}");
+ }
}
}
}
@@ -894,6 +936,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);
@@ -983,6 +1047,25 @@ namespace StardewModdingAPI
this.ModRegistry.AreAllModsInitialised = true;
}
+ /// <summary>Get the warning text for a mod warning bit mask.</summary>
+ /// <param name="mask">The mod warning bit mask.</param>
+ private IEnumerable<string> 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.";
+
+ }
+
/// <summary>Load a mod's entry class.</summary>
/// <param name="modAssembly">The mod assembly.</param>
/// <param name="onError">A callback invoked when loading fails.</param>
@@ -1019,7 +1102,7 @@ namespace StardewModdingAPI
/// <param name="mods">The mods for which to reload translations.</param>
private void ReloadTranslations(IEnumerable<IModMetadata> mods)
{
- JsonHelper jsonHelper = new JsonHelper();
+ JsonHelper jsonHelper = this.Toolkit.JsonHelper;
foreach (IModMetadata metadata in mods)
{
if (metadata.IsContentPack)
@@ -1045,8 +1128,17 @@ namespace StardewModdingAPI
}
// validate translations
- foreach (string locale in translations.Keys)
+ foreach (string locale in translations.Keys.ToArray())
{
+ // skip empty files
+ if (translations[locale] == null || !translations[locale].Keys.Any())
+ {
+ metadata.LogAsMod($"Mod's i18n/{locale}.json is empty and will be ignored.", LogLevel.Warn);
+ translations.Remove(locale);
+ continue;
+ }
+
+ // handle duplicates
HashSet<string> keys = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
HashSet<string> duplicateKeys = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
foreach (string key in translations[locale].Keys.ToArray())
@@ -1057,7 +1149,6 @@ namespace StardewModdingAPI
translations[locale].Remove(key);
}
}
-
if (duplicateKeys.Any())
metadata.LogAsMod($"Mod's i18n/{locale}.json has duplicate translation keys: [{string.Join(", ", duplicateKeys)}]. Keys are case-insensitive.", LogLevel.Warn);
}
@@ -1078,7 +1169,7 @@ namespace StardewModdingAPI
case "help":
if (arguments.Any())
{
- Command result = this.CommandManager.Get(arguments[0]);
+ Command result = this.GameInstance.CommandManager.Get(arguments[0]);
if (result == null)
this.Monitor.Log("There's no command with that name.", LogLevel.Error);
else
@@ -1087,7 +1178,7 @@ namespace StardewModdingAPI
else
{
string message = "The following commands are registered:\n";
- IGrouping<string, string>[] groups = (from command in this.CommandManager.GetAll() orderby command.ModName, command.Name group command.Name by command.ModName).ToArray();
+ IGrouping<string, string>[] groups = (from command in this.GameInstance.CommandManager.GetAll() orderby command.ModName, command.Name group command.Name by command.ModName).ToArray();
foreach (var group in groups)
{
string modName = group.Key;
@@ -1148,7 +1239,7 @@ namespace StardewModdingAPI
/// <param name="name">The name of the module which will log messages with this instance.</param>
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,
@@ -1156,24 +1247,6 @@ namespace StardewModdingAPI
};
}
- /// <summary>Get a human-readable name for the current platform.</summary>
- [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<ManagementObject>()
- .Select(entry => entry.GetPropertyValue("Caption").ToString())
- .FirstOrDefault();
- }
- catch { }
-#endif
- return Environment.OSVersion.ToString();
- }
-
/// <summary>Log a message if verbose mode is enabled.</summary>
/// <param name="message">The message to log.</param>
private void VerboseLog(string message)
@@ -1181,5 +1254,50 @@ namespace StardewModdingAPI
if (this.Settings.VerboseLogging)
this.Monitor.Log(message, LogLevel.Trace);
}
+
+ /// <summary>Get the absolute path to the next available log file.</summary>
+ private string GetLogPath()
+ {
+ // default path
+ {
+ FileInfo defaultFile = new FileInfo(Path.Combine(Constants.LogDir, $"{Constants.LogNamePrefix}.{Constants.LogNameExtension}"));
+ if (!defaultFile.Exists)
+ return defaultFile.FullName;
+ }
+
+ // get first disambiguated path
+ for (int i = 2; i < int.MaxValue; i++)
+ {
+ FileInfo file = new FileInfo(Path.Combine(Constants.LogDir, $"{Constants.LogNamePrefix}.player-{i}.{Constants.LogNameExtension}"));
+ if (!file.Exists)
+ return file.FullName;
+ }
+
+ // should never happen
+ throw new InvalidOperationException("Could not find an available log path.");
+ }
+
+ /// <summary>Delete all log files created by SMAPI.</summary>
+ private void PurgeLogFiles()
+ {
+ DirectoryInfo logsDir = new DirectoryInfo(Constants.LogDir);
+ if (!logsDir.Exists)
+ return;
+
+ foreach (FileInfo logFile in logsDir.EnumerateFiles())
+ {
+ if (logFile.Name.StartsWith(Constants.LogNamePrefix, StringComparison.InvariantCultureIgnoreCase))
+ {
+ try
+ {
+ FileUtilities.ForceDelete(logFile);
+ }
+ catch (IOException)
+ {
+ // ignore file if it's in use
+ }
+ }
+ }
+ }
}
}
diff --git a/src/SMAPI/SemanticVersion.cs b/src/SMAPI/SemanticVersion.cs
index 4826c947..587ff286 100644
--- a/src/SMAPI/SemanticVersion.cs
+++ b/src/SMAPI/SemanticVersion.cs
@@ -1,6 +1,5 @@
using System;
using Newtonsoft.Json;
-using StardewModdingAPI.Common;
namespace StardewModdingAPI
{
@@ -11,23 +10,23 @@ namespace StardewModdingAPI
** Properties
*********/
/// <summary>The underlying semantic version implementation.</summary>
- private readonly SemanticVersionImpl Version;
+ private readonly ISemanticVersion Version;
/*********
** Accessors
*********/
/// <summary>The major version incremented for major API changes.</summary>
- public int MajorVersion => this.Version.Major;
+ public int MajorVersion => this.Version.MajorVersion;
/// <summary>The minor version incremented for backwards-compatible changes.</summary>
- public int MinorVersion => this.Version.Minor;
+ public int MinorVersion => this.Version.MinorVersion;
/// <summary>The patch version for backwards-compatible bug fixes.</summary>
- public int PatchVersion => this.Version.Patch;
+ public int PatchVersion => this.Version.PatchVersion;
/// <summary>An optional build tag.</summary>
- public string Build => this.Version.Tag;
+ public string Build => this.Version.Build;
/*********
@@ -40,20 +39,33 @@ namespace StardewModdingAPI
/// <param name="build">An optional build tag.</param>
[JsonConstructor]
public SemanticVersion(int majorVersion, int minorVersion, int patchVersion, string build = null)
- : this(new SemanticVersionImpl(majorVersion, minorVersion, patchVersion, build)) { }
+ : this(new Toolkit.SemanticVersion(majorVersion, minorVersion, patchVersion, build)) { }
/// <summary>Construct an instance.</summary>
/// <param name="version">The semantic version string.</param>
/// <exception cref="ArgumentNullException">The <paramref name="version"/> is null.</exception>
/// <exception cref="FormatException">The <paramref name="version"/> is not a valid semantic version.</exception>
public SemanticVersion(string version)
- : this(new SemanticVersionImpl(version)) { }
+ : this(new Toolkit.SemanticVersion(version)) { }
/// <summary>Construct an instance.</summary>
/// <param name="version">The assembly version.</param>
/// <exception cref="ArgumentNullException">The <paramref name="version"/> is null.</exception>
public SemanticVersion(Version version)
- : this(new SemanticVersionImpl(version)) { }
+ : this(new Toolkit.SemanticVersion(version)) { }
+
+ /// <summary>Construct an instance.</summary>
+ /// <param name="version">The underlying semantic version implementation.</param>
+ internal SemanticVersion(ISemanticVersion version)
+ {
+ this.Version = version;
+ }
+
+ /// <summary>Whether this is a pre-release version.</summary>
+ public bool IsPrerelease()
+ {
+ return this.Version.IsPrerelease();
+ }
/// <summary>Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version.</summary>
/// <param name="other">The version to compare with this instance.</param>
@@ -61,14 +73,14 @@ namespace StardewModdingAPI
/// <remarks>The implementation is defined by Semantic Version 2.0 (http://semver.org/).</remarks>
public int CompareTo(ISemanticVersion other)
{
- return this.Version.CompareTo(other.MajorVersion, other.MinorVersion, other.PatchVersion, other.Build);
+ return this.Version.CompareTo(other);
}
/// <summary>Get whether this version is older than the specified version.</summary>
/// <param name="other">The version to compare with this instance.</param>
public bool IsOlderThan(ISemanticVersion other)
{
- return this.CompareTo(other) < 0;
+ return this.Version.IsOlderThan(other);
}
/// <summary>Get whether this version is older than the specified version.</summary>
@@ -76,14 +88,14 @@ namespace StardewModdingAPI
/// <exception cref="FormatException">The specified version is not a valid semantic version.</exception>
public bool IsOlderThan(string other)
{
- return this.IsOlderThan(new SemanticVersion(other));
+ return this.Version.IsOlderThan(other);
}
/// <summary>Get whether this version is newer than the specified version.</summary>
/// <param name="other">The version to compare with this instance.</param>
public bool IsNewerThan(ISemanticVersion other)
{
- return this.CompareTo(other) > 0;
+ return this.Version.IsNewerThan(other);
}
/// <summary>Get whether this version is newer than the specified version.</summary>
@@ -91,7 +103,7 @@ namespace StardewModdingAPI
/// <exception cref="FormatException">The specified version is not a valid semantic version.</exception>
public bool IsNewerThan(string other)
{
- return this.IsNewerThan(new SemanticVersion(other));
+ return this.Version.IsNewerThan(other);
}
/// <summary>Get whether this version is between two specified versions (inclusively).</summary>
@@ -99,7 +111,7 @@ namespace StardewModdingAPI
/// <param name="max">The maximum version.</param>
public bool IsBetween(ISemanticVersion min, ISemanticVersion max)
{
- return this.CompareTo(min) >= 0 && this.CompareTo(max) <= 0;
+ return this.Version.IsBetween(min, max);
}
/// <summary>Get whether this version is between two specified versions (inclusively).</summary>
@@ -108,7 +120,7 @@ namespace StardewModdingAPI
/// <exception cref="FormatException">One of the specified versions is not a valid semantic version.</exception>
public bool IsBetween(string min, string max)
{
- return this.IsBetween(new SemanticVersion(min), new SemanticVersion(max));
+ return this.Version.IsBetween(min, max);
}
/// <summary>Indicates whether the current object is equal to another object of the same type.</summary>
@@ -131,7 +143,7 @@ namespace StardewModdingAPI
/// <returns>Returns whether parsing the version succeeded.</returns>
internal static bool TryParse(string version, out ISemanticVersion parsed)
{
- if (SemanticVersionImpl.TryParse(version, out SemanticVersionImpl versionImpl))
+ if (Toolkit.SemanticVersion.TryParse(version, out ISemanticVersion versionImpl))
{
parsed = new SemanticVersion(versionImpl);
return true;
@@ -140,17 +152,5 @@ namespace StardewModdingAPI
parsed = null;
return false;
}
-
-
- /*********
- ** Private methods
- *********/
- /// <summary>Construct an instance.</summary>
- /// <param name="version">The underlying semantic version implementation.</param>
- private SemanticVersion(SemanticVersionImpl version)
- {
- this.Version = version;
- }
-
}
}
diff --git a/src/SMAPI/StardewModdingAPI.config.json b/src/SMAPI/StardewModdingAPI.config.json
index f7082e96..115997ba 100644
--- a/src/SMAPI/StardewModdingAPI.config.json
+++ b/src/SMAPI/StardewModdingAPI.config.json
@@ -22,6 +22,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha
"CheckForUpdates": true,
/**
+ * Whether SMAPI should show newer beta versions as an available update. If not specified, SMAPI
+ * will only show beta updates if the current version is beta.
+ */
+ //"UseBetaChannel": true,
+
+ /**
* SMAPI's GitHub project name, used to perform update checks.
*/
"GitHubProjectName": "Pathoschild/SMAPI",
@@ -39,1837 +45,24 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha
"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.
+ * 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.
*/
- "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
- },
+ "DumpMetadata": false,
- "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
- },
+ /**
+ * 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.
+ * - 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",
- "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
- }
- }
+ /**
+ * The mod IDs SMAPI should ignore when performing update checks or validating update keys.
+ */
+ "SuppressUpdateChecks": [
+ "SMAPI.ConsoleCommands",
+ "SMAPI.SaveBackup"
+ ]
}
diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj
index edddbd2a..0d0a5fe9 100644
--- a/src/SMAPI/StardewModdingAPI.csproj
+++ b/src/SMAPI/StardewModdingAPI.csproj
@@ -27,6 +27,7 @@
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
<UseApplicationTrust>false</UseApplicationTrust>
<BootstrapperEnabled>true</BootstrapperEnabled>
+ <LargeAddressAware Condition="'$(OS)' == 'Windows_NT'">true</LargeAddressAware>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<PlatformTarget>x86</PlatformTarget>
@@ -42,7 +43,7 @@
<PlatformTarget>x86</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
<OutputPath>$(SolutionDir)\..\bin\Release\SMAPI</OutputPath>
- <DocumentationFile>$(SolutionDir)\..\bin\Debug\SMAPI\StardewModdingAPI.xml</DocumentationFile>
+ <DocumentationFile>$(SolutionDir)\..\bin\Release\SMAPI\StardewModdingAPI.xml</DocumentationFile>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
@@ -53,16 +54,20 @@
<ApplicationIcon>icon.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
- <Reference Include="Mono.Cecil, Version=0.9.6.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL">
- <HintPath>..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll</HintPath>
+ <Reference Include="0Harmony, Version=1.0.9.1, Culture=neutral, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\lib\0Harmony.dll</HintPath>
+ </Reference>
+ <Reference Include="Mono.Cecil, Version=0.10.0.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
+ <HintPath>..\packages\Mono.Cecil.0.10.0\lib\net40\Mono.Cecil.dll</HintPath>
<Private>True</Private>
</Reference>
- <Reference Include="Mono.Cecil.Mdb, Version=0.9.6.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL">
- <HintPath>..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Mdb.dll</HintPath>
+ <Reference Include="Mono.Cecil.Mdb, Version=0.10.0.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
+ <HintPath>..\packages\Mono.Cecil.0.10.0\lib\net40\Mono.Cecil.Mdb.dll</HintPath>
<Private>True</Private>
</Reference>
- <Reference Include="Mono.Cecil.Pdb, Version=0.9.6.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL">
- <HintPath>..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Pdb.dll</HintPath>
+ <Reference Include="Mono.Cecil.Pdb, Version=0.10.0.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
+ <HintPath>..\packages\Mono.Cecil.0.10.0\lib\net40\Mono.Cecil.Pdb.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Newtonsoft.Json, Version=11.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
@@ -72,7 +77,6 @@
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
- <Reference Include="System.Management" Condition="$(OS) == 'Windows_NT'" />
<Reference Include="System.Numerics">
<Private>True</Private>
</Reference>
@@ -86,20 +90,55 @@
<Compile Include="..\..\build\GlobalAssemblyInfo.cs">
<Link>Properties\GlobalAssemblyInfo.cs</Link>
</Compile>
+ <Compile Include="Events\GameLoopUpdatedEventArgs.cs" />
+ <Compile Include="Events\GameLoopLaunchedEventArgs.cs" />
+ <Compile Include="Events\InputMouseWheelScrolledEventArgs.cs" />
+ <Compile Include="Events\InputCursorMovedEventArgs.cs" />
+ <Compile Include="Events\InputButtonReleasedEventArgs.cs" />
+ <Compile Include="Events\InputButtonPressedEventArgs.cs" />
+ <Compile Include="Events\EventArgsLocationBuildingsChanged.cs" />
+ <Compile Include="Events\IInputEvents.cs" />
+ <Compile Include="Events\IGameLoopEvents.cs" />
+ <Compile Include="Events\IWorldEvents.cs" />
+ <Compile Include="Events\MultiplayerEvents.cs" />
+ <Compile Include="Events\WorldDebrisListChangedEventArgs.cs" />
+ <Compile Include="Events\GameLoopUpdatingEventArgs.cs" />
+ <Compile Include="Events\WorldNpcListChangedEventArgs.cs" />
+ <Compile Include="Events\WorldLargeTerrainFeatureListChangedEventArgs.cs" />
+ <Compile Include="Events\WorldTerrainFeatureListChangedEventArgs.cs" />
+ <Compile Include="Events\WorldBuildingListChangedEventArgs.cs" />
+ <Compile Include="Events\WorldLocationListChangedEventArgs.cs" />
+ <Compile Include="Events\WorldObjectListChangedEventArgs.cs" />
+ <Compile Include="Framework\ContentManagers\BaseContentManager.cs" />
+ <Compile Include="Framework\ContentManagers\GameContentManager.cs" />
+ <Compile Include="Framework\ContentManagers\IContentManager.cs" />
+ <Compile Include="Framework\ContentManagers\ModContentManager.cs" />
+ <Compile Include="Framework\Models\ModFolderExport.cs" />
+ <Compile Include="Framework\ModLoading\TypeReferenceComparer.cs" />
+ <Compile Include="Framework\Patching\GamePatcher.cs" />
+ <Compile Include="Framework\Patching\IHarmonyPatch.cs" />
+ <Compile Include="Framework\Serialisation\ColorConverter.cs" />
+ <Compile Include="Framework\Serialisation\PointConverter.cs" />
+ <Compile Include="Framework\Serialisation\RectangleConverter.cs" />
+ <Compile Include="Framework\Events\ModEventsBase.cs" />
<Compile Include="Framework\Events\EventManager.cs" />
+ <Compile Include="Events\IModEvents.cs" />
<Compile Include="Framework\Events\ManagedEvent.cs" />
<Compile Include="Events\SpecialisedEvents.cs" />
<Compile Include="Framework\ContentPack.cs" />
<Compile Include="Framework\Content\ContentCache.cs" />
<Compile Include="Framework\Events\ManagedEventBase.cs" />
- <Compile Include="Framework\Input\InputState.cs" />
+ <Compile Include="Framework\Events\ModEvents.cs" />
+ <Compile Include="Framework\Events\ModGameLoopEvents.cs" />
+ <Compile Include="Framework\Events\ModInputEvents.cs" />
+ <Compile Include="Framework\Input\GamePadStateBuilder.cs" />
+ <Compile Include="Framework\ModHelpers\InputHelper.cs" />
+ <Compile Include="Framework\StateTracking\Comparers\GenericEqualsComparer.cs" />
+ <Compile Include="Framework\WatcherCore.cs" />
+ <Compile Include="IInputHelper.cs" />
+ <Compile Include="Framework\Input\SInputState.cs" />
<Compile Include="Framework\Input\InputStatus.cs" />
- <Compile Include="Framework\LegacyManifestVersion.cs" />
- <Compile Include="Framework\ModData\ModDatabase.cs" />
- <Compile Include="Framework\ModData\ModDataField.cs" />
- <Compile Include="Framework\ModData\ModDataFieldKey.cs" />
- <Compile Include="Framework\ModData\ParsedModDataRecord.cs" />
- <Compile Include="Framework\Models\ManifestContentPackFor.cs" />
+ <Compile Include="Framework\ModHelpers\MultiplayerHelper.cs" />
<Compile Include="Framework\ModLoading\Finders\EventFinder.cs" />
<Compile Include="Framework\ModLoading\Finders\FieldFinder.cs" />
<Compile Include="Framework\ModLoading\Finders\MethodFinder.cs" />
@@ -109,7 +148,7 @@
<Compile Include="Framework\ModLoading\IInstructionHandler.cs" />
<Compile Include="Framework\ModLoading\IncompatibleInstructionException.cs" />
<Compile Include="Framework\ModLoading\InstructionHandleResult.cs" />
- <Compile Include="Framework\ModLoading\Platform.cs" />
+ <Compile Include="Framework\ModLoading\ModWarning.cs" />
<Compile Include="Framework\ModLoading\PlatformAssemblyMap.cs" />
<Compile Include="Framework\ModLoading\RewriteHelper.cs" />
<Compile Include="Framework\ModLoading\Rewriters\FieldReplaceRewriter.cs" />
@@ -119,22 +158,32 @@
<Compile Include="Framework\ModLoading\Rewriters\VirtualEntryCallRemover.cs" />
<Compile Include="Framework\ModLoading\Rewriters\MethodParentRewriter.cs" />
<Compile Include="Framework\ModLoading\Rewriters\TypeReferenceRewriter.cs" />
- <Compile Include="Framework\ContentManagerShim.cs" />
<Compile Include="Framework\Exceptions\SAssemblyLoadFailedException.cs" />
<Compile Include="Framework\ModLoading\AssemblyLoadStatus.cs" />
+ <Compile Include="Framework\Events\ModWorldEvents.cs" />
<Compile Include="Framework\Reflection\InterfaceProxyBuilder.cs" />
<Compile Include="Framework\Reflection\InterfaceProxyFactory.cs" />
- <Compile Include="Framework\Serialisation\SmapiConverters\ManifestContentPackForConverter.cs" />
- <Compile Include="Framework\Serialisation\SmapiConverters\ManifestDependencyArrayConverter.cs" />
- <Compile Include="Framework\Serialisation\SmapiConverters\SemanticVersionConverter.cs" />
- <Compile Include="Framework\Serialisation\SimpleReadOnlyConverter.cs" />
- <Compile Include="Framework\Serialisation\CrossplatformConverters\RectangleConverter.cs" />
- <Compile Include="Framework\Serialisation\CrossplatformConverters\ColorConverter.cs" />
- <Compile Include="Framework\Serialisation\CrossplatformConverters\PointConverter.cs" />
+ <Compile Include="Framework\RewriteFacades\SpriteBatchMethods.cs" />
+ <Compile Include="Framework\SMultiplayer.cs" />
+ <Compile Include="Framework\StateTracking\Comparers\EquatableComparer.cs" />
+ <Compile Include="Framework\StateTracking\Comparers\ObjectReferenceComparer.cs" />
+ <Compile Include="Framework\StateTracking\FieldWatchers\BaseDisposableWatcher.cs" />
+ <Compile Include="Framework\StateTracking\FieldWatchers\ComparableWatcher.cs" />
+ <Compile Include="Framework\StateTracking\FieldWatchers\NetDictionaryWatcher.cs" />
+ <Compile Include="Framework\StateTracking\FieldWatchers\NetValueWatcher.cs" />
+ <Compile Include="Framework\StateTracking\FieldWatchers\NetCollectionWatcher.cs" />
+ <Compile Include="Framework\StateTracking\FieldWatchers\ObservableCollectionWatcher.cs" />
+ <Compile Include="Framework\StateTracking\FieldWatchers\WatcherFactory.cs" />
+ <Compile Include="Framework\StateTracking\ICollectionWatcher.cs" />
+ <Compile Include="Framework\StateTracking\IDictionaryWatcher.cs" />
+ <Compile Include="Framework\StateTracking\IValueWatcher.cs" />
+ <Compile Include="Framework\StateTracking\IWatcher.cs" />
+ <Compile Include="Framework\StateTracking\WorldLocationsTracker.cs" />
+ <Compile Include="Framework\StateTracking\LocationTracker.cs" />
+ <Compile Include="Framework\StateTracking\PlayerTracker.cs" />
<Compile Include="Framework\Utilities\ContextHash.cs" />
- <Compile Include="Framework\Utilities\PathUtilities.cs" />
<Compile Include="IContentPack.cs" />
- <Compile Include="IManifestContentPackFor.cs" />
+ <Compile Include="IMultiplayerHelper.cs" />
<Compile Include="IReflectedField.cs" />
<Compile Include="IReflectedMethod.cs" />
<Compile Include="IReflectedProperty.cs" />
@@ -155,8 +204,8 @@
<Compile Include="Events\EventArgsControllerButtonReleased.cs" />
<Compile Include="Events\EventArgsControllerTriggerPressed.cs" />
<Compile Include="Events\EventArgsControllerTriggerReleased.cs" />
- <Compile Include="Events\EventArgsCurrentLocationChanged.cs" />
- <Compile Include="Events\EventArgsGameLocationsChanged.cs" />
+ <Compile Include="Events\EventArgsPlayerWarped.cs" />
+ <Compile Include="Events\EventArgsLocationsChanged.cs" />
<Compile Include="Events\EventArgsIntChanged.cs" />
<Compile Include="Events\EventArgsInventoryChanged.cs" />
<Compile Include="Events\EventArgsKeyboardStateChanged.cs" />
@@ -191,16 +240,11 @@
<Compile Include="Context.cs" />
<Compile Include="Framework\Logging\ConsoleInterceptionManager.cs" />
<Compile Include="Framework\Logging\InterceptingTextWriter.cs" />
- <Compile Include="Framework\Models\ManifestDependency.cs" />
- <Compile Include="Framework\ModData\ModStatus.cs" />
<Compile Include="Framework\Models\SConfig.cs" />
<Compile Include="Framework\ModLoading\ModMetadata.cs" />
<Compile Include="Framework\Reflection\ReflectedProperty.cs" />
<Compile Include="Framework\RequestExitDelegate.cs" />
- <Compile Include="Framework\ContentCore.cs" />
- <Compile Include="Framework\Exceptions\SParseException.cs" />
- <Compile Include="Framework\Serialisation\JsonHelper.cs" />
- <Compile Include="Framework\Serialisation\SmapiConverters\StringEnumConverter.cs" />
+ <Compile Include="Framework\ContentCoordinator.cs" />
<Compile Include="IAssetEditor.cs" />
<Compile Include="IAssetInfo.cs" />
<Compile Include="IAssetLoader.cs" />
@@ -209,7 +253,6 @@
<Compile Include="IAssetDataForDictionary.cs" />
<Compile Include="IAssetDataForImage.cs" />
<Compile Include="IContentHelper.cs" />
- <Compile Include="IManifestDependency.cs" />
<Compile Include="IModRegistry.cs" />
<Compile Include="Events\LocationEvents.cs" />
<Compile Include="Events\MenuEvents.cs" />
@@ -220,36 +263,29 @@
<Compile Include="Framework\DeprecationLevel.cs" />
<Compile Include="Framework\DeprecationManager.cs" />
<Compile Include="Framework\InternalExtensions.cs" />
- <Compile Include="Framework\ModData\ModDataRecord.cs" />
<Compile Include="Framework\ModLoading\AssemblyLoader.cs" />
<Compile Include="Framework\Reflection\CacheEntry.cs" />
<Compile Include="Framework\Reflection\ReflectedField.cs" />
<Compile Include="Framework\Reflection\ReflectedMethod.cs" />
<Compile Include="Framework\Reflection\Reflector.cs" />
- <Compile Include="IManifest.cs" />
<Compile Include="IMod.cs" />
<Compile Include="IModHelper.cs" />
<Compile Include="IModLinked.cs" />
<Compile Include="Framework\Logging\LogFileManager.cs" />
- <Compile Include="IPrivateProperty.cs" />
- <Compile Include="ISemanticVersion.cs" />
<Compile Include="ITranslationHelper.cs" />
<Compile Include="LogLevel.cs" />
<Compile Include="Framework\ModRegistry.cs" />
- <Compile Include="Framework\WebApiClient.cs" />
<Compile Include="IMonitor.cs" />
<Compile Include="Events\ChangeType.cs" />
<Compile Include="Events\ItemStackChange.cs" />
<Compile Include="Framework\Monitor.cs" />
- <Compile Include="Framework\Models\Manifest.cs" />
<Compile Include="Metadata\InstructionMetadata.cs" />
<Compile Include="Mod.cs" />
<Compile Include="PatchMode.cs" />
+ <Compile Include="GamePlatform.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Framework\SGame.cs" />
- <Compile Include="IPrivateField.cs" />
- <Compile Include="IPrivateMethod.cs" />
<Compile Include="IReflectionHelper.cs" />
<Compile Include="SemanticVersion.cs" />
<Compile Include="Translation.cs" />
@@ -263,13 +299,17 @@
<SubType>Designer</SubType>
</None>
<Content Include="StardewModdingAPI.config.json">
- <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="..\SMAPI.Web\wwwroot\StardewModdingAPI.metadata.json">
+ <Link>StardewModdingAPI.metadata.json</Link>
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Content Include="icon.ico" />
<Content Include="steam_appid.txt">
- <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
@@ -284,13 +324,27 @@
<Install>false</Install>
</BootstrapperPackage>
</ItemGroup>
- <Import Project="..\SMAPI.Common\StardewModdingAPI.Common.projitems" Label="Shared" />
<ItemGroup>
- <ProjectReference Include="..\SMAPI.AssemblyRewriters\StardewModdingAPI.AssemblyRewriters.csproj">
- <Project>{10db0676-9fc1-4771-a2c8-e2519f091e49}</Project>
- <Name>StardewModdingAPI.AssemblyRewriters</Name>
+ <Analyzer Include="..\SMAPI.ModBuildConfig.Analyzer\bin\netstandard1.3\StardewModdingAPI.ModBuildConfig.Analyzer.dll" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\StardewModdingAPI.Toolkit.CoreInterfaces\StardewModdingAPI.Toolkit.CoreInterfaces.csproj">
+ <Project>{d5cfd923-37f1-4bc3-9be8-e506e202ac28}</Project>
+ <Name>StardewModdingAPI.Toolkit.CoreInterfaces</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\StardewModdingAPI.Toolkit\StardewModdingAPI.Toolkit.csproj">
+ <Project>{ea5cfd2e-9453-4d29-b80f-8e0ea23f4ac6}</Project>
+ <Name>StardewModdingAPI.Toolkit</Name>
</ProjectReference>
</ItemGroup>
+ <Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\..\build\common.targets" />
+ <Import Project="..\packages\LargeAddressAware.1.0.3\build\LargeAddressAware.targets" Condition="Exists('..\packages\LargeAddressAware.1.0.3\build\LargeAddressAware.targets')" />
+ <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
+ <PropertyGroup>
+ <ErrorText>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}.</ErrorText>
+ </PropertyGroup>
+ <Error Condition="!Exists('..\packages\LargeAddressAware.1.0.3\build\LargeAddressAware.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\LargeAddressAware.1.0.3\build\LargeAddressAware.targets'))" />
+ </Target>
</Project> \ No newline at end of file
diff --git a/src/SMAPI/packages.config b/src/SMAPI/packages.config
index 3e876922..84c6bed0 100644
--- a/src/SMAPI/packages.config
+++ b/src/SMAPI/packages.config
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
- <package id="Mono.Cecil" version="0.9.6.4" targetFramework="net45" />
+ <package id="LargeAddressAware" version="1.0.3" targetFramework="net45" />
+ <package id="Mono.Cecil" version="0.10.0" targetFramework="net45" />
<package id="Newtonsoft.Json" version="11.0.2" targetFramework="net45" />
</packages> \ No newline at end of file