summaryrefslogtreecommitdiff
path: root/src/StardewModdingAPI
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <github@jplamondonw.com>2017-05-19 18:04:57 -0400
committerJesse Plamondon-Willard <github@jplamondonw.com>2017-05-19 18:04:57 -0400
commit16281fb58944e7e829b184b014e27822c91c9f43 (patch)
treee9db3b9943d61163a87190c4293673a002d17da1 /src/StardewModdingAPI
parentc84310dfebafd3085dc418f3620154f9934865de (diff)
parentcbb1777ba00f581b428e61a0f7245a87ac53cf09 (diff)
downloadSMAPI-16281fb58944e7e829b184b014e27822c91c9f43.tar.gz
SMAPI-16281fb58944e7e829b184b014e27822c91c9f43.tar.bz2
SMAPI-16281fb58944e7e829b184b014e27822c91c9f43.zip
Merge branch 'develop' into stable
Diffstat (limited to 'src/StardewModdingAPI')
-rw-r--r--src/StardewModdingAPI/Constants.cs8
-rw-r--r--src/StardewModdingAPI/Context.cs17
-rw-r--r--src/StardewModdingAPI/Events/ContentEvents.cs7
-rw-r--r--src/StardewModdingAPI/Events/ControlEvents.cs20
-rw-r--r--src/StardewModdingAPI/Events/GameEvents.cs39
-rw-r--r--src/StardewModdingAPI/Framework/Countdown.cs44
-rw-r--r--src/StardewModdingAPI/Framework/InternalExtensions.cs22
-rw-r--r--src/StardewModdingAPI/Framework/ModHelper.cs7
-rw-r--r--src/StardewModdingAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs (renamed from src/StardewModdingAPI/Framework/AssemblyDefinitionResolver.cs)2
-rw-r--r--src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs (renamed from src/StardewModdingAPI/Framework/AssemblyLoader.cs)2
-rw-r--r--src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs (renamed from src/StardewModdingAPI/Framework/AssemblyParseResult.cs)2
-rw-r--r--src/StardewModdingAPI/Framework/ModLoading/IModMetadata.cs39
-rw-r--r--src/StardewModdingAPI/Framework/ModLoading/InvalidModStateException.cs14
-rw-r--r--src/StardewModdingAPI/Framework/ModLoading/ModDependencyStatus.cs18
-rw-r--r--src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs57
-rw-r--r--src/StardewModdingAPI/Framework/ModLoading/ModMetadataStatus.cs12
-rw-r--r--src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs291
-rw-r--r--src/StardewModdingAPI/Framework/ModRegistry.cs28
-rw-r--r--src/StardewModdingAPI/Framework/Models/Manifest.cs (renamed from src/StardewModdingAPI/Framework/Manifest.cs)11
-rw-r--r--src/StardewModdingAPI/Framework/Models/ManifestDependency.cs23
-rw-r--r--src/StardewModdingAPI/Framework/Models/ModCompatibility.cs7
-rw-r--r--src/StardewModdingAPI/Framework/Models/SConfig.cs3
-rw-r--r--src/StardewModdingAPI/Framework/SGame.cs1968
-rw-r--r--src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs (renamed from src/StardewModdingAPI/Framework/Serialisation/SemanticVersionConverter.cs)39
-rw-r--r--src/StardewModdingAPI/IManifest.cs22
-rw-r--r--src/StardewModdingAPI/IManifestDependency.cs12
-rw-r--r--src/StardewModdingAPI/Mod.cs21
-rw-r--r--src/StardewModdingAPI/Program.cs281
-rw-r--r--src/StardewModdingAPI/Properties/AssemblyInfo.cs3
-rw-r--r--src/StardewModdingAPI/SemanticVersion.cs17
-rw-r--r--src/StardewModdingAPI/StardewModdingAPI.config.json312
-rw-r--r--src/StardewModdingAPI/StardewModdingAPI.csproj49
32 files changed, 2103 insertions, 1294 deletions
diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs
index 1860795d..5e4759e9 100644
--- a/src/StardewModdingAPI/Constants.cs
+++ b/src/StardewModdingAPI/Constants.cs
@@ -33,7 +33,7 @@ namespace StardewModdingAPI
** Public
****/
/// <summary>SMAPI's current semantic version.</summary>
- public static ISemanticVersion ApiVersion { get; } = new SemanticVersion(1, 12, 0);
+ public static ISemanticVersion ApiVersion { get; } = new SemanticVersion(1, 13, 0);
/// <summary>The minimum supported version of Stardew Valley.</summary>
public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.2.26");
@@ -71,6 +71,12 @@ namespace StardewModdingAPI
/// <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>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");
+
+ /// <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 full path to the folder containing mods.</summary>
internal static string ModPath { get; } = Path.Combine(Constants.ExecutionPath, "Mods");
diff --git a/src/StardewModdingAPI/Context.cs b/src/StardewModdingAPI/Context.cs
index 2da14eed..6bc5ae56 100644
--- a/src/StardewModdingAPI/Context.cs
+++ b/src/StardewModdingAPI/Context.cs
@@ -4,18 +4,27 @@ using StardewValley.Menus;
namespace StardewModdingAPI
{
/// <summary>Provides information about the current game state.</summary>
- internal static class Context
+ public static class Context
{
/*********
** Accessors
*********/
+ /****
+ ** Public
+ ****/
+ /// <summary>Whether the player has loaded a save and the world has finished initialising.</summary>
+ public static bool IsWorldReady { get; internal set; }
+
+ /****
+ ** Internal
+ ****/
/// <summary>Whether a player save has been loaded.</summary>
- public static bool IsSaveLoaded => Game1.hasLoadedGame && !string.IsNullOrEmpty(Game1.player.name);
+ internal static bool IsSaveLoaded => Game1.hasLoadedGame && !string.IsNullOrEmpty(Game1.player.name);
/// <summary>Whether the game is currently writing to the save file.</summary>
- public static bool IsSaving => SaveGame.IsProcessing && (Game1.activeClickableMenu is SaveGameMenu || Game1.activeClickableMenu is ShippingMenu); // IsProcessing is never set to false on Linux/Mac
+ 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
/// <summary>Whether the game is currently running the draw loop.</summary>
- public static bool IsInDrawLoop { get; set; }
+ internal static bool IsInDrawLoop { get; set; }
}
}
diff --git a/src/StardewModdingAPI/Events/ContentEvents.cs b/src/StardewModdingAPI/Events/ContentEvents.cs
index 5b4146c5..0dcd2cc6 100644
--- a/src/StardewModdingAPI/Events/ContentEvents.cs
+++ b/src/StardewModdingAPI/Events/ContentEvents.cs
@@ -27,7 +27,12 @@ namespace StardewModdingAPI.Events
public static event EventHandler<EventArgsValueChanged<string>> AfterLocaleChanged;
/// <summary>Raised when an XNB file is being read into the cache. Mods can change the data here before it's cached.</summary>
- internal static event EventHandler<IContentEventHelper> AfterAssetLoaded;
+#if EXPERIMENTAL
+ public
+#else
+ internal
+#endif
+ static event EventHandler<IContentEventHelper> AfterAssetLoaded;
/*********
diff --git a/src/StardewModdingAPI/Events/ControlEvents.cs b/src/StardewModdingAPI/Events/ControlEvents.cs
index 790bf193..80d0f547 100644
--- a/src/StardewModdingAPI/Events/ControlEvents.cs
+++ b/src/StardewModdingAPI/Events/ControlEvents.cs
@@ -77,40 +77,36 @@ namespace StardewModdingAPI.Events
/// <summary>Raise a <see cref="ControllerButtonPressed"/> event.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
- /// <param name="playerIndex">The player who pressed the button.</param>
/// <param name="button">The controller button that was pressed.</param>
- internal static void InvokeButtonPressed(IMonitor monitor, PlayerIndex playerIndex, Buttons button)
+ internal static void InvokeButtonPressed(IMonitor monitor, Buttons button)
{
- monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerButtonPressed)}", ControlEvents.ControllerButtonPressed?.GetInvocationList(), null, new EventArgsControllerButtonPressed(playerIndex, button));
+ monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerButtonPressed)}", ControlEvents.ControllerButtonPressed?.GetInvocationList(), null, new EventArgsControllerButtonPressed(PlayerIndex.One, button));
}
/// <summary>Raise a <see cref="ControllerButtonReleased"/> event.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
- /// <param name="playerIndex">The player who released the button.</param>
/// <param name="button">The controller button that was released.</param>
- internal static void InvokeButtonReleased(IMonitor monitor, PlayerIndex playerIndex, Buttons button)
+ internal static void InvokeButtonReleased(IMonitor monitor, Buttons button)
{
- monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerButtonReleased)}", ControlEvents.ControllerButtonReleased?.GetInvocationList(), null, new EventArgsControllerButtonReleased(playerIndex, button));
+ monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerButtonReleased)}", ControlEvents.ControllerButtonReleased?.GetInvocationList(), null, new EventArgsControllerButtonReleased(PlayerIndex.One, button));
}
/// <summary>Raise a <see cref="ControllerTriggerPressed"/> event.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
- /// <param name="playerIndex">The player who pressed the trigger button.</param>
/// <param name="button">The trigger button that was pressed.</param>
/// <param name="value">The current trigger value.</param>
- internal static void InvokeTriggerPressed(IMonitor monitor, PlayerIndex playerIndex, Buttons button, float value)
+ internal static void InvokeTriggerPressed(IMonitor monitor, Buttons button, float value)
{
- monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerTriggerPressed)}", ControlEvents.ControllerTriggerPressed?.GetInvocationList(), null, new EventArgsControllerTriggerPressed(playerIndex, button, value));
+ monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerTriggerPressed)}", ControlEvents.ControllerTriggerPressed?.GetInvocationList(), null, new EventArgsControllerTriggerPressed(PlayerIndex.One, button, value));
}
/// <summary>Raise a <see cref="ControllerTriggerReleased"/> event.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
- /// <param name="playerIndex">The player who pressed the trigger button.</param>
/// <param name="button">The trigger button that was pressed.</param>
/// <param name="value">The current trigger value.</param>
- internal static void InvokeTriggerReleased(IMonitor monitor, PlayerIndex playerIndex, Buttons button, float value)
+ internal static void InvokeTriggerReleased(IMonitor monitor, Buttons button, float value)
{
- monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerTriggerReleased)}", ControlEvents.ControllerTriggerReleased?.GetInvocationList(), null, new EventArgsControllerTriggerReleased(playerIndex, button, value));
+ monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerTriggerReleased)}", ControlEvents.ControllerTriggerReleased?.GetInvocationList(), null, new EventArgsControllerTriggerReleased(PlayerIndex.One, button, value));
}
}
}
diff --git a/src/StardewModdingAPI/Events/GameEvents.cs b/src/StardewModdingAPI/Events/GameEvents.cs
index 029ec1f9..4f9ce7a7 100644
--- a/src/StardewModdingAPI/Events/GameEvents.cs
+++ b/src/StardewModdingAPI/Events/GameEvents.cs
@@ -19,6 +19,9 @@ namespace StardewModdingAPI.Events
/// <summary>Raised during launch after configuring XNA or MonoGame. The game window hasn't been opened by this point. Called after <see cref="Microsoft.Xna.Framework.Game.Initialize"/>.</summary>
internal static event EventHandler InitializeInternal;
+ /// <summary>Raised during launch after configuring Stardew Valley, loading it into memory, and opening the game window. The window is still blank by this point.</summary>
+ internal static event EventHandler GameLoadedInternal;
+
/// <summary>Raised during launch after configuring XNA or MonoGame. The game window hasn't been opened by this point. Called after <see cref="Microsoft.Xna.Framework.Game.Initialize"/>.</summary>
[Obsolete("The " + nameof(Mod) + "." + nameof(Mod.Entry) + " method is now called after the " + nameof(GameEvents.Initialize) + " event, so any contained logic can be done directly in " + nameof(Mod.Entry) + ".")]
public static event EventHandler Initialize;
@@ -28,9 +31,11 @@ namespace StardewModdingAPI.Events
public static event EventHandler LoadContent;
/// <summary>Raised during launch after configuring Stardew Valley, loading it into memory, and opening the game window. The window is still blank by this point.</summary>
+ [Obsolete("The " + nameof(Mod) + "." + nameof(Mod.Entry) + " method is now called after the game loads, so any contained logic can be done directly in " + nameof(Mod.Entry) + ".")]
public static event EventHandler GameLoaded;
/// <summary>Raised during the first game update tick.</summary>
+ [Obsolete("The " + nameof(Mod) + "." + nameof(Mod.Entry) + " method is now called after the game loads, so any contained logic can be done directly in " + nameof(Mod.Entry) + ".")]
public static event EventHandler FirstUpdateTick;
/// <summary>Raised when the game updates its state (≈60 times per second).</summary>
@@ -99,7 +104,32 @@ namespace StardewModdingAPI.Events
/// <param name="monitor">Encapsulates monitoring and logging.</param>
internal static void InvokeGameLoaded(IMonitor monitor)
{
- monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.GameLoaded)}", GameEvents.GameLoaded?.GetInvocationList());
+ // notify SMAPI
+ monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.GameLoadedInternal)}", GameEvents.GameLoadedInternal?.GetInvocationList());
+
+ // notify mods
+ if (GameEvents.GameLoaded == null)
+ return;
+
+ string name = $"{nameof(GameEvents)}.{nameof(GameEvents.GameLoaded)}";
+ Delegate[] handlers = GameEvents.GameLoaded.GetInvocationList();
+
+ GameEvents.DeprecationManager.WarnForEvent(handlers, name, "1.12", DeprecationLevel.Info);
+ monitor.SafelyRaisePlainEvent(name, handlers);
+ }
+
+ /// <summary>Raise a <see cref="FirstUpdateTick"/> event.</summary>
+ /// <param name="monitor">Encapsulates monitoring and logging.</param>
+ internal static void InvokeFirstUpdateTick(IMonitor monitor)
+ {
+ if (GameEvents.FirstUpdateTick == null)
+ return;
+
+ string name = $"{nameof(GameEvents)}.{nameof(GameEvents.FirstUpdateTick)}";
+ Delegate[] handlers = GameEvents.FirstUpdateTick.GetInvocationList();
+
+ GameEvents.DeprecationManager.WarnForEvent(handlers, name, "1.12", DeprecationLevel.Info);
+ monitor.SafelyRaisePlainEvent(name, handlers);
}
/// <summary>Raise an <see cref="UpdateTick"/> event.</summary>
@@ -150,12 +180,5 @@ namespace StardewModdingAPI.Events
{
monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.OneSecondTick)}", GameEvents.OneSecondTick?.GetInvocationList());
}
-
- /// <summary>Raise a <see cref="FirstUpdateTick"/> event.</summary>
- /// <param name="monitor">Encapsulates monitoring and logging.</param>
- internal static void InvokeFirstUpdateTick(IMonitor monitor)
- {
- monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.FirstUpdateTick)}", GameEvents.FirstUpdateTick?.GetInvocationList());
- }
}
}
diff --git a/src/StardewModdingAPI/Framework/Countdown.cs b/src/StardewModdingAPI/Framework/Countdown.cs
new file mode 100644
index 00000000..25ca2546
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/Countdown.cs
@@ -0,0 +1,44 @@
+namespace StardewModdingAPI.Framework
+{
+ /// <summary>Counts down from a baseline value.</summary>
+ internal class Countdown
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The initial value from which to count down.</summary>
+ public int Initial { get; }
+
+ /// <summary>The current value.</summary>
+ public int Current { get; private set; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="initial">The initial value from which to count down.</param>
+ public Countdown(int initial)
+ {
+ this.Initial = initial;
+ this.Current = initial;
+ }
+
+ /// <summary>Reduce the current value by one.</summary>
+ /// <returns>Returns whether the value was decremented (i.e. wasn't already zero).</returns>
+ public bool Decrement()
+ {
+ if (this.Current <= 0)
+ return false;
+
+ this.Current--;
+ return true;
+ }
+
+ /// <summary>Restart the countdown.</summary>
+ public void Reset()
+ {
+ this.Current = this.Initial;
+ }
+ }
+}
diff --git a/src/StardewModdingAPI/Framework/InternalExtensions.cs b/src/StardewModdingAPI/Framework/InternalExtensions.cs
index 5199c72d..cadf6598 100644
--- a/src/StardewModdingAPI/Framework/InternalExtensions.cs
+++ b/src/StardewModdingAPI/Framework/InternalExtensions.cs
@@ -2,6 +2,8 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
+using Microsoft.Xna.Framework.Graphics;
+using StardewValley;
namespace StardewModdingAPI.Framework
{
@@ -128,5 +130,25 @@ namespace StardewModdingAPI.Framework
deprecationManager.Warn(modName, nounPhrase, version, severity);
}
}
+
+ /****
+ ** Sprite batch
+ ****/
+ /// <summary>Get whether the sprite batch is between a begin and end pair.</summary>
+ /// <param name="spriteBatch">The sprite batch to check.</param>
+ /// <param name="reflection">The reflection helper with which to access private fields.</param>
+ public static bool IsOpen(this SpriteBatch spriteBatch, IReflectionHelper reflection)
+ {
+ // get field name
+ const string fieldName =
+#if SMAPI_FOR_WINDOWS
+ "inBeginEndPair";
+#else
+ "_beginCalled";
+#endif
+
+ // get result
+ return reflection.GetPrivateValue<bool>(Game1.spriteBatch, fieldName);
+ }
}
}
diff --git a/src/StardewModdingAPI/Framework/ModHelper.cs b/src/StardewModdingAPI/Framework/ModHelper.cs
index 09297a65..7810148c 100644
--- a/src/StardewModdingAPI/Framework/ModHelper.cs
+++ b/src/StardewModdingAPI/Framework/ModHelper.cs
@@ -1,6 +1,5 @@
using System;
using System.IO;
-using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Framework.Serialisation;
namespace StardewModdingAPI.Framework
@@ -25,7 +24,7 @@ namespace StardewModdingAPI.Framework
public IContentHelper Content { get; }
/// <summary>Simplifies access to private game code.</summary>
- public IReflectionHelper Reflection { get; } = new ReflectionHelper();
+ public IReflectionHelper Reflection { get; }
/// <summary>Metadata about loaded mods.</summary>
public IModRegistry ModRegistry { get; }
@@ -44,9 +43,10 @@ namespace StardewModdingAPI.Framework
/// <param name="modRegistry">Metadata about loaded mods.</param>
/// <param name="commandManager">Manages console commands.</param>
/// <param name="contentManager">The content manager which loads content assets.</param>
+ /// <param name="reflection">Simplifies access to private game code.</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(IManifest manifest, string modDirectory, JsonHelper jsonHelper, IModRegistry modRegistry, CommandManager commandManager, SContentManager contentManager)
+ public ModHelper(IManifest manifest, string modDirectory, JsonHelper jsonHelper, IModRegistry modRegistry, CommandManager commandManager, SContentManager contentManager, IReflectionHelper reflection)
{
// validate
if (string.IsNullOrWhiteSpace(modDirectory))
@@ -64,6 +64,7 @@ namespace StardewModdingAPI.Framework
this.Content = new ContentHelper(contentManager, modDirectory, manifest.Name);
this.ModRegistry = modRegistry;
this.ConsoleCommands = new CommandHelper(manifest.Name, commandManager);
+ this.Reflection = reflection;
}
/****
diff --git a/src/StardewModdingAPI/Framework/AssemblyDefinitionResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs
index b4e69fcd..4378798c 100644
--- a/src/StardewModdingAPI/Framework/AssemblyDefinitionResolver.cs
+++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs
@@ -1,7 +1,7 @@
using System.Collections.Generic;
using Mono.Cecil;
-namespace StardewModdingAPI.Framework
+namespace StardewModdingAPI.Framework.ModLoading
{
/// <summary>A minimal assembly definition resolver which resolves references to known assemblies.</summary>
internal class AssemblyDefinitionResolver : DefaultAssemblyResolver
diff --git a/src/StardewModdingAPI/Framework/AssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs
index 2c9973c1..42bd7bfb 100644
--- a/src/StardewModdingAPI/Framework/AssemblyLoader.cs
+++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs
@@ -7,7 +7,7 @@ using Mono.Cecil;
using Mono.Cecil.Cil;
using StardewModdingAPI.AssemblyRewriters;
-namespace StardewModdingAPI.Framework
+namespace StardewModdingAPI.Framework.ModLoading
{
/// <summary>Preprocesses and loads mod assemblies.</summary>
internal class AssemblyLoader
diff --git a/src/StardewModdingAPI/Framework/AssemblyParseResult.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs
index bff976aa..69c99afe 100644
--- a/src/StardewModdingAPI/Framework/AssemblyParseResult.cs
+++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs
@@ -1,7 +1,7 @@
using System.IO;
using Mono.Cecil;
-namespace StardewModdingAPI.Framework
+namespace StardewModdingAPI.Framework.ModLoading
{
/// <summary>Metadata about a parsed assembly definition.</summary>
internal class AssemblyParseResult
diff --git a/src/StardewModdingAPI/Framework/ModLoading/IModMetadata.cs b/src/StardewModdingAPI/Framework/ModLoading/IModMetadata.cs
new file mode 100644
index 00000000..3771ffdd
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/ModLoading/IModMetadata.cs
@@ -0,0 +1,39 @@
+using StardewModdingAPI.Framework.Models;
+
+namespace StardewModdingAPI.Framework.ModLoading
+{
+ /// <summary>Metadata for a mod.</summary>
+ internal interface IModMetadata
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The mod's display name.</summary>
+ string DisplayName { get; }
+
+ /// <summary>The mod's full directory path.</summary>
+ string DirectoryPath { get; }
+
+ /// <summary>The mod manifest.</summary>
+ IManifest Manifest { get; }
+
+ /// <summary>Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</summary>
+ ModCompatibility Compatibility { get; }
+
+ /// <summary>The metadata resolution status.</summary>
+ ModMetadataStatus Status { get; }
+
+ /// <summary>The reason the metadata is invalid, if any.</summary>
+ string Error { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Set the mod status.</summary>
+ /// <param name="status">The metadata resolution status.</param>
+ /// <param name="error">The reason the metadata is invalid, if any.</param>
+ /// <returns>Return the instance for chaining.</returns>
+ IModMetadata SetStatus(ModMetadataStatus status, string error = null);
+ }
+}
diff --git a/src/StardewModdingAPI/Framework/ModLoading/InvalidModStateException.cs b/src/StardewModdingAPI/Framework/ModLoading/InvalidModStateException.cs
new file mode 100644
index 00000000..ab11272a
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/ModLoading/InvalidModStateException.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace StardewModdingAPI.Framework.ModLoading
+{
+ /// <summary>An exception which indicates that something went seriously wrong while loading mods, and SMAPI should abort outright.</summary>
+ public class InvalidModStateException : Exception
+ {
+ /// <summary>Construct an instance.</summary>
+ /// <param name="message">The error message.</param>
+ /// <param name="ex">The underlying exception, if any.</param>
+ public InvalidModStateException(string message, Exception ex = null)
+ : base(message, ex) { }
+ }
+}
diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModDependencyStatus.cs b/src/StardewModdingAPI/Framework/ModLoading/ModDependencyStatus.cs
new file mode 100644
index 00000000..0774b487
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/ModLoading/ModDependencyStatus.cs
@@ -0,0 +1,18 @@
+namespace StardewModdingAPI.Framework.ModLoading
+{
+ /// <summary>The status of a given mod in the dependency-sorting algorithm.</summary>
+ internal enum ModDependencyStatus
+ {
+ /// <summary>The mod hasn't been visited yet.</summary>
+ Queued,
+
+ /// <summary>The mod is currently being analysed as part of a dependency chain.</summary>
+ Checking,
+
+ /// <summary>The mod has already been sorted.</summary>
+ Sorted,
+
+ /// <summary>The mod couldn't be sorted due to a metadata issue (e.g. missing dependencies).</summary>
+ Failed
+ }
+}
diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs b/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs
new file mode 100644
index 00000000..7b25e090
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs
@@ -0,0 +1,57 @@
+using StardewModdingAPI.Framework.Models;
+
+namespace StardewModdingAPI.Framework.ModLoading
+{
+ /// <summary>Metadata for a mod.</summary>
+ internal class ModMetadata : IModMetadata
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The mod's display name.</summary>
+ public string DisplayName { get; }
+
+ /// <summary>The mod's full directory path.</summary>
+ public string DirectoryPath { get; }
+
+ /// <summary>The mod manifest.</summary>
+ public IManifest Manifest { get; }
+
+ /// <summary>Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</summary>
+ public ModCompatibility Compatibility { get; }
+
+ /// <summary>The metadata resolution status.</summary>
+ public ModMetadataStatus Status { get; private set; }
+
+ /// <summary>The reason the metadata is invalid, if any.</summary>
+ public string Error { get; private set; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="displayName">The mod's display name.</param>
+ /// <param name="directoryPath">The mod's full directory path.</param>
+ /// <param name="manifest">The mod manifest.</param>
+ /// <param name="compatibility">Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</param>
+ public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModCompatibility compatibility)
+ {
+ this.DisplayName = displayName;
+ this.DirectoryPath = directoryPath;
+ this.Manifest = manifest;
+ this.Compatibility = compatibility;
+ }
+
+ /// <summary>Set the mod status.</summary>
+ /// <param name="status">The metadata resolution status.</param>
+ /// <param name="error">The reason the metadata is invalid, if any.</param>
+ /// <returns>Return the instance for chaining.</returns>
+ public IModMetadata SetStatus(ModMetadataStatus status, string error = null)
+ {
+ this.Status = status;
+ this.Error = error;
+ return this;
+ }
+ }
+}
diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModMetadataStatus.cs b/src/StardewModdingAPI/Framework/ModLoading/ModMetadataStatus.cs
new file mode 100644
index 00000000..1b2b0b55
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/ModLoading/ModMetadataStatus.cs
@@ -0,0 +1,12 @@
+namespace StardewModdingAPI.Framework.ModLoading
+{
+ /// <summary>Indicates the status of a mod's metadata resolution.</summary>
+ internal enum ModMetadataStatus
+ {
+ /// <summary>The mod has been found, but hasn't been processed yet.</summary>
+ Found,
+
+ /// <summary>The mod cannot be loaded.</summary>
+ Failed
+ }
+} \ No newline at end of file
diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs
new file mode 100644
index 00000000..2c68a639
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs
@@ -0,0 +1,291 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using StardewModdingAPI.Framework.Models;
+using StardewModdingAPI.Framework.Serialisation;
+
+namespace StardewModdingAPI.Framework.ModLoading
+{
+ /// <summary>Finds and processes mod metadata.</summary>
+ internal class ModResolver
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Get manifest metadata for each folder in the given root path.</summary>
+ /// <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="compatibilityRecords">Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</param>
+ /// <returns>Returns the manifests by relative folder.</returns>
+ public IEnumerable<IModMetadata> ReadManifests(string rootPath, JsonHelper jsonHelper, IEnumerable<ModCompatibility> compatibilityRecords)
+ {
+ compatibilityRecords = compatibilityRecords.ToArray();
+ foreach (DirectoryInfo modDir in this.GetModFolders(rootPath))
+ {
+ // read file
+ Manifest manifest = null;
+ string path = Path.Combine(modDir.FullName, "manifest.json");
+ string error = null;
+ try
+ {
+ // read manifest
+ manifest = jsonHelper.ReadJsonFile<Manifest>(path);
+
+ // validate
+ if (manifest == null)
+ {
+ error = File.Exists(path)
+ ? "its manifest is invalid."
+ : "it doesn't have a manifest.";
+ }
+ else if (string.IsNullOrWhiteSpace(manifest.EntryDll))
+ error = "its manifest doesn't set an entry DLL.";
+ }
+ catch (Exception ex)
+ {
+ error = $"parsing its manifest failed:\n{ex.GetLogSummary()}";
+ }
+
+ // get compatibility record
+ ModCompatibility compatibility = null;
+ if (manifest != null)
+ {
+ string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll;
+ compatibility = (
+ from mod in compatibilityRecords
+ where
+ mod.ID.Contains(key, StringComparer.InvariantCultureIgnoreCase)
+ && (mod.LowerSemanticVersion == null || !manifest.Version.IsOlderThan(mod.LowerSemanticVersion))
+ && !manifest.Version.IsNewerThan(mod.UpperSemanticVersion)
+ select mod
+ ).FirstOrDefault();
+ }
+ // build metadata
+ string displayName = !string.IsNullOrWhiteSpace(manifest?.Name)
+ ? manifest.Name
+ : modDir.FullName.Replace(rootPath, "").Trim('/', '\\');
+ ModMetadataStatus status = error == null
+ ? ModMetadataStatus.Found
+ : ModMetadataStatus.Failed;
+
+ yield return new ModMetadata(displayName, modDir.FullName, manifest, compatibility).SetStatus(status, error);
+ }
+ }
+
+ /// <summary>Validate manifest metadata.</summary>
+ /// <param name="mods">The mod manifests to validate.</param>
+ /// <param name="apiVersion">The current SMAPI version.</param>
+ public void ValidateManifests(IEnumerable<IModMetadata> mods, ISemanticVersion apiVersion)
+ {
+ foreach (IModMetadata mod in mods)
+ {
+ // skip if already failed
+ if (mod.Status == ModMetadataStatus.Failed)
+ continue;
+
+ // validate compatibility
+ {
+ ModCompatibility compatibility = mod.Compatibility;
+ if (compatibility?.Compatibility == ModCompatibilityType.AssumeBroken)
+ {
+ bool hasOfficialUrl = !string.IsNullOrWhiteSpace(mod.Compatibility.UpdateUrl);
+ bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(mod.Compatibility.UnofficialUpdateUrl);
+
+ string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game";
+ string error = $"{reasonPhrase}. Please check for a version newer than {compatibility.UpperVersionLabel ?? compatibility.UpperVersion} here:";
+ if (hasOfficialUrl)
+ error += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}";
+ if (hasUnofficialUrl)
+ error += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}";
+
+ mod.SetStatus(ModMetadataStatus.Failed, error);
+ continue;
+ }
+ }
+
+ // validate SMAPI version
+ if (!string.IsNullOrWhiteSpace(mod.Manifest.MinimumApiVersion))
+ {
+ if (!SemanticVersion.TryParse(mod.Manifest.MinimumApiVersion, out ISemanticVersion minVersion))
+ {
+ mod.SetStatus(ModMetadataStatus.Failed, $"it has an invalid minimum SMAPI version '{mod.Manifest.MinimumApiVersion}'. This should be a semantic version number like {apiVersion}.");
+ continue;
+ }
+ if (minVersion.IsNewerThan(apiVersion))
+ {
+ mod.SetStatus(ModMetadataStatus.Failed, $"it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod.");
+ continue;
+ }
+ }
+
+ // validate DLL path
+ string assemblyPath = Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll);
+ if (!File.Exists(assemblyPath))
+ mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist.");
+ }
+ }
+
+#if EXPERIMENTAL
+ /// <summary>Sort the given mods by the order they should be loaded.</summary>
+ /// <param name="mods">The mods to process.</param>
+ public IEnumerable<IModMetadata> ProcessDependencies(IEnumerable<IModMetadata> mods)
+ {
+ // initialise metadata
+ mods = mods.ToArray();
+ var sortedMods = new Stack<IModMetadata>();
+ var states = mods.ToDictionary(mod => mod, mod => ModDependencyStatus.Queued);
+
+ // handle failed mods
+ foreach (IModMetadata mod in mods.Where(m => m.Status == ModMetadataStatus.Failed))
+ {
+ states[mod] = ModDependencyStatus.Failed;
+ sortedMods.Push(mod);
+ }
+
+ // sort mods
+ foreach (IModMetadata mod in mods)
+ this.ProcessDependencies(mods.ToArray(), mod, states, sortedMods, new List<IModMetadata>());
+
+ return sortedMods.Reverse();
+ }
+#endif
+
+
+ /*********
+ ** Private methods
+ *********/
+#if EXPERIMENTAL
+ /// <summary>Sort a mod's dependencies by the order they should be loaded, and remove any mods that can't be loaded due to missing or conflicting dependencies.</summary>
+ /// <param name="mods">The full list of mods being validated.</param>
+ /// <param name="mod">The mod whose dependencies to process.</param>
+ /// <param name="states">The dependency state for each mod.</param>
+ /// <param name="sortedMods">The list in which to save mods sorted by dependency order.</param>
+ /// <param name="currentChain">The current change of mod dependencies.</param>
+ /// <returns>Returns the mod dependency status.</returns>
+ private ModDependencyStatus ProcessDependencies(IModMetadata[] mods, IModMetadata mod, IDictionary<IModMetadata, ModDependencyStatus> states, Stack<IModMetadata> sortedMods, ICollection<IModMetadata> currentChain)
+ {
+ // check if already visited
+ switch (states[mod])
+ {
+ // already sorted or failed
+ case ModDependencyStatus.Sorted:
+ case ModDependencyStatus.Failed:
+ return states[mod];
+
+ // dependency loop
+ case ModDependencyStatus.Checking:
+ // This should never happen. The higher-level mod checks if the dependency is
+ // already being checked, so it can fail without visiting a mod twice. If this
+ // case is hit, that logic didn't catch the dependency loop for some reason.
+ throw new InvalidModStateException($"A dependency loop was not caught by the calling iteration ({string.Join(" => ", currentChain.Select(p => p.DisplayName))} => {mod.DisplayName})).");
+
+ // not visited yet, start processing
+ case ModDependencyStatus.Queued:
+ break;
+
+ // sanity check
+ default:
+ throw new InvalidModStateException($"Unknown dependency status '{states[mod]}'.");
+ }
+
+ // no dependencies, mark sorted
+ if (mod.Manifest.Dependencies == null || !mod.Manifest.Dependencies.Any())
+ {
+ sortedMods.Push(mod);
+ return states[mod] = ModDependencyStatus.Sorted;
+ }
+
+ // missing required dependencies, mark failed
+ {
+ string[] missingModIDs =
+ (
+ from dependency in mod.Manifest.Dependencies
+ where mods.All(m => m.Manifest.UniqueID != dependency.UniqueID)
+ orderby dependency.UniqueID
+ select dependency.UniqueID
+ )
+ .ToArray();
+ if (missingModIDs.Any())
+ {
+ sortedMods.Push(mod);
+ mod.SetStatus(ModMetadataStatus.Failed, $"it requires mods which aren't installed ({string.Join(", ", missingModIDs)}).");
+ return states[mod] = ModDependencyStatus.Failed;
+ }
+ }
+
+ // process dependencies
+ {
+ states[mod] = ModDependencyStatus.Checking;
+
+ // get mods to load first
+ IModMetadata[] modsToLoadFirst =
+ (
+ from other in mods
+ where mod.Manifest.Dependencies.Any(required => required.UniqueID == other.Manifest.UniqueID)
+ select other
+ )
+ .ToArray();
+
+ // recursively sort dependencies
+ foreach (IModMetadata requiredMod in modsToLoadFirst)
+ {
+ var subchain = new List<IModMetadata>(currentChain) { mod };
+
+ // detect dependency loop
+ if (states[requiredMod] == ModDependencyStatus.Checking)
+ {
+ sortedMods.Push(mod);
+ mod.SetStatus(ModMetadataStatus.Failed, $"its dependencies have a circular reference: {string.Join(" => ", subchain.Select(p => p.DisplayName))} => {requiredMod.DisplayName}).");
+ return states[mod] = ModDependencyStatus.Failed;
+ }
+
+ // recursively process each dependency
+ var substatus = this.ProcessDependencies(mods, requiredMod, states, sortedMods, subchain);
+ switch (substatus)
+ {
+ // sorted successfully
+ case ModDependencyStatus.Sorted:
+ break;
+
+ // failed, which means this mod can't be loaded either
+ case ModDependencyStatus.Failed:
+ sortedMods.Push(mod);
+ mod.SetStatus(ModMetadataStatus.Failed, $"it needs the '{requiredMod.DisplayName}' mod, which couldn't be loaded.");
+ return states[mod] = ModDependencyStatus.Failed;
+
+ // unexpected status
+ case ModDependencyStatus.Queued:
+ case ModDependencyStatus.Checking:
+ throw new InvalidModStateException($"Something went wrong sorting dependencies: mod '{requiredMod.DisplayName}' unexpectedly stayed in the '{substatus}' status.");
+
+ // sanity check
+ default:
+ throw new InvalidModStateException($"Unknown dependency status '{states[mod]}'.");
+ }
+ }
+
+ // all requirements sorted successfully
+ sortedMods.Push(mod);
+ return states[mod] = ModDependencyStatus.Sorted;
+ }
+ }
+#endif
+
+ /// <summary>Get all mod folders in a root folder, passing through empty folders as needed.</summary>
+ /// <param name="rootPath">The root folder path to search.</param>
+ private IEnumerable<DirectoryInfo> GetModFolders(string rootPath)
+ {
+ foreach (string modRootPath in Directory.GetDirectories(rootPath))
+ {
+ DirectoryInfo directory = new DirectoryInfo(modRootPath);
+
+ // if a folder only contains another folder, check the inner folder instead
+ while (!directory.GetFiles().Any() && directory.GetDirectories().Length == 1)
+ directory = directory.GetDirectories().First();
+
+ yield return directory;
+ }
+ }
+ }
+}
diff --git a/src/StardewModdingAPI/Framework/ModRegistry.cs b/src/StardewModdingAPI/Framework/ModRegistry.cs
index f015b7ba..3899aa3f 100644
--- a/src/StardewModdingAPI/Framework/ModRegistry.cs
+++ b/src/StardewModdingAPI/Framework/ModRegistry.cs
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
-using StardewModdingAPI.Framework.Models;
namespace StardewModdingAPI.Framework
{
@@ -19,21 +18,10 @@ namespace StardewModdingAPI.Framework
/// <summary>The friendly mod names treated as deprecation warning sources (assembly full name => mod name).</summary>
private readonly IDictionary<string, string> ModNamesByAssembly = new Dictionary<string, string>();
- /// <summary>Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</summary>
- private readonly ModCompatibility[] CompatibilityRecords;
-
/*********
** Public methods
*********/
- /// <summary>Construct an instance.</summary>
- /// <param name="compatibilityRecords">Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</param>
- public ModRegistry(IEnumerable<ModCompatibility> compatibilityRecords)
- {
- this.CompatibilityRecords = compatibilityRecords.ToArray();
- }
-
-
/****
** IModRegistry
****/
@@ -125,21 +113,5 @@ namespace StardewModdingAPI.Framework
// no known assembly found
return null;
}
-
- /// <summary>Get metadata that indicates whether SMAPI should assume the mod is compatible or broken, regardless of whether it detects incompatible code.</summary>
- /// <param name="manifest">The mod manifest.</param>
- /// <returns>Returns the incompatibility record if applicable, else <c>null</c>.</returns>
- internal ModCompatibility GetCompatibilityRecord(IManifest manifest)
- {
- string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll;
- return (
- from mod in this.CompatibilityRecords
- where
- mod.ID == key
- && (mod.LowerSemanticVersion == null || !manifest.Version.IsOlderThan(mod.LowerSemanticVersion))
- && !manifest.Version.IsNewerThan(mod.UpperSemanticVersion)
- select mod
- ).FirstOrDefault();
- }
}
}
diff --git a/src/StardewModdingAPI/Framework/Manifest.cs b/src/StardewModdingAPI/Framework/Models/Manifest.cs
index 62c711e2..53384852 100644
--- a/src/StardewModdingAPI/Framework/Manifest.cs
+++ b/src/StardewModdingAPI/Framework/Models/Manifest.cs
@@ -1,10 +1,9 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
using StardewModdingAPI.Framework.Serialisation;
-namespace StardewModdingAPI.Framework
+namespace StardewModdingAPI.Framework.Models
{
/// <summary>A manifest which describes a mod for SMAPI.</summary>
internal class Manifest : IManifest
@@ -22,7 +21,7 @@ namespace StardewModdingAPI.Framework
public string Author { get; set; }
/// <summary>The mod version.</summary>
- [JsonConverter(typeof(SemanticVersionConverter))]
+ [JsonConverter(typeof(ManifestFieldConverter))]
public ISemanticVersion Version { get; set; }
/// <summary>The minimum SMAPI version required by this mod, if any.</summary>
@@ -31,6 +30,12 @@ namespace StardewModdingAPI.Framework
/// <summary>The name of the DLL in the directory that has the <see cref="Mod.Entry"/> method.</summary>
public string EntryDll { get; set; }
+#if EXPERIMENTAL
+ /// <summary>The other mods that must be loaded before this mod.</summary>
+ [JsonConverter(typeof(ManifestFieldConverter))]
+ public IManifestDependency[] Dependencies { get; set; }
+#endif
+
/// <summary>The unique mod ID.</summary>
public string UniqueID { get; set; }
diff --git a/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs b/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs
new file mode 100644
index 00000000..2f580c1d
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs
@@ -0,0 +1,23 @@
+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; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="uniqueID">The unique mod ID to require.</param>
+ public ManifestDependency(string uniqueID)
+ {
+ this.UniqueID = uniqueID;
+ }
+ }
+}
diff --git a/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs b/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs
index 1e71dae0..90cbd237 100644
--- a/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs
+++ b/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs
@@ -12,8 +12,8 @@ namespace StardewModdingAPI.Framework.Models
/****
** From config
****/
- /// <summary>The unique mod ID.</summary>
- public string ID { get; set; }
+ /// <summary>The unique mod IDs.</summary>
+ public string[] ID { get; set; }
/// <summary>The mod name.</summary>
public string Name { get; set; }
@@ -24,6 +24,9 @@ namespace StardewModdingAPI.Framework.Models
/// <summary>The most recent incompatible mod version.</summary>
public string UpperVersion { get; set; }
+ /// <summary>A label to show to the user instead of <see cref="UpperVersion"/>, when the manifest version differs from the user-facing version.</summary>
+ public string UpperVersionLabel { get; set; }
+
/// <summary>The URL the user can check for an official updated version.</summary>
public string UpdateUrl { get; set; }
diff --git a/src/StardewModdingAPI/Framework/Models/SConfig.cs b/src/StardewModdingAPI/Framework/Models/SConfig.cs
index 0de96297..c3f0816e 100644
--- a/src/StardewModdingAPI/Framework/Models/SConfig.cs
+++ b/src/StardewModdingAPI/Framework/Models/SConfig.cs
@@ -12,6 +12,9 @@
/// <summary>Whether to check if a newer version of SMAPI is available on startup.</summary>
public bool CheckForUpdates { get; set; } = true;
+ /// <summary>Whether SMAPI should log more information about the game context.</summary>
+ public bool VerboseLogging { get; set; } = false;
+
/// <summary>A list of mod versions which should be considered compatible or incompatible regardless of whether SMAPI detects incompatible code.</summary>
public ModCompatibility[] ModCompatibility { get; set; }
}
diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs
index 1f2bf3ac..3d421a37 100644
--- a/src/StardewModdingAPI/Framework/SGame.cs
+++ b/src/StardewModdingAPI/Framework/SGame.cs
@@ -9,7 +9,6 @@ using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using StardewModdingAPI.Events;
-using StardewModdingAPI.Framework.Reflection;
using StardewValley;
using StardewValley.BellsAndWhistles;
using StardewValley.Locations;
@@ -30,15 +29,21 @@ namespace StardewModdingAPI.Framework
/****
** SMAPI state
****/
+ /// <summary>The maximum number of consecutive attempts SMAPI should make to recover from a draw error.</summary>
+ private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second
+
+ /// <summary>The maximum number of consecutive attempts SMAPI should make to recover from an update error.</summary>
+ private readonly Countdown UpdateCrashTimer = new Countdown(60); // 60 ticks = roughly one second
+
/// <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;
- /// <summary>Whether the player has loaded a save and the world has finished initialising.</summary>
- private bool IsWorldReady => this.AfterLoadTimer < 0;
-
/// <summary>Whether the game is returning to the menu.</summary>
- private bool IsExiting;
+ private bool IsExitingToTitle;
+
+ /// <summary>Whether the game is saving and SMAPI has already raised <see cref="SaveEvents.BeforeSave"/>.</summary>
+ private bool IsBetweenSaveEvents;
/// <summary>Whether the game's zoom level is at 100% (i.e. nothing should be scaled).</summary>
public bool ZoomLevelIsOne => Game1.options.zoomLevel.Equals(1.0f);
@@ -50,7 +55,7 @@ namespace StardewModdingAPI.Framework
** Game state
****/
/// <summary>Arrays of pressed controller buttons indexed by <see cref="PlayerIndex"/>.</summary>
- private readonly Buttons[][] PreviouslyPressedButtons = { new Buttons[0], new Buttons[0], new Buttons[0], new Buttons[0] };
+ private Buttons[] PreviouslyPressedButtons = new Buttons[0];
/// <summary>A record of the keyboard state (i.e. the up/down state for each button) as of the latest tick.</summary>
private KeyboardState KStateNow;
@@ -82,6 +87,9 @@ namespace StardewModdingAPI.Framework
/// <summary>The keys that just entered the up state.</summary>
private Keys[] FrameReleasedKeys => this.PreviouslyPressedKeys.Except(this.CurrentlyPressedKeys).ToArray();
+ /// <summary>The previous save ID at last check.</summary>
+ private ulong PreviousSaveID;
+
/// <summary>A hash of <see cref="Game1.locations"/> at last check.</summary>
private int PreviousGameLocations;
@@ -151,14 +159,16 @@ namespace StardewModdingAPI.Framework
/****
** Private wrappers
****/
+ /// <summary>Simplifies access to private game code.</summary>
+ private static IReflectionHelper Reflection;
+
// ReSharper disable ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming
/// <summary>Used to access private fields and methods.</summary>
- private static readonly IReflectionHelper Reflection = new ReflectionHelper();
private static List<float> _fpsList => SGame.Reflection.GetPrivateField<List<float>>(typeof(Game1), nameof(_fpsList)).GetValue();
private static Stopwatch _fpsStopwatch => SGame.Reflection.GetPrivateField<Stopwatch>(typeof(Game1), nameof(SGame._fpsStopwatch)).GetValue();
private static float _fps
{
- set { SGame.Reflection.GetPrivateField<float>(typeof(Game1), nameof(_fps)).SetValue(value); }
+ set => SGame.Reflection.GetPrivateField<float>(typeof(Game1), nameof(_fps)).SetValue(value);
}
private static Task _newDayTask => SGame.Reflection.GetPrivateField<Task>(typeof(Game1), nameof(_newDayTask)).GetValue();
private Color bgColor => SGame.Reflection.GetPrivateField<Color>(this, nameof(bgColor)).GetValue();
@@ -172,17 +182,35 @@ namespace StardewModdingAPI.Framework
/*********
+ ** Accessors
+ *********/
+ /// <summary>Whether SMAPI should log more information about the game context.</summary>
+ public bool VerboseLogging { get; set; }
+
+
+ /*********
** Protected methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
- internal SGame(IMonitor monitor)
+ /// <param name="reflection">Simplifies access to private game code.</param>
+ internal SGame(IMonitor monitor, IReflectionHelper reflection)
{
this.Monitor = monitor;
this.FirstUpdate = true;
SGame.Instance = this;
+ SGame.Reflection = reflection;
Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; // required by Stardew Valley
+
+ // The game uses the default content manager instead of Game1.CreateContentManager in
+ // several cases (See http://community.playstarbound.com/threads/130058/page-27#post-3159274).
+ // The workaround is...
+ // 1. Override the default content manager.
+ // 2. Since Game1.content isn't initialised yet, and we need one main instance to
+ // support custom map tilesheets, detect when Game1.content is being initialised
+ // and use the same instance.
+ this.Content = new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, this.Monitor);
}
/****
@@ -193,6 +221,12 @@ namespace StardewModdingAPI.Framework
/// <param name="rootDirectory">The root directory to search for content.</param>
protected override LocalizedContentManager CreateContentManager(IServiceProvider serviceProvider, string rootDirectory)
{
+ // When Game1.content is being initialised, use SMAPI's main content manager instance.
+ // See comment in SGame constructor.
+ if (Game1.content == null && this.Content is SContentManager mainContentManager)
+ return mainContentManager;
+
+ // build new instance
return new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, this.Monitor);
}
@@ -200,92 +234,399 @@ namespace StardewModdingAPI.Framework
/// <param name="gameTime">A snapshot of the game timing state.</param>
protected override void Update(GameTime gameTime)
{
- // SMAPI exiting, stop processing game updates
- if (this.Monitor.IsExiting)
+ try
{
- this.Monitor.Log("SMAPI shutting down: aborting update.", LogLevel.Trace);
- return;
- }
+ /*********
+ ** Skip conditions
+ *********/
+ // SMAPI exiting, stop processing game updates
+ 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.
- //
- // 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 (SGame._newDayTask != null)
- {
- base.Update(gameTime);
- 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.
+ //
+ // 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 (SGame._newDayTask != null)
+ {
+ base.Update(gameTime);
+ return;
+ }
- // While the game is writing to the save file in the background, mods can unexpectedly
- // fail since they don't have exclusive access to resources (e.g. collection changed
- // during enumeration errors). To avoid problems, events are not invoked while a save
- // is in progress.
- if (Context.IsSaving)
- {
- base.Update(gameTime);
- return;
- }
+ // While the game is writing to the save file in the background, mods can unexpectedly
+ // fail since they don't have exclusive access to resources (e.g. collection changed
+ // during enumeration errors). To avoid problems, events are not invoked while a save
+ // is in progress. It's safe to raise SaveEvents.BeforeSave as soon as the menu is
+ // opened (since the save hasn't started yet), but all other events should be suppressed.
+ if (Context.IsSaving)
+ {
+ // raise before-save
+ if (!this.IsBetweenSaveEvents)
+ {
+ this.IsBetweenSaveEvents = true;
+ this.Monitor.Log("Context: before save.", LogLevel.Trace);
+ SaveEvents.InvokeBeforeSave(this.Monitor);
+ }
- // raise game loaded
- if (this.FirstUpdate)
- {
- GameEvents.InvokeInitialize(this.Monitor);
- GameEvents.InvokeLoadContent(this.Monitor);
- GameEvents.InvokeGameLoaded(this.Monitor);
+ // suppress non-save events
+ base.Update(gameTime);
+ return;
+ }
+ if (this.IsBetweenSaveEvents)
+ {
+ // raise after-save
+ this.IsBetweenSaveEvents = false;
+ this.Monitor.Log($"Context: after save, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace);
+ SaveEvents.InvokeAfterSave(this.Monitor);
+ TimeEvents.InvokeAfterDayStarted(this.Monitor);
+ }
+
+ /*********
+ ** Game loaded events
+ *********/
+ if (this.FirstUpdate)
+ {
+ GameEvents.InvokeInitialize(this.Monitor);
+ GameEvents.InvokeLoadContent(this.Monitor);
+ GameEvents.InvokeGameLoaded(this.Monitor);
+ }
+
+ /*********
+ ** Locale changed events
+ *********/
+ 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)
+ ContentEvents.InvokeAfterLocaleChanged(this.Monitor, oldValue.ToString(), newValue.ToString());
+ this.PreviousLocale = newValue;
+ }
+
+ /*********
+ ** After load events
+ *********/
+ if (Context.IsSaveLoaded && !SaveGame.IsProcessing /*still loading save*/ && this.AfterLoadTimer >= 0)
+ {
+ 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;
+
+ SaveEvents.InvokeAfterLoad(this.Monitor);
+ PlayerEvents.InvokeLoadedGame(this.Monitor, new EventArgsLoadedGameChanged(Game1.hasLoadedGame));
+ TimeEvents.InvokeAfterDayStarted(this.Monitor);
+ }
+ this.AfterLoadTimer--;
+ }
+
+ /*********
+ ** Exit 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)
+ {
+ this.Monitor.Log("Context: returned to title", LogLevel.Trace);
+
+ this.IsExitingToTitle = false;
+ this.CleanupAfterReturnToTitle();
+ SaveEvents.InvokeAfterReturnToTitle(this.Monitor);
+ }
+
+ /*********
+ ** Input events
+ *********/
+ {
+ // get latest state
+ this.KStateNow = Keyboard.GetState();
+ this.MStateNow = Mouse.GetState();
+ this.MPositionNow = new Point(Game1.getMouseX(), Game1.getMouseY());
+
+ // raise key pressed
+ foreach (Keys key in this.FramePressedKeys)
+ ControlEvents.InvokeKeyPressed(this.Monitor, key);
+
+ // raise key released
+ foreach (Keys key in this.FrameReleasedKeys)
+ ControlEvents.InvokeKeyReleased(this.Monitor, key);
+
+ // raise controller button pressed
+ foreach (Buttons button in this.GetFramePressedButtons())
+ {
+ if (button == Buttons.LeftTrigger || button == Buttons.RightTrigger)
+ {
+ var triggers = GamePad.GetState(PlayerIndex.One).Triggers;
+ ControlEvents.InvokeTriggerPressed(this.Monitor, button, button == Buttons.LeftTrigger ? triggers.Left : triggers.Right);
+ }
+ else
+ ControlEvents.InvokeButtonPressed(this.Monitor, button);
+ }
+
+ // raise controller button released
+ foreach (Buttons button in this.GetFrameReleasedButtons())
+ {
+ if (button == Buttons.LeftTrigger || button == Buttons.RightTrigger)
+ {
+ var triggers = GamePad.GetState(PlayerIndex.One).Triggers;
+ ControlEvents.InvokeTriggerReleased(this.Monitor, button, button == Buttons.LeftTrigger ? triggers.Left : triggers.Right);
+ }
+ else
+ ControlEvents.InvokeButtonReleased(this.Monitor, button);
+ }
+
+ // raise keyboard state changed
+ if (this.KStateNow != this.KStatePrior)
+ ControlEvents.InvokeKeyboardChanged(this.Monitor, this.KStatePrior, this.KStateNow);
+
+ // raise mouse state changed
+ if (this.MStateNow != this.MStatePrior)
+ {
+ ControlEvents.InvokeMouseChanged(this.Monitor, this.MStatePrior, this.MStateNow, this.MPositionPrior, this.MPositionNow);
+ this.MStatePrior = this.MStateNow;
+ this.MPositionPrior = this.MPositionNow;
+ }
+ }
+
+ /*********
+ ** Menu events
+ *********/
+ if (Game1.activeClickableMenu != this.PreviousActiveMenu)
+ {
+ IClickableMenu previousMenu = this.PreviousActiveMenu;
+ IClickableMenu newMenu = Game1.activeClickableMenu;
+
+ // 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);
+ }
+
+ // raise menu events
+ if (newMenu != null)
+ MenuEvents.InvokeMenuChanged(this.Monitor, previousMenu, newMenu);
+ else
+ MenuEvents.InvokeMenuClosed(this.Monitor, previousMenu);
+
+ // update previous menu
+ // (if the menu was changed in one of the handlers, deliberately defer detection until the next update so mods can be notified of the new menu change)
+ this.PreviousActiveMenu = newMenu;
+ }
+
+ /*********
+ ** World & player events
+ *********/
+ if (Context.IsWorldReady)
+ {
+ // raise current location changed
+ if (Game1.currentLocation != this.PreviousGameLocation)
+ {
+ if (this.VerboseLogging)
+ this.Monitor.Log($"Context: set location to {Game1.currentLocation?.Name ?? "(none)"}.", LogLevel.Trace);
+ LocationEvents.InvokeCurrentLocationChanged(this.Monitor, this.PreviousGameLocation, Game1.currentLocation);
+ }
+
+ // raise location list changed
+ if (this.GetHash(Game1.locations) != this.PreviousGameLocations)
+ LocationEvents.InvokeLocationsChanged(this.Monitor, Game1.locations);
+
+ // raise player changed
+ if (Game1.player != this.PreviousFarmer)
+ PlayerEvents.InvokeFarmerChanged(this.Monitor, this.PreviousFarmer, Game1.player);
+
+ // raise events that shouldn't be triggered on initial load
+ if (Game1.uniqueIDForThisGame == this.PreviousSaveID)
+ {
+ // raise player leveled up a skill
+ if (Game1.player.combatLevel != this.PreviousCombatLevel)
+ PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Combat, Game1.player.combatLevel);
+ if (Game1.player.farmingLevel != this.PreviousFarmingLevel)
+ PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Farming, Game1.player.farmingLevel);
+ if (Game1.player.fishingLevel != this.PreviousFishingLevel)
+ PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Fishing, Game1.player.fishingLevel);
+ if (Game1.player.foragingLevel != this.PreviousForagingLevel)
+ PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Foraging, Game1.player.foragingLevel);
+ if (Game1.player.miningLevel != this.PreviousMiningLevel)
+ PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Mining, Game1.player.miningLevel);
+ if (Game1.player.luckLevel != this.PreviousLuckLevel)
+ PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Luck, Game1.player.luckLevel);
+
+ // raise player inventory changed
+ ItemStackChange[] changedItems = this.GetInventoryChanges(Game1.player.items, this.PreviousItems).ToArray();
+ if (changedItems.Any())
+ PlayerEvents.InvokeInventoryChanged(this.Monitor, Game1.player.items, changedItems);
+
+ // raise current location's object list changed
+ if (this.GetHash(Game1.currentLocation.objects) != this.PreviousLocationObjects)
+ LocationEvents.InvokeOnNewLocationObject(this.Monitor, Game1.currentLocation.objects);
+
+ // raise time changed
+ if (Game1.timeOfDay != this.PreviousTime)
+ TimeEvents.InvokeTimeOfDayChanged(this.Monitor, this.PreviousTime, Game1.timeOfDay);
+ if (Game1.dayOfMonth != this.PreviousDay)
+ TimeEvents.InvokeDayOfMonthChanged(this.Monitor, this.PreviousDay, Game1.dayOfMonth);
+ if (Game1.currentSeason != this.PreviousSeason)
+ TimeEvents.InvokeSeasonOfYearChanged(this.Monitor, this.PreviousSeason, Game1.currentSeason);
+ if (Game1.year != this.PreviousYear)
+ TimeEvents.InvokeYearOfGameChanged(this.Monitor, this.PreviousYear, Game1.year);
+
+ // raise mine level changed
+ if (Game1.mine != null && Game1.mine.mineLevel != this.PreviousMineLevel)
+ MineEvents.InvokeMineLevelChanged(this.Monitor, this.PreviousMineLevel, Game1.mine.mineLevel);
+ }
+
+ // update state
+ this.PreviousGameLocations = this.GetHash(Game1.locations);
+ this.PreviousGameLocation = Game1.currentLocation;
+ this.PreviousFarmer = Game1.player;
+ 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).ToDictionary(n => n, n => n.Stack);
+ this.PreviousLocationObjects = this.GetHash(Game1.currentLocation.objects);
+ this.PreviousTime = Game1.timeOfDay;
+ this.PreviousDay = Game1.dayOfMonth;
+ this.PreviousSeason = Game1.currentSeason;
+ this.PreviousYear = Game1.year;
+ this.PreviousMineLevel = Game1.mine?.mineLevel ?? 0;
+ this.PreviousSaveID = Game1.uniqueIDForThisGame;
+ }
+
+ /*********
+ ** Game day transition event (obsolete)
+ *********/
+ if (Game1.newDay != this.PreviousIsNewDay)
+ {
+ TimeEvents.InvokeOnNewDay(this.Monitor, this.PreviousDay, Game1.dayOfMonth, Game1.newDay);
+ this.PreviousIsNewDay = Game1.newDay;
+ }
+
+ /*********
+ ** Game update
+ *********/
+ try
+ {
+ base.Update(gameTime);
+ }
+ catch (Exception ex)
+ {
+ this.Monitor.Log($"An error occured in the base update loop: {ex.GetLogSummary()}", LogLevel.Error);
+ }
+
+ /*********
+ ** Update events
+ *********/
+ GameEvents.InvokeUpdateTick(this.Monitor);
+ if (this.FirstUpdate)
+ {
+ GameEvents.InvokeFirstUpdateTick(this.Monitor);
+ this.FirstUpdate = false;
+ }
+ if (this.CurrentUpdateTick % 2 == 0)
+ GameEvents.InvokeSecondUpdateTick(this.Monitor);
+ if (this.CurrentUpdateTick % 4 == 0)
+ GameEvents.InvokeFourthUpdateTick(this.Monitor);
+ if (this.CurrentUpdateTick % 8 == 0)
+ GameEvents.InvokeEighthUpdateTick(this.Monitor);
+ if (this.CurrentUpdateTick % 15 == 0)
+ GameEvents.InvokeQuarterSecondTick(this.Monitor);
+ if (this.CurrentUpdateTick % 30 == 0)
+ GameEvents.InvokeHalfSecondTick(this.Monitor);
+ if (this.CurrentUpdateTick % 60 == 0)
+ GameEvents.InvokeOneSecondTick(this.Monitor);
+ this.CurrentUpdateTick += 1;
+ if (this.CurrentUpdateTick >= 60)
+ this.CurrentUpdateTick = 0;
+
+ /*********
+ ** Update input state
+ *********/
+ this.KStatePrior = this.KStateNow;
+ this.PreviouslyPressedButtons = this.GetButtonsDown();
+
+ this.UpdateCrashTimer.Reset();
}
+ catch (Exception ex)
+ {
+ // log error
+ this.Monitor.Log($"An error occured in the overridden update loop: {ex.GetLogSummary()}", LogLevel.Error);
- // update SMAPI events
- this.UpdateEventCalls();
+ // exit if irrecoverable
+ if (!this.UpdateCrashTimer.Decrement())
+ this.Monitor.ExitGameImmediately("the game crashed when updating, and SMAPI was unable to recover the game.");
+ }
+ }
- // let game update
+ /// <summary>The method called to draw everything to the screen.</summary>
+ /// <param name="gameTime">A snapshot of the game timing state.</param>
+ protected override void Draw(GameTime gameTime)
+ {
+ Context.IsInDrawLoop = true;
try
{
- base.Update(gameTime);
+ this.DrawImpl(gameTime);
+ this.DrawCrashTimer.Reset();
}
catch (Exception ex)
{
- this.Monitor.Log($"An error occured in the base update loop: {ex.GetLogSummary()}", LogLevel.Error);
- }
+ // log error
+ this.Monitor.Log($"An error occured in the overridden draw loop: {ex.GetLogSummary()}", LogLevel.Error);
- // raise update events
- GameEvents.InvokeUpdateTick(this.Monitor);
- if (this.FirstUpdate)
- {
- GameEvents.InvokeFirstUpdateTick(this.Monitor);
- this.FirstUpdate = false;
+ // exit if irrecoverable
+ if (!this.DrawCrashTimer.Decrement())
+ {
+ this.Monitor.ExitGameImmediately("the game crashed when drawing, and SMAPI was unable to recover the game.");
+ return;
+ }
+
+ // abort in known unrecoverable cases
+ if (Game1.toolSpriteSheet?.IsDisposed == true)
+ {
+ this.Monitor.ExitGameImmediately("the game unexpectedly disposed the tool spritesheet, so it crashed trying to draw a tool. This is a known bug in Stardew Valley 1.2.29, and there's no way to recover from it.");
+ return;
+ }
+
+ // recover sprite batch
+ try
+ {
+ if (Game1.spriteBatch.IsOpen(SGame.Reflection))
+ {
+ this.Monitor.Log("Recovering sprite batch from error...", LogLevel.Trace);
+ Game1.spriteBatch.End();
+ }
+ }
+ catch (Exception innerEx)
+ {
+ this.Monitor.Log($"Could not recover sprite batch state: {innerEx.GetLogSummary()}", LogLevel.Error);
+ }
}
- if (this.CurrentUpdateTick % 2 == 0)
- GameEvents.InvokeSecondUpdateTick(this.Monitor);
- if (this.CurrentUpdateTick % 4 == 0)
- GameEvents.InvokeFourthUpdateTick(this.Monitor);
- if (this.CurrentUpdateTick % 8 == 0)
- GameEvents.InvokeEighthUpdateTick(this.Monitor);
- if (this.CurrentUpdateTick % 15 == 0)
- GameEvents.InvokeQuarterSecondTick(this.Monitor);
- if (this.CurrentUpdateTick % 30 == 0)
- GameEvents.InvokeHalfSecondTick(this.Monitor);
- if (this.CurrentUpdateTick % 60 == 0)
- GameEvents.InvokeOneSecondTick(this.Monitor);
- this.CurrentUpdateTick += 1;
- if (this.CurrentUpdateTick >= 60)
- this.CurrentUpdateTick = 0;
-
- // track keyboard state
- this.KStatePrior = this.KStateNow;
-
- // track controller button state
- for (var i = PlayerIndex.One; i <= PlayerIndex.Four; i++)
- this.PreviouslyPressedButtons[(int)i] = this.GetButtonsDown(i);
+ Context.IsInDrawLoop = false;
}
- /// <summary>The method called to draw everything to the screen.</summary>
+ /// <summary>Replicate the game's draw logic with some changes for SMAPI.</summary>
/// <param name="gameTime">A snapshot of the game timing state.</param>
/// <remarks>This implementation is identical to <see cref="Game1.Draw"/>, except for try..catch around menu draw code, private field references replaced by wrappers, and added events.</remarks>
[SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator", Justification = "copied from game code as-is")]
@@ -295,692 +636,669 @@ 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")]
- protected override void Draw(GameTime gameTime)
+ private void DrawImpl(GameTime gameTime)
{
- Context.IsInDrawLoop = true;
- try
+ if (Game1.debugMode)
{
- if (Game1.debugMode)
+ if (SGame._fpsStopwatch.IsRunning)
{
- 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();
+ 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));
}
- else
- {
- if (SGame._fpsStopwatch.IsRunning)
- SGame._fpsStopwatch.Reset();
- SGame._fps = 0.0f;
- SGame._fpsList.Clear();
- }
- if (SGame._newDayTask != null)
+ 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
+ {
+ GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor);
+ activeClickableMenu.draw(Game1.spriteBatch);
+ GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor);
+ }
+ 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();
+ }
+ Game1.spriteBatch.End();
+ }
//base.Draw(gameTime);
+ this.renderScreenBuffer();
}
else
{
- if ((double)Game1.options.zoomLevel != 1.0)
- this.GraphicsDevice.SetRenderTarget(this.screenWrapper);
- if (this.IsSaving)
+ this.GraphicsDevice.Clear(this.bgColor);
+ if (Game1.activeClickableMenu != null && Game1.options.showMenuBackground && Game1.activeClickableMenu.showWithoutTransparencyIfOptionIsSet())
{
- 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
{
- Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
- try
- {
- GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor);
- activeClickableMenu.draw(Game1.spriteBatch);
- GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor);
- }
- 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();
- }
+ Game1.activeClickableMenu.drawBackground(Game1.spriteBatch);
+ GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor);
+ Game1.activeClickableMenu.draw(Game1.spriteBatch);
+ GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor);
+ }
+ 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();
+ }
+ 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();
}
- //base.Draw(gameTime);
- this.renderScreenBuffer();
+ if (Game1.overlayMenu == null)
+ return;
+ Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
+ Game1.overlayMenu.draw(Game1.spriteBatch);
+ Game1.spriteBatch.End();
}
- else
+ 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);
+ Game1.spriteBatch.End();
+ }
+ else if (Game1.currentMinigame != null)
{
- this.GraphicsDevice.Clear(this.bgColor);
- if (Game1.activeClickableMenu != null && Game1.options.showMenuBackground && Game1.activeClickableMenu.showWithoutTransparencyIfOptionIsSet())
+ 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();
+ }
+ 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)
+ return;
+ 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
{
- Game1.activeClickableMenu.drawBackground(Game1.spriteBatch);
GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor);
Game1.activeClickableMenu.draw(Game1.spriteBatch);
GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor);
}
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);
+ 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();
}
- 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)
- return;
- 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.End();
+ if ((double)Game1.options.zoomLevel != 1.0)
{
- 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.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();
}
- else if (Game1.currentMinigame != null)
+ if (Game1.overlayMenu == null)
+ return;
+ 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 == 6)
+ {
+ 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);
+ 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);
+ Game1.spriteBatch.End();
+ if ((double)Game1.options.zoomLevel != 1.0)
{
- 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();
- }
- 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)
- return;
- Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
- Game1.overlayMenu.draw(Game1.spriteBatch);
+ 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();
}
- else if (Game1.showingEndOfNightStuff)
+ if (Game1.overlayMenu == null)
+ return;
+ Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
+ Game1.overlayMenu.draw(Game1.spriteBatch);
+ Game1.spriteBatch.End();
+ }
+ else
+ {
+ Microsoft.Xna.Framework.Rectangle rectangle;
+ if ((int)Game1.gameMode == 0)
{
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
- if (Game1.activeClickableMenu != null)
- {
- try
- {
- GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor);
- Game1.activeClickableMenu.draw(Game1.spriteBatch);
- GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor);
- }
- 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();
- }
- }
- 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)
- return;
- 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 == 6)
+ else
{
- 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);
- 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);
- Game1.spriteBatch.End();
- if ((double)Game1.options.zoomLevel != 1.0)
+ if (Game1.drawLighting)
{
- 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);
+ 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));
+ 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);
+ }
Game1.spriteBatch.End();
+ this.GraphicsDevice.SetRenderTarget((double)Game1.options.zoomLevel == 1.0 ? (RenderTarget2D)null : this.screenWrapper);
}
- if (Game1.overlayMenu == null)
- return;
+ 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);
- Game1.overlayMenu.draw(Game1.spriteBatch);
- Game1.spriteBatch.End();
- }
- else
- {
- Microsoft.Xna.Framework.Rectangle rectangle;
- if ((int)Game1.gameMode == 0)
+ GraphicsEvents.InvokeOnPreRenderEvent(this.Monitor);
+ 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.drawWater(Game1.spriteBatch);
+ if (Game1.CurrentEvent == null)
{
- Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
+ foreach (NPC character in Game1.currentLocation.characters)
+ {
+ 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);
+ }
}
else
{
- 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.Equals("UndergroundMine") ? Game1.mine.getLightingColor(gameTime) : (Game1.ambientLight.Equals(Color.White) || Game1.isRaining && 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);
- }
- Game1.spriteBatch.End();
- this.GraphicsDevice.SetRenderTarget((double)Game1.options.zoomLevel == 1.0 ? (RenderTarget2D)null : this.screenWrapper);
- }
- 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);
- GraphicsEvents.InvokeOnPreRenderEvent(this.Monitor);
- 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.drawWater(Game1.spriteBatch);
- if (Game1.CurrentEvent == null)
+ foreach (NPC actor in Game1.CurrentEvent.actors)
{
- foreach (NPC character in Game1.currentLocation.characters)
- {
- 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);
- }
+ 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);
}
- else
- {
- foreach (NPC actor in Game1.CurrentEvent.actors)
- {
- 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);
- }
- }
- 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)
+ }
+ 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 (NPC character in Game1.currentLocation.characters)
+ if (!character.swimming && !character.hideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation()))
{
- if (!character.swimming && !character.hideShadow && 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 * Game1.pixelZoom) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : Game1.pixelZoom * 3))));
- 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;
- 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;
- int num3 = 0;
- double num4 = (double)Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 9.99999997475243E-07;
- spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4);
- }
+ 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))));
+ 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;
+ 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;
+ int num3 = 0;
+ double num4 = (double)Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 9.99999997475243E-07;
+ spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4);
}
}
- else
+ }
+ else
+ {
+ foreach (NPC actor in Game1.CurrentEvent.actors)
{
- foreach (NPC actor in Game1.CurrentEvent.actors)
+ if (!actor.swimming && !actor.hideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation()))
{
- if (!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 * Game1.pixelZoom) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : Game1.pixelZoom * 3))));
- 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;
- 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;
- int num3 = 0;
- double num4 = (double)Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 9.99999997475243E-07;
- spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4);
- }
+ 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))));
+ 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;
+ 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;
+ int num3 = 0;
+ double num4 = (double)Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 9.99999997475243E-07;
+ 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.currentLocation.draw(Game1.spriteBatch);
- if (Game1.eventUp && Game1.currentLocation.currentEvent != null)
- {
- string messageToScreen = Game1.currentLocation.currentEvent.messageToScreen;
- }
- 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((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);
- 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.mapDisplayDevice.BeginScene(Game1.spriteBatch);
- Game1.currentLocation.Map.GetLayer("Front").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom);
- 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)
- Game1.drawPlayerHeldObject(Game1.player);
- else if (Game1.displayFarmer && Game1.player.ActiveObject != null)
+ }
+ 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.currentLocation.draw(Game1.spriteBatch);
+ if (Game1.eventUp && Game1.currentLocation.currentEvent != null)
+ {
+ string messageToScreen = Game1.currentLocation.currentEvent.messageToScreen;
+ }
+ 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((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);
+ 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.mapDisplayDevice.BeginScene(Game1.spriteBatch);
+ Game1.currentLocation.Map.GetLayer("Front").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom);
+ 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)
+ 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 - 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"))
+ 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);
+ Size size1 = Game1.viewport.Size;
+ if (layer1.PickTile(mapDisplayLocation1, size1) != null)
{
- Layer layer1 = Game1.currentLocation.Map.GetLayer("Front");
+ Layer layer2 = Game1.currentLocation.Map.GetLayer("Front");
rectangle = Game1.player.GetBoundingBox();
- Location mapDisplayLocation1 = new Location(rectangle.Right, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5);
- 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);
- Size size2 = Game1.viewport.Size;
- if (layer2.PickTile(mapDisplayLocation2, size2).TileIndexProperties.ContainsKey("FrontAlways"))
- goto label_127;
- }
- else
+ Location mapDisplayLocation2 = new Location(rectangle.Right, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5);
+ Size size2 = Game1.viewport.Size;
+ if (layer2.PickTile(mapDisplayLocation2, size2).TileIndexProperties.ContainsKey("FrontAlways"))
goto label_127;
}
- 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)))
- 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.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 : 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);
- }
- if (Game1.isDebrisWeather && Game1.currentLocation.IsOutdoors && (!Game1.currentLocation.ignoreDebrisWeather && !Game1.currentLocation.Name.Equals("Desert")) && Game1.viewport.X > -10)
- {
- foreach (WeatherDebris weatherDebris in Game1.debrisWeather)
- weatherDebris.draw(Game1.spriteBatch);
+ else
+ goto label_127;
}
- 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 / Game1.tileSize), (float)(Game1.viewport.Y / Game1.tileSize)))))
+ 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)))
+ 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.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)
{
- 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);
+ 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.End();
- //base.Draw(gameTime);
- Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
- if (Game1.eventUp && Game1.currentLocation.currentEvent != null)
+ 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);
+ }
+ if (Game1.isDebrisWeather && Game1.currentLocation.IsOutdoors && (!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 / Game1.tileSize), (float)(Game1.viewport.Y / Game1.tileSize)))))
+ {
+ 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)
+ {
+ foreach (NPC actor in Game1.currentLocation.currentEvent.actors)
{
- foreach (NPC actor in Game1.currentLocation.currentEvent.actors)
+ if (actor.isEmoting)
{
- 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);
- }
+ 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);
}
}
+ }
+ 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 && Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert))
+ Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.OrangeRed * 0.45f);
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 && 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 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;
- }
- 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;
- }
- }
- if (Game1.currentBillboard != 0)
- this.drawBillboard();
- if ((Game1.displayHUD || Game1.eventUp) && (Game1.currentBillboard == 0 && (int)Game1.gameMode == 3) && (!Game1.freezeControls && !Game1.panMode))
+ }
+ 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 x2 = x1;
+ while (x2 < Game1.graphics.GraphicsDevice.Viewport.Width)
{
- GraphicsEvents.InvokeOnPreRenderHudEvent(this.Monitor);
- this.drawHUD();
- GraphicsEvents.InvokeOnPostRenderHudEvent(this.Monitor);
+ 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;
}
- 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()))
+ float num2 = num1;
+ while ((double)num2 < (double)Game1.graphics.GraphicsDevice.Viewport.Height)
{
- for (int i = Game1.hudMessages.Count - 1; i >= 0; --i)
- Game1.hudMessages[i].draw(Game1.spriteBatch, i);
+ 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;
}
}
- 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();
- Viewport viewport;
- if (Game1.progressBar)
+ if (Game1.currentBillboard != 0)
+ this.drawBillboard();
+ if ((Game1.displayHUD || Game1.eventUp) && (Game1.currentBillboard == 0 && (int)Game1.gameMode == 3) && (!Game1.freezeControls && !Game1.panMode))
{
- 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 dialogueWidth = Game1.dialogueWidth;
- int height1 = Game1.tileSize / 2;
- 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 width = (int)((double)Game1.pauseAccumulator / (double)Game1.pauseTime * (double)Game1.dialogueWidth);
- int height2 = Game1.tileSize / 2;
- Microsoft.Xna.Framework.Rectangle destinationRectangle2 = new Microsoft.Xna.Framework.Rectangle(x2, y2, width, height2);
- Color dimGray = Color.DimGray;
- spriteBatch2.Draw(staminaRect, destinationRectangle2, dimGray);
+ GraphicsEvents.InvokeOnPreRenderHudEvent(this.Monitor);
+ this.drawHUD();
+ GraphicsEvents.InvokeOnPostRenderHudEvent(this.Monitor);
}
- 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)))
+ 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()))
{
- 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);
+ for (int i = Game1.hudMessages.Count - 1; i >= 0; --i)
+ Game1.hudMessages[i].draw(Game1.spriteBatch, i);
}
- if ((Game1.fadeToBlack || Game1.globalFade) && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause))
+ }
+ 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();
+ 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 dialogueWidth = Game1.dialogueWidth;
+ int height1 = Game1.tileSize / 2;
+ 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 width = (int)((double)Game1.pauseAccumulator / (double)Game1.pauseTime * (double)Game1.dialogueWidth);
+ int height2 = Game1.tileSize / 2;
+ 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.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);
+ }
+ 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.Black * ((int)Game1.gameMode == 0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha);
+ Color color = Color.White * Math.Min(1f, Game1.flashAlpha);
spriteBatch.Draw(fadeToBlackRect, bounds, color);
}
- 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.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);
- if (Game1.debugMode)
+ 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);
+ 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);
+ 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);
+ }
+ 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);
+ if (Game1.activeClickableMenu != null)
+ {
+ try
{
- 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);
- 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);
+ GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor);
+ Game1.activeClickableMenu.draw(Game1.spriteBatch);
+ GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor);
}
- 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);
- if (Game1.activeClickableMenu != null)
+ catch (Exception ex)
{
- try
- {
- GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor);
- Game1.activeClickableMenu.draw(Game1.spriteBatch);
- GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor);
- }
- 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.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);
+ }
+ else if (Game1.farmEvent != null)
+ Game1.farmEvent.drawAboveEverything(Game1.spriteBatch);
+ 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();
- 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();
- }
-
- if (GraphicsEvents.HasPostRenderListeners())
- {
- Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
- GraphicsEvents.InvokeOnPostRenderEvent(this.Monitor);
- Game1.spriteBatch.End();
- }
+ }
- this.renderScreenBuffer();
+ if (GraphicsEvents.HasPostRenderListeners())
+ {
+ Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
+ GraphicsEvents.InvokeOnPostRenderEvent(this.Monitor);
+ Game1.spriteBatch.End();
}
- }
- }
- }
- catch (Exception ex)
- {
- // log error
- this.Monitor.Log($"An error occured in the overridden draw loop: {ex.GetLogSummary()}", LogLevel.Error);
- // fix sprite batch
- try
- {
- bool isSpriteBatchOpen =
-#if SMAPI_FOR_WINDOWS
- SGame.Reflection.GetPrivateValue<bool>(Game1.spriteBatch, "inBeginEndPair");
-#else
- SGame.Reflection.GetPrivateValue<bool>(Game1.spriteBatch, "_beginCalled");
-#endif
- if (isSpriteBatchOpen)
- {
- this.Monitor.Log("Recovering sprite batch from error...", LogLevel.Trace);
- Game1.spriteBatch.End();
+ this.renderScreenBuffer();
}
}
- catch (Exception innerEx)
- {
- this.Monitor.Log($"Could not recover sprite batch state: {innerEx.GetLogSummary()}", LogLevel.Error);
- }
}
- Context.IsInDrawLoop = false;
}
/****
** Methods
****/
+ /// <summary>Perform any cleanup needed when the player unloads a save and returns to the title screen.</summary>
+ private void CleanupAfterReturnToTitle()
+ {
+ Context.IsWorldReady = false;
+ this.AfterLoadTimer = 5;
+ this.PreviousSaveID = 0;
+ }
+
/// <summary>Get the controller buttons which are currently pressed.</summary>
- /// <param name="index">The controller to check.</param>
- private Buttons[] GetButtonsDown(PlayerIndex index)
+ private Buttons[] GetButtonsDown()
{
- var state = GamePad.GetState(index);
+ var state = GamePad.GetState(PlayerIndex.One);
var buttons = new List<Buttons>();
if (state.IsConnected)
{
@@ -1006,59 +1324,57 @@ namespace StardewModdingAPI.Framework
}
/// <summary>Get the controller buttons which were pressed after the last update.</summary>
- /// <param name="index">The controller to check.</param>
- private Buttons[] GetFramePressedButtons(PlayerIndex index)
+ private Buttons[] GetFramePressedButtons()
{
- var state = GamePad.GetState(index);
+ var state = GamePad.GetState(PlayerIndex.One);
var buttons = new List<Buttons>();
if (state.IsConnected)
{
- if (this.WasButtonJustPressed(Buttons.A, state.Buttons.A, index)) buttons.Add(Buttons.A);
- if (this.WasButtonJustPressed(Buttons.B, state.Buttons.B, index)) buttons.Add(Buttons.B);
- if (this.WasButtonJustPressed(Buttons.Back, state.Buttons.Back, index)) buttons.Add(Buttons.Back);
- if (this.WasButtonJustPressed(Buttons.BigButton, state.Buttons.BigButton, index)) buttons.Add(Buttons.BigButton);
- if (this.WasButtonJustPressed(Buttons.LeftShoulder, state.Buttons.LeftShoulder, index)) buttons.Add(Buttons.LeftShoulder);
- if (this.WasButtonJustPressed(Buttons.LeftStick, state.Buttons.LeftStick, index)) buttons.Add(Buttons.LeftStick);
- if (this.WasButtonJustPressed(Buttons.RightShoulder, state.Buttons.RightShoulder, index)) buttons.Add(Buttons.RightShoulder);
- if (this.WasButtonJustPressed(Buttons.RightStick, state.Buttons.RightStick, index)) buttons.Add(Buttons.RightStick);
- if (this.WasButtonJustPressed(Buttons.Start, state.Buttons.Start, index)) buttons.Add(Buttons.Start);
- if (this.WasButtonJustPressed(Buttons.X, state.Buttons.X, index)) buttons.Add(Buttons.X);
- if (this.WasButtonJustPressed(Buttons.Y, state.Buttons.Y, index)) buttons.Add(Buttons.Y);
- if (this.WasButtonJustPressed(Buttons.DPadUp, state.DPad.Up, index)) buttons.Add(Buttons.DPadUp);
- if (this.WasButtonJustPressed(Buttons.DPadDown, state.DPad.Down, index)) buttons.Add(Buttons.DPadDown);
- if (this.WasButtonJustPressed(Buttons.DPadLeft, state.DPad.Left, index)) buttons.Add(Buttons.DPadLeft);
- if (this.WasButtonJustPressed(Buttons.DPadRight, state.DPad.Right, index)) buttons.Add(Buttons.DPadRight);
- if (this.WasButtonJustPressed(Buttons.LeftTrigger, state.Triggers.Left, index)) buttons.Add(Buttons.LeftTrigger);
- if (this.WasButtonJustPressed(Buttons.RightTrigger, state.Triggers.Right, index)) buttons.Add(Buttons.RightTrigger);
+ if (this.WasButtonJustPressed(Buttons.A, state.Buttons.A)) buttons.Add(Buttons.A);
+ if (this.WasButtonJustPressed(Buttons.B, state.Buttons.B)) buttons.Add(Buttons.B);
+ if (this.WasButtonJustPressed(Buttons.Back, state.Buttons.Back)) buttons.Add(Buttons.Back);
+ if (this.WasButtonJustPressed(Buttons.BigButton, state.Buttons.BigButton)) buttons.Add(Buttons.BigButton);
+ if (this.WasButtonJustPressed(Buttons.LeftShoulder, state.Buttons.LeftShoulder)) buttons.Add(Buttons.LeftShoulder);
+ if (this.WasButtonJustPressed(Buttons.LeftStick, state.Buttons.LeftStick)) buttons.Add(Buttons.LeftStick);
+ if (this.WasButtonJustPressed(Buttons.RightShoulder, state.Buttons.RightShoulder)) buttons.Add(Buttons.RightShoulder);
+ if (this.WasButtonJustPressed(Buttons.RightStick, state.Buttons.RightStick)) buttons.Add(Buttons.RightStick);
+ if (this.WasButtonJustPressed(Buttons.Start, state.Buttons.Start)) buttons.Add(Buttons.Start);
+ if (this.WasButtonJustPressed(Buttons.X, state.Buttons.X)) buttons.Add(Buttons.X);
+ if (this.WasButtonJustPressed(Buttons.Y, state.Buttons.Y)) buttons.Add(Buttons.Y);
+ if (this.WasButtonJustPressed(Buttons.DPadUp, state.DPad.Up)) buttons.Add(Buttons.DPadUp);
+ if (this.WasButtonJustPressed(Buttons.DPadDown, state.DPad.Down)) buttons.Add(Buttons.DPadDown);
+ if (this.WasButtonJustPressed(Buttons.DPadLeft, state.DPad.Left)) buttons.Add(Buttons.DPadLeft);
+ if (this.WasButtonJustPressed(Buttons.DPadRight, state.DPad.Right)) buttons.Add(Buttons.DPadRight);
+ if (this.WasButtonJustPressed(Buttons.LeftTrigger, state.Triggers.Left)) buttons.Add(Buttons.LeftTrigger);
+ if (this.WasButtonJustPressed(Buttons.RightTrigger, state.Triggers.Right)) buttons.Add(Buttons.RightTrigger);
}
return buttons.ToArray();
}
/// <summary>Get the controller buttons which were released after the last update.</summary>
- /// <param name="index">The controller to check.</param>
- private Buttons[] GetFrameReleasedButtons(PlayerIndex index)
+ private Buttons[] GetFrameReleasedButtons()
{
- var state = GamePad.GetState(index);
+ var state = GamePad.GetState(PlayerIndex.One);
var buttons = new List<Buttons>();
if (state.IsConnected)
{
- if (this.WasButtonJustReleased(Buttons.A, state.Buttons.A, index)) buttons.Add(Buttons.A);
- if (this.WasButtonJustReleased(Buttons.B, state.Buttons.B, index)) buttons.Add(Buttons.B);
- if (this.WasButtonJustReleased(Buttons.Back, state.Buttons.Back, index)) buttons.Add(Buttons.Back);
- if (this.WasButtonJustReleased(Buttons.BigButton, state.Buttons.BigButton, index)) buttons.Add(Buttons.BigButton);
- if (this.WasButtonJustReleased(Buttons.LeftShoulder, state.Buttons.LeftShoulder, index)) buttons.Add(Buttons.LeftShoulder);
- if (this.WasButtonJustReleased(Buttons.LeftStick, state.Buttons.LeftStick, index)) buttons.Add(Buttons.LeftStick);
- if (this.WasButtonJustReleased(Buttons.RightShoulder, state.Buttons.RightShoulder, index)) buttons.Add(Buttons.RightShoulder);
- if (this.WasButtonJustReleased(Buttons.RightStick, state.Buttons.RightStick, index)) buttons.Add(Buttons.RightStick);
- if (this.WasButtonJustReleased(Buttons.Start, state.Buttons.Start, index)) buttons.Add(Buttons.Start);
- if (this.WasButtonJustReleased(Buttons.X, state.Buttons.X, index)) buttons.Add(Buttons.X);
- if (this.WasButtonJustReleased(Buttons.Y, state.Buttons.Y, index)) buttons.Add(Buttons.Y);
- if (this.WasButtonJustReleased(Buttons.DPadUp, state.DPad.Up, index)) buttons.Add(Buttons.DPadUp);
- if (this.WasButtonJustReleased(Buttons.DPadDown, state.DPad.Down, index)) buttons.Add(Buttons.DPadDown);
- if (this.WasButtonJustReleased(Buttons.DPadLeft, state.DPad.Left, index)) buttons.Add(Buttons.DPadLeft);
- if (this.WasButtonJustReleased(Buttons.DPadRight, state.DPad.Right, index)) buttons.Add(Buttons.DPadRight);
- if (this.WasButtonJustReleased(Buttons.LeftTrigger, state.Triggers.Left, index)) buttons.Add(Buttons.LeftTrigger);
- if (this.WasButtonJustReleased(Buttons.RightTrigger, state.Triggers.Right, index)) buttons.Add(Buttons.RightTrigger);
+ if (this.WasButtonJustReleased(Buttons.A, state.Buttons.A)) buttons.Add(Buttons.A);
+ if (this.WasButtonJustReleased(Buttons.B, state.Buttons.B)) buttons.Add(Buttons.B);
+ if (this.WasButtonJustReleased(Buttons.Back, state.Buttons.Back)) buttons.Add(Buttons.Back);
+ if (this.WasButtonJustReleased(Buttons.BigButton, state.Buttons.BigButton)) buttons.Add(Buttons.BigButton);
+ if (this.WasButtonJustReleased(Buttons.LeftShoulder, state.Buttons.LeftShoulder)) buttons.Add(Buttons.LeftShoulder);
+ if (this.WasButtonJustReleased(Buttons.LeftStick, state.Buttons.LeftStick)) buttons.Add(Buttons.LeftStick);
+ if (this.WasButtonJustReleased(Buttons.RightShoulder, state.Buttons.RightShoulder)) buttons.Add(Buttons.RightShoulder);
+ if (this.WasButtonJustReleased(Buttons.RightStick, state.Buttons.RightStick)) buttons.Add(Buttons.RightStick);
+ if (this.WasButtonJustReleased(Buttons.Start, state.Buttons.Start)) buttons.Add(Buttons.Start);
+ if (this.WasButtonJustReleased(Buttons.X, state.Buttons.X)) buttons.Add(Buttons.X);
+ if (this.WasButtonJustReleased(Buttons.Y, state.Buttons.Y)) buttons.Add(Buttons.Y);
+ if (this.WasButtonJustReleased(Buttons.DPadUp, state.DPad.Up)) buttons.Add(Buttons.DPadUp);
+ if (this.WasButtonJustReleased(Buttons.DPadDown, state.DPad.Down)) buttons.Add(Buttons.DPadDown);
+ if (this.WasButtonJustReleased(Buttons.DPadLeft, state.DPad.Left)) buttons.Add(Buttons.DPadLeft);
+ if (this.WasButtonJustReleased(Buttons.DPadRight, state.DPad.Right)) buttons.Add(Buttons.DPadRight);
+ if (this.WasButtonJustReleased(Buttons.LeftTrigger, state.Triggers.Left)) buttons.Add(Buttons.LeftTrigger);
+ if (this.WasButtonJustReleased(Buttons.RightTrigger, state.Triggers.Right)) buttons.Add(Buttons.RightTrigger);
}
return buttons.ToArray();
}
@@ -1066,267 +1382,33 @@ namespace StardewModdingAPI.Framework
/// <summary>Get whether a controller button was pressed since the last check.</summary>
/// <param name="button">The controller button to check.</param>
/// <param name="buttonState">The last known state.</param>
- /// <param name="stateIndex">The player whose controller to check.</param>
- private bool WasButtonJustPressed(Buttons button, ButtonState buttonState, PlayerIndex stateIndex)
+ private bool WasButtonJustPressed(Buttons button, ButtonState buttonState)
{
- return buttonState == ButtonState.Pressed && !this.PreviouslyPressedButtons[(int)stateIndex].Contains(button);
+ return buttonState == ButtonState.Pressed && !this.PreviouslyPressedButtons.Contains(button);
}
/// <summary>Get whether a controller button was released since the last check.</summary>
/// <param name="button">The controller button to check.</param>
/// <param name="buttonState">The last known state.</param>
- /// <param name="stateIndex">The player whose controller to check.</param>
- private bool WasButtonJustReleased(Buttons button, ButtonState buttonState, PlayerIndex stateIndex)
+ private bool WasButtonJustReleased(Buttons button, ButtonState buttonState)
{
- return buttonState == ButtonState.Released && this.PreviouslyPressedButtons[(int)stateIndex].Contains(button);
+ return buttonState == ButtonState.Released && this.PreviouslyPressedButtons.Contains(button);
}
/// <summary>Get whether an analogue controller button was pressed since the last check.</summary>
/// <param name="button">The controller button to check.</param>
/// <param name="value">The last known value.</param>
- /// <param name="stateIndex">The player whose controller to check.</param>
- private bool WasButtonJustPressed(Buttons button, float value, PlayerIndex stateIndex)
+ private bool WasButtonJustPressed(Buttons button, float value)
{
- return this.WasButtonJustPressed(button, value > 0.2f ? ButtonState.Pressed : ButtonState.Released, stateIndex);
+ return this.WasButtonJustPressed(button, value > 0.2f ? ButtonState.Pressed : ButtonState.Released);
}
/// <summary>Get whether an analogue controller button was released since the last check.</summary>
/// <param name="button">The controller button to check.</param>
/// <param name="value">The last known value.</param>
- /// <param name="stateIndex">The player whose controller to check.</param>
- private bool WasButtonJustReleased(Buttons button, float value, PlayerIndex stateIndex)
- {
- return this.WasButtonJustReleased(button, value > 0.2f ? ButtonState.Pressed : ButtonState.Released, stateIndex);
- }
-
- /// <summary>Detect changes since the last update ticket and trigger mod events.</summary>
- private void UpdateEventCalls()
+ private bool WasButtonJustReleased(Buttons button, float value)
{
- // content locale changed event
- if (this.PreviousLocale != LocalizedContentManager.CurrentLanguageCode)
- {
- var oldValue = this.PreviousLocale;
- var newValue = LocalizedContentManager.CurrentLanguageCode;
-
- if (oldValue != null)
- ContentEvents.InvokeAfterLocaleChanged(this.Monitor, oldValue.ToString(), newValue.ToString());
- this.PreviousLocale = newValue;
- }
-
- // save loaded event
- if (Context.IsSaveLoaded && !SaveGame.IsProcessing/*still loading save*/ && this.AfterLoadTimer >= 0)
- {
- if (this.AfterLoadTimer == 0)
- {
- SaveEvents.InvokeAfterLoad(this.Monitor);
- PlayerEvents.InvokeLoadedGame(this.Monitor, new EventArgsLoadedGameChanged(Game1.hasLoadedGame));
- TimeEvents.InvokeAfterDayStarted(this.Monitor);
- }
- this.AfterLoadTimer--;
- }
-
- // before exit to title
- if (Game1.exitToTitle)
- this.IsExiting = true;
-
- // after exit to title
- if (this.IsWorldReady && this.IsExiting && Game1.activeClickableMenu is TitleMenu)
- {
- SaveEvents.InvokeAfterReturnToTitle(this.Monitor);
- this.AfterLoadTimer = 5;
- this.IsExiting = false;
- }
-
- // input events
- {
- // get latest state
- this.KStateNow = Keyboard.GetState();
- this.MStateNow = Mouse.GetState();
- this.MPositionNow = new Point(Game1.getMouseX(), Game1.getMouseY());
-
- // raise key pressed
- foreach (var key in this.FramePressedKeys)
- ControlEvents.InvokeKeyPressed(this.Monitor, key);
-
- // raise key released
- foreach (var key in this.FrameReleasedKeys)
- ControlEvents.InvokeKeyReleased(this.Monitor, key);
-
- // raise controller button pressed
- for (var i = PlayerIndex.One; i <= PlayerIndex.Four; i++)
- {
- var buttons = this.GetFramePressedButtons(i);
- foreach (var button in buttons)
- {
- if (button == Buttons.LeftTrigger || button == Buttons.RightTrigger)
- ControlEvents.InvokeTriggerPressed(this.Monitor, i, button, button == Buttons.LeftTrigger ? GamePad.GetState(i).Triggers.Left : GamePad.GetState(i).Triggers.Right);
- else
- ControlEvents.InvokeButtonPressed(this.Monitor, i, button);
- }
- }
-
- // raise controller button released
- for (var i = PlayerIndex.One; i <= PlayerIndex.Four; i++)
- {
- foreach (var button in this.GetFrameReleasedButtons(i))
- {
- if (button == Buttons.LeftTrigger || button == Buttons.RightTrigger)
- ControlEvents.InvokeTriggerReleased(this.Monitor, i, button, button == Buttons.LeftTrigger ? GamePad.GetState(i).Triggers.Left : GamePad.GetState(i).Triggers.Right);
- else
- ControlEvents.InvokeButtonReleased(this.Monitor, i, button);
- }
- }
-
- // raise keyboard state changed
- if (this.KStateNow != this.KStatePrior)
- ControlEvents.InvokeKeyboardChanged(this.Monitor, this.KStatePrior, this.KStateNow);
-
- // raise mouse state changed
- if (this.MStateNow != this.MStatePrior)
- {
- ControlEvents.InvokeMouseChanged(this.Monitor, this.MStatePrior, this.MStateNow, this.MPositionPrior, this.MPositionNow);
- this.MStatePrior = this.MStateNow;
- this.MPositionPrior = this.MPositionNow;
- }
- }
-
- // menu events
- if (Game1.activeClickableMenu != this.PreviousActiveMenu)
- {
- IClickableMenu previousMenu = this.PreviousActiveMenu;
- IClickableMenu newMenu = Game1.activeClickableMenu;
-
- // raise save events
- // (saving is performed by SaveGameMenu; on days when the player shipping something, ShippingMenu wraps SaveGameMenu)
- if (newMenu is SaveGameMenu || newMenu is ShippingMenu)
- SaveEvents.InvokeBeforeSave(this.Monitor);
- else if (previousMenu is SaveGameMenu || previousMenu is ShippingMenu)
- {
- SaveEvents.InvokeAfterSave(this.Monitor);
- TimeEvents.InvokeAfterDayStarted(this.Monitor);
- }
-
- // raise menu events
- if (newMenu != null)
- MenuEvents.InvokeMenuChanged(this.Monitor, previousMenu, newMenu);
- else
- MenuEvents.InvokeMenuClosed(this.Monitor, previousMenu);
-
- // update previous menu
- // (if the menu was changed in one of the handlers, deliberately defer detection until the next update so mods can be notified of the new menu change)
- this.PreviousActiveMenu = newMenu;
- }
-
- // world & player events
- if (this.IsWorldReady)
- {
- // raise location list changed
- if (this.GetHash(Game1.locations) != this.PreviousGameLocations)
- {
- LocationEvents.InvokeLocationsChanged(this.Monitor, Game1.locations);
- this.PreviousGameLocations = this.GetHash(Game1.locations);
- }
-
- // raise current location changed
- if (Game1.currentLocation != this.PreviousGameLocation)
- {
- LocationEvents.InvokeCurrentLocationChanged(this.Monitor, this.PreviousGameLocation, Game1.currentLocation);
- this.PreviousGameLocation = Game1.currentLocation;
- }
-
- // raise player changed
- if (Game1.player != this.PreviousFarmer)
- {
- PlayerEvents.InvokeFarmerChanged(this.Monitor, this.PreviousFarmer, Game1.player);
- this.PreviousFarmer = Game1.player;
- }
-
- // raise player leveled up a skill
- if (Game1.player.combatLevel != this.PreviousCombatLevel)
- {
- PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Combat, Game1.player.combatLevel);
- this.PreviousCombatLevel = Game1.player.combatLevel;
- }
- if (Game1.player.farmingLevel != this.PreviousFarmingLevel)
- {
- PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Farming, Game1.player.farmingLevel);
- this.PreviousFarmingLevel = Game1.player.farmingLevel;
- }
- if (Game1.player.fishingLevel != this.PreviousFishingLevel)
- {
- PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Fishing, Game1.player.fishingLevel);
- this.PreviousFishingLevel = Game1.player.fishingLevel;
- }
- if (Game1.player.foragingLevel != this.PreviousForagingLevel)
- {
- PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Foraging, Game1.player.foragingLevel);
- this.PreviousForagingLevel = Game1.player.foragingLevel;
- }
- if (Game1.player.miningLevel != this.PreviousMiningLevel)
- {
- PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Mining, Game1.player.miningLevel);
- this.PreviousMiningLevel = Game1.player.miningLevel;
- }
- if (Game1.player.luckLevel != this.PreviousLuckLevel)
- {
- PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Luck, Game1.player.luckLevel);
- this.PreviousLuckLevel = Game1.player.luckLevel;
- }
-
- // raise player inventory changed
- ItemStackChange[] changedItems = this.GetInventoryChanges(Game1.player.items, this.PreviousItems).ToArray();
- if (changedItems.Any())
- {
- PlayerEvents.InvokeInventoryChanged(this.Monitor, Game1.player.items, changedItems);
- this.PreviousItems = Game1.player.items.Where(n => n != null).ToDictionary(n => n, n => n.Stack);
- }
-
- // raise current location's object list changed
- {
- int? objectHash = Game1.currentLocation?.objects != null ? this.GetHash(Game1.currentLocation.objects) : (int?)null;
- if (objectHash != null && this.PreviousLocationObjects != objectHash)
- {
- LocationEvents.InvokeOnNewLocationObject(this.Monitor, Game1.currentLocation.objects);
- this.PreviousLocationObjects = objectHash.Value;
- }
- }
-
- // raise time changed
- if (Game1.timeOfDay != this.PreviousTime)
- {
- TimeEvents.InvokeTimeOfDayChanged(this.Monitor, this.PreviousTime, Game1.timeOfDay);
- this.PreviousTime = Game1.timeOfDay;
- }
- if (Game1.dayOfMonth != this.PreviousDay)
- {
- TimeEvents.InvokeDayOfMonthChanged(this.Monitor, this.PreviousDay, Game1.dayOfMonth);
- this.PreviousDay = Game1.dayOfMonth;
- }
- if (Game1.currentSeason != this.PreviousSeason)
- {
- TimeEvents.InvokeSeasonOfYearChanged(this.Monitor, this.PreviousSeason, Game1.currentSeason);
- this.PreviousSeason = Game1.currentSeason;
- }
- if (Game1.year != this.PreviousYear)
- {
- TimeEvents.InvokeYearOfGameChanged(this.Monitor, this.PreviousYear, Game1.year);
- this.PreviousYear = Game1.year;
- }
-
- // raise mine level changed
- if (Game1.mine != null && Game1.mine.mineLevel != this.PreviousMineLevel)
- {
- MineEvents.InvokeMineLevelChanged(this.Monitor, this.PreviousMineLevel, Game1.mine.mineLevel);
- this.PreviousMineLevel = Game1.mine.mineLevel;
- }
- }
-
- // raise game day transition event (obsolete)
- if (Game1.newDay != this.PreviousIsNewDay)
- {
- TimeEvents.InvokeOnNewDay(this.Monitor, this.PreviousDay, Game1.dayOfMonth, Game1.newDay);
- this.PreviousIsNewDay = Game1.newDay;
- }
+ return this.WasButtonJustReleased(button, value > 0.2f ? ButtonState.Pressed : ButtonState.Released);
}
/// <summary>Get the player inventory changes between two states.</summary>
diff --git a/src/StardewModdingAPI/Framework/Serialisation/SemanticVersionConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs
index 52ec999e..6b5a6aaa 100644
--- a/src/StardewModdingAPI/Framework/Serialisation/SemanticVersionConverter.cs
+++ b/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs
@@ -1,11 +1,13 @@
using System;
+using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
+using StardewModdingAPI.Framework.Models;
namespace StardewModdingAPI.Framework.Serialisation
{
- /// <summary>Overrides how SMAPI reads and writes <see cref="ISemanticVersion"/>.</summary>
- internal class SemanticVersionConverter : JsonConverter
+ /// <summary>Overrides how SMAPI reads and writes <see cref="ISemanticVersion"/> and <see cref="IManifestDependency"/> fields.</summary>
+ internal class ManifestFieldConverter : JsonConverter
{
/*********
** Accessors
@@ -21,7 +23,7 @@ namespace StardewModdingAPI.Framework.Serialisation
/// <param name="objectType">The object type.</param>
public override bool CanConvert(Type objectType)
{
- return objectType == typeof(ISemanticVersion);
+ return objectType == typeof(ISemanticVersion) || objectType == typeof(IManifestDependency[]);
}
/// <summary>Reads the JSON representation of the object.</summary>
@@ -31,12 +33,31 @@ namespace StardewModdingAPI.Framework.Serialisation
/// <param name="serializer">The calling serializer.</param>
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
- JObject obj = JObject.Load(reader);
- int major = obj.Value<int>("MajorVersion");
- int minor = obj.Value<int>("MinorVersion");
- int patch = obj.Value<int>("PatchVersion");
- string build = obj.Value<string>("Build");
- return new SemanticVersion(major, minor, patch, build);
+ // semantic version
+ if (objectType == typeof(ISemanticVersion))
+ {
+ JObject obj = JObject.Load(reader);
+ int major = obj.Value<int>(nameof(ISemanticVersion.MajorVersion));
+ int minor = obj.Value<int>(nameof(ISemanticVersion.MinorVersion));
+ int patch = obj.Value<int>(nameof(ISemanticVersion.PatchVersion));
+ string build = obj.Value<string>(nameof(ISemanticVersion.Build));
+ return new SemanticVersion(major, minor, patch, build);
+ }
+
+ // manifest dependency
+ if (objectType == typeof(IManifestDependency[]))
+ {
+ List<IManifestDependency> result = new List<IManifestDependency>();
+ foreach (JObject obj in JArray.Load(reader).Children<JObject>())
+ {
+ string uniqueID = obj.Value<string>(nameof(IManifestDependency.UniqueID));
+ result.Add(new ManifestDependency(uniqueID));
+ }
+ return result.ToArray();
+ }
+
+ // unknown
+ throw new NotSupportedException($"Unknown type '{objectType?.FullName}'.");
}
/// <summary>Writes the JSON representation of the object.</summary>
diff --git a/src/StardewModdingAPI/IManifest.cs b/src/StardewModdingAPI/IManifest.cs
index d7c503a4..c036fdd3 100644
--- a/src/StardewModdingAPI/IManifest.cs
+++ b/src/StardewModdingAPI/IManifest.cs
@@ -5,28 +5,36 @@ namespace StardewModdingAPI
/// <summary>A manifest which describes a mod for SMAPI.</summary>
public interface IManifest
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>The mod name.</summary>
- string Name { get; set; }
+ string Name { get; }
/// <summary>A brief description of the mod.</summary>
- string Description { get; set; }
+ string Description { get; }
/// <summary>The mod author's name.</summary>
string Author { get; }
/// <summary>The mod version.</summary>
- ISemanticVersion Version { get; set; }
+ ISemanticVersion Version { get; }
/// <summary>The minimum SMAPI version required by this mod, if any.</summary>
- string MinimumApiVersion { get; set; }
+ string MinimumApiVersion { get; }
/// <summary>The unique mod ID.</summary>
- string UniqueID { get; set; }
+ string UniqueID { get; }
/// <summary>The name of the DLL in the directory that has the <see cref="Mod.Entry"/> method.</summary>
- string EntryDll { get; set; }
+ string EntryDll { get; }
+
+#if EXPERIMENTAL
+ /// <summary>The other mods that must be loaded before this mod.</summary>
+ IManifestDependency[] Dependencies { get; }
+#endif
/// <summary>Any manifest fields which didn't match a valid field.</summary>
- IDictionary<string, object> ExtraFields { get; set; }
+ IDictionary<string, object> ExtraFields { get; }
}
} \ No newline at end of file
diff --git a/src/StardewModdingAPI/IManifestDependency.cs b/src/StardewModdingAPI/IManifestDependency.cs
new file mode 100644
index 00000000..7bd2e8b6
--- /dev/null
+++ b/src/StardewModdingAPI/IManifestDependency.cs
@@ -0,0 +1,12 @@
+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; }
+ }
+}
diff --git a/src/StardewModdingAPI/Mod.cs b/src/StardewModdingAPI/Mod.cs
index 8033e1fd..171088cf 100644
--- a/src/StardewModdingAPI/Mod.cs
+++ b/src/StardewModdingAPI/Mod.cs
@@ -1,11 +1,12 @@
using System;
using System.IO;
using StardewModdingAPI.Framework;
+using StardewModdingAPI.Framework.Models;
namespace StardewModdingAPI
{
/// <summary>The base class for a mod.</summary>
- public class Mod : IMod
+ public class Mod : IMod, IDisposable
{
/*********
** Properties
@@ -88,6 +89,14 @@ namespace StardewModdingAPI
/// <param name="helper">Provides simplified APIs for writing mods.</param>
public virtual void Entry(IModHelper helper) { }
+ /// <summary>Release or reset unmanaged resources.</summary>
+ public void Dispose()
+ {
+ (this.Helper as IDisposable)?.Dispose(); // deliberate do this outside overridable dispose method so mods don't accidentally suppress it
+ this.Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
/*********
** Private methods
@@ -106,5 +115,15 @@ namespace StardewModdingAPI
}
return Path.Combine(this.PathOnDisk, "psconfigs");
}
+
+ /// <summary>Release or reset unmanaged resources when the game exits. There's no guarantee this will be called on every exit.</summary>
+ /// <param name="disposing">Whether the instance is being disposed explicitly rather than finalised. If this is false, the instance shouldn't dispose other objects since they may already be finalised.</param>
+ protected virtual void Dispose(bool disposing) { }
+
+ /// <summary>Destruct the instance.</summary>
+ ~Mod()
+ {
+ this.Dispose(false);
+ }
}
}
diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs
index 1e5fcfc3..3a7cb9ce 100644
--- a/src/StardewModdingAPI/Program.cs
+++ b/src/StardewModdingAPI/Program.cs
@@ -15,6 +15,8 @@ using StardewModdingAPI.Events;
using StardewModdingAPI.Framework;
using StardewModdingAPI.Framework.Logging;
using StardewModdingAPI.Framework.Models;
+using StardewModdingAPI.Framework.ModLoading;
+using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Framework.Serialisation;
using StardewValley;
using Monitor = StardewModdingAPI.Framework.Monitor;
@@ -40,6 +42,9 @@ namespace StardewModdingAPI
/// <summary>Tracks whether the game should exit immediately and any pending initialisation should be cancelled.</summary>
private readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource();
+ /// <summary>Simplifies access to private game code.</summary>
+ private readonly IReflectionHelper Reflection = new ReflectionHelper();
+
/// <summary>The underlying game instance.</summary>
private SGame GameInstance;
@@ -141,7 +146,7 @@ namespace StardewModdingAPI
AppDomain.CurrentDomain.UnhandledException += (sender, e) => this.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error);
// override game
- this.GameInstance = new SGame(this.Monitor);
+ this.GameInstance = new SGame(this.Monitor, this.Reflection);
StardewValley.Program.gamePtr = this.GameInstance;
// add exit handler
@@ -150,7 +155,16 @@ namespace StardewModdingAPI
this.CancellationTokenSource.Token.WaitHandle.WaitOne();
if (this.IsGameRunning)
{
- this.GameInstance.Exiting += (sender, e) => this.PressAnyKeyToExit();
+ try
+ {
+ File.WriteAllText(Constants.FatalCrashMarker, string.Empty);
+ File.Copy(Constants.DefaultLogPath, Constants.FatalCrashLog, overwrite: true);
+ }
+ catch (Exception ex)
+ {
+ this.Monitor.Log($"SMAPI failed trying to track the crash details: {ex.GetLogSummary()}");
+ }
+
this.GameInstance.Exit();
}
}).Start();
@@ -162,7 +176,7 @@ namespace StardewModdingAPI
this.GameInstance.Exiting += (sender, e) => this.Dispose();
this.GameInstance.Window.ClientSizeChanged += (sender, e) => GraphicsEvents.InvokeResize(this.Monitor, sender, e);
GameEvents.InitializeInternal += (sender, e) => this.InitialiseAfterGameStart();
- GameEvents.GameLoaded += (sender, e) => this.CheckForUpdateAsync();
+ GameEvents.GameLoadedInternal += (sender, e) => this.CheckForUpdateAsync();
// set window titles
this.GameInstance.Window.Title = $"Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)} - running SMAPI {Constants.ApiVersion}";
@@ -175,6 +189,17 @@ namespace StardewModdingAPI
return;
}
+ // 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("Press any key to delete the crash data and continue playing.", LogLevel.Info);
+ Console.ReadKey();
+ File.Delete(Constants.FatalCrashLog);
+ File.Delete(Constants.FatalCrashMarker);
+ }
+
// start game
this.Monitor.Log("Starting game...");
try
@@ -204,14 +229,25 @@ 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;
- // dispose mod helpers
- foreach (var mod in this.ModRegistry.GetMods())
- (mod.Helper as IDisposable)?.Dispose();
+ // dispose mod data
+ foreach (IMod mod in this.ModRegistry.GetMods())
+ {
+ try
+ {
+ (mod as IDisposable)?.Dispose();
+ }
+ catch (Exception ex)
+ {
+ this.Monitor.Log($"The {mod.ModManifest.Name} mod failed during disposal: {ex.GetLogSummary()}.", LogLevel.Warn);
+ }
+ }
// dispose core components
this.IsGameRunning = false;
@@ -230,9 +266,10 @@ namespace StardewModdingAPI
{
// load settings
this.Settings = JsonConvert.DeserializeObject<SConfig>(File.ReadAllText(Constants.ApiConfigPath));
+ this.GameInstance.VerboseLogging = this.Settings.VerboseLogging;
// load core components
- this.ModRegistry = new ModRegistry(this.Settings.ModCompatibility);
+ this.ModRegistry = new ModRegistry();
this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry);
this.CommandManager = new CommandManager();
@@ -266,13 +303,70 @@ namespace StardewModdingAPI
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)
this.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn);
+ if (this.Settings.VerboseLogging)
+ this.Monitor.Log("Verbose logging enabled.", LogLevel.Trace);
// validate XNB integrity
if (!this.ValidateContentIntegrity())
this.Monitor.Log("SMAPI found problems in the game's XNB files which may cause errors or crashes while you're playing. Consider uninstalling XNB mods or reinstalling the game.", LogLevel.Warn);
// load mods
- int modsLoaded = this.LoadMods();
+ int modsLoaded;
+ {
+ this.Monitor.Log("Loading mod metadata...");
+ ModResolver resolver = new ModResolver();
+
+ // load manifests
+ IModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, new JsonHelper(), this.Settings.ModCompatibility).ToArray();
+ resolver.ValidateManifests(mods, Constants.ApiVersion);
+
+ // check for deprecated metadata
+ IList<Action> deprecationWarnings = new List<Action>();
+ foreach (IModMetadata mod in mods.Where(m => m.Status != ModMetadataStatus.Failed))
+ {
+ // missing fields that will be required in SMAPI 2.0
+ {
+ List<string> missingFields = new List<string>(3);
+
+ if (string.IsNullOrWhiteSpace(mod.Manifest.Name))
+ missingFields.Add(nameof(IManifest.Name));
+ if (mod.Manifest.Version.ToString() == "0.0")
+ missingFields.Add(nameof(IManifest.Version));
+ if (string.IsNullOrWhiteSpace(mod.Manifest.UniqueID))
+ missingFields.Add(nameof(IManifest.UniqueID));
+
+ if (missingFields.Any())
+ deprecationWarnings.Add(() => this.Monitor.Log($"{mod.Manifest.Name} is missing some manifest fields ({string.Join(", ", missingFields)}) which will be required in an upcoming SMAPI version.", LogLevel.Warn));
+ }
+
+ // per-save directories
+ if ((mod.Manifest as Manifest)?.PerSaveConfigs == true)
+ {
+ deprecationWarnings.Add(() => this.DeprecationManager.Warn(mod.DisplayName, $"{nameof(Manifest)}.{nameof(Manifest.PerSaveConfigs)}", "1.0", DeprecationLevel.Info));
+ try
+ {
+ string psDir = Path.Combine(mod.DirectoryPath, "psconfigs");
+ Directory.CreateDirectory(psDir);
+ if (!Directory.Exists(psDir))
+ mod.SetStatus(ModMetadataStatus.Failed, "it requires per-save configuration files ('psconfigs') which couldn't be created for some reason.");
+ }
+ catch (Exception ex)
+ {
+ mod.SetStatus(ModMetadataStatus.Failed, $"it requires per-save configuration files ('psconfigs') which couldn't be created: {ex.GetLogSummary()}");
+ }
+ }
+ }
+
+#if EXPERIMENTAL
+ // process dependencies
+ mods = resolver.ProcessDependencies(mods).ToArray();
+#endif
+
+ // load mods
+ modsLoaded = this.LoadMods(mods, new JsonHelper(), (SContentManager)Game1.content, deprecationWarnings);
+ foreach (Action warning in deprecationWarnings)
+ warning();
+ }
if (this.Monitor.IsExiting)
{
this.Monitor.Log("SMAPI shutting down: aborting initialisation.", LogLevel.Warn);
@@ -347,7 +441,7 @@ namespace StardewModdingAPI
string[] fields = entry.Value.Split('/');
if (fields.Length < SObject.objectInfoDescriptionIndex + 1)
{
- LogIssue(entry.Key, $"too few fields for an object");
+ LogIssue(entry.Key, "too few fields for an object");
issuesFound = true;
continue;
}
@@ -406,157 +500,48 @@ namespace StardewModdingAPI
}
}
- /// <summary>Load and hook up all mods in the mod directory.</summary>
- /// <returns>Returns the number of mods loaded.</returns>
- private int LoadMods()
+ /// <summary>Load and hook up the given mods.</summary>
+ /// <param name="mods">The mods to load.</param>
+ /// <param name="jsonHelper">The JSON helper with which to read mods' JSON files.</param>
+ /// <param name="contentManager">The content manager to use for mod content.</param>
+ /// <param name="deprecationWarnings">A list to populate with any deprecation warnings.</param>
+ /// <returns>Returns the number of mods successfully loaded.</returns>
+ private int LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, SContentManager contentManager, IList<Action> deprecationWarnings)
{
this.Monitor.Log("Loading mods...");
-
- // get JSON helper
- JsonHelper jsonHelper = new JsonHelper();
-
- // get assembly loader
- AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor);
- AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name);
+ void LogSkip(IModMetadata mod, string reasonPhrase, LogLevel level = LogLevel.Error) => this.Monitor.Log($"Skipped {mod.DisplayName} because {reasonPhrase}", level);
// load mod assemblies
int modsLoaded = 0;
- List<Action> deprecationWarnings = new List<Action>(); // queue up deprecation warnings to show after mod list
- foreach (string directoryPath in Directory.GetDirectories(Constants.ModPath))
+ AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor);
+ AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name);
+ foreach (IModMetadata metadata in mods)
{
- if (this.Monitor.IsExiting)
- {
- this.Monitor.Log("SMAPI shutting down: aborting mod scan.", LogLevel.Warn);
- return modsLoaded;
- }
-
- // passthrough empty directories
- DirectoryInfo directory = new DirectoryInfo(directoryPath);
- while (!directory.GetFiles().Any() && directory.GetDirectories().Length == 1)
- directory = directory.GetDirectories().First();
-
- // get manifest path
- string manifestPath = Path.Combine(directory.FullName, "manifest.json");
- if (!File.Exists(manifestPath))
- {
- this.Monitor.Log($"Ignored folder \"{directory.Name}\" which doesn't have a manifest.json.", LogLevel.Warn);
- continue;
- }
- string skippedPrefix = $"Skipped {manifestPath.Replace(Constants.ModPath, "").Trim('/', '\\')}";
-
- // read manifest
- Manifest manifest;
- try
+ // validate status
+ if (metadata.Status == ModMetadataStatus.Failed)
{
- // read manifest text
- string json = File.ReadAllText(manifestPath);
- if (string.IsNullOrEmpty(json))
- {
- this.Monitor.Log($"{skippedPrefix} because the manifest is empty.", LogLevel.Error);
- continue;
- }
-
- // deserialise manifest
- manifest = jsonHelper.ReadJsonFile<Manifest>(Path.Combine(directory.FullName, "manifest.json"));
- if (manifest == null)
- {
- this.Monitor.Log($"{skippedPrefix} because its manifest is invalid.", LogLevel.Error);
- continue;
- }
- if (string.IsNullOrEmpty(manifest.EntryDll))
- {
- this.Monitor.Log($"{skippedPrefix} because its manifest doesn't specify an entry DLL.", LogLevel.Error);
- continue;
- }
- }
- catch (Exception ex)
- {
- this.Monitor.Log($"{skippedPrefix} because manifest parsing failed.\n{ex.GetLogSummary()}", LogLevel.Error);
- continue;
- }
- if (!string.IsNullOrWhiteSpace(manifest.Name))
- skippedPrefix = $"Skipped {manifest.Name}";
-
- // validate compatibility
- ModCompatibility compatibility = this.ModRegistry.GetCompatibilityRecord(manifest);
- if (compatibility?.Compatibility == ModCompatibilityType.AssumeBroken)
- {
- bool hasOfficialUrl = !string.IsNullOrWhiteSpace(compatibility.UpdateUrl);
- bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl);
-
- string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game";
- string warning = $"{skippedPrefix} because {reasonPhrase}. Please check for a version newer than {compatibility.UpperVersion} here:";
- if (hasOfficialUrl)
- warning += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}";
- if (hasUnofficialUrl)
- warning += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}";
-
- this.Monitor.Log(warning, LogLevel.Error);
+ LogSkip(metadata, metadata.Error);
continue;
}
- // validate SMAPI version
- if (!string.IsNullOrWhiteSpace(manifest.MinimumApiVersion))
- {
- try
- {
- ISemanticVersion minVersion = new SemanticVersion(manifest.MinimumApiVersion);
- if (minVersion.IsNewerThan(Constants.ApiVersion))
- {
- this.Monitor.Log($"{skippedPrefix} because it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod.", LogLevel.Error);
- continue;
- }
- }
- catch (FormatException ex) when (ex.Message.Contains("not a valid semantic version"))
- {
- this.Monitor.Log($"{skippedPrefix} because it has an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}.", LogLevel.Error);
- continue;
- }
- }
-
- // create per-save directory
- if (manifest.PerSaveConfigs)
- {
- deprecationWarnings.Add(() => this.DeprecationManager.Warn(manifest.Name, $"{nameof(Manifest)}.{nameof(Manifest.PerSaveConfigs)}", "1.0", DeprecationLevel.Info));
- try
- {
- string psDir = Path.Combine(directory.FullName, "psconfigs");
- Directory.CreateDirectory(psDir);
- if (!Directory.Exists(psDir))
- {
- this.Monitor.Log($"{skippedPrefix} because it requires per-save configuration files ('psconfigs') which couldn't be created for some reason.", LogLevel.Error);
- continue;
- }
- }
- catch (Exception ex)
- {
- this.Monitor.Log($"{skippedPrefix} because it requires per-save configuration files ('psconfigs') which couldn't be created:\n{ex.GetLogSummary()}", LogLevel.Error);
- continue;
- }
- }
-
- // validate mod path to simplify errors
- string assemblyPath = Path.Combine(directory.FullName, manifest.EntryDll);
- if (!File.Exists(assemblyPath))
- {
- this.Monitor.Log($"{skippedPrefix} because its DLL '{manifest.EntryDll}' doesn't exist.", LogLevel.Error);
- continue;
- }
+ // get basic info
+ IManifest manifest = metadata.Manifest;
+ string assemblyPath = Path.Combine(metadata.DirectoryPath, metadata.Manifest.EntryDll);
// preprocess & load mod assembly
Assembly modAssembly;
try
{
- modAssembly = modAssemblyLoader.Load(assemblyPath, assumeCompatible: compatibility?.Compatibility == ModCompatibilityType.AssumeCompatible);
+ modAssembly = modAssemblyLoader.Load(assemblyPath, assumeCompatible: metadata.Compatibility?.Compatibility == ModCompatibilityType.AssumeCompatible);
}
catch (IncompatibleInstructionException ex)
{
- this.Monitor.Log($"{skippedPrefix} because it's not compatible with the latest version of the game (detected {ex.NounPhrase}). Please check for a newer version of the mod (you have v{manifest.Version}).", LogLevel.Error);
+ LogSkip(metadata, $"it's not compatible with the latest version of the game (detected {ex.NounPhrase}). Please check for a newer version of the mod (you have v{manifest.Version}).");
continue;
}
catch (Exception ex)
{
- this.Monitor.Log($"{skippedPrefix} because its DLL '{manifest.EntryDll}' couldn't be loaded.\n{ex.GetLogSummary()}", LogLevel.Error);
+ LogSkip(metadata, $"its DLL '{manifest.EntryDll}' couldn't be loaded:\n{ex.GetLogSummary()}");
continue;
}
@@ -566,18 +551,18 @@ namespace StardewModdingAPI
int modEntries = modAssembly.DefinedTypes.Count(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract);
if (modEntries == 0)
{
- this.Monitor.Log($"{skippedPrefix} because its DLL has no '{nameof(Mod)}' subclass.", LogLevel.Error);
+ LogSkip(metadata, $"its DLL has no '{nameof(Mod)}' subclass.");
continue;
}
if (modEntries > 1)
{
- this.Monitor.Log($"{skippedPrefix} because its DLL contains multiple '{nameof(Mod)}' subclasses.", LogLevel.Error);
+ LogSkip(metadata, $"its DLL contains multiple '{nameof(Mod)}' subclasses.");
continue;
}
}
catch (Exception ex)
{
- this.Monitor.Log($"{skippedPrefix} because its DLL couldn't be loaded.\n{ex.GetLogSummary()}", LogLevel.Error);
+ LogSkip(metadata, $"its DLL couldn't be loaded:\n{ex.GetLogSummary()}");
continue;
}
@@ -589,16 +574,15 @@ namespace StardewModdingAPI
Mod mod = (Mod)modAssembly.CreateInstance(modEntryType.ToString());
if (mod == null)
{
- this.Monitor.Log($"{skippedPrefix} because its entry class couldn't be instantiated.");
+ LogSkip(metadata, "its entry class couldn't be instantiated.");
continue;
}
// inject data
- // get helper
mod.ModManifest = manifest;
- mod.Helper = new ModHelper(manifest, directory.FullName, jsonHelper, this.ModRegistry, this.CommandManager, (SContentManager)Game1.content);
+ mod.Helper = new ModHelper(manifest, metadata.DirectoryPath, jsonHelper, this.ModRegistry, this.CommandManager, contentManager, this.Reflection);
mod.Monitor = this.GetSecondaryMonitor(manifest.Name);
- mod.PathOnDisk = directory.FullName;
+ mod.PathOnDisk = metadata.DirectoryPath;
// track mod
this.ModRegistry.Add(mod);
@@ -607,11 +591,11 @@ namespace StardewModdingAPI
}
catch (Exception ex)
{
- this.Monitor.Log($"{skippedPrefix} because initialisation failed:\n{ex.GetLogSummary()}", LogLevel.Error);
+ LogSkip(metadata, $"initialisation failed:\n{ex.GetLogSummary()}");
}
}
- // initialise mods
+ // initialise loaded mods
foreach (IMod mod in this.ModRegistry.GetMods())
{
try
@@ -632,9 +616,6 @@ namespace StardewModdingAPI
// print result
this.Monitor.Log($"Loaded {modsLoaded} mods.");
- foreach (Action warning in deprecationWarnings)
- warning();
-
return modsLoaded;
}
diff --git a/src/StardewModdingAPI/Properties/AssemblyInfo.cs b/src/StardewModdingAPI/Properties/AssemblyInfo.cs
index 348c2109..b0a065f5 100644
--- a/src/StardewModdingAPI/Properties/AssemblyInfo.cs
+++ b/src/StardewModdingAPI/Properties/AssemblyInfo.cs
@@ -1,6 +1,9 @@
using System.Reflection;
+using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("Stardew Modding API (SMAPI)")]
[assembly: AssemblyDescription("A modding API for Stardew Valley.")]
[assembly: Guid("5c3f7f42-fefd-43db-aaea-92ea3bcad531")]
+[assembly: InternalsVisibleTo("StardewModdingAPI.Tests")]
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Moq for unit testing
diff --git a/src/StardewModdingAPI/SemanticVersion.cs b/src/StardewModdingAPI/SemanticVersion.cs
index db25dc11..a2adb657 100644
--- a/src/StardewModdingAPI/SemanticVersion.cs
+++ b/src/StardewModdingAPI/SemanticVersion.cs
@@ -182,6 +182,23 @@ namespace StardewModdingAPI
return result;
}
+ /// <summary>Parse a version string without throwing an exception if it fails.</summary>
+ /// <param name="version">The version string.</param>
+ /// <param name="parsed">The parsed representation.</param>
+ /// <returns>Returns whether parsing the version succeeded.</returns>
+ internal static bool TryParse(string version, out ISemanticVersion parsed)
+ {
+ try
+ {
+ parsed = new SemanticVersion(version);
+ return true;
+ }
+ catch
+ {
+ parsed = null;
+ return false;
+ }
+ }
/*********
** Private methods
diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json
index 9438c621..08bd3cff 100644
--- a/src/StardewModdingAPI/StardewModdingAPI.config.json
+++ b/src/StardewModdingAPI/StardewModdingAPI.config.json
@@ -22,6 +22,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha
"CheckForUpdates": true,
/**
+ * Whether SMAPI should log more information about the game context.
+ */
+ "VerboseLogging": false,
+
+ /**
* A list of mod versions SMAPI should consider compatible or broken regardless of whether it
* detects incompatible code. Each record can be set to `AssumeCompatible` or `AssumeBroken`.
* Changing this field is not recommended and may destabilise your game.
@@ -29,251 +34,384 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha
"ModCompatibility": [
{
"Name": "AccessChestAnywhere",
- "ID": "AccessChestAnywhere",
+ "ID": [ "AccessChestAnywhere" ],
"UpperVersion": "1.1",
"Compatibility": "AssumeBroken",
"UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/257",
"UnofficialUpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/518",
- "Notes": "Crashes with 'Method not found: Void StardewValley.Item.set_Name(System.String)'."
+ "Notes": "Needs update for SDV 1.1."
},
{
"Name": "Almighty Tool",
- "ID": "AlmightyTool.dll",
+ "ID": [ "AlmightyTool.dll" ],
"UpperVersion": "1.1.1",
"Compatibility": "AssumeBroken",
"UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/439",
- "Notes": "Uses obsolete StardewModdingAPI.Extensions."
+ "Notes": "Needs update for SDV 1.2."
},
{
"Name": "Better Sprinklers",
- "ID": "SPDSprinklersMod",
+ "ID": [ "SPDSprinklersMod", /*since 2.3*/ "Speeder.BetterSprinklers" ],
"UpperVersion": "2.3",
"Compatibility": "AssumeBroken",
"UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/41",
- "Notes": "Uses obsolete StardewModdingAPI.Extensions."
+ "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096",
+ "Notes": "Needs update for SDV 1.2."
},
{
- "Name": "Better Sprinklers",
- "ID": "Speeder.BetterSprinklers",
- "UpperVersion": "2.3",
+ "Name": "Birthday Mail",
+ "ID": [ "005e02dc-d900-425c-9c68-1ff55c5a295d" ],
+ "UpperVersion": "1.2.2",
"Compatibility": "AssumeBroken",
- "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/41",
- "Notes": "ID changed in 2.3. Uses obsolete StardewModdingAPI.Extensions."
+ "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/276",
+ "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096",
+ "Notes": "Needs update for SDV 1.2."
},
{
"Name": "Chest Label System",
- "ID": "SPDChestLabel",
- "UpperVersion": "1.5",
+ "ID": [ "SPDChestLabel" ],
+ "UpperVersion": "1.6",
"Compatibility": "AssumeBroken",
"UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/242",
- "Notes": "Not compatible with Stardew Valley 1.1+"
+ "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096",
+ "Notes": "Needs update for SDV 1.1."
},
{
- "Name": "Chests Anywhere",
- "ID": "ChestsAnywhere",
- "UpperVersion": "1.8.2",
+ "Name": "Chest Pooling",
+ "ID": [ "ChestPooling.dll" ],
+ "UpperVersion": "1.2",
"Compatibility": "AssumeBroken",
- "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/518",
- "Notes": "Crashes with 'Method not found: Void StardewValley.Menus.TextBox.set_Highlighted(Boolean)'."
+ "UpdateUrl": "http://community.playstarbound.com/threads/111988",
+ "Notes": "Needs update for SDV 1.2."
},
{
"Name": "Chests Anywhere",
- "ID": "Pathoschild.ChestsAnywhere",
+ "ID": [ "ChestsAnywhere", /*since 1.9*/ "Pathoschild.ChestsAnywhere" ],
"UpperVersion": "1.9-beta",
"Compatibility": "AssumeBroken",
"UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/518",
- "Notes": "ID changed in 1.9. Crashes with InvalidOperationException: 'The menu doesn't seem to have a player inventory'."
+ "Notes": "Needs update for SDV 1.2."
},
{
"Name": "CJB Automation",
- "ID": "CJBAutomation",
+ "ID": [ "CJBAutomation" ],
"UpperVersion": "1.4",
"Compatibility": "AssumeBroken",
"UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/211",
- "Notes": "Crashes with 'Method not found: Void StardewValley.Item.set_Name(System.String)'."
+ "UnofficialUpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/1063",
+ "Notes": "Needs update for SDV 1.2."
},
{
"Name": "CJB Cheats Menu",
- "ID": "CJBCheatsMenu",
+ "ID": [ "CJBCheatsMenu" ],
"UpperVersion": "1.12",
"UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/4",
- "Notes": "Not compatible with Stardew Valley 1.1+"
+ "Notes": "Needs update for SDV 1.1."
},
{
"Name": "CJB Item Spawner",
- "ID": "CJBItemSpawner",
+ "ID": [ "CJBItemSpawner" ],
"UpperVersion": "1.5",
"UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/93",
- "Notes": "Not compatible with Stardew Valley 1.1+"
+ "Notes": "Needs update for SDV 1.1."
},
{
"Name": "CJB Show Item Sell Price",
- "ID": "CJBShowItemSellPrice",
+ "ID": [ "CJBShowItemSellPrice" ],
"UpperVersion": "1.6",
"Compatibility": "AssumeBroken",
"UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/93",
- "Notes": "Uses SMAPI's internal SGame class."
+ "Notes": "Needs update for SDV 1.2."
},
{
"Name": "Cooking Skill",
- "ID": "CookingSkill",
+ "ID": [ "CookingSkill" ],
"UpperVersion": "1.0.3",
"Compatibility": "AssumeBroken",
"UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/522",
- "Notes": "Crashes with 'Method not found: Void StardewValley.Buff..ctor(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, System.String)'."
+ "Notes": "Needs update for SDV 1.2."
+ },
+ {
+ "Name": "Cooking Skill Prestige Adapter",
+ "ID": [ "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63" ],
+ "UpperVersion": "1.0.4",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/569",
+ "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096",
+ "Notes": "Needs update for SDV 1.2."
},
{
"Name": "Enemy Health Bars",
- "ID": "SPDHealthBar",
+ "ID": [ "SPDHealthBar" ],
"UpperVersion": "1.7",
"Compatibility": "AssumeBroken",
"UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/193",
- "Notes": "Uses obsolete GraphicsEvents.DrawTick."
+ "UnofficialUrl": "http://community.playstarbound.com/threads/132096",
+ "Notes": "Needs update for SDV 1.2."
},
{
"Name": "Entoarox Framework",
- "ID": "eacdb74b-4080-4452-b16b-93773cda5cf9",
- "UpperVersion": "1.6.5",
+ "ID": [ "eacdb74b-4080-4452-b16b-93773cda5cf9", /*since ???*/ "Entoarox.EntoaroxFramework" ],
+ "UpperVersion": "1.7.5",
"Compatibility": "AssumeBroken",
"UpdateUrl": "http://community.playstarbound.com/resources/4228",
- "Notes": "Uses obsolete StardewModdingAPI.Inheritance.SObject until 1.6.1; then crashes until 1.6.4 ('Entoarox Framework requested an immediate game shutdown: Fatal error attempting to update player tick properties System.NullReferenceException: Object reference not set to an instance of an object. at Entoarox.Framework.PlayerHelper.Update(Object s, EventArgs e)')."
+ "Notes": "Needs update for SDV 1.2."
},
{
"Name": "Extended Fridge",
- "ID": "Mystra007ExtendedFridge",
+ "ID": [ "Mystra007ExtendedFridge" ],
"UpperVersion": "1.0",
+ "UpperVersionLabel": "0.94",
"Compatibility": "AssumeBroken",
"UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/485",
- "Notes": "Actual upper version is 0.94, but mod incorrectly sets it to 1.0 in the manifest. Crashes with 'Field not found: StardewValley.Game1.mouseCursorTransparency'."
+ "Notes": "Needs update for SDV 1.2. Actual upper version is 0.94, but mod incorrectly sets it to 1.0 in the manifest."
+ },
+ {
+ "Name": "FarmAutomation.ItemCollector",
+ "ID": [ "FarmAutomation.ItemCollector.dll", /*since 0.4*/ "Maddy99.FarmAutomation.ItemCollector" ],
+ "UpperVersion": "0.4",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://community.playstarbound.com/threads/125172",
+ "Notes": "Needs update for SDV 1.2."
+ },
+ {
+ "Name": "Instant Geode",
+ "ID": [ "InstantGeode" ],
+ "UpperVersion": "1.12",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://community.playstarbound.com/threads/109038",
+ "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096",
+ "Notes": "Needs update for SDV 1.2."
+ },
+ {
+ "Name": "Gate Opener",
+ "ID": [ "GateOpener.dll", /*since 1.1*/ "mralbobo.GateOpener" ],
+ "UpperVersion": "1.0.1",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://community.playstarbound.com/threads/111988",
+ "Notes": "Needs update for SDV 1.2."
},
{
"Name": "Get Dressed",
- "ID": "GetDressed.dll",
- "UpperVersion": "3.2",
+ "ID": [ "GetDressed.dll", /*since 3.3*/ "Advize.GetDressed" ],
+ "UpperVersion": "3.3",
"Compatibility": "AssumeBroken",
"UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/331",
- "Notes": "Crashes with NullReferenceException in GameEvents.UpdateTick."
+ "Notes": "Needs update for SDV 1.2."
},
{
- "Name": "Lookup Anything",
- "ID": "LookupAnything",
- "UpperVersion": "1.10",
+ "Name": "Gift Taste Helper",
+ "ID": [ "8008db57-fa67-4730-978e-34b37ef191d6" ],
+ "UpperVersion": "2.3.1",
"Compatibility": "AssumeBroken",
- "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/541",
- "Notes": "Crashes with FormatException when looking up NPCs."
+ "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/229",
+ "Notes": "Needs update for SDV 1.2."
},
{
"Name": "Lookup Anything",
- "ID": "Pathoschild.LookupAnything",
+ "ID": [ "LookupAnything", /*since 1.10.1*/ "Pathoschild.LookupAnything" ],
"UpperVersion": "1.10.1",
"Compatibility": "AssumeBroken",
"UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/541",
- "Notes": "ID changed in 1.10.1. Crashes with FormatException when looking up NPCs."
+ "Notes": "Needs update for SDV 1.2."
},
{
"Name": "Makeshift Multiplayer",
- "ID": "StardewValleyMP",
+ "ID": [ "StardewValleyMP", /*since 0.3*/ "spacechase0.StardewValleyMP" ],
"Compatibility": "AssumeBroken",
- "UpperVersion": "0.2.10",
+ "UpperVersion": "0.3.3",
"UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/501",
- "Notes": "Uses obsolete GraphicsEvents.OnPreRenderHudEventNoCheck."
+ "Notes": "Needs update for SDV 1.2."
},
{
"Name": "NoSoilDecay",
- "ID": "289dee03-5f38-4d8e-8ffc-e440198e8610",
+ "ID": [ "289dee03-5f38-4d8e-8ffc-e440198e8610" ],
"UpperVersion": "0.5",
"Compatibility": "AssumeBroken",
"UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/237",
- "Notes": "Uses obsolete StardewModdingAPI.Extensions and Assembly.GetExecutingAssembly().Location."
+ "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096",
+ "Notes": "Needs update for SDV 1.2, and uses Assembly.GetExecutingAssembly().Location."
},
{
"Name": "NPC Map Locations",
- "ID": "NPCMapLocationsMod",
+ "ID": [ "NPCMapLocationsMod" ],
"LowerVersion": "1.42",
"UpperVersion": "1.43",
"Compatibility": "AssumeBroken",
"UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/239",
- "ReasonPhrase": "this version has an update check error which crashes the game"
+ "ReasonPhrase": "These versions have an update check error which crash the game."
+ },
+ {
+ "Name": "Part of the Community",
+ "ID": [ "SB_PotC" ],
+ "UpperVersion": "1.0.8",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/923",
+ "ReasonPhrase": "Needs update for SDV 1.2."
},
{
"Name": "Point-and-Plant",
- "ID": "PointAndPlant.dll",
+ "ID": [ "PointAndPlant.dll" ],
"UpperVersion": "1.0.2",
"Compatibility": "AssumeBroken",
"UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/572",
- "Notes": "Uses obsolete StardewModdingAPI.Extensions."
+ "Notes": "Needs update for SDV 1.2."
+ },
+ {
+ "Name": "PrairieKingMadeEasy",
+ "ID": [ "PrairieKingMadeEasy.dll" ],
+ "UpperVersion": "1.0.0",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://community.playstarbound.com/resources/3594",
+ "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096",
+ "Notes": "Needs update for SDV 1.2."
+ },
+ {
+ "Name": "Rush Orders",
+ "ID": [ "RushOrders", /*since 1.1*/ "spacechase0.RushOrders" ],
+ "UpperVersion": "1.1",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/605",
+ "Notes": "Needs update for SDV 1.2."
},
{
"Name": "Save Anywhere",
- "ID": "SaveAnywhere",
- "UpperVersion": "2.0",
+ "ID": [ "SaveAnywhere" ],
+ "UpperVersion": "2.3",
"Compatibility": "AssumeBroken",
"UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/444",
- "Notes": "Depends on StarDustCore."
+ "Notes": "Needs update for SDV 1.2."
},
{
- "Name": "StackSplitX",
- "ID": "StackSplitX.dll",
+ "Name": "Simple Sprinklers",
+ "ID": [ "SimpleSprinkler.dll" ],
+ "UpperVersion": "1.4",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/76",
+ "Notes": "Needs update for SDV 1.2."
+ },
+ {
+ "Name": "Sprint and Dash",
+ "ID": [ "SPDSprintAndDash" ],
"UpperVersion": "1.0",
"Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://community.playstarbound.com/resources/3531",
+ "UnofficialUpdateUrl": "http://community.playstarbound.com/resources/4201",
+ "Notes": "Needs update for SDV 1.2."
+ },
+ {
+ "Name": "Sprint and Dash Redux",
+ "ID": [ "SPDSprintAndDash" ],
+ "UpperVersion": "1.2",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://community.playstarbound.com/resources/4201",
+ "Notes": "Needs update for SDV 1.2."
+ },
+ {
+ "Name": "StackSplitX",
+ "ID": [ "StackSplitX.dll" ],
+ "UpperVersion": "1.2",
+ "Compatibility": "AssumeBroken",
"UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/798",
- "Notes": "Uses SMAPI's internal SGame class."
+ "Notes": "Needs update for SDV 1.2."
},
{
"Name": "StarDustCore",
- "ID": "StarDustCore",
+ "ID": [ "StarDustCore" ],
"UpperVersion": "1.0",
"Compatibility": "AssumeBroken",
"UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/683",
- "Notes": "Crashes with 'Method not found: Void StardewModdingAPI.Command.CallCommand(System.String)'."
+ "Notes": "Obsolete (originally needed by Save Anywhere); broken in SDV 1.2."
},
{
"Name": "Teleporter",
- "ID": "Teleporter",
+ "ID": [ "Teleporter" ],
"UpperVersion": "1.0.2",
"Compatibility": "AssumeBroken",
"UpdateUrl": "http://community.playstarbound.com/resources/4374",
- "Notes": "Crashes with 'InvalidOperationException: The StardewValley.Menus.MapPage object doesn't have a private 'points' instance field'."
+ "Notes": "Needs update for SDV 1.2."
+ },
+ {
+ "Name": "UiModSuite",
+ "ID": [ "Demiacle.UiModSuite" ],
+ "UpperVersion": "0.5",
+ "UpperVersionLabel": "1.0",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/1023",
+ "Notes": "Needs update for SDV 1.2. Actual upper version is 1.0, but mod incorrectly sets it to 0.5 in the manifest."
+ },
+ {
+ "Name": "Weather Controller",
+ "ID": [ "WeatherController.dll" ],
+ "UpperVersion": "1.0.2",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://community.playstarbound.com/threads/111526",
+ "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096",
+ "Notes": "Needs update for SDV 1.2."
+ },
+ {
+ "Name": "zDailyIncrease",
+ "ID": [ "zdailyincrease" ],
+ "UpperVersion": "1.2",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://community.playstarbound.com/resources/4247",
+ "Notes": "Needs update for SDV 1.2."
+ },
+ {
+ "Name": "Zoom Out Extreme",
+ "ID": [ "ZoomMod" ],
+ "UpperVersion": "0.1",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://community.playstarbound.com/threads/115028",
+ "Notes": "Needs update for SDV 1.2."
},
{
"Name": "Zoryn's Better RNG",
- "ID": "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6",
- "UpperVersion": "1.5",
+ "ID": [ "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6", /*since 1.6*/ "Zoryn.BetterRNG" ],
+ "UpperVersion": "1.6",
"Compatibility": "AssumeBroken",
- "UpdateUrl": "http://community.playstarbound.com/threads/108756",
- "Notes": "Uses SMAPI's internal SGame class."
+ "UpdateUrl": "https://github.com/Zoryn4163/SMAPI-Mods/releases",
+ "Notes": "Needs update for SDV 1.2."
},
{
"Name": "Zoryn's Calendar Anywhere",
- "ID": "a41c01cd-0437-43eb-944f-78cb5a53002a",
- "UpperVersion": "1.5",
+ "ID": [ "a41c01cd-0437-43eb-944f-78cb5a53002a", /*since 1.6*/ "Zoryn.CalendarAnywhere" ],
+ "UpperVersion": "1.6",
"Compatibility": "AssumeBroken",
- "UpdateUrl": "http://community.playstarbound.com/threads/108756",
- "Notes": "Uses SMAPI's internal SGame class."
+ "UpdateUrl": "https://github.com/Zoryn4163/SMAPI-Mods/releases",
+ "Notes": "Needs update for SDV 1.2."
},
{
"Name": "Zoryn's Health Bars",
- "ID": "HealthBars.dll",
- "UpperVersion": "1.5",
+ "ID": [ "HealthBars.dll", /*since 1.6*/ "Zoryn.HealthBars" ],
+ "UpperVersion": "1.6",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "https://github.com/Zoryn4163/SMAPI-Mods/releases",
+ "Notes": "Needs update for SDV 1.2."
+ },
+ {
+ "Name": "Zoryn's Junimo Deposit Anywhere",
+ "ID": [ "f93a4fe8-cade-4146-9335-b5f82fbbf7bc", /*since 1.6*/ "Zoryn.JunimoDepositAnywhere" ],
+ "UpperVersion": "1.7",
"Compatibility": "AssumeBroken",
- "UpdateUrl": "http://community.playstarbound.com/threads/108756",
- "Notes": "Uses SMAPI's internal SGame class."
+ "UpdateUrl": "https://github.com/Zoryn4163/SMAPI-Mods/releases",
+ "Notes": "Needs update for SDV 1.2."
},
{
"Name": "Zoryn's Movement Mod",
- "ID": "8a632929-8335-484f-87dd-c29d2ba3215d",
- "UpperVersion": "1.5",
+ "ID": [ "8a632929-8335-484f-87dd-c29d2ba3215d", /*since 1.6*/ "Zoryn.MovementModifier" ],
+ "UpperVersion": "1.6",
"Compatibility": "AssumeBroken",
- "UpdateUrl": "http://community.playstarbound.com/threads/108756",
- "Notes": "Uses SMAPI's internal SGame class."
+ "UpdateUrl": "https://github.com/Zoryn4163/SMAPI-Mods/releases",
+ "Notes": "Needs update for SDV 1.2."
},
{
"Name": "Zoryn's Regen Mod",
- "ID": "dfac4383-1b6b-4f33-ae4e-37fc23e5252e",
- "UpperVersion": "1.5",
+ "ID": [ "dfac4383-1b6b-4f33-ae4e-37fc23e5252e", /*since 1.6*/ "Zoryn.RegenMod" ],
+ "UpperVersion": "1.6",
"Compatibility": "AssumeBroken",
- "UpdateUrl": "http://community.playstarbound.com/threads/108756",
- "Notes": "Uses SMAPI's internal SGame class."
+ "UpdateUrl": "https://github.com/Zoryn4163/SMAPI-Mods/releases",
+ "Notes": "Needs update for SDV 1.2."
}
]
}
diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj
index 87ce65b0..60171493 100644
--- a/src/StardewModdingAPI/StardewModdingAPI.csproj
+++ b/src/StardewModdingAPI/StardewModdingAPI.csproj
@@ -3,7 +3,7 @@
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
- <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <Platform Condition=" '$(Platform)' == '' ">x86</Platform>
<ProjectGuid>{F1A573B0-F436-472C-AE29-0B91EA6B9F8F}</ProjectGuid>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
@@ -28,29 +28,6 @@
<UseApplicationTrust>false</UseApplicationTrust>
<BootstrapperEnabled>true</BootstrapperEnabled>
</PropertyGroup>
- <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
- <PlatformTarget>AnyCPU</PlatformTarget>
- <DebugSymbols>true</DebugSymbols>
- <DebugType>full</DebugType>
- <Optimize>true</Optimize>
- <OutputPath>$(SolutionDir)\..\bin\Debug\SMAPI</OutputPath>
- <DefineConstants>TRACE;DEBUG</DefineConstants>
- <ErrorReport>prompt</ErrorReport>
- <WarningLevel>4</WarningLevel>
- <Prefer32Bit>false</Prefer32Bit>
- <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
- </PropertyGroup>
- <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
- <PlatformTarget>AnyCPU</PlatformTarget>
- <DebugType>pdbonly</DebugType>
- <Optimize>true</Optimize>
- <OutputPath>$(SolutionDir)\..\bin\Release\SMAPI</OutputPath>
- <DefineConstants>TRACE</DefineConstants>
- <ErrorReport>prompt</ErrorReport>
- <WarningLevel>4</WarningLevel>
- <Prefer32Bit>false</Prefer32Bit>
- <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
- </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<PlatformTarget>x86</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
@@ -144,8 +121,14 @@
<Compile Include="Events\EventArgsStringChanged.cs" />
<Compile Include="Events\GameEvents.cs" />
<Compile Include="Events\GraphicsEvents.cs" />
- <Compile Include="Framework\AssemblyDefinitionResolver.cs" />
- <Compile Include="Framework\AssemblyParseResult.cs" />
+ <Compile Include="Framework\Countdown.cs" />
+ <Compile Include="Framework\ModLoading\IModMetadata.cs" />
+ <Compile Include="Framework\ModLoading\InvalidModStateException.cs" />
+ <Compile Include="Framework\ModLoading\ModDependencyStatus.cs" />
+ <Compile Include="Framework\ModLoading\ModMetadataStatus.cs" />
+ <Compile Include="Framework\ModLoading\ModResolver.cs" />
+ <Compile Include="Framework\ModLoading\AssemblyDefinitionResolver.cs" />
+ <Compile Include="Framework\ModLoading\AssemblyParseResult.cs" />
<Compile Include="Framework\CommandManager.cs" />
<Compile Include="Framework\ContentHelper.cs" />
<Compile Include="Framework\Content\ContentEventData.cs" />
@@ -156,20 +139,23 @@
<Compile Include="Framework\Logging\ConsoleInterceptionManager.cs" />
<Compile Include="Framework\Logging\InterceptingTextWriter.cs" />
<Compile Include="Framework\CommandHelper.cs" />
+ <Compile Include="Framework\Models\ManifestDependency.cs" />
<Compile Include="Framework\Models\ModCompatibilityType.cs" />
<Compile Include="Framework\Models\SConfig.cs" />
+ <Compile Include="Framework\ModLoading\ModMetadata.cs" />
<Compile Include="Framework\Reflection\PrivateProperty.cs" />
<Compile Include="Framework\RequestExitDelegate.cs" />
<Compile Include="Framework\SContentManager.cs" />
<Compile Include="Framework\Serialisation\JsonHelper.cs" />
<Compile Include="Framework\Serialisation\SelectiveStringEnumConverter.cs" />
- <Compile Include="Framework\Serialisation\SemanticVersionConverter.cs" />
+ <Compile Include="Framework\Serialisation\ManifestFieldConverter.cs" />
<Compile Include="ICommandHelper.cs" />
<Compile Include="IContentEventData.cs" />
<Compile Include="IContentEventHelper.cs" />
<Compile Include="IContentEventHelperForDictionary.cs" />
<Compile Include="IContentEventHelperForImage.cs" />
<Compile Include="IContentHelper.cs" />
+ <Compile Include="IManifestDependency.cs" />
<Compile Include="IModRegistry.cs" />
<Compile Include="Events\LocationEvents.cs" />
<Compile Include="Events\MenuEvents.cs" />
@@ -181,7 +167,7 @@
<Compile Include="Framework\DeprecationManager.cs" />
<Compile Include="Framework\InternalExtensions.cs" />
<Compile Include="Framework\Models\ModCompatibility.cs" />
- <Compile Include="Framework\AssemblyLoader.cs" />
+ <Compile Include="Framework\ModLoading\AssemblyLoader.cs" />
<Compile Include="Framework\Reflection\CacheEntry.cs" />
<Compile Include="Framework\Reflection\PrivateField.cs" />
<Compile Include="Framework\Reflection\PrivateMethod.cs" />
@@ -201,7 +187,7 @@
<Compile Include="Events\ItemStackChange.cs" />
<Compile Include="Log.cs" />
<Compile Include="Framework\Monitor.cs" />
- <Compile Include="Framework\Manifest.cs" />
+ <Compile Include="Framework\Models\Manifest.cs" />
<Compile Include="Mod.cs" />
<Compile Include="Framework\ModHelper.cs" />
<Compile Include="PatchMode.cs" />
@@ -267,9 +253,8 @@
<Copy SourceFiles="$(TargetDir)\$(TargetName).exe" DestinationFolder="$(GamePath)" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).config.json" DestinationFolder="$(GamePath)" />
<Copy SourceFiles="$(TargetDir)\StardewModdingAPI.AssemblyRewriters.dll" DestinationFolder="$(GamePath)" />
- <Copy SourceFiles="$(TargetDir)\$(TargetName).exe.mdb" DestinationFolder="$(GamePath)" Condition="$(OS) != 'Windows_NT'" />
- <Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)" Condition="$(OS) == 'Windows_NT'" />
- <Copy SourceFiles="$(TargetDir)\$(TargetName).xml" DestinationFolder="$(GamePath)" Condition="$(OS) == 'Windows_NT'" />
+ <Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)" />
+ <Copy SourceFiles="$(TargetDir)\$(TargetName).xml" DestinationFolder="$(GamePath)" />
<Copy SourceFiles="$(TargetDir)\Newtonsoft.Json.dll" DestinationFolder="$(GamePath)" />
<Copy SourceFiles="$(TargetDir)\Mono.Cecil.dll" DestinationFolder="$(GamePath)" />
</Target>