summaryrefslogtreecommitdiff
path: root/src/SMAPI
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI')
-rw-r--r--src/SMAPI/Constants.cs6
-rw-r--r--src/SMAPI/Framework/Content/AssetDataForImage.cs2
-rw-r--r--src/SMAPI/Framework/Content/AssetInfo.cs4
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs5
-rw-r--r--src/SMAPI/Framework/Deprecations/DeprecationManager.cs2
-rw-r--r--src/SMAPI/Framework/ModHelpers/CommandHelper.cs2
-rw-r--r--src/SMAPI/Framework/ModHelpers/ContentHelper.cs4
-rw-r--r--src/SMAPI/Framework/ModHelpers/ModHelper.cs2
-rw-r--r--src/SMAPI/Framework/ModLoading/ModResolver.cs129
-rw-r--r--src/SMAPI/Framework/Models/SConfig.cs50
-rw-r--r--src/SMAPI/Framework/SCore.cs49
-rw-r--r--src/SMAPI/Metadata/CoreAssetPropagator.cs11
-rw-r--r--src/SMAPI/SMAPI.config.json20
-rw-r--r--src/SMAPI/Utilities/PerScreen.cs2
14 files changed, 154 insertions, 134 deletions
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index ea41a4ee..4cb30dc9 100644
--- a/src/SMAPI/Constants.cs
+++ b/src/SMAPI/Constants.cs
@@ -52,7 +52,7 @@ namespace StardewModdingAPI
internal static int? LogScreenId { get; set; }
/// <summary>SMAPI's current raw semantic version.</summary>
- internal static string RawApiVersion = "3.17.1";
+ internal static string RawApiVersion = "3.17.2";
}
/// <summary>Contains SMAPI's constants and assumptions.</summary>
@@ -71,7 +71,7 @@ namespace StardewModdingAPI
public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.6");
/// <summary>The maximum supported version of Stardew Valley, if any.</summary>
- public static ISemanticVersion? MaximumGameVersion { get; } = null;
+ public static ISemanticVersion? MaximumGameVersion { get; } = new GameVersion("1.5.6");
/// <summary>The target game platform.</summary>
public static GamePlatform TargetPlatform { get; } = EarlyConstants.Platform;
@@ -90,7 +90,7 @@ namespace StardewModdingAPI
source: null,
nounPhrase: $"{nameof(Constants)}.{nameof(Constants.ExecutionPath)}",
version: "3.14.0",
- severity: DeprecationLevel.Info
+ severity: DeprecationLevel.PendingRemoval
);
return Constants.GamePath;
diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs
index 241c09a8..7c8cc6a8 100644
--- a/src/SMAPI/Framework/Content/AssetDataForImage.cs
+++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs
@@ -192,7 +192,7 @@ namespace StardewModdingAPI.Framework.Content
int topOffset = startIndex / sourceArea.Width;
int bottomOffset = endIndex / sourceArea.Width;
- targetArea = new(targetArea.X, targetArea.Y + topOffset, targetArea.Width, bottomOffset - topOffset + 1);
+ targetArea = new(targetArea.X, targetArea.Y + topOffset - startRow, targetArea.Width, bottomOffset - topOffset + 1);
pixelCount = targetArea.Width * targetArea.Height;
sourceOffset = topOffset * sourceArea.Width;
}
diff --git a/src/SMAPI/Framework/Content/AssetInfo.cs b/src/SMAPI/Framework/Content/AssetInfo.cs
index af000300..52ef02e6 100644
--- a/src/SMAPI/Framework/Content/AssetInfo.cs
+++ b/src/SMAPI/Framework/Content/AssetInfo.cs
@@ -45,7 +45,7 @@ namespace StardewModdingAPI.Framework.Content
source: null,
nounPhrase: $"{nameof(IAssetInfo)}.{nameof(IAssetInfo.AssetName)}",
version: "3.14.0",
- severity: DeprecationLevel.Info,
+ severity: DeprecationLevel.PendingRemoval,
unlessStackIncludes: new[]
{
$"{typeof(AssetInterceptorChange).FullName}.{nameof(AssetInterceptorChange.CanIntercept)}",
@@ -84,7 +84,7 @@ namespace StardewModdingAPI.Framework.Content
source: null,
nounPhrase: $"{nameof(IAssetInfo)}.{nameof(IAssetInfo.AssetNameEquals)}",
version: "3.14.0",
- severity: DeprecationLevel.Info,
+ severity: DeprecationLevel.PendingRemoval,
unlessStackIncludes: new[]
{
$"{typeof(AssetInterceptorChange).FullName}.{nameof(AssetInterceptorChange.CanIntercept)}",
diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs
index cf26307f..86415a5f 100644
--- a/src/SMAPI/Framework/ContentCoordinator.cs
+++ b/src/SMAPI/Framework/ContentCoordinator.cs
@@ -129,6 +129,7 @@ namespace StardewModdingAPI.Framework
/// <param name="rootDirectory">The root directory to search for content.</param>
/// <param name="currentCulture">The current culture for which to localize content.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
+ /// <param name="multiplayer">The multiplayer instance whose map cache to update during asset propagation.</param>
/// <param name="reflection">Simplifies access to private code.</param>
/// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param>
/// <param name="onLoadingFirstAsset">A callback to invoke the first time *any* game content manager loads an asset.</param>
@@ -136,7 +137,7 @@ namespace StardewModdingAPI.Framework
/// <param name="getFileLookup">Get a file lookup for the given directory.</param>
/// <param name="onAssetsInvalidated">A callback to invoke when any asset names have been invalidated from the cache.</param>
/// <param name="requestAssetOperations">Get the load/edit operations to apply to an asset by querying registered <see cref="IContentEvents.AssetRequested"/> event handlers.</param>
- public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset, Action<BaseContentManager, IAssetName> onAssetLoaded, Func<string, IFileLookup> getFileLookup, Action<IList<IAssetName>> onAssetsInvalidated, Func<IAssetInfo, AssetOperationGroup?> requestAssetOperations)
+ public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Multiplayer multiplayer, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset, Action<BaseContentManager, IAssetName> onAssetLoaded, Func<string, IFileLookup> getFileLookup, Action<IList<IAssetName>> onAssetsInvalidated, Func<IAssetInfo, AssetOperationGroup?> requestAssetOperations)
{
this.GetFileLookup = getFileLookup;
this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor));
@@ -177,7 +178,7 @@ namespace StardewModdingAPI.Framework
this.ContentManagers.Add(contentManagerForAssetPropagation);
this.VanillaContentManager = new LocalizedContentManager(serviceProvider, rootDirectory);
- this.CoreAssets = new CoreAssetPropagator(this.MainContentManager, contentManagerForAssetPropagation, this.Monitor, reflection, name => this.ParseAssetName(name, allowLocales: true));
+ this.CoreAssets = new CoreAssetPropagator(this.MainContentManager, contentManagerForAssetPropagation, this.Monitor, multiplayer, reflection, name => this.ParseAssetName(name, allowLocales: true));
this.LocaleCodes = new Lazy<Dictionary<string, LocalizedContentManager.LanguageCode>>(() => this.GetLocaleCodes(customLanguages: Enumerable.Empty<ModLanguage>()));
}
diff --git a/src/SMAPI/Framework/Deprecations/DeprecationManager.cs b/src/SMAPI/Framework/Deprecations/DeprecationManager.cs
index 5a5850d1..f44b5d7d 100644
--- a/src/SMAPI/Framework/Deprecations/DeprecationManager.cs
+++ b/src/SMAPI/Framework/Deprecations/DeprecationManager.cs
@@ -101,7 +101,7 @@ namespace StardewModdingAPI.Framework.Deprecations
foreach (DeprecationWarning warning in this.QueuedWarnings.OrderBy(p => p.ModName).ThenBy(p => p.NounPhrase))
{
// build message
- string message = $"{warning.ModName} uses deprecated code ({warning.NounPhrase}) and will break in the upcoming SMAPI 4.0.0.";
+ string message = $"{warning.ModName} uses deprecated code ({warning.NounPhrase}) and will break in the next major SMAPI update.";
// get log level
LogLevel level;
diff --git a/src/SMAPI/Framework/ModHelpers/CommandHelper.cs b/src/SMAPI/Framework/ModHelpers/CommandHelper.cs
index b7d4861f..90edc137 100644
--- a/src/SMAPI/Framework/ModHelpers/CommandHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/CommandHelper.cs
@@ -43,7 +43,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
source: this.Mod,
nounPhrase: $"{nameof(IModHelper)}.{nameof(IModHelper.ConsoleCommands)}.{nameof(ICommandHelper.Trigger)}",
version: "3.8.1",
- severity: DeprecationLevel.Info
+ severity: DeprecationLevel.PendingRemoval
);
return this.CommandManager.Trigger(name, arguments);
diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
index 0a1633bf..152b264c 100644
--- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
@@ -62,7 +62,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
source: this.Mod,
nounPhrase: $"{nameof(IContentHelper)}.{nameof(IContentHelper.AssetLoaders)}",
version: "3.14.0",
- severity: DeprecationLevel.Info
+ severity: DeprecationLevel.PendingRemoval
);
return this.ObservableAssetLoaders;
@@ -78,7 +78,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
source: this.Mod,
nounPhrase: $"{nameof(IContentHelper)}.{nameof(IContentHelper.AssetEditors)}",
version: "3.14.0",
- severity: DeprecationLevel.Info
+ severity: DeprecationLevel.PendingRemoval
);
return this.ObservableAssetEditors;
diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs
index 1cdd8536..531289d0 100644
--- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs
@@ -41,7 +41,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
source: this.Mod,
nounPhrase: $"{nameof(IModHelper)}.{nameof(IModHelper.Content)}",
version: "3.14.0",
- severity: DeprecationLevel.Info
+ severity: DeprecationLevel.PendingRemoval
);
return this.ContentImpl;
diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs
index fe56f4d2..96975e05 100644
--- a/src/SMAPI/Framework/ModLoading/ModResolver.cs
+++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs
@@ -4,11 +4,11 @@ using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using StardewModdingAPI.Toolkit;
+using StardewModdingAPI.Toolkit.Framework;
using StardewModdingAPI.Toolkit.Framework.ModData;
using StardewModdingAPI.Toolkit.Framework.ModScanning;
using StardewModdingAPI.Toolkit.Framework.UpdateData;
using StardewModdingAPI.Toolkit.Serialization.Models;
-using StardewModdingAPI.Toolkit.Utilities;
using StardewModdingAPI.Toolkit.Utilities.PathLookups;
namespace StardewModdingAPI.Framework.ModLoading
@@ -126,101 +126,24 @@ namespace StardewModdingAPI.Framework.ModLoading
continue;
}
- // validate DLL / content pack fields
+ // validate manifest format
+ if (!ManifestValidator.TryValidateFields(mod.Manifest, out string manifestError))
{
- bool hasDll = !string.IsNullOrWhiteSpace(mod.Manifest.EntryDll);
- bool isContentPack = mod.Manifest.ContentPackFor != null;
-
- // validate field presence
- if (!hasDll && !isContentPack)
- {
- mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one.");
- continue;
- }
- if (hasDll && isContentPack)
- {
- mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive.");
- continue;
- }
-
- // validate DLL
- if (hasDll)
- {
- // invalid filename format
- if (mod.Manifest.EntryDll!.Intersect(Path.GetInvalidFileNameChars()).Any())
- {
- mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has invalid filename '{mod.Manifest.EntryDll}' for the EntryDLL field.");
- continue;
- }
-
- // file doesn't exist
- if (validateFilesExist)
- {
- IFileLookup pathLookup = getFileLookup(mod.DirectoryPath);
- FileInfo file = pathLookup.GetFile(mod.Manifest.EntryDll!);
- if (!file.Exists)
- {
- mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist.");
- continue;
- }
- }
- }
-
- // validate content pack
- else
- {
- // invalid content pack ID
- if (string.IsNullOrWhiteSpace(mod.Manifest.ContentPackFor!.UniqueID))
- {
- mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field.");
- continue;
- }
- }
+ mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its {manifestError}");
+ continue;
}
- // validate required fields
+ // check that DLL exists if applicable
+ if (!string.IsNullOrEmpty(mod.Manifest.EntryDll) && validateFilesExist)
{
- List<string> missingFields = new List<string>(3);
-
- if (string.IsNullOrWhiteSpace(mod.Manifest.Name))
- missingFields.Add(nameof(IManifest.Name));
- if (mod.Manifest.Version == null || mod.Manifest.Version.ToString() == "0.0.0")
- missingFields.Add(nameof(IManifest.Version));
- if (string.IsNullOrWhiteSpace(mod.Manifest.UniqueID))
- missingFields.Add(nameof(IManifest.UniqueID));
-
- if (missingFields.Any())
+ IFileLookup pathLookup = getFileLookup(mod.DirectoryPath);
+ FileInfo file = pathLookup.GetFile(mod.Manifest.EntryDll!);
+ if (!file.Exists)
{
- mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest is missing required fields ({string.Join(", ", missingFields)}).");
+ mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist.");
continue;
}
}
-
- // validate ID format
- if (!PathUtilities.IsSlug(mod.Manifest.UniqueID))
- mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, "its manifest specifies an invalid ID (IDs must only contain letters, numbers, underscores, periods, or hyphens).");
-
- // validate dependencies
- foreach (IManifestDependency? dependency in mod.Manifest.Dependencies)
- {
- // null dependency
- if (dependency == null)
- {
- mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has a null entry under {nameof(IManifest.Dependencies)}.");
- continue;
- }
-
- // missing ID
- if (string.IsNullOrWhiteSpace(dependency.UniqueID))
- {
- mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has a {nameof(IManifest.Dependencies)} entry with no {nameof(IManifestDependency.UniqueID)} field.");
- continue;
- }
-
- // invalid ID
- if (!PathUtilities.IsSlug(dependency.UniqueID))
- mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has a {nameof(IManifest.Dependencies)} entry with an invalid {nameof(IManifestDependency.UniqueID)} field (IDs must only contain letters, numbers, underscores, periods, or hyphens).");
- }
}
// validate IDs are unique
@@ -242,10 +165,32 @@ namespace StardewModdingAPI.Framework.ModLoading
}
}
+ /// <summary>Apply preliminary overrides to the load order based on the SMAPI configuration.</summary>
+ /// <param name="mods">The mods to process.</param>
+ /// <param name="modIdsToLoadEarly">The mod IDs SMAPI should load before any other mods (except those needed to load them).</param>
+ /// <param name="modIdsToLoadLate">The mod IDs SMAPI should load after any other mods.</param>
+ public IModMetadata[] ApplyLoadOrderOverrides(IModMetadata[] mods, HashSet<string> modIdsToLoadEarly, HashSet<string> modIdsToLoadLate)
+ {
+ if (!modIdsToLoadEarly.Any() && !modIdsToLoadLate.Any())
+ return mods;
+
+ return mods
+ .OrderBy(mod =>
+ {
+ string id = mod.Manifest.UniqueID;
+ if (modIdsToLoadEarly.Contains(id))
+ return -1;
+ if (modIdsToLoadLate.Contains(id))
+ return 1;
+ return 0;
+ })
+ .ToArray();
+ }
+
/// <summary>Sort the given mods by the order they should be loaded.</summary>
/// <param name="mods">The mods to process.</param>
/// <param name="modDatabase">Handles access to SMAPI's internal mod metadata list.</param>
- public IEnumerable<IModMetadata> ProcessDependencies(IEnumerable<IModMetadata> mods, ModDatabase modDatabase)
+ public IEnumerable<IModMetadata> ProcessDependencies(IReadOnlyList<IModMetadata> mods, ModDatabase modDatabase)
{
// initialize metadata
mods = mods.ToArray();
@@ -261,7 +206,7 @@ namespace StardewModdingAPI.Framework.ModLoading
// sort mods
foreach (IModMetadata mod in mods)
- this.ProcessDependencies(mods.ToArray(), modDatabase, mod, states, sortedMods, new List<IModMetadata>());
+ this.ProcessDependencies(mods, modDatabase, mod, states, sortedMods, new List<IModMetadata>());
return sortedMods.Reverse();
}
@@ -278,7 +223,7 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <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, ModDatabase modDatabase, IModMetadata mod, IDictionary<IModMetadata, ModDependencyStatus> states, Stack<IModMetadata> sortedMods, ICollection<IModMetadata> currentChain)
+ private ModDependencyStatus ProcessDependencies(IReadOnlyList<IModMetadata> mods, ModDatabase modDatabase, IModMetadata mod, IDictionary<IModMetadata, ModDependencyStatus> states, Stack<IModMetadata> sortedMods, ICollection<IModMetadata> currentChain)
{
// check if already visited
switch (states[mod])
@@ -409,7 +354,7 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <summary>Get the dependencies declared in a manifest.</summary>
/// <param name="manifest">The mod manifest.</param>
/// <param name="loadedMods">The loaded mods.</param>
- private IEnumerable<ModDependency> GetDependenciesFrom(IManifest manifest, IModMetadata[] loadedMods)
+ private IEnumerable<ModDependency> GetDependenciesFrom(IManifest manifest, IReadOnlyList<IModMetadata> loadedMods)
{
IModMetadata? FindMod(string id) => loadedMods.FirstOrDefault(m => m.HasID(id));
diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs
index bceb0940..87970e6c 100644
--- a/src/SMAPI/Framework/Models/SConfig.cs
+++ b/src/SMAPI/Framework/Models/SConfig.cs
@@ -16,6 +16,7 @@ namespace StardewModdingAPI.Framework.Models
private static readonly IDictionary<string, object> DefaultValues = new Dictionary<string, object>
{
[nameof(CheckForUpdates)] = true,
+ [nameof(ListenForConsoleInput)] = true,
[nameof(ParanoidWarnings)] = Constants.IsDebugBuild,
[nameof(UseBetaChannel)] = Constants.ApiVersion.IsPrerelease(),
[nameof(GitHubProjectName)] = "Pathoschild/SMAPI",
@@ -48,6 +49,9 @@ namespace StardewModdingAPI.Framework.Models
/// <summary>Whether to check for newer versions of SMAPI and mods on startup.</summary>
public bool CheckForUpdates { get; set; }
+ /// <summary>Whether SMAPI should listen for console input to support console commands.</summary>
+ public bool ListenForConsoleInput { get; set; }
+
/// <summary>Whether to add a section to the 'mod issues' list for mods which which directly use potentially sensitive .NET APIs like file or shell access.</summary>
public bool ParanoidWarnings { get; set; }
@@ -82,28 +86,38 @@ namespace StardewModdingAPI.Framework.Models
/// <summary>The mod IDs SMAPI should ignore when performing update checks or validating update keys.</summary>
public HashSet<string> SuppressUpdateChecks { get; set; }
+ /// <summary>The mod IDs SMAPI should load before any other mods (except those needed to load them).</summary>
+ public HashSet<string> ModsToLoadEarly { get; set; }
+
+ /// <summary>The mod IDs SMAPI should load after any other mods.</summary>
+ public HashSet<string> ModsToLoadLate { get; set; }
+
/********
** Public methods
********/
/// <summary>Construct an instance.</summary>
- /// <param name="developerMode">Whether to enable development features.</param>
- /// <param name="checkForUpdates">Whether to check for newer versions of SMAPI and mods on startup.</param>
- /// <param name="paranoidWarnings">Whether to add a section to the 'mod issues' list for mods which which directly use potentially sensitive .NET APIs like file or shell access.</param>
- /// <param name="useBetaChannel">Whether to show beta versions as valid updates.</param>
- /// <param name="gitHubProjectName">SMAPI's GitHub project name, used to perform update checks.</param>
- /// <param name="webApiBaseUrl">The base URL for SMAPI's web API, used to perform update checks.</param>
- /// <param name="verboseLogging">The log contexts for which to enable verbose logging, which may show a lot more information to simplify troubleshooting.</param>
- /// <param name="rewriteMods">Whether SMAPI should rewrite mods for compatibility.</param>
- /// <param name="useCaseInsensitivePaths">>Whether to make SMAPI file APIs case-insensitive, even on Linux.</param>
- /// <param name="logNetworkTraffic">Whether SMAPI should log network traffic.</param>
- /// <param name="consoleColors">The colors to use for text written to the SMAPI console.</param>
- /// <param name="suppressHarmonyDebugMode">Whether to prevent mods from enabling Harmony's debug mode, which impacts performance and creates a file on your desktop. Debug mode should never be enabled by a released mod.</param>
- /// <param name="suppressUpdateChecks">The mod IDs SMAPI should ignore when performing update checks or validating update keys.</param>
- public SConfig(bool developerMode, bool? checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, bool? suppressHarmonyDebugMode, string[]? suppressUpdateChecks)
+ /// <param name="developerMode"><inheritdoc cref="DeveloperMode" path="/summary" /></param>
+ /// <param name="checkForUpdates"><inheritdoc cref="CheckForUpdates" path="/summary" /></param>
+ /// <param name="listenForConsoleInput"><inheritdoc cref="ListenForConsoleInput" path="/summary" /></param>
+ /// <param name="paranoidWarnings"><inheritdoc cref="ParanoidWarnings" path="/summary" /></param>
+ /// <param name="useBetaChannel"><inheritdoc cref="UseBetaChannel" path="/summary" /></param>
+ /// <param name="gitHubProjectName"><inheritdoc cref="GitHubProjectName" path="/summary" /></param>
+ /// <param name="webApiBaseUrl"><inheritdoc cref="WebApiBaseUrl" path="/summary" /></param>
+ /// <param name="verboseLogging"><inheritdoc cref="VerboseLogging" path="/summary" /></param>
+ /// <param name="rewriteMods"><inheritdoc cref="RewriteMods" path="/summary" /></param>
+ /// <param name="useCaseInsensitivePaths"><inheritdoc cref="UseCaseInsensitivePaths" path="/summary" /></param>
+ /// <param name="logNetworkTraffic"><inheritdoc cref="LogNetworkTraffic" path="/summary" /></param>
+ /// <param name="consoleColors"><inheritdoc cref="ConsoleColors" path="/summary" /></param>
+ /// <param name="suppressHarmonyDebugMode"><inheritdoc cref="SuppressHarmonyDebugMode" path="/summary" /></param>
+ /// <param name="suppressUpdateChecks"><inheritdoc cref="SuppressUpdateChecks" path="/summary" /></param>
+ /// <param name="modsToLoadEarly"><inheritdoc cref="ModsToLoadEarly" path="/summary" /></param>
+ /// <param name="modsToLoadLate"><inheritdoc cref="ModsToLoadLate" path="/summary" /></param>
+ public SConfig(bool developerMode, bool? checkForUpdates, bool? listenForConsoleInput, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, bool? suppressHarmonyDebugMode, string[]? suppressUpdateChecks, string[]? modsToLoadEarly, string[]? modsToLoadLate)
{
this.DeveloperMode = developerMode;
this.CheckForUpdates = checkForUpdates ?? (bool)SConfig.DefaultValues[nameof(this.CheckForUpdates)];
+ this.ListenForConsoleInput = listenForConsoleInput ?? (bool)SConfig.DefaultValues[nameof(this.ListenForConsoleInput)];
this.ParanoidWarnings = paranoidWarnings ?? (bool)SConfig.DefaultValues[nameof(this.ParanoidWarnings)];
this.UseBetaChannel = useBetaChannel ?? (bool)SConfig.DefaultValues[nameof(this.UseBetaChannel)];
this.GitHubProjectName = gitHubProjectName;
@@ -115,6 +129,8 @@ namespace StardewModdingAPI.Framework.Models
this.ConsoleColors = consoleColors;
this.SuppressHarmonyDebugMode = suppressHarmonyDebugMode ?? (bool)SConfig.DefaultValues[nameof(this.SuppressHarmonyDebugMode)];
this.SuppressUpdateChecks = new HashSet<string>(suppressUpdateChecks ?? Array.Empty<string>(), StringComparer.OrdinalIgnoreCase);
+ this.ModsToLoadEarly = new HashSet<string>(modsToLoadEarly ?? Array.Empty<string>(), StringComparer.OrdinalIgnoreCase);
+ this.ModsToLoadLate = new HashSet<string>(modsToLoadLate ?? Array.Empty<string>(), StringComparer.OrdinalIgnoreCase);
}
/// <summary>Override the value of <see cref="DeveloperMode"/>.</summary>
@@ -136,6 +152,12 @@ namespace StardewModdingAPI.Framework.Models
custom[name] = value;
}
+ if (this.ModsToLoadEarly.Any())
+ custom[nameof(this.ModsToLoadEarly)] = $"[{string.Join(", ", this.ModsToLoadEarly)}]";
+
+ if (this.ModsToLoadLate.Any())
+ custom[nameof(this.ModsToLoadLate)] = $"[{string.Join(", ", this.ModsToLoadLate)}]";
+
if (!this.SuppressUpdateChecks.SetEquals(SConfig.DefaultSuppressUpdateChecks))
custom[nameof(this.SuppressUpdateChecks)] = $"[{string.Join(", ", this.SuppressUpdateChecks)}]";
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index 40979b09..c977ad65 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -423,8 +423,29 @@ namespace StardewModdingAPI.Framework
this.Monitor.Log($" Skipped {mod.GetRelativePathWithRoot()} (folder name starts with a dot).");
mods = mods.Where(p => !p.IsIgnored).ToArray();
- // load mods
+ // validate manifests
resolver.ValidateManifests(mods, Constants.ApiVersion, toolkit.GetUpdateUrl, getFileLookup: this.GetFileLookup);
+
+ // apply load order customizations
+ if (this.Settings.ModsToLoadEarly.Any() || this.Settings.ModsToLoadLate.Any())
+ {
+ HashSet<string> installedIds = new HashSet<string>(mods.Select(p => p.Manifest.UniqueID), StringComparer.OrdinalIgnoreCase);
+
+ string[] missingEarlyMods = this.Settings.ModsToLoadEarly.Where(id => !installedIds.Contains(id)).OrderBy(p => p, StringComparer.OrdinalIgnoreCase).ToArray();
+ string[] missingLateMods = this.Settings.ModsToLoadLate.Where(id => !installedIds.Contains(id)).OrderBy(p => p, StringComparer.OrdinalIgnoreCase).ToArray();
+ string[] duplicateMods = this.Settings.ModsToLoadLate.Where(id => this.Settings.ModsToLoadEarly.Contains(id)).OrderBy(p => p, StringComparer.OrdinalIgnoreCase).ToArray();
+
+ if (missingEarlyMods.Any())
+ this.Monitor.Log($" The 'smapi-internal/config.json' file lists mod IDs in {nameof(this.Settings.ModsToLoadEarly)} which aren't installed: '{string.Join("', '", missingEarlyMods)}'.", LogLevel.Warn);
+ if (missingLateMods.Any())
+ this.Monitor.Log($" The 'smapi-internal/config.json' file lists mod IDs in {nameof(this.Settings.ModsToLoadLate)} which aren't installed: '{string.Join("', '", missingLateMods)}'.", LogLevel.Warn);
+ if (duplicateMods.Any())
+ this.Monitor.Log($" The 'smapi-internal/config.json' file lists mod IDs which are in both {nameof(this.Settings.ModsToLoadEarly)} and {nameof(this.Settings.ModsToLoadLate)}: '{string.Join("', '", duplicateMods)}'. These will be loaded early.", LogLevel.Warn);
+
+ mods = resolver.ApplyLoadOrderOverrides(mods, this.Settings.ModsToLoadEarly, this.Settings.ModsToLoadLate);
+ }
+
+ // load mods
mods = resolver.ProcessDependencies(mods, modDatabase).ToArray();
this.LoadMods(mods, this.Toolkit.JsonHelper, this.ContentCore, modDatabase);
@@ -447,14 +468,17 @@ namespace StardewModdingAPI.Framework
this.Monitor.Log("SMAPI found problems in your game's content files which are likely to cause errors or crashes. Consider uninstalling XNB mods or reinstalling the game.", LogLevel.Error);
// start SMAPI console
- new Thread(
- () => this.LogManager.RunConsoleInputLoop(
- commandManager: this.CommandManager,
- reloadTranslations: this.ReloadTranslations,
- handleInput: input => this.RawCommandQueue.Add(input),
- continueWhile: () => this.IsGameRunning && !this.IsExiting
- )
- ).Start();
+ if (this.Settings.ListenForConsoleInput)
+ {
+ new Thread(
+ () => this.LogManager.RunConsoleInputLoop(
+ commandManager: this.CommandManager,
+ reloadTranslations: this.ReloadTranslations,
+ handleInput: input => this.RawCommandQueue.Add(input),
+ continueWhile: () => this.IsGameRunning && !this.IsExiting
+ )
+ ).Start();
+ }
}
/// <summary>Raised after an instance finishes loading its initial content.</summary>
@@ -1327,6 +1351,7 @@ namespace StardewModdingAPI.Framework
rootDirectory: rootDirectory,
currentCulture: Thread.CurrentThread.CurrentUICulture,
monitor: this.Monitor,
+ multiplayer: this.Multiplayer,
reflection: this.Reflection,
jsonHelper: this.Toolkit.JsonHelper,
onLoadingFirstAsset: this.InitializeBeforeFirstAssetLoaded,
@@ -1712,7 +1737,7 @@ namespace StardewModdingAPI.Framework
source: metadata,
nounPhrase: $"{nameof(IAssetEditor)}",
version: "3.14.0",
- severity: DeprecationLevel.Info,
+ severity: DeprecationLevel.PendingRemoval,
logStackTrace: false
);
@@ -1725,7 +1750,7 @@ namespace StardewModdingAPI.Framework
source: metadata,
nounPhrase: $"{nameof(IAssetLoader)}",
version: "3.14.0",
- severity: DeprecationLevel.Info,
+ severity: DeprecationLevel.PendingRemoval,
logStackTrace: false
);
@@ -1757,7 +1782,7 @@ namespace StardewModdingAPI.Framework
metadata,
$"using {name} without bundling it",
"3.14.7",
- DeprecationLevel.Info,
+ DeprecationLevel.PendingRemoval,
logStackTrace: false
);
}
diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs
index 1ef9a8f2..037e4573 100644
--- a/src/SMAPI/Metadata/CoreAssetPropagator.cs
+++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs
@@ -40,6 +40,9 @@ namespace StardewModdingAPI.Metadata
/// <summary>Writes messages to the console.</summary>
private readonly IMonitor Monitor;
+ /// <summary>The multiplayer instance whose map cache to update.</summary>
+ private readonly Multiplayer Multiplayer;
+
/// <summary>Simplifies access to private game code.</summary>
private readonly Reflector Reflection;
@@ -70,13 +73,15 @@ namespace StardewModdingAPI.Metadata
/// <param name="mainContent">The main content manager through which to reload assets.</param>
/// <param name="disposableContent">An internal content manager used only for asset propagation.</param>
/// <param name="monitor">Writes messages to the console.</param>
+ /// <param name="multiplayer">The multiplayer instance whose map cache to update.</param>
/// <param name="reflection">Simplifies access to private code.</param>
/// <param name="parseAssetName">Parse a raw asset name.</param>
- public CoreAssetPropagator(LocalizedContentManager mainContent, GameContentManagerForAssetPropagation disposableContent, IMonitor monitor, Reflector reflection, Func<string, IAssetName> parseAssetName)
+ public CoreAssetPropagator(LocalizedContentManager mainContent, GameContentManagerForAssetPropagation disposableContent, IMonitor monitor, Multiplayer multiplayer, Reflector reflection, Func<string, IAssetName> parseAssetName)
{
this.MainContentManager = mainContent;
this.DisposableContentManager = disposableContent;
this.Monitor = monitor;
+ this.Multiplayer = multiplayer;
this.Reflection = reflection;
this.ParseAssetName = parseAssetName;
}
@@ -1166,6 +1171,10 @@ namespace StardewModdingAPI.Metadata
GameLocation location = locationInfo.Location;
Vector2? playerPos = Game1.player?.Position;
+ // clear multiplayer cache for farmhands
+ if (!Context.IsMainPlayer)
+ this.Multiplayer.cachedMultiplayerMaps.Remove(location.NameOrUniqueName);
+
// reload map
location.interiorDoors.Clear(); // prevent errors when doors try to update tiles which no longer exist
location.reloadMap();
diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json
index 635e3add..0d00db4d 100644
--- a/src/SMAPI/SMAPI.config.json
+++ b/src/SMAPI/SMAPI.config.json
@@ -42,6 +42,12 @@ copy all the settings, or you may cause bugs due to overridden changes in future
"DeveloperMode": true,
/**
+ * Whether SMAPI should listen for console input. Disabling this will prevent you from using
+ * console commands. On some specific Linux systems, disabling this may reduce CPU usage.
+ */
+ "ListenForConsoleInput": true,
+
+ /**
* Whether SMAPI should rewrite mods for compatibility. This may prevent older mods from
* loading, but bypasses a Visual Studio crash when debugging.
*/
@@ -141,5 +147,17 @@ copy all the settings, or you may cause bugs due to overridden changes in future
"SMAPI.ConsoleCommands",
"SMAPI.ErrorHandler",
"SMAPI.SaveBackup"
- ]
+ ],
+
+ /**
+ * The mod IDs SMAPI should load before any other mods (except those needed to load them)
+ * or after any other mods.
+ *
+ * This lets you manually fix the load order if needed, but this is a last resort — SMAPI
+ * automatically adjusts the load order based on mods' dependencies, so needing to manually
+ * edit the order is usually a problem with one or both mods' metadata that can be reported to
+ * the mod author.
+ */
+ "ModsToLoadEarly": [],
+ "ModsToLoadLate": []
}
diff --git a/src/SMAPI/Utilities/PerScreen.cs b/src/SMAPI/Utilities/PerScreen.cs
index 468df0bd..87bf2027 100644
--- a/src/SMAPI/Utilities/PerScreen.cs
+++ b/src/SMAPI/Utilities/PerScreen.cs
@@ -59,7 +59,7 @@ namespace StardewModdingAPI.Utilities
null,
$"calling the {nameof(PerScreen<T>)} constructor with null",
"3.14.0",
- DeprecationLevel.Info
+ DeprecationLevel.PendingRemoval
);
#else
throw new ArgumentNullException(nameof(createNewState));