summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework/SCore.cs
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2022-07-06 22:26:09 -0400
committerJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2022-07-06 22:26:09 -0400
commitd51ffe58f7b7450cd4c4a7ee3d8b4da1cf55e7e4 (patch)
treeec19cdd7567125e47cfd49c7c044fc09f09f6b2f /src/SMAPI/Framework/SCore.cs
parent8e9237bdd7ec179975c9be5e28c811b42007e707 (diff)
parentbcb9e25d8666d2c1384515063ffbf987c36b8b0e (diff)
downloadSMAPI-d51ffe58f7b7450cd4c4a7ee3d8b4da1cf55e7e4.tar.gz
SMAPI-d51ffe58f7b7450cd4c4a7ee3d8b4da1cf55e7e4.tar.bz2
SMAPI-d51ffe58f7b7450cd4c4a7ee3d8b4da1cf55e7e4.zip
Merge branch 'develop' into stable
Diffstat (limited to 'src/SMAPI/Framework/SCore.cs')
-rw-r--r--src/SMAPI/Framework/SCore.cs146
1 files changed, 83 insertions, 63 deletions
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index 385a94ea..46d65f6a 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
@@ -11,6 +10,7 @@ using System.Runtime.ExceptionServices;
using System.Security;
using System.Text;
using System.Threading;
+using System.Threading.Tasks;
using Microsoft.Xna.Framework;
#if SMAPI_FOR_WINDOWS
using Microsoft.Win32;
@@ -32,7 +32,9 @@ using StardewModdingAPI.Framework.Networking;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Framework.Rendering;
using StardewModdingAPI.Framework.Serialization;
+#if SMAPI_DEPRECATED
using StardewModdingAPI.Framework.StateTracking.Comparers;
+#endif
using StardewModdingAPI.Framework.StateTracking.Snapshots;
using StardewModdingAPI.Framework.Utilities;
using StardewModdingAPI.Internal;
@@ -65,8 +67,8 @@ namespace StardewModdingAPI.Framework
/****
** Low-level components
****/
- /// <summary>Tracks whether the game should exit immediately and any pending initialization should be cancelled.</summary>
- private readonly CancellationTokenSource CancellationToken = new();
+ /// <summary>Whether the game should exit immediately and any pending initialization should be cancelled.</summary>
+ private bool IsExiting;
/// <summary>Manages the SMAPI console window and log file.</summary>
private readonly LogManager LogManager;
@@ -139,12 +141,13 @@ namespace StardewModdingAPI.Framework
/// <summary>The maximum number of consecutive attempts SMAPI should make to recover from an update error.</summary>
private readonly Countdown UpdateCrashTimer = new(60); // 60 ticks = roughly one second
+#if SMAPI_DEPRECATED
/// <summary>Asset interceptors added or removed since the last tick.</summary>
private readonly List<AssetInterceptorChange> ReloadAssetInterceptorsQueue = new();
+#endif
/// <summary>A list of queued commands to parse and execute.</summary>
- /// <remarks>This property must be thread-safe, since it's accessed from a separate console input thread.</remarks>
- private readonly ConcurrentQueue<string> RawCommandQueue = new();
+ private readonly CommandQueue RawCommandQueue = new();
/// <summary>A list of commands to execute on each screen.</summary>
private readonly PerScreen<List<QueuedCommand>> ScreenCommandQueue = new(() => new List<QueuedCommand>());
@@ -188,7 +191,7 @@ namespace StardewModdingAPI.Framework
string logPath = this.GetLogPath();
// init basics
- this.Settings = JsonConvert.DeserializeObject<SConfig>(File.ReadAllText(Constants.ApiConfigPath));
+ this.Settings = JsonConvert.DeserializeObject<SConfig>(File.ReadAllText(Constants.ApiConfigPath)) ?? throw new InvalidOperationException("The 'smapi-internal/config.json' file is missing or invalid. You can reinstall SMAPI to fix this.");
if (File.Exists(Constants.ApiUserConfigPath))
JsonConvert.PopulateObject(File.ReadAllText(Constants.ApiUserConfigPath), this.Settings);
if (developerMode.HasValue)
@@ -270,16 +273,6 @@ namespace StardewModdingAPI.Framework
new TitleMenuPatcher(this.OnLoadStageChanged)
);
- // add exit handler
- this.CancellationToken.Token.Register(() =>
- {
- if (this.IsGameRunning)
- {
- this.LogManager.WriteCrashLog();
- this.Game.Exit();
- }
- });
-
// set window titles
this.UpdateWindowTitles();
}
@@ -332,7 +325,7 @@ namespace StardewModdingAPI.Framework
}
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
- [SuppressMessage("ReSharper", "ConstantConditionalAccessQualifier", Justification = "May be disposed before SMAPI is fully initialized.")]
+ [SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract", Justification = "May be disposed before SMAPI is fully initialized.")]
public void Dispose()
{
// skip if already disposed
@@ -356,8 +349,8 @@ namespace StardewModdingAPI.Framework
// dispose core components
this.IsGameRunning = false;
+ this.IsExiting = true;
this.ContentCore?.Dispose();
- this.CancellationToken.Dispose();
this.Game?.Dispose();
this.LogManager.Dispose(); // dispose last to allow for any last-second log messages
@@ -372,7 +365,7 @@ namespace StardewModdingAPI.Framework
/// <summary>Initialize mods before the first game asset is loaded. At this point the core content managers are loaded (so mods can load their own assets), but the game is mostly uninitialized.</summary>
private void InitializeBeforeFirstAssetLoaded()
{
- if (this.CancellationToken.IsCancellationRequested)
+ if (this.IsExiting)
{
this.Monitor.Log("SMAPI shutting down: aborting initialization.", LogLevel.Warn);
return;
@@ -414,7 +407,7 @@ namespace StardewModdingAPI.Framework
this.CheckForSoftwareConflicts();
// check for updates
- this.CheckForUpdatesAsync(mods);
+ _ = this.CheckForUpdatesAsync(mods); // ignore task since the main thread doesn't need to wait for it
}
// update window titles
@@ -433,8 +426,8 @@ namespace StardewModdingAPI.Framework
() => this.LogManager.RunConsoleInputLoop(
commandManager: this.CommandManager,
reloadTranslations: this.ReloadTranslations,
- handleInput: input => this.RawCommandQueue.Enqueue(input),
- continueWhile: () => this.IsGameRunning && !this.CancellationToken.IsCancellationRequested
+ handleInput: input => this.RawCommandQueue.Add(input),
+ continueWhile: () => this.IsGameRunning && !this.IsExiting
)
).Start();
}
@@ -477,12 +470,13 @@ namespace StardewModdingAPI.Framework
** Special cases
*********/
// Abort if SMAPI is exiting.
- if (this.CancellationToken.IsCancellationRequested)
+ if (this.IsExiting)
{
this.Monitor.Log("SMAPI shutting down: aborting update.");
return;
}
+#if SMAPI_DEPRECATED
/*********
** Reload assets when interceptors are added/removed
*********/
@@ -515,33 +509,37 @@ namespace StardewModdingAPI.Framework
// reload affected assets
this.ContentCore.InvalidateCache(asset => interceptors.Any(p => p.CanIntercept(asset)));
}
+#endif
/*********
** Parse commands
*********/
- while (this.RawCommandQueue.TryDequeue(out string? rawInput))
+ if (this.RawCommandQueue.TryDequeue(out string[]? rawCommands))
{
- // parse command
- string? name;
- string[]? args;
- Command? command;
- int screenId;
- try
+ foreach (string rawInput in rawCommands)
{
- if (!this.CommandManager.TryParse(rawInput, out name, out args, out command, out screenId))
+ // parse command
+ string? name;
+ string[]? args;
+ Command? command;
+ int screenId;
+ try
{
- this.Monitor.Log("Unknown command; type 'help' for a list of available commands.", LogLevel.Error);
+ if (!this.CommandManager.TryParse(rawInput, out name, out args, out command, out screenId))
+ {
+ this.Monitor.Log("Unknown command; type 'help' for a list of available commands.", LogLevel.Error);
+ continue;
+ }
+ }
+ catch (Exception ex)
+ {
+ this.Monitor.Log($"Failed parsing that command:\n{ex.GetLogSummary()}", LogLevel.Error);
continue;
}
- }
- catch (Exception ex)
- {
- this.Monitor.Log($"Failed parsing that command:\n{ex.GetLogSummary()}", LogLevel.Error);
- continue;
- }
- // queue command for screen
- this.ScreenCommandQueue.GetValueForScreen(screenId).Add(new(command, name, args));
+ // queue command for screen
+ this.ScreenCommandQueue.GetValueForScreen(screenId).Add(new(command, name, args));
+ }
}
@@ -1287,7 +1285,7 @@ namespace StardewModdingAPI.Framework
private LocalizedContentManager CreateContentManager(IServiceProvider serviceProvider, string rootDirectory)
{
// Game1._temporaryContent initializing from SGame constructor
- // ReSharper disable once ConditionIsAlwaysTrueOrFalse -- this is the method that initializes it
+ // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract -- this is the method that initializes it
if (this.ContentCore == null)
{
this.ContentCore = new ContentCoordinator(
@@ -1453,16 +1451,15 @@ namespace StardewModdingAPI.Framework
/// <summary>Asynchronously check for a new version of SMAPI and any installed mods, and print alerts to the console if an update is available.</summary>
/// <param name="mods">The mods to include in the update check (if eligible).</param>
- private void CheckForUpdatesAsync(IModMetadata[] mods)
+ private async Task CheckForUpdatesAsync(IModMetadata[] mods)
{
- if (!this.Settings.CheckForUpdates)
- return;
-
- new Thread(() =>
+ try
{
+ if (!this.Settings.CheckForUpdates)
+ return;
+
// create client
- string url = this.Settings.WebApiBaseUrl;
- WebApiClient client = new(url, Constants.ApiVersion);
+ using WebApiClient client = new(this.Settings.WebApiBaseUrl, Constants.ApiVersion);
this.Monitor.Log("Checking for updates...");
// check SMAPI version
@@ -1472,9 +1469,15 @@ namespace StardewModdingAPI.Framework
try
{
// fetch update check
- ModEntryModel response = client.GetModInfo(new[] { new ModSearchEntryModel("Pathoschild.SMAPI", Constants.ApiVersion, new[] { $"GitHub:{this.Settings.GitHubProjectName}" }) }, apiVersion: Constants.ApiVersion, gameVersion: Constants.GameVersion, platform: Constants.Platform).Single().Value;
- updateFound = response.SuggestedUpdate?.Version;
- updateUrl = response.SuggestedUpdate?.Url;
+ IDictionary<string, ModEntryModel> response = await client.GetModInfoAsync(
+ mods: new[] { new ModSearchEntryModel("Pathoschild.SMAPI", Constants.ApiVersion, new[] { $"GitHub:{this.Settings.GitHubProjectName}" }) },
+ apiVersion: Constants.ApiVersion,
+ gameVersion: Constants.GameVersion,
+ platform: Constants.Platform
+ );
+ ModEntryModel updateInfo = response.Single().Value;
+ updateFound = updateInfo.SuggestedUpdate?.Version;
+ updateUrl = updateInfo.SuggestedUpdate?.Url;
// log message
if (updateFound != null)
@@ -1483,10 +1486,10 @@ namespace StardewModdingAPI.Framework
this.Monitor.Log(" SMAPI okay.");
// show errors
- if (response.Errors.Any())
+ if (updateInfo.Errors.Any())
{
this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.", LogLevel.Warn);
- this.Monitor.Log($"Error: {string.Join("\n", response.Errors)}");
+ this.Monitor.Log($"Error: {string.Join("\n", updateInfo.Errors)}");
}
}
catch (Exception ex)
@@ -1526,7 +1529,7 @@ namespace StardewModdingAPI.Framework
// fetch results
this.Monitor.Log($" Checking for updates to {searchMods.Count} mods...");
- IDictionary<string, ModEntryModel> results = client.GetModInfo(searchMods.ToArray(), apiVersion: Constants.ApiVersion, gameVersion: Constants.GameVersion, platform: Constants.Platform);
+ IDictionary<string, ModEntryModel> results = await client.GetModInfoAsync(searchMods.ToArray(), apiVersion: Constants.ApiVersion, gameVersion: Constants.GameVersion, platform: Constants.Platform);
// extract update alerts & errors
var updates = new List<Tuple<IModMetadata, ISemanticVersion, string>>();
@@ -1562,7 +1565,7 @@ namespace StardewModdingAPI.Framework
this.Monitor.Newline();
this.Monitor.Log($"You can update {updates.Count} mod{(updates.Count != 1 ? "s" : "")}:", LogLevel.Alert);
foreach ((IModMetadata mod, ISemanticVersion newVersion, string newUrl) in updates)
- this.Monitor.Log($" {mod.DisplayName} {newVersion}: {newUrl}", LogLevel.Alert);
+ this.Monitor.Log($" {mod.DisplayName} {newVersion}: {newUrl} (you have {mod.Manifest.Version})", LogLevel.Alert);
}
else
this.Monitor.Log(" All mods up to date.");
@@ -1576,7 +1579,15 @@ namespace StardewModdingAPI.Framework
);
}
}
- }).Start();
+ }
+ catch (Exception ex)
+ {
+ this.Monitor.Log("Couldn't check for updates. This won't affect your game, but you won't be notified of SMAPI or mod updates if this keeps happening.", LogLevel.Warn);
+ this.Monitor.Log(ex is WebException && ex.InnerException == null
+ ? ex.Message
+ : ex.ToString()
+ );
+ }
}
/// <summary>Create a directory path if it doesn't exist.</summary>
@@ -1646,9 +1657,9 @@ namespace StardewModdingAPI.Framework
// initialize loaded non-content-pack mods
this.Monitor.Log("Launching mods...", LogLevel.Debug);
-#pragma warning disable CS0612, CS0618 // deprecated code
foreach (IModMetadata metadata in loadedMods)
{
+#if SMAPI_DEPRECATED
// add interceptors
if (metadata.Mod?.Helper is ModHelper helper)
{
@@ -1684,7 +1695,6 @@ namespace StardewModdingAPI.Framework
content.ObservableAssetEditors.CollectionChanged += (_, e) => this.OnAssetInterceptorsChanged(metadata, e.NewItems?.Cast<IAssetEditor>(), e.OldItems?.Cast<IAssetEditor>(), this.ContentCore.Editors);
content.ObservableAssetLoaders.CollectionChanged += (_, e) => this.OnAssetInterceptorsChanged(metadata, e.NewItems?.Cast<IAssetLoader>(), e.OldItems?.Cast<IAssetLoader>(), this.ContentCore.Loaders);
}
-#pragma warning restore CS0612, CS0618
// log deprecation warnings
if (metadata.HasWarnings(ModWarning.DetectedLegacyCachingDll, ModWarning.DetectedLegacyConfigurationDll, ModWarning.DetectedLegacyPermissionsDll))
@@ -1710,6 +1720,7 @@ namespace StardewModdingAPI.Framework
);
}
}
+#endif
// call entry method
Context.HeuristicModsRunningCode.Push(metadata);
@@ -1750,6 +1761,7 @@ namespace StardewModdingAPI.Framework
this.Monitor.Log("Mods loaded and ready!", LogLevel.Debug);
}
+#if SMAPI_DEPRECATED
/// <summary>Raised after a mod adds or removes asset interceptors.</summary>
/// <typeparam name="T">The asset interceptor type (one of <see cref="IAssetEditor"/> or <see cref="IAssetLoader"/>).</typeparam>
/// <param name="mod">The mod metadata.</param>
@@ -1772,6 +1784,7 @@ namespace StardewModdingAPI.Framework
list.Remove(entry);
}
}
+#endif
/// <summary>Load a given mod.</summary>
/// <param name="mod">The mod to load.</param>
@@ -1795,7 +1808,7 @@ namespace StardewModdingAPI.Framework
string relativePath = mod.GetRelativePathWithRoot();
if (mod.IsContentPack)
this.Monitor.Log($" {mod.DisplayName} (from {relativePath}) [content pack]...");
- // ReSharper disable once ConstantConditionalAccessQualifier -- mod may be invalid at this point
+ // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract -- mod may be invalid at this point
else if (mod.Manifest?.EntryDll != null)
this.Monitor.Log($" {mod.DisplayName} (from {relativePath}{Path.DirectorySeparatorChar}{mod.Manifest.EntryDll})..."); // don't use Path.Combine here, since EntryDLL might not be valid
else
@@ -1915,9 +1928,9 @@ namespace StardewModdingAPI.Framework
{
IModEvents events = new ModEvents(mod, this.EventManager);
ICommandHelper commandHelper = new CommandHelper(mod, this.CommandManager);
-#pragma warning disable CS0612 // deprecated code
+#if SMAPI_DEPRECATED
ContentHelper contentHelper = new(contentCore, mod.DirectoryPath, mod, monitor, this.Reflection);
-#pragma warning restore CS0612
+#endif
GameContentHelper gameContentHelper = new(contentCore, mod, mod.DisplayName, monitor, this.Reflection);
IModContentHelper modContentHelper = new ModContentHelper(contentCore, mod.DirectoryPath, mod, mod.DisplayName, gameContentHelper.GetUnderlyingContentManager(), this.Reflection);
IContentPackHelper contentPackHelper = new ContentPackHelper(
@@ -1930,7 +1943,11 @@ namespace StardewModdingAPI.Framework
IModRegistry modRegistryHelper = new ModRegistryHelper(mod, this.ModRegistry, proxyFactory, monitor);
IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(mod, this.Multiplayer);
- modHelper = new ModHelper(mod, mod.DirectoryPath, () => this.GetCurrentGameInstance().Input, events, contentHelper, gameContentHelper, modContentHelper, contentPackHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper);
+ modHelper = new ModHelper(mod, mod.DirectoryPath, () => this.GetCurrentGameInstance().Input, events,
+#if SMAPI_DEPRECATED
+ contentHelper,
+#endif
+ gameContentHelper, modContentHelper, contentPackHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper);
}
// init mod
@@ -2220,7 +2237,10 @@ namespace StardewModdingAPI.Framework
private void ExitGameImmediately(string message)
{
this.Monitor.LogFatal(message);
- this.CancellationToken.Cancel();
+ this.LogManager.WriteCrashLog();
+
+ this.IsExiting = true;
+ this.Game.Exit();
}
/// <summary>Get the screen ID that should be logged to distinguish between players in split-screen mode, if any.</summary>