summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2022-04-13 21:07:43 -0400
committerJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2022-04-13 21:07:43 -0400
commit4adf8611131a5d86b15f017a42a0366837d14528 (patch)
tree879ba8dfc546382c3795e3d13a623b0e17f96469 /src/SMAPI/Framework
parent5b24fff4771dd11c627ae20c827599fe37fa89ad (diff)
downloadSMAPI-4adf8611131a5d86b15f017a42a0366837d14528.tar.gz
SMAPI-4adf8611131a5d86b15f017a42a0366837d14528.tar.bz2
SMAPI-4adf8611131a5d86b15f017a42a0366837d14528.zip
enable nullable annotations in the rest of SMAPI core (#837)
Diffstat (limited to 'src/SMAPI/Framework')
-rw-r--r--src/SMAPI/Framework/CommandManager.cs34
-rw-r--r--src/SMAPI/Framework/Commands/HelpCommand.cs8
-rw-r--r--src/SMAPI/Framework/Content/AssetDataForMap.cs23
-rw-r--r--src/SMAPI/Framework/Content/AssetName.cs21
-rw-r--r--src/SMAPI/Framework/Content/ContentCache.cs11
-rw-r--r--src/SMAPI/Framework/ContentManagers/BaseContentManager.cs21
-rw-r--r--src/SMAPI/Framework/ContentManagers/ModContentManager.cs21
-rw-r--r--src/SMAPI/Framework/DeprecationManager.cs25
-rw-r--r--src/SMAPI/Framework/Input/GamePadStateBuilder.cs9
-rw-r--r--src/SMAPI/Framework/Input/SInputState.cs6
-rw-r--r--src/SMAPI/Framework/InternalExtensions.cs9
-rw-r--r--src/SMAPI/Framework/Logging/InterceptingTextWriter.cs24
-rw-r--r--src/SMAPI/Framework/Logging/LogManager.cs36
-rw-r--r--src/SMAPI/Framework/ModHelpers/CommandHelper.cs4
-rw-r--r--src/SMAPI/Framework/ModLoading/AssemblyLoader.cs35
-rw-r--r--src/SMAPI/Framework/ModLoading/AssemblyParseResult.cs15
-rw-r--r--src/SMAPI/Framework/ModLoading/ModResolver.cs75
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs20
-rw-r--r--src/SMAPI/Framework/Models/SConfig.cs78
-rw-r--r--src/SMAPI/Framework/Networking/ModMessageModel.cs21
-rw-r--r--src/SMAPI/Framework/Networking/MultiplayerPeer.cs16
-rw-r--r--src/SMAPI/Framework/Networking/MultiplayerPeerMod.cs5
-rw-r--r--src/SMAPI/Framework/Networking/RemoteContextModModel.cs30
-rw-r--r--src/SMAPI/Framework/Networking/RemoteContextModel.cs33
-rw-r--r--src/SMAPI/Framework/Reflection/CacheEntry.cs12
-rw-r--r--src/SMAPI/Framework/Reflection/Reflector.cs127
-rw-r--r--src/SMAPI/Framework/SCore.cs172
-rw-r--r--src/SMAPI/Framework/SMultiplayer.cs89
-rw-r--r--src/SMAPI/Framework/Serialization/KeybindConverter.cs4
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableWatcher.cs5
-rw-r--r--src/SMAPI/Framework/StateTracking/PlayerTracker.cs16
-rw-r--r--src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs11
-rw-r--r--src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs122
33 files changed, 626 insertions, 512 deletions
diff --git a/src/SMAPI/Framework/CommandManager.cs b/src/SMAPI/Framework/CommandManager.cs
index 80c08f34..d3b9c8ee 100644
--- a/src/SMAPI/Framework/CommandManager.cs
+++ b/src/SMAPI/Framework/CommandManager.cs
@@ -1,7 +1,6 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using StardewModdingAPI.Framework.Commands;
@@ -39,9 +38,9 @@ namespace StardewModdingAPI.Framework
/// <exception cref="ArgumentNullException">The <paramref name="name"/> or <paramref name="callback"/> is null or empty.</exception>
/// <exception cref="FormatException">The <paramref name="name"/> is not a valid format.</exception>
/// <exception cref="ArgumentException">There's already a command with that name.</exception>
- public CommandManager Add(IModMetadata mod, string name, string documentation, Action<string, string[]> callback)
+ public CommandManager Add(IModMetadata? mod, string name, string documentation, Action<string, string[]> callback)
{
- name = this.GetNormalizedName(name);
+ name = this.GetNormalizedName(name)!; // null-checked below
// validate format
if (string.IsNullOrWhiteSpace(name))
@@ -72,10 +71,13 @@ namespace StardewModdingAPI.Framework
/// <summary>Get a command by its unique name.</summary>
/// <param name="name">The command name.</param>
/// <returns>Returns the matching command, or <c>null</c> if not found.</returns>
- public Command Get(string name)
+ public Command? Get(string? name)
{
- name = this.GetNormalizedName(name);
- this.Commands.TryGetValue(name, out Command command);
+ name = this.GetNormalizedName(name)!;
+ if (string.IsNullOrWhiteSpace(name))
+ return null;
+
+ this.Commands.TryGetValue(name, out Command? command);
return command;
}
@@ -94,7 +96,7 @@ namespace StardewModdingAPI.Framework
/// <param name="command">The command which can handle the input.</param>
/// <param name="screenId">The screen ID on which to run the command.</param>
/// <returns>Returns true if the input was successfully parsed and matched to a command; else false.</returns>
- public bool TryParse(string input, out string name, out string[] args, out Command command, out int screenId)
+ public bool TryParse(string? input, [NotNullWhen(true)] out string? name, [NotNullWhen(true)] out string[]? args, [NotNullWhen(true)] out Command? command, out int screenId)
{
// ignore if blank
if (string.IsNullOrWhiteSpace(input))
@@ -108,7 +110,7 @@ namespace StardewModdingAPI.Framework
// parse input
args = this.ParseArgs(input);
- name = this.GetNormalizedName(args[0]);
+ name = this.GetNormalizedName(args[0])!;
args = args.Skip(1).ToArray();
// get screen ID argument
@@ -116,7 +118,7 @@ namespace StardewModdingAPI.Framework
for (int i = 0; i < args.Length; i++)
{
// consume arg & set screen ID
- if (this.TryParseScreenId(args[i], out int rawScreenId, out string error))
+ if (this.TryParseScreenId(args[i], out int rawScreenId, out string? error))
{
args = args.Take(i).Concat(args.Skip(i + 1)).ToArray();
screenId = rawScreenId;
@@ -140,15 +142,15 @@ namespace StardewModdingAPI.Framework
/// <param name="name">The command name.</param>
/// <param name="arguments">The command arguments.</param>
/// <returns>Returns whether a matching command was triggered.</returns>
- public bool Trigger(string name, string[] arguments)
+ public bool Trigger(string? name, string[] arguments)
{
// get normalized name
- name = this.GetNormalizedName(name);
- if (name == null)
+ name = this.GetNormalizedName(name)!;
+ if (string.IsNullOrWhiteSpace(name))
return false;
// get command
- if (this.Commands.TryGetValue(name, out Command command))
+ if (this.Commands.TryGetValue(name, out Command? command))
{
command.Callback.Invoke(name, arguments);
return true;
@@ -191,7 +193,7 @@ namespace StardewModdingAPI.Framework
/// <param name="screen">The parsed screen ID, if any.</param>
/// <param name="error">The error which indicates an invalid screen ID, if applicable.</param>
/// <returns>Returns whether the screen ID was parsed successfully.</returns>
- private bool TryParseScreenId(string arg, out int screen, out string error)
+ private bool TryParseScreenId(string arg, out int screen, out string? error)
{
screen = -1;
error = null;
@@ -220,7 +222,7 @@ namespace StardewModdingAPI.Framework
/// <summary>Get a normalized command name.</summary>
/// <param name="name">The command name.</param>
- private string GetNormalizedName(string name)
+ private string? GetNormalizedName(string? name)
{
name = name?.Trim().ToLower();
return !string.IsNullOrWhiteSpace(name)
diff --git a/src/SMAPI/Framework/Commands/HelpCommand.cs b/src/SMAPI/Framework/Commands/HelpCommand.cs
index eb6c74f5..65dc3bce 100644
--- a/src/SMAPI/Framework/Commands/HelpCommand.cs
+++ b/src/SMAPI/Framework/Commands/HelpCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Linq;
namespace StardewModdingAPI.Framework.Commands
@@ -41,7 +39,7 @@ namespace StardewModdingAPI.Framework.Commands
{
if (args.Any())
{
- Command result = this.CommandManager.Get(args[0]);
+ Command? result = this.CommandManager.Get(args[0]);
if (result == null)
monitor.Log("There's no command with that name. Type 'help' by itself for more info.", LogLevel.Error);
else
@@ -63,10 +61,10 @@ namespace StardewModdingAPI.Framework.Commands
+ "--------------\n"
+ "The following commands are registered. For more info about a command, type 'help command_name'.\n\n";
- IGrouping<string, string>[] groups = (from command in this.CommandManager.GetAll() orderby command.Mod?.DisplayName, command.Name group command.Name by command.Mod?.DisplayName).ToArray();
+ IGrouping<string, string>[] groups = (from command in this.CommandManager.GetAll() orderby command.Mod?.DisplayName, command.Name group command.Name by command.Mod?.DisplayName ?? "SMAPI").ToArray();
foreach (var group in groups)
{
- string modName = group.Key ?? "SMAPI";
+ string modName = group.Key;
string[] commandNames = group.ToArray();
message += $"{modName}:\n {string.Join("\n ", commandNames)}\n\n";
}
diff --git a/src/SMAPI/Framework/Content/AssetDataForMap.cs b/src/SMAPI/Framework/Content/AssetDataForMap.cs
index 93148277..133dcc6c 100644
--- a/src/SMAPI/Framework/Content/AssetDataForMap.cs
+++ b/src/SMAPI/Framework/Content/AssetDataForMap.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -35,7 +33,7 @@ namespace StardewModdingAPI.Framework.Content
/// <param name="getNormalizedPath">Normalizes an asset key to match the cache key.</param>
/// <param name="onDataReplaced">A callback to invoke when the data is replaced (if any).</param>
/// <param name="reflection">Simplifies access to private code.</param>
- public AssetDataForMap(string locale, IAssetName assetName, Map data, Func<string, string> getNormalizedPath, Action<Map> onDataReplaced, Reflector reflection)
+ public AssetDataForMap(string? locale, IAssetName assetName, Map data, Func<string, string> getNormalizedPath, Action<Map> onDataReplaced, Reflector reflection)
: base(locale, assetName, data, getNormalizedPath, onDataReplaced)
{
this.Reflection = reflection;
@@ -126,8 +124,7 @@ namespace StardewModdingAPI.Framework.Content
foreach (Layer sourceLayer in source.Layers)
{
// get layer
- Layer targetLayer = sourceToTargetLayers[sourceLayer];
- if (targetLayer == null)
+ if (!sourceToTargetLayers.TryGetValue(sourceLayer, out Layer? targetLayer))
{
target.AddLayer(targetLayer = new Layer(sourceLayer.Id, target, target.Layers[0].LayerSize, Layer.m_tileSize));
sourceToTargetLayers[sourceLayer] = target.GetLayer(sourceLayer.Id);
@@ -137,11 +134,13 @@ namespace StardewModdingAPI.Framework.Content
targetLayer.Properties.CopyFrom(sourceLayer.Properties);
// create new tile
- Tile sourceTile = sourceLayer.Tiles[sourcePos.X, sourcePos.Y];
- Tile newTile = sourceTile != null
- ? this.CreateTile(sourceTile, targetLayer, tilesheetMap[sourceTile.TileSheet])
- : null;
- newTile?.Properties.CopyFrom(sourceTile.Properties);
+ Tile? sourceTile = sourceLayer.Tiles[sourcePos.X, sourcePos.Y];
+ Tile? newTile = null;
+ if (sourceTile != null)
+ {
+ newTile = this.CreateTile(sourceTile, targetLayer, tilesheetMap[sourceTile.TileSheet]);
+ newTile?.Properties.CopyFrom(sourceTile.Properties);
+ }
// replace tile
if (newTile != null || replaceByLayer || replaceAll)
@@ -195,7 +194,7 @@ namespace StardewModdingAPI.Framework.Content
/// <param name="sourceTile">The source tile to copy.</param>
/// <param name="targetLayer">The target layer.</param>
/// <param name="targetSheet">The target tilesheet.</param>
- private Tile CreateTile(Tile sourceTile, Layer targetLayer, TileSheet targetSheet)
+ private Tile? CreateTile(Tile sourceTile, Layer targetLayer, TileSheet targetSheet)
{
switch (sourceTile)
{
@@ -220,7 +219,7 @@ namespace StardewModdingAPI.Framework.Content
}
/// <summary>Normalize a map tilesheet path for comparison. This value should *not* be used as the actual tilesheet path.</summary>
/// <param name="path">The path to normalize.</param>
- private string NormalizeTilesheetPathForComparison(string path)
+ private string NormalizeTilesheetPathForComparison(string? path)
{
if (string.IsNullOrWhiteSpace(path))
return string.Empty;
diff --git a/src/SMAPI/Framework/Content/AssetName.cs b/src/SMAPI/Framework/Content/AssetName.cs
index 4d583d82..4c691b9a 100644
--- a/src/SMAPI/Framework/Content/AssetName.cs
+++ b/src/SMAPI/Framework/Content/AssetName.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using StardewModdingAPI.Toolkit.Utilities;
using StardewValley;
@@ -26,7 +24,7 @@ namespace StardewModdingAPI.Framework.Content
public string BaseName { get; }
/// <inheritdoc />
- public string LocaleCode { get; }
+ public string? LocaleCode { get; }
/// <inheritdoc />
public LocalizedContentManager.LanguageCode? LanguageCode { get; }
@@ -39,7 +37,7 @@ namespace StardewModdingAPI.Framework.Content
/// <param name="baseName">The base asset name without the locale code.</param>
/// <param name="localeCode">The locale code specified in the <see cref="Name"/>, if it's a valid code recognized by the game content.</param>
/// <param name="languageCode">The language code matching the <see cref="LocaleCode"/>, if applicable.</param>
- public AssetName(string baseName, string localeCode, LocalizedContentManager.LanguageCode? languageCode)
+ public AssetName(string baseName, string? localeCode, LocalizedContentManager.LanguageCode? languageCode)
{
// validate
if (string.IsNullOrWhiteSpace(baseName))
@@ -69,7 +67,7 @@ namespace StardewModdingAPI.Framework.Content
throw new ArgumentException("The asset name can't be null or empty.", nameof(rawName));
string baseName = rawName;
- string localeCode = null;
+ string? localeCode = null;
LocalizedContentManager.LanguageCode? languageCode = null;
int lastPeriodIndex = rawName.LastIndexOf('.');
@@ -90,7 +88,7 @@ namespace StardewModdingAPI.Framework.Content
}
/// <inheritdoc />
- public bool IsEquivalentTo(string assetName, bool useBaseName = false)
+ public bool IsEquivalentTo(string? assetName, bool useBaseName = false)
{
// empty asset key is never equivalent
if (string.IsNullOrWhiteSpace(assetName))
@@ -103,7 +101,7 @@ namespace StardewModdingAPI.Framework.Content
}
/// <inheritdoc />
- public bool IsEquivalentTo(IAssetName assetName, bool useBaseName = false)
+ public bool IsEquivalentTo(IAssetName? assetName, bool useBaseName = false)
{
if (useBaseName)
return this.BaseName.Equals(assetName?.BaseName, StringComparison.OrdinalIgnoreCase);
@@ -115,7 +113,7 @@ namespace StardewModdingAPI.Framework.Content
}
/// <inheritdoc />
- public bool StartsWith(string prefix, bool allowPartialWord = true, bool allowSubfolder = true)
+ public bool StartsWith(string? prefix, bool allowPartialWord = true, bool allowSubfolder = true)
{
// asset keys never start with null
if (prefix is null)
@@ -157,8 +155,11 @@ namespace StardewModdingAPI.Framework.Content
/// <inheritdoc />
- public bool IsDirectlyUnderPath(string assetFolder)
+ public bool IsDirectlyUnderPath(string? assetFolder)
{
+ if (assetFolder is null)
+ return false;
+
return this.StartsWith(assetFolder + "/", allowPartialWord: false, allowSubfolder: false);
}
@@ -171,7 +172,7 @@ namespace StardewModdingAPI.Framework.Content
}
/// <inheritdoc />
- public bool Equals(IAssetName other)
+ public bool Equals(IAssetName? other)
{
return other switch
{
diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs
index 4e620d28..d8862eb3 100644
--- a/src/SMAPI/Framework/Content/ContentCache.cs
+++ b/src/SMAPI/Framework/Content/ContentCache.cs
@@ -1,7 +1,6 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Linq;
using StardewModdingAPI.Framework.Reflection;
@@ -46,7 +45,8 @@ namespace StardewModdingAPI.Framework.Content
/// <param name="reflection">Simplifies access to private game code.</param>
public ContentCache(LocalizedContentManager contentManager, Reflector reflection)
{
- this.Cache = reflection.GetField<Dictionary<string, object>>(contentManager, "loadedAssets").GetValue();
+ this.Cache = reflection.GetField<Dictionary<string, object>>(contentManager, "loadedAssets").GetValue()
+ ?? throw new InvalidOperationException("Can't initialize content cache: required field 'loadedAssets' is missing.");
}
/****
@@ -66,7 +66,8 @@ namespace StardewModdingAPI.Framework.Content
/// <summary>Normalize path separators in an asset name.</summary>
/// <param name="path">The file path to normalize.</param>
[Pure]
- public string NormalizePathSeparators(string path)
+ [return: NotNullIfNotNull("path")]
+ public string? NormalizePathSeparators(string? path)
{
return PathUtilities.NormalizeAssetName(path);
}
@@ -93,7 +94,7 @@ namespace StardewModdingAPI.Framework.Content
public bool Remove(string key, bool dispose)
{
// get entry
- if (!this.Cache.TryGetValue(key, out object value))
+ if (!this.Cache.TryGetValue(key, out object? value))
return false;
// dispose & remove entry
diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
index f1ccab48..5ae5313d 100644
--- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@@ -99,11 +97,13 @@ namespace StardewModdingAPI.Framework.ContentManagers
this.AggressiveMemoryOptimizations = aggressiveMemoryOptimizations;
// get asset data
- this.BaseDisposableReferences = reflection.GetField<List<IDisposable>>(this, "disposableAssets").GetValue();
+ this.BaseDisposableReferences = reflection.GetField<List<IDisposable>>(this, "disposableAssets").GetValue()
+ ?? throw new InvalidOperationException("Can't initialize content manager: the required 'disposableAssets' field wasn't found.");
}
/// <inheritdoc />
public virtual bool DoesAssetExist<T>(IAssetName assetName)
+ where T : notnull
{
return this.Cache.ContainsKey(assetName.Name);
}
@@ -131,6 +131,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <inheritdoc />
public T LoadLocalized<T>(IAssetName assetName, LanguageCode language, bool useCache)
+ where T : notnull
{
// ignore locale in English (or if disabled)
if (!this.TryLocalizeKeys || language == LocalizedContentManager.LanguageCode.en)
@@ -172,11 +173,12 @@ namespace StardewModdingAPI.Framework.ContentManagers
}
/// <inheritdoc />
- public abstract T LoadExact<T>(IAssetName assetName, bool useCache);
+ public abstract T LoadExact<T>(IAssetName assetName, bool useCache)
+ where T : notnull;
/// <inheritdoc />
[SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")]
- public string AssertAndNormalizeAssetName(string assetName)
+ public string AssertAndNormalizeAssetName(string? assetName)
{
// NOTE: the game checks for ContentLoadException to handle invalid keys, so avoid
// throwing other types like ArgumentException here.
@@ -253,7 +255,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
// dispose uncached assets
foreach (WeakReference<IDisposable> reference in this.Disposables)
{
- if (reference.TryGetTarget(out IDisposable disposable))
+ if (reference.TryGetTarget(out IDisposable? disposable))
{
try
{
@@ -285,7 +287,8 @@ namespace StardewModdingAPI.Framework.ContentManagers
*********/
/// <summary>Apply initial normalization to a raw asset name before it's parsed.</summary>
/// <param name="assetName">The asset name to normalize.</param>
- private string PrenormalizeRawAssetName(string assetName)
+ [return: NotNullIfNotNull("assetName")]
+ private string? PrenormalizeRawAssetName(string? assetName)
{
// trim
assetName = assetName?.Trim();
@@ -301,7 +304,8 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>Normalize path separators in a file path. For asset keys, see <see cref="AssertAndNormalizeAssetName"/> instead.</summary>
/// <param name="path">The file path to normalize.</param>
[Pure]
- protected string NormalizePathSeparators(string path)
+ [return: NotNullIfNotNull("path")]
+ protected string? NormalizePathSeparators(string? path)
{
return this.Cache.NormalizePathSeparators(path);
}
@@ -323,6 +327,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="value">The asset value.</param>
/// <param name="useCache">Whether to save the asset to the asset cache.</param>
protected virtual void TrackAsset<T>(IAssetName assetName, T value, bool useCache)
+ where T : notnull
{
// track asset key
if (value is Texture2D texture)
diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
index 8051c296..f0f4bce9 100644
--- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Globalization;
using System.IO;
@@ -92,7 +90,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
// resolve managed asset key
{
- if (this.Coordinator.TryParseManagedAssetKey(assetName.Name, out string contentManagerID, out IAssetName relativePath))
+ if (this.Coordinator.TryParseManagedAssetKey(assetName.Name, out string? contentManagerID, out IAssetName? relativePath))
{
if (contentManagerID != this.Name)
throw this.GetLoadError(assetName, "can't load a different mod's managed asset key through this mod content manager.");
@@ -173,7 +171,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="file">The file to load.</param>
private T LoadDataFile<T>(IAssetName assetName, FileInfo file)
{
- if (!this.JsonHelper.ReadJsonFileIfExists(file.FullName, out T asset))
+ if (!this.JsonHelper.ReadJsonFileIfExists(file.FullName, out T? asset))
throw this.GetLoadError(assetName, "the JSON file is invalid."); // should never happen since we check for file existence before calling this method
return asset;
@@ -249,7 +247,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="assetName">The asset name that failed to load.</param>
/// <param name="reasonPhrase">The reason the file couldn't be loaded.</param>
/// <param name="exception">The underlying exception, if applicable.</param>
- private SContentLoadException GetLoadError(IAssetName assetName, string reasonPhrase, Exception exception = null)
+ private SContentLoadException GetLoadError(IAssetName assetName, string reasonPhrase, Exception? exception = null)
{
return new($"Failed loading asset '{assetName}' from {this.Name}: {reasonPhrase}", exception);
}
@@ -338,13 +336,16 @@ namespace StardewModdingAPI.Framework.ContentManagers
// load best match
try
{
- if (!this.TryGetTilesheetAssetName(relativeMapFolder, imageSource, out IAssetName assetName, out string error))
+ if (!this.TryGetTilesheetAssetName(relativeMapFolder, imageSource, out IAssetName? assetName, out string? error))
throw new SContentLoadException($"{errorPrefix} {error}");
- if (!assetName.IsEquivalentTo(tilesheet.ImageSource))
- this.Monitor.VerboseLog($" Mapped tilesheet '{tilesheet.ImageSource}' to '{assetName}'.");
+ if (assetName is not null)
+ {
+ if (!assetName.IsEquivalentTo(tilesheet.ImageSource))
+ this.Monitor.VerboseLog($" Mapped tilesheet '{tilesheet.ImageSource}' to '{assetName}'.");
- tilesheet.ImageSource = assetName.Name;
+ tilesheet.ImageSource = assetName.Name;
+ }
}
catch (Exception ex) when (ex is not SContentLoadException)
{
@@ -360,7 +361,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="error">A message indicating why the file couldn't be loaded.</param>
/// <returns>Returns whether the asset name was found.</returns>
/// <remarks>See remarks on <see cref="FixTilesheetPaths"/>.</remarks>
- private bool TryGetTilesheetAssetName(string modRelativeMapFolder, string relativePath, out IAssetName assetName, out string error)
+ private bool TryGetTilesheetAssetName(string modRelativeMapFolder, string relativePath, out IAssetName? assetName, out string? error)
{
assetName = null;
error = null;
diff --git a/src/SMAPI/Framework/DeprecationManager.cs b/src/SMAPI/Framework/DeprecationManager.cs
index fe1b623f..44b0ba2f 100644
--- a/src/SMAPI/Framework/DeprecationManager.cs
+++ b/src/SMAPI/Framework/DeprecationManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -38,14 +36,14 @@ namespace StardewModdingAPI.Framework
}
/// <summary>Get the source name for a mod from its unique ID.</summary>
- public string GetSourceNameFromStack()
+ public string? GetSourceNameFromStack()
{
return this.ModRegistry.GetFromStack()?.DisplayName;
}
/// <summary>Get the source name for a mod from its unique ID.</summary>
/// <param name="modId">The mod's unique ID.</param>
- public string GetSourceName(string modId)
+ public string? GetSourceName(string modId)
{
return this.ModRegistry.Get(modId)?.DisplayName;
}
@@ -55,10 +53,12 @@ namespace StardewModdingAPI.Framework
/// <param name="nounPhrase">A noun phrase describing what is deprecated.</param>
/// <param name="version">The SMAPI version which deprecated it.</param>
/// <param name="severity">How deprecated the code is.</param>
- public void Warn(string source, string nounPhrase, string version, DeprecationLevel severity)
+ public void Warn(string? source, string nounPhrase, string version, DeprecationLevel severity)
{
+ source ??= this.GetSourceNameFromStack() ?? "<unknown>";
+
// ignore if already warned
- if (!this.MarkWarned(source ?? this.GetSourceNameFromStack() ?? "<unknown>", nounPhrase, version))
+ if (!this.MarkWarned(source, nounPhrase, version))
return;
// queue warning
@@ -99,17 +99,12 @@ namespace StardewModdingAPI.Framework
}
// log message
- if (warning.ModName != null)
- this.Monitor.Log(message, level);
+ if (level == LogLevel.Trace)
+ this.Monitor.Log($"{message}\n{warning.StackTrace}", level);
else
{
- if (level == LogLevel.Trace)
- this.Monitor.Log($"{message}\n{warning.StackTrace}", level);
- else
- {
- this.Monitor.Log(message, level);
- this.Monitor.Log(warning.StackTrace, LogLevel.Debug);
- }
+ this.Monitor.Log(message, level);
+ this.Monitor.Log(warning.StackTrace, LogLevel.Debug);
}
}
diff --git a/src/SMAPI/Framework/Input/GamePadStateBuilder.cs b/src/SMAPI/Framework/Input/GamePadStateBuilder.cs
index 21168b7a..4ac3332c 100644
--- a/src/SMAPI/Framework/Input/GamePadStateBuilder.cs
+++ b/src/SMAPI/Framework/Input/GamePadStateBuilder.cs
@@ -1,6 +1,5 @@
-#nullable disable
-
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
@@ -23,7 +22,7 @@ namespace StardewModdingAPI.Framework.Input
private GamePadState? State;
/// <summary>The current button states.</summary>
- private readonly IDictionary<SButton, ButtonState> ButtonStates;
+ private readonly IDictionary<SButton, ButtonState>? ButtonStates;
/// <summary>The left trigger value.</summary>
private float LeftTrigger;
@@ -42,6 +41,7 @@ namespace StardewModdingAPI.Framework.Input
** Accessors
*********/
/// <summary>Whether the gamepad is currently connected.</summary>
+ [MemberNotNullWhen(true, nameof(GamePadStateBuilder.ButtonStates))]
public bool IsConnected { get; }
@@ -213,6 +213,9 @@ namespace StardewModdingAPI.Framework.Input
/// <summary>Get the pressed gamepad buttons.</summary>
private IEnumerable<Buttons> GetPressedGamePadButtons()
{
+ if (!this.IsConnected)
+ yield break;
+
foreach (var pair in this.ButtonStates)
{
if (pair.Value == ButtonState.Pressed && pair.Key.TryGetController(out Buttons button))
diff --git a/src/SMAPI/Framework/Input/SInputState.cs b/src/SMAPI/Framework/Input/SInputState.cs
index 37b3c8ef..fef83af7 100644
--- a/src/SMAPI/Framework/Input/SInputState.cs
+++ b/src/SMAPI/Framework/Input/SInputState.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -17,7 +15,7 @@ namespace StardewModdingAPI.Framework.Input
** Accessors
*********/
/// <summary>The cursor position on the screen adjusted for the zoom level.</summary>
- private CursorPosition CursorPositionImpl;
+ private CursorPosition CursorPositionImpl = new(Vector2.Zero, Vector2.Zero, Vector2.Zero, Vector2.Zero);
/// <summary>The player's last known tile position.</summary>
private Vector2? LastPlayerTile;
@@ -106,7 +104,7 @@ namespace StardewModdingAPI.Framework.Input
this.KeyboardState = keyboard.GetState();
this.MouseState = mouse.GetState();
this.ButtonStates = activeButtons;
- if (cursorAbsolutePos != this.CursorPositionImpl?.AbsolutePixels || playerTilePos != this.LastPlayerTile)
+ if (cursorAbsolutePos != this.CursorPositionImpl.AbsolutePixels || playerTilePos != this.LastPlayerTile)
{
this.LastPlayerTile = playerTilePos;
this.CursorPositionImpl = this.GetCursorPosition(this.MouseState, cursorAbsolutePos, zoomMultiplier);
diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs
index a1d87487..fd8cc86f 100644
--- a/src/SMAPI/Framework/InternalExtensions.cs
+++ b/src/SMAPI/Framework/InternalExtensions.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -43,6 +41,9 @@ namespace StardewModdingAPI.Framework
/// <param name="level">The log severity level.</param>
public static void LogAsMod(this IModMetadata metadata, string message, LogLevel level = LogLevel.Trace)
{
+ if (metadata.Monitor is null)
+ throw new InvalidOperationException($"Can't log as mod {metadata.DisplayName}: mod is broken or a content pack. Logged message:\n[{level}] {message}");
+
metadata.Monitor.Log(message, level);
}
@@ -52,7 +53,7 @@ namespace StardewModdingAPI.Framework
/// <param name="level">The log severity level.</param>
public static void LogAsModOnce(this IModMetadata metadata, string message, LogLevel level = LogLevel.Trace)
{
- metadata.Monitor.LogOnce(message, level);
+ metadata.Monitor?.LogOnce(message, level);
}
/****
@@ -159,7 +160,7 @@ namespace StardewModdingAPI.Framework
/// <param name="reflection">The reflection helper with which to access private fields.</param>
public static bool IsOpen(this SpriteBatch spriteBatch, Reflector reflection)
{
- return reflection.GetField<bool>(spriteBatch, "_beginCalled").GetValue();
+ return reflection.GetField<bool>(spriteBatch, "_beginCalled")!.GetValue();
}
}
}
diff --git a/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs b/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs
index a0957b90..9ecc1626 100644
--- a/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs
+++ b/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.IO;
using System.Text;
@@ -10,6 +8,13 @@ namespace StardewModdingAPI.Framework.Logging
internal class InterceptingTextWriter : TextWriter
{
/*********
+ ** Fields
+ *********/
+ /// <summary>The event raised when a message is written to the console directly.</summary>
+ private readonly Action<string> OnMessageIntercepted;
+
+
+ /*********
** Accessors
*********/
/// <summary>Prefixing a message with this character indicates that the console interceptor should write the string without intercepting it. (The character itself is not written.)</summary>
@@ -21,9 +26,6 @@ namespace StardewModdingAPI.Framework.Logging
/// <inheritdoc />
public override Encoding Encoding => this.Out.Encoding;
- /// <summary>The event raised when a message is written to the console directly.</summary>
- public event Action<string> OnMessageIntercepted;
-
/// <summary>Whether the text writer should ignore the next input if it's a newline.</summary>
/// <remarks>This is used when log output is suppressed from the console, since <c>Console.WriteLine</c> writes the trailing newline as a separate call.</remarks>
public bool IgnoreNextIfNewline { get; set; }
@@ -34,9 +36,11 @@ namespace StardewModdingAPI.Framework.Logging
*********/
/// <summary>Construct an instance.</summary>
/// <param name="output">The underlying output writer.</param>
- public InterceptingTextWriter(TextWriter output)
+ /// <param name="onMessageIntercepted">The event raised when a message is written to the console directly.</param>
+ public InterceptingTextWriter(TextWriter output, Action<string> onMessageIntercepted)
{
this.Out = output;
+ this.OnMessageIntercepted = onMessageIntercepted;
}
/// <inheritdoc />
@@ -65,7 +69,7 @@ namespace StardewModdingAPI.Framework.Logging
this.Out.Write(buffer, index, count);
}
else
- this.OnMessageIntercepted?.Invoke(new string(buffer, index, count));
+ this.OnMessageIntercepted(new string(buffer, index, count));
}
/// <inheritdoc />
@@ -74,12 +78,6 @@ namespace StardewModdingAPI.Framework.Logging
this.Out.Write(ch);
}
- /// <inheritdoc />
- protected override void Dispose(bool disposing)
- {
- this.OnMessageIntercepted = null;
- }
-
/*********
** Private methods
diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs
index dab7f554..a5989673 100644
--- a/src/SMAPI/Framework/Logging/LogManager.cs
+++ b/src/SMAPI/Framework/Logging/LogManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@@ -95,6 +93,11 @@ namespace StardewModdingAPI.Framework.Logging
/// <param name="getScreenIdForLog">Get the screen ID that should be logged to distinguish between players in split-screen mode, if any.</param>
public LogManager(string logPath, ColorSchemeConfig colorConfig, bool writeToConsole, bool isVerbose, bool isDeveloperMode, Func<int?> getScreenIdForLog)
{
+ // init fields
+ this.LogFile = new LogFileManager(logPath);
+ this.Monitor = this.GetMonitor("SMAPI");
+ this.MonitorForGame = this.GetMonitor("game");
+
// init construction logic
this.GetMonitorImpl = name => new Monitor(name, LogManager.IgnoreChar, this.LogFile, colorConfig, isVerbose, getScreenIdForLog)
{
@@ -103,15 +106,13 @@ namespace StardewModdingAPI.Framework.Logging
ShowFullStampInConsole = isDeveloperMode
};
- // init fields
- this.LogFile = new LogFileManager(logPath);
- this.Monitor = this.GetMonitor("SMAPI");
- this.MonitorForGame = this.GetMonitor("game");
-
// redirect direct console output
- this.ConsoleInterceptor = new InterceptingTextWriter(Console.Out);
- if (writeToConsole)
- this.ConsoleInterceptor.OnMessageIntercepted += message => this.HandleConsoleMessage(this.MonitorForGame, message);
+ this.ConsoleInterceptor = new InterceptingTextWriter(
+ output: Console.Out,
+ onMessageIntercepted: writeToConsole
+ ? message => this.HandleConsoleMessage(this.MonitorForGame, message)
+ : _ => { }
+ );
Console.SetOut(this.ConsoleInterceptor);
// enable Unicode handling on Windows
@@ -156,7 +157,7 @@ namespace StardewModdingAPI.Framework.Logging
while (true)
{
// get input
- string input = Console.ReadLine();
+ string? input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input))
continue;
@@ -222,7 +223,7 @@ namespace StardewModdingAPI.Framework.Logging
if (File.Exists(Constants.UpdateMarker))
{
string[] rawUpdateFound = File.ReadAllText(Constants.UpdateMarker).Split(new[] { '|' }, 2);
- if (SemanticVersion.TryParse(rawUpdateFound[0], out ISemanticVersion updateFound))
+ if (SemanticVersion.TryParse(rawUpdateFound[0], out ISemanticVersion? updateFound))
{
if (Constants.ApiVersion.IsPrerelease() && updateFound.IsNewerThan(Constants.ApiVersion))
{
@@ -264,7 +265,7 @@ namespace StardewModdingAPI.Framework.Logging
/// <summary>Log the initial header with general SMAPI and system details.</summary>
/// <param name="modsPath">The path from which mods will be loaded.</param>
/// <param name="customSettings">The custom SMAPI settings.</param>
- public void LogIntro(string modsPath, IDictionary<string, object> customSettings)
+ public void LogIntro(string modsPath, IDictionary<string, object?> customSettings)
{
// log platform
this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} (build {Constants.GetBuildVersionLabel()}) on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info);
@@ -326,7 +327,7 @@ namespace StardewModdingAPI.Framework.Logging
// log loaded content packs
if (loadedContentPacks.Any())
{
- string GetModDisplayName(string id) => loadedMods.FirstOrDefault(p => p.HasID(id))?.DisplayName;
+ string? GetModDisplayName(string id) => loadedMods.FirstOrDefault(p => p.HasID(id))?.DisplayName;
this.Monitor.Log($"Loaded {loadedContentPacks.Length} content packs:", LogLevel.Info);
foreach (IModMetadata metadata in loadedContentPacks.OrderBy(p => p.DisplayName))
@@ -335,7 +336,7 @@ namespace StardewModdingAPI.Framework.Logging
this.Monitor.Log(
$" {metadata.DisplayName} {manifest.Version}"
+ (!string.IsNullOrWhiteSpace(manifest.Author) ? $" by {manifest.Author}" : "")
- + $" | for {GetModDisplayName(metadata.Manifest.ContentPackFor.UniqueID)}"
+ + $" | for {GetModDisplayName(metadata.Manifest.ContentPackFor!.UniqueID)}"
+ (!string.IsNullOrWhiteSpace(manifest.Description) ? $" | {manifest.Description}" : ""),
LogLevel.Info
);
@@ -398,6 +399,7 @@ namespace StardewModdingAPI.Framework.Logging
/// <param name="mods">The loaded mods.</param>
/// <param name="skippedMods">The mods which could not be loaded.</param>
/// <param name="logParanoidWarnings">Whether to log issues for mods which directly use potentially sensitive .NET APIs like file or shell access.</param>
+ [SuppressMessage("ReSharper", "ConstantConditionalAccessQualifier", Justification = "Manifests aren't guaranteed non-null at this point in the loading process.")]
private void LogModWarnings(IEnumerable<IModMetadata> mods, IModMetadata[] skippedMods, bool logParanoidWarnings)
{
// get mods with warnings
@@ -431,7 +433,7 @@ namespace StardewModdingAPI.Framework.Logging
// duplicate mod: log first one only, don't show redundant version
if (mod.FailReason == ModFailReason.Duplicate && mod.HasManifest())
{
- if (loggedDuplicateIds.Add(mod.Manifest.UniqueID))
+ if (loggedDuplicateIds.Add(mod.Manifest!.UniqueID))
continue; // already logged
message = $" - {mod.DisplayName} because {mod.Error}";
@@ -610,7 +612,7 @@ namespace StardewModdingAPI.Framework.Logging
/// <param name="heading">A brief heading label for the group.</param>
/// <param name="blurb">A detailed explanation of the warning, split into lines.</param>
/// <param name="modLabel">Formats the mod label, or <c>null</c> to use the <see cref="IModMetadata.DisplayName"/>.</param>
- private void LogModWarningGroup(IModMetadata[] mods, Func<IModMetadata, bool> match, LogLevel level, string heading, string[] blurb, Func<IModMetadata, string> modLabel = null)
+ private void LogModWarningGroup(IModMetadata[] mods, Func<IModMetadata, bool> match, LogLevel level, string heading, string[] blurb, Func<IModMetadata, string>? modLabel = null)
{
// get matching mods
string[] modLabels = mods
diff --git a/src/SMAPI/Framework/ModHelpers/CommandHelper.cs b/src/SMAPI/Framework/ModHelpers/CommandHelper.cs
index c2b5092e..7d25979c 100644
--- a/src/SMAPI/Framework/ModHelpers/CommandHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/CommandHelper.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI.Framework.ModHelpers
@@ -24,7 +22,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <param name="mod">The mod using this instance.</param>
/// <param name="commandManager">Manages console commands.</param>
public CommandHelper(IModMetadata mod, CommandManager commandManager)
- : base(mod?.Manifest?.UniqueID ?? "SMAPI")
+ : base(mod.Manifest.UniqueID)
{
this.Mod = mod;
this.CommandManager = commandManager;
diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
index 070ee803..72b547b1 100644
--- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
+++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.IO;
@@ -96,7 +94,12 @@ namespace StardewModdingAPI.Framework.ModLoading
// get referenced local assemblies
AssemblyParseResult[] assemblies;
{
- HashSet<string> visitedAssemblyNames = new HashSet<string>(AppDomain.CurrentDomain.GetAssemblies().Select(p => p.GetName().Name)); // don't try loading assemblies that are already loaded
+ HashSet<string> visitedAssemblyNames = new HashSet<string>( // don't try loading assemblies that are already loaded
+ from assembly in AppDomain.CurrentDomain.GetAssemblies()
+ let name = assembly.GetName().Name
+ where name != null
+ select name
+ );
assemblies = this.GetReferencedLocalAssemblies(new FileInfo(assemblyPath), visitedAssemblyNames, this.AssemblyDefinitionResolver).ToArray();
}
@@ -113,11 +116,11 @@ namespace StardewModdingAPI.Framework.ModLoading
// rewrite & load assemblies in leaf-to-root order
bool oneAssembly = assemblies.Length == 1;
- Assembly lastAssembly = null;
+ Assembly? lastAssembly = null;
HashSet<string> loggedMessages = new HashSet<string>();
foreach (AssemblyParseResult assembly in assemblies)
{
- if (assembly.Status == AssemblyLoadStatus.AlreadyLoaded)
+ if (!assembly.HasDefinition)
continue;
// rewrite assembly
@@ -165,7 +168,7 @@ namespace StardewModdingAPI.Framework.ModLoading
throw new IncompatibleInstructionException();
// last assembly loaded is the root
- return lastAssembly;
+ return lastAssembly!;
}
/// <summary>Get whether an assembly is loaded.</summary>
@@ -174,7 +177,8 @@ namespace StardewModdingAPI.Framework.ModLoading
{
try
{
- return this.AssemblyDefinitionResolver.Resolve(reference) != null;
+ _ = this.AssemblyDefinitionResolver.Resolve(reference);
+ return true;
}
catch (AssemblyResolutionException)
{
@@ -190,7 +194,7 @@ namespace StardewModdingAPI.Framework.ModLoading
/// assemblies (especially with Mono). Since this is meant to be called on <see cref="AppDomain.AssemblyResolve"/>,
/// the implicit assumption is that loading the exact assembly failed.
/// </remarks>
- public static Assembly ResolveAssembly(string name)
+ public static Assembly? ResolveAssembly(string name)
{
string shortName = name.Split(new[] { ',' }, 2).First(); // get simple name (without version and culture)
return AppDomain.CurrentDomain
@@ -212,7 +216,8 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <summary>Track an object for disposal as part of the assembly loader.</summary>
/// <typeparam name="T">The instance type.</typeparam>
/// <param name="instance">The disposable instance.</param>
- private T TrackForDisposal<T>(T instance) where T : IDisposable
+ private T TrackForDisposal<T>(T instance)
+ where T : IDisposable
{
this.Disposables.Add(instance);
return instance;
@@ -321,9 +326,9 @@ namespace StardewModdingAPI.Framework.ModLoading
// rewrite types using custom attributes
foreach (TypeDefinition type in module.GetTypes())
{
- foreach (var attr in type.CustomAttributes)
+ foreach (CustomAttribute attr in type.CustomAttributes)
{
- foreach (var conField in attr.ConstructorArguments)
+ foreach (CustomAttributeArgument conField in attr.ConstructorArguments)
{
if (conField.Value is TypeReference typeRef)
this.ChangeTypeScope(typeRef);
@@ -382,7 +387,7 @@ namespace StardewModdingAPI.Framework.ModLoading
{
// get message template
// ($phrase is replaced with the noun phrase or messages)
- string template = null;
+ string? template = null;
switch (result)
{
case InstructionHandleResult.Rewritten:
@@ -441,20 +446,20 @@ namespace StardewModdingAPI.Framework.ModLoading
// format messages
string phrase = handler.Phrases.Any()
? string.Join(", ", handler.Phrases)
- : handler.DefaultPhrase ?? handler.GetType().Name;
+ : handler.DefaultPhrase;
this.Monitor.LogOnce(loggedMessages, template.Replace("$phrase", phrase));
}
/// <summary>Get the correct reference to use for compatibility with the current platform.</summary>
/// <param name="type">The type reference to rewrite.</param>
- private void ChangeTypeScope(TypeReference type)
+ private void ChangeTypeScope(TypeReference? type)
{
// check skip conditions
if (type == null || type.FullName.StartsWith("System."))
return;
// get assembly
- if (!this.TypeAssemblies.TryGetValue(type.FullName, out Assembly assembly))
+ if (!this.TypeAssemblies.TryGetValue(type.FullName, out Assembly? assembly))
return;
// replace scope
diff --git a/src/SMAPI/Framework/ModLoading/AssemblyParseResult.cs b/src/SMAPI/Framework/ModLoading/AssemblyParseResult.cs
index 56bd5a8b..b133f8d6 100644
--- a/src/SMAPI/Framework/ModLoading/AssemblyParseResult.cs
+++ b/src/SMAPI/Framework/ModLoading/AssemblyParseResult.cs
@@ -1,5 +1,5 @@
-#nullable disable
-
+using System;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using Mono.Cecil;
@@ -15,11 +15,15 @@ namespace StardewModdingAPI.Framework.ModLoading
public readonly FileInfo File;
/// <summary>The assembly definition.</summary>
- public readonly AssemblyDefinition Definition;
+ public readonly AssemblyDefinition? Definition;
/// <summary>The result of the assembly load.</summary>
public AssemblyLoadStatus Status;
+ /// <summary>Whether the <see cref="Definition"/> is loaded and ready (i.e. the <see cref="Status"/> is not <see cref="AssemblyLoadStatus.AlreadyLoaded"/> or <see cref="AssemblyLoadStatus.Failed"/>).</summary>
+ [MemberNotNullWhen(true, nameof(AssemblyParseResult.Definition))]
+ public bool HasDefinition => this.Status == AssemblyLoadStatus.Okay;
+
/*********
** Public methods
@@ -28,11 +32,14 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <param name="file">The original assembly file.</param>
/// <param name="assembly">The assembly definition.</param>
/// <param name="status">The result of the assembly load.</param>
- public AssemblyParseResult(FileInfo file, AssemblyDefinition assembly, AssemblyLoadStatus status)
+ public AssemblyParseResult(FileInfo file, AssemblyDefinition? assembly, AssemblyLoadStatus status)
{
this.File = file;
this.Definition = assembly;
this.Status = status;
+
+ if (status == AssemblyLoadStatus.Okay && assembly == null)
+ throw new InvalidOperationException($"Invalid assembly parse result: load status {status} with a null assembly.");
}
}
}
diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs
index 4fdeefbc..afb388d0 100644
--- a/src/SMAPI/Framework/ModLoading/ModResolver.cs
+++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs
@@ -1,7 +1,6 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using StardewModdingAPI.Toolkit;
@@ -28,10 +27,10 @@ namespace StardewModdingAPI.Framework.ModLoading
{
foreach (ModFolder folder in toolkit.GetModFolders(rootPath))
{
- Manifest manifest = folder.Manifest;
+ Manifest? manifest = folder.Manifest;
// parse internal data record (if any)
- ModDataRecordVersionedFields dataRecord = modDatabase.Get(manifest?.UniqueID)?.GetVersionedFields(manifest);
+ ModDataRecordVersionedFields? dataRecord = modDatabase.Get(manifest?.UniqueID)?.GetVersionedFields(manifest);
// apply defaults
if (manifest != null && dataRecord?.UpdateKey is not null)
@@ -43,7 +42,7 @@ namespace StardewModdingAPI.Framework.ModLoading
? ModMetadataStatus.Found
: ModMetadataStatus.Failed;
- var metadata = new ModMetadata(folder.DisplayName, folder.Directory.FullName, rootPath, manifest, dataRecord, isIgnored: shouldIgnore);
+ IModMetadata metadata = new ModMetadata(folder.DisplayName, folder.Directory.FullName, rootPath, manifest, dataRecord, isIgnored: shouldIgnore);
if (shouldIgnore)
metadata.SetStatus(status, ModFailReason.DisabledByDotConvention, "disabled by dot convention");
else
@@ -57,7 +56,9 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <param name="mods">The mod manifests to validate.</param>
/// <param name="apiVersion">The current SMAPI version.</param>
/// <param name="getUpdateUrl">Get an update URL for an update key (if valid).</param>
- public void ValidateManifests(IEnumerable<IModMetadata> mods, ISemanticVersion apiVersion, Func<string, string> getUpdateUrl)
+ [SuppressMessage("ReSharper", "ConstantConditionalAccessQualifier", Justification = "Manifest values may be null before they're validated.")]
+ [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalse", Justification = "Manifest values may be null before they're validated.")]
+ public void ValidateManifests(IEnumerable<IModMetadata> mods, ISemanticVersion apiVersion, Func<string, string?> getUpdateUrl)
{
mods = mods.ToArray();
@@ -84,7 +85,7 @@ namespace StardewModdingAPI.Framework.ModLoading
List<string> updateUrls = new List<string>();
foreach (UpdateKey key in mod.GetUpdateKeys(validOnly: true))
{
- string url = getUpdateUrl(key.ToString());
+ string? url = getUpdateUrl(key.ToString());
if (url != null)
updateUrls.Add(url);
}
@@ -94,7 +95,7 @@ namespace StardewModdingAPI.Framework.ModLoading
// build error
string error = $"{reasonPhrase}. Please check for a ";
- if (mod.DataRecord.StatusUpperVersion == null || mod.Manifest.Version.Equals(mod.DataRecord.StatusUpperVersion))
+ if (mod.DataRecord.StatusUpperVersion == null || mod.Manifest.Version?.Equals(mod.DataRecord.StatusUpperVersion) == true)
error += "newer version";
else
error += $"version newer than {mod.DataRecord.StatusUpperVersion}";
@@ -133,21 +134,21 @@ namespace StardewModdingAPI.Framework.ModLoading
if (hasDll)
{
// invalid filename format
- if (mod.Manifest.EntryDll.Intersect(Path.GetInvalidFileNameChars()).Any())
+ 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;
}
// invalid path
- if (!File.Exists(Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll)))
+ if (!File.Exists(Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll!)))
{
mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist.");
continue;
}
// invalid capitalization
- string actualFilename = new DirectoryInfo(mod.DirectoryPath).GetFiles(mod.Manifest.EntryDll).FirstOrDefault()?.Name;
+ string? actualFilename = new DirectoryInfo(mod.DirectoryPath).GetFiles(mod.Manifest.EntryDll!).FirstOrDefault()?.Name;
if (actualFilename != mod.Manifest.EntryDll)
{
mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its {nameof(IManifest.EntryDll)} value '{mod.Manifest.EntryDll}' doesn't match the actual file capitalization '{actualFilename}'. The capitalization must match for crossplatform compatibility.");
@@ -159,7 +160,7 @@ namespace StardewModdingAPI.Framework.ModLoading
else
{
// invalid content pack ID
- if (string.IsNullOrWhiteSpace(mod.Manifest.ContentPackFor.UniqueID))
+ 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;
@@ -190,7 +191,7 @@ namespace StardewModdingAPI.Framework.ModLoading
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 (var dependency in mod.Manifest.Dependencies)
+ foreach (IManifestDependency? dependency in mod.Manifest.Dependencies)
{
// null dependency
if (dependency == null)
@@ -328,8 +329,11 @@ namespace StardewModdingAPI.Framework.ModLoading
string[] failedLabels =
(
from entry in dependencies
- where entry.Mod != null && entry.MinVersion != null && entry.MinVersion.IsNewerThan(entry.Mod.Manifest.Version)
- select $"{entry.Mod.DisplayName} (needs {entry.MinVersion} or later)"
+ where
+ entry.Mod != null
+ && entry.MinVersion != null
+ && entry.MinVersion.IsNewerThan(entry.Mod.Manifest.Version)
+ select $"{entry.Mod!.DisplayName} (needs {entry.MinVersion} or later)"
)
.ToArray();
if (failedLabels.Any())
@@ -345,16 +349,14 @@ namespace StardewModdingAPI.Framework.ModLoading
states[mod] = ModDependencyStatus.Checking;
// recursively sort dependencies
- foreach (var dependency in dependencies)
+ foreach (ModDependency dependency in dependencies)
{
- IModMetadata requiredMod = dependency.Mod;
- var subchain = new List<IModMetadata>(currentChain) { mod };
-
- // ignore missing optional dependency
- if (!dependency.IsRequired && requiredMod == null)
- continue;
+ IModMetadata? requiredMod = dependency.Mod;
+ if (requiredMod == null)
+ continue; // missing dependencies are handled earlier
// detect dependency loop
+ var subchain = new List<IModMetadata>(currentChain) { mod };
if (states[requiredMod] == ModDependencyStatus.Checking)
{
sortedMods.Push(mod);
@@ -363,8 +365,8 @@ namespace StardewModdingAPI.Framework.ModLoading
}
// recursively process each dependency
- var substatus = this.ProcessDependencies(mods, modDatabase, requiredMod, states, sortedMods, subchain);
- switch (substatus)
+ var subStatus = this.ProcessDependencies(mods, modDatabase, requiredMod, states, sortedMods, subchain);
+ switch (subStatus)
{
// sorted successfully
case ModDependencyStatus.Sorted:
@@ -380,7 +382,7 @@ namespace StardewModdingAPI.Framework.ModLoading
// 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.");
+ throw new InvalidModStateException($"Something went wrong sorting dependencies: mod '{requiredMod.DisplayName}' unexpectedly stayed in the '{subStatus}' status.");
// sanity check
default:
@@ -399,14 +401,11 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <param name="loadedMods">The loaded mods.</param>
private IEnumerable<ModDependency> GetDependenciesFrom(IManifest manifest, IModMetadata[] loadedMods)
{
- IModMetadata FindMod(string id) => loadedMods.FirstOrDefault(m => m.HasID(id));
+ IModMetadata? FindMod(string id) => loadedMods.FirstOrDefault(m => m.HasID(id));
// yield dependencies
- if (manifest.Dependencies != null)
- {
- foreach (var entry in manifest.Dependencies)
- yield return new ModDependency(entry.UniqueID, entry.MinimumVersion, FindMod(entry.UniqueID), entry.IsRequired);
- }
+ foreach (IManifestDependency entry in manifest.Dependencies)
+ yield return new ModDependency(entry.UniqueID, entry.MinimumVersion, FindMod(entry.UniqueID), entry.IsRequired);
// yield content pack parent
if (manifest.ContentPackFor != null)
@@ -415,10 +414,10 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <summary>Get a technical message indicating why a mod's compatibility status was overridden, if applicable.</summary>
/// <param name="mod">The mod metadata.</param>
- private string GetTechnicalReasonForStatusOverride(IModMetadata mod)
+ private string? GetTechnicalReasonForStatusOverride(IModMetadata mod)
{
// get compatibility list record
- var data = mod.DataRecord;
+ ModDataRecordVersionedFields? data = mod.DataRecord;
if (data == null)
return null;
@@ -432,14 +431,14 @@ namespace StardewModdingAPI.Framework.ModLoading
};
// get reason
- string[] reasons = new[] { mod.DataRecord.StatusReasonPhrase, mod.DataRecord.StatusReasonDetails }
+ string?[] reasons = new[] { data.StatusReasonPhrase, data.StatusReasonDetails }
.Where(p => !string.IsNullOrWhiteSpace(p))
.ToArray();
// build message
return
$"marked {statusLabel} in SMAPI's internal compatibility list for "
- + (mod.DataRecord.StatusUpperVersion != null ? $"versions up to {mod.DataRecord.StatusUpperVersion}" : "all versions")
+ + (data.StatusUpperVersion != null ? $"versions up to {data.StatusUpperVersion}" : "all versions")
+ ": "
+ (reasons.Any() ? string.Join(": ", reasons) : "no reason given")
+ ".";
@@ -459,13 +458,13 @@ namespace StardewModdingAPI.Framework.ModLoading
public string ID { get; }
/// <summary>The minimum required version (if any).</summary>
- public ISemanticVersion MinVersion { get; }
+ public ISemanticVersion? MinVersion { get; }
/// <summary>Whether the mod shouldn't be loaded if the dependency isn't available.</summary>
public bool IsRequired { get; }
/// <summary>The loaded mod that fulfills the dependency (if available).</summary>
- public IModMetadata Mod { get; }
+ public IModMetadata? Mod { get; }
/*********
@@ -476,7 +475,7 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <param name="minVersion">The minimum required version (if any).</param>
/// <param name="mod">The loaded mod that fulfills the dependency (if available).</param>
/// <param name="isRequired">Whether the mod shouldn't be loaded if the dependency isn't available.</param>
- public ModDependency(string id, ISemanticVersion minVersion, IModMetadata mod, bool isRequired)
+ public ModDependency(string id, ISemanticVersion? minVersion, IModMetadata? mod, bool isRequired)
{
this.ID = id;
this.MinVersion = minVersion;
diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs
index 806fca62..d5f4cf4a 100644
--- a/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs
+++ b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Reflection;
@@ -33,13 +31,19 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
/// <param name="toFieldName">The new field name to reference.</param>
public FieldReplaceRewriter AddField(Type fromType, string fromFieldName, Type toType, string toFieldName)
{
+ // validate parameters
+ if (fromType == null)
+ throw new InvalidOperationException("Can't replace a field on a null source type.");
+ if (toType == null)
+ throw new InvalidOperationException("Can't replace a field on a null target type.");
+
// get full type name
- string fromTypeName = fromType?.FullName;
+ string? fromTypeName = fromType.FullName;
if (fromTypeName == null)
throw new InvalidOperationException($"Can't replace field for invalid type reference {toType}.");
// get target field
- FieldInfo toField = toType.GetField(toFieldName);
+ FieldInfo? toField = toType.GetField(toFieldName);
if (toField == null)
throw new InvalidOperationException($"The {toType.FullName} class doesn't have a {toFieldName} field.");
@@ -54,15 +58,15 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
/// <inheritdoc />
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction)
{
- FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction);
- string declaringType = fieldRef?.DeclaringType?.FullName;
+ FieldReference? fieldRef = RewriteHelper.AsFieldReference(instruction);
+ string? declaringType = fieldRef?.DeclaringType?.FullName;
// get mapped field
- if (declaringType == null || !this.FieldMaps.TryGetValue(declaringType, out var fieldMap) || !fieldMap.TryGetValue(fieldRef.Name, out FieldInfo toField))
+ if (declaringType == null || !this.FieldMaps.TryGetValue(declaringType, out var fieldMap) || !fieldMap.TryGetValue(fieldRef!.Name, out FieldInfo? toField))
return false;
// replace with new field
- this.Phrases.Add($"{fieldRef.DeclaringType.Name}.{fieldRef.Name} field");
+ this.Phrases.Add($"{fieldRef.DeclaringType!.Name}.{fieldRef.Name} field");
instruction.Operand = module.ImportReference(toField);
return this.MarkRewritten();
}
diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs
index e74d73b5..d626ab4d 100644
--- a/src/SMAPI/Framework/Models/SConfig.cs
+++ b/src/SMAPI/Framework/Models/SConfig.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -40,60 +38,96 @@ namespace StardewModdingAPI.Framework.Models
** Accessors
********/
/// <summary>Whether to enable development features.</summary>
- public bool DeveloperMode { get; set; }
+ public bool DeveloperMode { get; private set; }
/// <summary>Whether to check for newer versions of SMAPI and mods on startup.</summary>
- public bool CheckForUpdates { get; set; }
+ public bool CheckForUpdates { get; }
/// <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; } = (bool)SConfig.DefaultValues[nameof(SConfig.ParanoidWarnings)];
+ public bool ParanoidWarnings { get; }
/// <summary>Whether to show beta versions as valid updates.</summary>
- public bool UseBetaChannel { get; set; } = (bool)SConfig.DefaultValues[nameof(SConfig.UseBetaChannel)];
+ public bool UseBetaChannel { get; }
/// <summary>SMAPI's GitHub project name, used to perform update checks.</summary>
- public string GitHubProjectName { get; set; }
+ public string GitHubProjectName { get; }
/// <summary>The base URL for SMAPI's web API, used to perform update checks.</summary>
- public string WebApiBaseUrl { get; set; }
+ public string WebApiBaseUrl { get; }
/// <summary>Whether SMAPI should log more information about the game context.</summary>
- public bool VerboseLogging { get; set; }
+ public bool VerboseLogging { get; }
/// <summary>Whether SMAPI should rewrite mods for compatibility.</summary>
- public bool RewriteMods { get; set; } = (bool)SConfig.DefaultValues[nameof(SConfig.RewriteMods)];
+ public bool RewriteMods { get; }
/// <summary>Whether to enable more aggressive memory optimizations.</summary>
- public bool AggressiveMemoryOptimizations { get; set; } = (bool)SConfig.DefaultValues[nameof(SConfig.AggressiveMemoryOptimizations)];
+ public bool AggressiveMemoryOptimizations { get; }
/// <summary>Whether SMAPI should log network traffic. Best combined with <see cref="VerboseLogging"/>, which includes network metadata.</summary>
- public bool LogNetworkTraffic { get; set; }
+ public bool LogNetworkTraffic { get; }
/// <summary>The colors to use for text written to the SMAPI console.</summary>
- public ColorSchemeConfig ConsoleColors { get; set; }
+ public ColorSchemeConfig ConsoleColors { get; }
/// <summary>The mod IDs SMAPI should ignore when performing update checks or validating update keys.</summary>
- public string[] SuppressUpdateChecks { get; set; }
+ public string[] SuppressUpdateChecks { get; }
/********
** 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">Whether SMAPI should log more information about the game context.</param>
+ /// <param name="rewriteMods">Whether SMAPI should rewrite mods for compatibility.</param>
+ /// <param name="aggressiveMemoryOptimizations">Whether to enable more aggressive memory optimizations.</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="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, bool verboseLogging, bool? rewriteMods, bool? aggressiveMemoryOptimizations, bool logNetworkTraffic, ColorSchemeConfig consoleColors, string[]? suppressUpdateChecks)
+ {
+ this.DeveloperMode = developerMode;
+ this.CheckForUpdates = checkForUpdates;
+ this.ParanoidWarnings = paranoidWarnings ?? (bool)SConfig.DefaultValues[nameof(SConfig.ParanoidWarnings)];
+ this.UseBetaChannel = useBetaChannel ?? (bool)SConfig.DefaultValues[nameof(SConfig.UseBetaChannel)];
+ this.GitHubProjectName = gitHubProjectName;
+ this.WebApiBaseUrl = webApiBaseUrl;
+ this.VerboseLogging = verboseLogging;
+ this.RewriteMods = rewriteMods ?? (bool)SConfig.DefaultValues[nameof(SConfig.RewriteMods)];
+ this.AggressiveMemoryOptimizations = aggressiveMemoryOptimizations ?? (bool)SConfig.DefaultValues[nameof(SConfig.AggressiveMemoryOptimizations)];
+ this.LogNetworkTraffic = logNetworkTraffic;
+ this.ConsoleColors = consoleColors;
+ this.SuppressUpdateChecks = suppressUpdateChecks ?? Array.Empty<string>();
+ }
+
+ /// <summary>Override the value of <see cref="DeveloperMode"/>.</summary>
+ /// <param name="value">The value to set.</param>
+ public void OverrideDeveloperMode(bool value)
+ {
+ this.DeveloperMode = value;
+ }
+
/// <summary>Get the settings which have been customized by the player.</summary>
- public IDictionary<string, object> GetCustomSettings()
+ public IDictionary<string, object?> GetCustomSettings()
{
- IDictionary<string, object> custom = new Dictionary<string, object>();
+ Dictionary<string, object?> custom = new();
- foreach (var pair in SConfig.DefaultValues)
+ foreach ((string? name, object defaultValue) in SConfig.DefaultValues)
{
- object value = typeof(SConfig).GetProperty(pair.Key)?.GetValue(this);
- if (!pair.Value.Equals(value))
- custom[pair.Key] = value;
+ object? value = typeof(SConfig).GetProperty(name)?.GetValue(this);
+ if (!defaultValue.Equals(value))
+ custom[name] = value;
}
- HashSet<string> curSuppressUpdateChecks = new HashSet<string>(this.SuppressUpdateChecks ?? Array.Empty<string>(), StringComparer.OrdinalIgnoreCase);
+ HashSet<string> curSuppressUpdateChecks = new(this.SuppressUpdateChecks, StringComparer.OrdinalIgnoreCase);
if (SConfig.DefaultSuppressUpdateChecks.Count != curSuppressUpdateChecks.Count || SConfig.DefaultSuppressUpdateChecks.Any(p => !curSuppressUpdateChecks.Contains(p)))
- custom[nameof(this.SuppressUpdateChecks)] = "[" + string.Join(", ", this.SuppressUpdateChecks ?? Array.Empty<string>()) + "]";
+ custom[nameof(this.SuppressUpdateChecks)] = "[" + string.Join(", ", this.SuppressUpdateChecks) + "]";
return custom;
}
diff --git a/src/SMAPI/Framework/Networking/ModMessageModel.cs b/src/SMAPI/Framework/Networking/ModMessageModel.cs
index 4e7d01eb..01672714 100644
--- a/src/SMAPI/Framework/Networking/ModMessageModel.cs
+++ b/src/SMAPI/Framework/Networking/ModMessageModel.cs
@@ -1,6 +1,5 @@
-#nullable disable
-
using System.Linq;
+using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace StardewModdingAPI.Framework.Networking
@@ -15,41 +14,39 @@ namespace StardewModdingAPI.Framework.Networking
** Origin
****/
/// <summary>The unique ID of the player who broadcast the message.</summary>
- public long FromPlayerID { get; set; }
+ public long FromPlayerID { get; }
/// <summary>The unique ID of the mod which broadcast the message.</summary>
- public string FromModID { get; set; }
+ public string FromModID { get; }
/****
** Destination
****/
/// <summary>The players who should receive the message.</summary>
- public long[] ToPlayerIDs { get; set; }
+ public long[]? ToPlayerIDs { get; init; }
/// <summary>The mods which should receive the message, or <c>null</c> for all mods.</summary>
- public string[] ToModIDs { get; set; }
+ public string[]? ToModIDs { get; }
/// <summary>A message type which receiving mods can use to decide whether it's the one they want to handle, like <c>SetPlayerLocation</c>. This doesn't need to be globally unique, since mods should check the originating mod ID.</summary>
- public string Type { get; set; }
+ public string Type { get; }
/// <summary>The custom mod data being broadcast.</summary>
- public JToken Data { get; set; }
+ public JToken Data { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
- public ModMessageModel() { }
-
- /// <summary>Construct an instance.</summary>
/// <param name="fromPlayerID">The unique ID of the player who broadcast the message.</param>
/// <param name="fromModID">The unique ID of the mod which broadcast the message.</param>
/// <param name="toPlayerIDs">The players who should receive the message, or <c>null</c> for all players.</param>
/// <param name="toModIDs">The mods which should receive the message, or <c>null</c> for all mods.</param>
/// <param name="type">A message type which receiving mods can use to decide whether it's the one they want to handle, like <c>SetPlayerLocation</c>. This doesn't need to be globally unique, since mods should check the originating mod ID.</param>
/// <param name="data">The custom mod data being broadcast.</param>
- public ModMessageModel(long fromPlayerID, string fromModID, long[] toPlayerIDs, string[] toModIDs, string type, JToken data)
+ [JsonConstructor]
+ public ModMessageModel(long fromPlayerID, string fromModID, long[]? toPlayerIDs, string[]? toModIDs, string type, JToken data)
{
this.FromPlayerID = fromPlayerID;
this.FromModID = fromModID;
diff --git a/src/SMAPI/Framework/Networking/MultiplayerPeer.cs b/src/SMAPI/Framework/Networking/MultiplayerPeer.cs
index 8ee5c309..b37c1e89 100644
--- a/src/SMAPI/Framework/Networking/MultiplayerPeer.cs
+++ b/src/SMAPI/Framework/Networking/MultiplayerPeer.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -39,10 +37,10 @@ namespace StardewModdingAPI.Framework.Networking
public GamePlatform? Platform { get; }
/// <inheritdoc />
- public ISemanticVersion GameVersion { get; }
+ public ISemanticVersion? GameVersion { get; }
/// <inheritdoc />
- public ISemanticVersion ApiVersion { get; }
+ public ISemanticVersion? ApiVersion { get; }
/// <inheritdoc />
public IEnumerable<IMultiplayerPeerMod> Mods { get; }
@@ -57,11 +55,12 @@ namespace StardewModdingAPI.Framework.Networking
/// <param name="model">The metadata to copy.</param>
/// <param name="sendMessage">A method which sends a message to the peer.</param>
/// <param name="isHost">Whether this is a connection to the host player.</param>
- public MultiplayerPeer(long playerID, int? screenID, RemoteContextModel model, Action<OutgoingMessage> sendMessage, bool isHost)
+ public MultiplayerPeer(long playerID, int? screenID, RemoteContextModel? model, Action<OutgoingMessage> sendMessage, bool isHost)
{
this.PlayerID = playerID;
this.ScreenID = screenID;
this.IsHost = isHost;
+
if (model != null)
{
this.Platform = model.Platform;
@@ -69,13 +68,16 @@ namespace StardewModdingAPI.Framework.Networking
this.ApiVersion = model.ApiVersion;
this.Mods = model.Mods.Select(mod => new MultiplayerPeerMod(mod)).ToArray();
}
+ else
+ this.Mods = Array.Empty<IMultiplayerPeerMod>();
+
this.SendMessageImpl = sendMessage;
}
/// <inheritdoc />
- public IMultiplayerPeerMod GetMod(string id)
+ public IMultiplayerPeerMod? GetMod(string? id)
{
- if (string.IsNullOrWhiteSpace(id) || this.Mods == null || !this.Mods.Any())
+ if (string.IsNullOrWhiteSpace(id) || !this.Mods.Any())
return null;
id = id.Trim();
diff --git a/src/SMAPI/Framework/Networking/MultiplayerPeerMod.cs b/src/SMAPI/Framework/Networking/MultiplayerPeerMod.cs
index 6fdb9e54..1e150508 100644
--- a/src/SMAPI/Framework/Networking/MultiplayerPeerMod.cs
+++ b/src/SMAPI/Framework/Networking/MultiplayerPeerMod.cs
@@ -1,4 +1,4 @@
-#nullable disable
+using System.Diagnostics.CodeAnalysis;
namespace StardewModdingAPI.Framework.Networking
{
@@ -22,10 +22,11 @@ namespace StardewModdingAPI.Framework.Networking
*********/
/// <summary>Construct an instance.</summary>
/// <param name="mod">The mod metadata.</param>
+ [SuppressMessage("ReSharper", "ConstantConditionalAccessQualifier", Justification = "The ID shouldn't be null, but we should handle it to avoid an error just in case.")]
public MultiplayerPeerMod(RemoteContextModModel mod)
{
this.Name = mod.Name;
- this.ID = mod.ID?.Trim();
+ this.ID = mod.ID?.Trim() ?? string.Empty;
this.Version = mod.Version;
}
}
diff --git a/src/SMAPI/Framework/Networking/RemoteContextModModel.cs b/src/SMAPI/Framework/Networking/RemoteContextModModel.cs
index 0383576c..7571acba 100644
--- a/src/SMAPI/Framework/Networking/RemoteContextModModel.cs
+++ b/src/SMAPI/Framework/Networking/RemoteContextModModel.cs
@@ -1,17 +1,33 @@
-#nullable disable
-
namespace StardewModdingAPI.Framework.Networking
{
/// <summary>Metadata about an installed mod exchanged with connected computers.</summary>
public class RemoteContextModModel
{
- /// <summary>The mod's display name.</summary>
- public string Name { get; set; }
-
+ /*********
+ ** Accessors
+ *********/
/// <summary>The unique mod ID.</summary>
- public string ID { get; set; }
+ public string ID { get; }
+
+ /// <summary>The mod's display name.</summary>
+ public string Name { get; }
/// <summary>The mod version.</summary>
- public ISemanticVersion Version { get; set; }
+ public ISemanticVersion Version { get; }
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="id">The unique mod ID.</param>
+ /// <param name="name">The mod's display name.</param>
+ /// <param name="version">The mod version.</param>
+ public RemoteContextModModel(string id, string name, ISemanticVersion version)
+ {
+ this.ID = id;
+ this.Name = name;
+ this.Version = version;
+ }
}
}
diff --git a/src/SMAPI/Framework/Networking/RemoteContextModel.cs b/src/SMAPI/Framework/Networking/RemoteContextModel.cs
index 37fafa67..7d53e732 100644
--- a/src/SMAPI/Framework/Networking/RemoteContextModel.cs
+++ b/src/SMAPI/Framework/Networking/RemoteContextModel.cs
@@ -1,4 +1,4 @@
-#nullable disable
+using System;
namespace StardewModdingAPI.Framework.Networking
{
@@ -9,18 +9,37 @@ namespace StardewModdingAPI.Framework.Networking
** Accessors
*********/
/// <summary>Whether this player is the host player.</summary>
- public bool IsHost { get; set; }
+ public bool IsHost { get; }
- /// <summary>The game's platform version.</summary>
- public GamePlatform Platform { get; set; }
+ /// <summary>The game's platform.</summary>
+ public GamePlatform Platform { get; }
/// <summary>The installed version of Stardew Valley.</summary>
- public ISemanticVersion GameVersion { get; set; }
+ public ISemanticVersion? GameVersion { get; }
/// <summary>The installed version of SMAPI.</summary>
- public ISemanticVersion ApiVersion { get; set; }
+ public ISemanticVersion? ApiVersion { get; }
/// <summary>The installed mods.</summary>
- public RemoteContextModModel[] Mods { get; set; }
+ public RemoteContextModModel[] Mods { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="isHost">Whether this player is the host player.</param>
+ /// <param name="platform">The game's platform.</param>
+ /// <param name="gameVersion">The installed version of Stardew Valley.</param>
+ /// <param name="apiVersion">The installed version of SMAPI.</param>
+ /// <param name="mods">The installed mods.</param>
+ public RemoteContextModel(bool isHost, GamePlatform platform, ISemanticVersion gameVersion, ISemanticVersion apiVersion, RemoteContextModModel[]? mods)
+ {
+ this.IsHost = isHost;
+ this.Platform = platform;
+ this.GameVersion = gameVersion;
+ this.ApiVersion = apiVersion;
+ this.Mods = mods ?? Array.Empty<RemoteContextModModel>();
+ }
}
}
diff --git a/src/SMAPI/Framework/Reflection/CacheEntry.cs b/src/SMAPI/Framework/Reflection/CacheEntry.cs
index 6b18d204..27f48a1f 100644
--- a/src/SMAPI/Framework/Reflection/CacheEntry.cs
+++ b/src/SMAPI/Framework/Reflection/CacheEntry.cs
@@ -1,5 +1,4 @@
-#nullable disable
-
+using System.Diagnostics.CodeAnalysis;
using System.Reflection;
namespace StardewModdingAPI.Framework.Reflection
@@ -11,21 +10,20 @@ namespace StardewModdingAPI.Framework.Reflection
** Accessors
*********/
/// <summary>Whether the lookup found a valid match.</summary>
- public bool IsValid { get; }
+ [MemberNotNullWhen(true, nameof(CacheEntry.MemberInfo))]
+ public bool IsValid => this.MemberInfo != null;
/// <summary>The reflection data for this member (or <c>null</c> if invalid).</summary>
- public MemberInfo MemberInfo { get; }
+ public MemberInfo? MemberInfo { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
- /// <param name="isValid">Whether the lookup found a valid match.</param>
/// <param name="memberInfo">The reflection data for this member (or <c>null</c> if invalid).</param>
- public CacheEntry(bool isValid, MemberInfo memberInfo)
+ public CacheEntry(MemberInfo? memberInfo)
{
- this.IsValid = isValid;
this.MemberInfo = memberInfo;
}
}
diff --git a/src/SMAPI/Framework/Reflection/Reflector.cs b/src/SMAPI/Framework/Reflection/Reflector.cs
index c6fc2c79..3490ee97 100644
--- a/src/SMAPI/Framework/Reflection/Reflector.cs
+++ b/src/SMAPI/Framework/Reflection/Reflector.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Reflection;
using System.Runtime.Caching;
@@ -14,7 +12,7 @@ namespace StardewModdingAPI.Framework.Reflection
** Fields
*********/
/// <summary>The cached fields and methods found via reflection.</summary>
- private readonly MemoryCache Cache = new(typeof(Reflector).FullName);
+ private readonly MemoryCache Cache = new(typeof(Reflector).FullName!);
/// <summary>The sliding cache expiration time.</summary>
private readonly TimeSpan SlidingCacheExpiry = TimeSpan.FromMinutes(5);
@@ -30,8 +28,9 @@ namespace StardewModdingAPI.Framework.Reflection
/// <typeparam name="TValue">The field type.</typeparam>
/// <param name="obj">The object which has the field.</param>
/// <param name="name">The field name.</param>
- /// <param name="required">Whether to throw an exception if the field is not found.</param>
- /// <returns>Returns the field wrapper, or <c>null</c> if the field doesn't exist and <paramref name="required"/> is <c>false</c>.</returns>
+ /// <param name="required">Whether to throw an exception if the field isn't found. <strong>Due to limitations with nullable reference types, setting this to <c>false</c> will still mark the value non-nullable.</strong></param>
+ /// <returns>Returns the field wrapper, or <c>null</c> if <paramref name="required"/> is <c>false</c> and the field doesn't exist.</returns>
+ /// <exception cref="InvalidOperationException">The target field doesn't exist, and <paramref name="required"/> is true.</exception>
public IReflectedField<TValue> GetField<TValue>(object obj, string name, bool required = true)
{
// validate
@@ -39,24 +38,26 @@ namespace StardewModdingAPI.Framework.Reflection
throw new ArgumentNullException(nameof(obj), "Can't get a instance field from a null object.");
// get field from hierarchy
- IReflectedField<TValue> field = this.GetFieldFromHierarchy<TValue>(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
+ IReflectedField<TValue>? field = this.GetFieldFromHierarchy<TValue>(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if (required && field == null)
throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a '{name}' instance field.");
- return field;
+ return field!;
}
/// <summary>Get a static field.</summary>
/// <typeparam name="TValue">The field type.</typeparam>
/// <param name="type">The type which has the field.</param>
/// <param name="name">The field name.</param>
- /// <param name="required">Whether to throw an exception if the field is not found.</param>
+ /// <param name="required">Whether to throw an exception if the field isn't found. <strong>Due to limitations with nullable reference types, setting this to <c>false</c> will still mark the value non-nullable.</strong></param>
+ /// <returns>Returns the field wrapper, or <c>null</c> if <paramref name="required"/> is <c>false</c> and the field doesn't exist.</returns>
+ /// <exception cref="InvalidOperationException">The target field doesn't exist, and <paramref name="required"/> is true.</exception>
public IReflectedField<TValue> GetField<TValue>(Type type, string name, bool required = true)
{
// get field from hierarchy
- IReflectedField<TValue> field = this.GetFieldFromHierarchy<TValue>(type, null, name, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public);
+ IReflectedField<TValue>? field = this.GetFieldFromHierarchy<TValue>(type, null, name, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public);
if (required && field == null)
throw new InvalidOperationException($"The {type.FullName} object doesn't have a '{name}' static field.");
- return field;
+ return field!;
}
/****
@@ -66,7 +67,9 @@ namespace StardewModdingAPI.Framework.Reflection
/// <typeparam name="TValue">The property type.</typeparam>
/// <param name="obj">The object which has the property.</param>
/// <param name="name">The property name.</param>
- /// <param name="required">Whether to throw an exception if the property is not found.</param>
+ /// <param name="required">Whether to throw an exception if the property isn't found. <strong>Due to limitations with nullable reference types, setting this to <c>false</c> will still mark the value non-nullable.</strong></param>
+ /// <returns>Returns the property wrapper, or <c>null</c> if <paramref name="required"/> is <c>false</c> and the property doesn't exist.</returns>
+ /// <exception cref="InvalidOperationException">The target property doesn't exist, and <paramref name="required"/> is true.</exception>
public IReflectedProperty<TValue> GetProperty<TValue>(object obj, string name, bool required = true)
{
// validate
@@ -74,24 +77,26 @@ namespace StardewModdingAPI.Framework.Reflection
throw new ArgumentNullException(nameof(obj), "Can't get a instance property from a null object.");
// get property from hierarchy
- IReflectedProperty<TValue> property = this.GetPropertyFromHierarchy<TValue>(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
+ IReflectedProperty<TValue>? property = this.GetPropertyFromHierarchy<TValue>(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if (required && property == null)
throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a '{name}' instance property.");
- return property;
+ return property!;
}
/// <summary>Get a static property.</summary>
/// <typeparam name="TValue">The property type.</typeparam>
/// <param name="type">The type which has the property.</param>
/// <param name="name">The property name.</param>
- /// <param name="required">Whether to throw an exception if the property is not found.</param>
+ /// <param name="required">Whether to throw an exception if the property isn't found. <strong>Due to limitations with nullable reference types, setting this to <c>false</c> will still mark the value non-nullable.</strong></param>
+ /// <returns>Returns the property wrapper, or <c>null</c> if <paramref name="required"/> is <c>false</c> and the property doesn't exist.</returns>
+ /// <exception cref="InvalidOperationException">The target property doesn't exist, and <paramref name="required"/> is true.</exception>
public IReflectedProperty<TValue> GetProperty<TValue>(Type type, string name, bool required = true)
{
// get field from hierarchy
- IReflectedProperty<TValue> property = this.GetPropertyFromHierarchy<TValue>(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
+ IReflectedProperty<TValue>? property = this.GetPropertyFromHierarchy<TValue>(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
if (required && property == null)
throw new InvalidOperationException($"The {type.FullName} object doesn't have a '{name}' static property.");
- return property;
+ return property!;
}
/****
@@ -99,8 +104,10 @@ namespace StardewModdingAPI.Framework.Reflection
****/
/// <summary>Get a instance method.</summary>
/// <param name="obj">The object which has the method.</param>
- /// <param name="name">The field name.</param>
- /// <param name="required">Whether to throw an exception if the field is not found.</param>
+ /// <param name="name">The method name.</param>
+ /// <param name="required">Whether to throw an exception if the method isn't found. <strong>Due to limitations with nullable reference types, setting this to <c>false</c> will still mark the value non-nullable.</strong></param>
+ /// <returns>Returns the method wrapper, or <c>null</c> if <paramref name="required"/> is <c>false</c> and the method doesn't exist.</returns>
+ /// <exception cref="InvalidOperationException">The target method doesn't exist, and <paramref name="required"/> is true.</exception>
public IReflectedMethod GetMethod(object obj, string name, bool required = true)
{
// validate
@@ -108,23 +115,25 @@ namespace StardewModdingAPI.Framework.Reflection
throw new ArgumentNullException(nameof(obj), "Can't get a instance method from a null object.");
// get method from hierarchy
- IReflectedMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
+ IReflectedMethod? method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if (required && method == null)
throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a '{name}' instance method.");
- return method;
+ return method!;
}
/// <summary>Get a static method.</summary>
/// <param name="type">The type which has the method.</param>
- /// <param name="name">The field name.</param>
- /// <param name="required">Whether to throw an exception if the field is not found.</param>
+ /// <param name="name">The method name.</param>
+ /// <param name="required">Whether to throw an exception if the method isn't found. <strong>Due to limitations with nullable reference types, setting this to <c>false</c> will still mark the value non-nullable.</strong></param>
+ /// <returns>Returns the method wrapper, or <c>null</c> if <paramref name="required"/> is <c>false</c> and the method doesn't exist.</returns>
+ /// <exception cref="InvalidOperationException">The target method doesn't exist, and <paramref name="required"/> is true.</exception>
public IReflectedMethod GetMethod(Type type, string name, bool required = true)
{
// get method from hierarchy
- IReflectedMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
+ IReflectedMethod? method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
if (required && method == null)
throw new InvalidOperationException($"The {type.FullName} object doesn't have a '{name}' static method.");
- return method;
+ return method!;
}
@@ -134,18 +143,25 @@ namespace StardewModdingAPI.Framework.Reflection
/// <summary>Get a field from the type hierarchy.</summary>
/// <typeparam name="TValue">The expected field type.</typeparam>
/// <param name="type">The type which has the field.</param>
- /// <param name="obj">The object which has the field.</param>
+ /// <param name="obj">The object which has the field, or <c>null</c> for a static field.</param>
/// <param name="name">The field name.</param>
/// <param name="bindingFlags">The reflection binding which flags which indicates what type of field to find.</param>
- private IReflectedField<TValue> GetFieldFromHierarchy<TValue>(Type type, object obj, string name, BindingFlags bindingFlags)
+ private IReflectedField<TValue>? GetFieldFromHierarchy<TValue>(Type type, object? obj, string name, BindingFlags bindingFlags)
{
bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
- FieldInfo field = this.GetCached<FieldInfo>($"field::{isStatic}::{type.FullName}::{name}", () =>
+ FieldInfo? field = this.GetCached($"field::{isStatic}::{type.FullName}::{name}", () =>
{
- FieldInfo fieldInfo = null;
- for (; type != null && fieldInfo == null; type = type.BaseType)
- fieldInfo = type.GetField(name, bindingFlags);
- return fieldInfo;
+ for (Type? curType = type; curType != null; curType = curType.BaseType)
+ {
+ FieldInfo? fieldInfo = type.GetField(name, bindingFlags);
+ if (fieldInfo != null)
+ {
+ type = curType;
+ return fieldInfo;
+ }
+ }
+
+ return null;
});
return field != null
@@ -156,18 +172,25 @@ namespace StardewModdingAPI.Framework.Reflection
/// <summary>Get a property from the type hierarchy.</summary>
/// <typeparam name="TValue">The expected property type.</typeparam>
/// <param name="type">The type which has the property.</param>
- /// <param name="obj">The object which has the property.</param>
+ /// <param name="obj">The object which has the property, or <c>null</c> for a static property.</param>
/// <param name="name">The property name.</param>
/// <param name="bindingFlags">The reflection binding which flags which indicates what type of property to find.</param>
- private IReflectedProperty<TValue> GetPropertyFromHierarchy<TValue>(Type type, object obj, string name, BindingFlags bindingFlags)
+ private IReflectedProperty<TValue>? GetPropertyFromHierarchy<TValue>(Type type, object? obj, string name, BindingFlags bindingFlags)
{
bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
- PropertyInfo property = this.GetCached<PropertyInfo>($"property::{isStatic}::{type.FullName}::{name}", () =>
+ PropertyInfo? property = this.GetCached<PropertyInfo>($"property::{isStatic}::{type.FullName}::{name}", () =>
{
- PropertyInfo propertyInfo = null;
- for (; type != null && propertyInfo == null; type = type.BaseType)
- propertyInfo = type.GetProperty(name, bindingFlags);
- return propertyInfo;
+ for (Type? curType = type; curType != null; curType = curType.BaseType)
+ {
+ PropertyInfo? propertyInfo = type.GetProperty(name, bindingFlags);
+ if (propertyInfo != null)
+ {
+ type = curType;
+ return propertyInfo;
+ }
+ }
+
+ return null;
});
return property != null
@@ -177,18 +200,25 @@ namespace StardewModdingAPI.Framework.Reflection
/// <summary>Get a method from the type hierarchy.</summary>
/// <param name="type">The type which has the method.</param>
- /// <param name="obj">The object which has the method.</param>
+ /// <param name="obj">The object which has the method, or <c>null</c> for a static method.</param>
/// <param name="name">The method name.</param>
/// <param name="bindingFlags">The reflection binding which flags which indicates what type of method to find.</param>
- private IReflectedMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags)
+ private IReflectedMethod? GetMethodFromHierarchy(Type type, object? obj, string name, BindingFlags bindingFlags)
{
bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
- MethodInfo method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}", () =>
+ MethodInfo? method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}", () =>
{
- MethodInfo methodInfo = null;
- for (; type != null && methodInfo == null; type = type.BaseType)
- methodInfo = type.GetMethod(name, bindingFlags);
- return methodInfo;
+ for (Type? curType = type; curType != null; curType = curType.BaseType)
+ {
+ MethodInfo? methodInfo = type.GetMethod(name, bindingFlags);
+ if (methodInfo != null)
+ {
+ type = curType;
+ return methodInfo;
+ }
+ }
+
+ return null;
});
return method != null
@@ -200,7 +230,8 @@ namespace StardewModdingAPI.Framework.Reflection
/// <typeparam name="TMemberInfo">The expected <see cref="MemberInfo"/> type.</typeparam>
/// <param name="key">The cache key.</param>
/// <param name="fetch">Fetches a new value to cache.</param>
- private TMemberInfo GetCached<TMemberInfo>(string key, Func<TMemberInfo> fetch) where TMemberInfo : MemberInfo
+ private TMemberInfo? GetCached<TMemberInfo>(string key, Func<TMemberInfo?> fetch)
+ where TMemberInfo : MemberInfo
{
// get from cache
if (this.Cache.Contains(key))
@@ -212,8 +243,8 @@ namespace StardewModdingAPI.Framework.Reflection
}
// fetch & cache new value
- TMemberInfo result = fetch();
- CacheEntry cacheEntry = new(result != null, result);
+ TMemberInfo? result = fetch();
+ CacheEntry cacheEntry = new(result);
this.Cache.Add(key, cacheEntry, new CacheItemPolicy { SlidingExpiration = this.SlidingCacheExpiry });
return result;
}
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index 364a7632..bce7cffa 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -92,13 +90,13 @@ namespace StardewModdingAPI.Framework
private readonly CommandManager CommandManager;
/// <summary>The underlying game instance.</summary>
- private SGameRunner Game;
+ private SGameRunner Game = null!; // initialized very early
/// <summary>SMAPI's content manager.</summary>
- private ContentCoordinator ContentCore;
+ private ContentCoordinator ContentCore = null!; // initialized very early
/// <summary>The game's core multiplayer utility for the main player.</summary>
- private SMultiplayer Multiplayer;
+ private SMultiplayer Multiplayer = null!; // initialized very early
/// <summary>Tracks the installed mods.</summary>
/// <remarks>This is initialized after the game starts.</remarks>
@@ -146,19 +144,18 @@ namespace StardewModdingAPI.Framework
private readonly ConcurrentQueue<string> RawCommandQueue = new();
/// <summary>A list of commands to execute on each screen.</summary>
- private readonly PerScreen<List<Tuple<Command, string, string[]>>> ScreenCommandQueue = new(() => new List<Tuple<Command, string, string[]>>());
-
+ private readonly PerScreen<List<QueuedCommand>> ScreenCommandQueue = new(() => new List<QueuedCommand>());
/*********
** Accessors
*********/
/// <summary>Manages deprecation warnings.</summary>
/// <remarks>This is initialized after the game starts. This is accessed directly because it's not part of the normal class model.</remarks>
- internal static DeprecationManager DeprecationManager { get; private set; }
+ internal static DeprecationManager DeprecationManager { get; private set; } = null!; // initialized in constructor, which happens before other code can access it
/// <summary>The singleton instance.</summary>
/// <remarks>This is only intended for use by external code like the Error Handler mod.</remarks>
- internal static SCore Instance { get; private set; }
+ internal static SCore Instance { get; private set; } = null!; // initialized in constructor, which happens before other code can access it
/// <summary>The number of game update ticks which have already executed. This is similar to <see cref="Game1.ticks"/>, but incremented more consistently for every tick.</summary>
internal static uint TicksElapsed { get; private set; }
@@ -191,7 +188,8 @@ namespace StardewModdingAPI.Framework
this.Settings = JsonConvert.DeserializeObject<SConfig>(File.ReadAllText(Constants.ApiConfigPath));
if (File.Exists(Constants.ApiUserConfigPath))
JsonConvert.PopulateObject(File.ReadAllText(Constants.ApiUserConfigPath), this.Settings);
- this.Settings.DeveloperMode = developerMode ?? this.Settings.DeveloperMode;
+ if (developerMode.HasValue)
+ this.Settings.OverrideDeveloperMode(developerMode.Value);
this.LogManager = new LogManager(logPath: logPath, colorConfig: this.Settings.ConsoleColors, writeToConsole: writeToConsole, isVerbose: this.Settings.VerboseLogging, isDeveloperMode: this.Settings.DeveloperMode, getScreenIdForLog: this.GetScreenIdForLog);
this.CommandManager = new CommandManager(this.Monitor);
@@ -331,6 +329,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.")]
public void Dispose()
{
// skip if already disposed
@@ -355,9 +354,9 @@ namespace StardewModdingAPI.Framework
// dispose core components
this.IsGameRunning = false;
this.ContentCore?.Dispose();
- this.CancellationToken?.Dispose();
+ this.CancellationToken.Dispose();
this.Game?.Dispose();
- this.LogManager?.Dispose(); // dispose last to allow for any last-second log messages
+ this.LogManager.Dispose(); // dispose last to allow for any last-second log messages
// end game (moved from Game1.OnExiting to let us clean up first)
Process.GetCurrentProcess().Kill();
@@ -517,12 +516,12 @@ namespace StardewModdingAPI.Framework
/*********
** Parse commands
*********/
- while (this.RawCommandQueue.TryDequeue(out string rawInput))
+ while (this.RawCommandQueue.TryDequeue(out string? rawInput))
{
// parse command
- string name;
- string[] args;
- Command command;
+ string? name;
+ string[]? args;
+ Command? command;
int screenId;
try
{
@@ -539,7 +538,7 @@ namespace StardewModdingAPI.Framework
}
// queue command for screen
- this.ScreenCommandQueue.GetValueForScreen(screenId).Add(Tuple.Create(command, name, args));
+ this.ScreenCommandQueue.GetValueForScreen(screenId).Add(new(command, name, args));
}
@@ -556,7 +555,7 @@ namespace StardewModdingAPI.Framework
catch (Exception ex)
{
// log error
- this.Monitor.Log($"An error occured in the overridden update loop: {ex.GetLogSummary()}", LogLevel.Error);
+ this.Monitor.Log($"An error occurred in the overridden update loop: {ex.GetLogSummary()}", LogLevel.Error);
// exit if irrecoverable
if (!this.UpdateCrashTimer.Decrement())
@@ -575,7 +574,7 @@ namespace StardewModdingAPI.Framework
/// <param name="runUpdate">Invoke the game's update logic.</param>
private void OnPlayerInstanceUpdating(SGame instance, GameTime gameTime, Action runUpdate)
{
- var events = this.EventManager;
+ EventManager events = this.EventManager;
try
{
@@ -595,12 +594,8 @@ namespace StardewModdingAPI.Framework
*********/
{
var commandQueue = this.ScreenCommandQueue.Value;
- foreach (var entry in commandQueue)
+ foreach ((Command? command, string? name, string[]? args) in commandQueue)
{
- Command command = entry.Item1;
- string name = entry.Item2;
- string[] args = entry.Item3;
-
try
{
command.Callback.Invoke(name, args);
@@ -637,6 +632,7 @@ namespace StardewModdingAPI.Framework
{
this.Monitor.Log("Game loader synchronizing...");
this.Reflection.GetMethod(Game1.game1, "UpdateTitleScreen").Invoke(Game1.currentGameTime); // run game logic to change music on load, etc
+ // ReSharper disable once ConstantConditionalAccessQualifier -- may become null within the loop
while (Game1.currentLoader?.MoveNext() == true)
{
SCore.ProcessTicksElapsed++;
@@ -825,7 +821,7 @@ namespace StardewModdingAPI.Framework
// raise cursor moved event
if (state.Cursor.IsChanged)
- events.CursorMoved.Raise(new CursorMovedEventArgs(state.Cursor.Old, state.Cursor.New));
+ events.CursorMoved.Raise(new CursorMovedEventArgs(state.Cursor.Old!, state.Cursor.New!));
// raise mouse wheel scrolled
if (state.MouseWheelScroll.IsChanged)
@@ -956,7 +952,7 @@ namespace StardewModdingAPI.Framework
// raise player events
if (raiseWorldEvents)
{
- PlayerSnapshot playerState = state.CurrentPlayer;
+ PlayerSnapshot playerState = state.CurrentPlayer!; // not null at this point
Farmer player = playerState.Player;
// raise current location changed
@@ -965,25 +961,25 @@ namespace StardewModdingAPI.Framework
if (this.Monitor.IsVerbose)
this.Monitor.Log($"Context: set location to {playerState.Location.New}.");
- events.Warped.Raise(new WarpedEventArgs(player, playerState.Location.Old, playerState.Location.New));
+ events.Warped.Raise(new WarpedEventArgs(player, playerState.Location.Old!, playerState.Location.New!));
}
// raise player leveled up a skill
- foreach (var pair in playerState.Skills)
+ foreach ((SkillType skill, var value) in playerState.Skills)
{
- if (!pair.Value.IsChanged)
+ if (!value.IsChanged)
continue;
if (this.Monitor.IsVerbose)
- this.Monitor.Log($"Events: player skill '{pair.Key}' changed from {pair.Value.Old} to {pair.Value.New}.");
+ this.Monitor.Log($"Events: player skill '{skill}' changed from {value.Old} to {value.New}.");
- events.LevelChanged.Raise(new LevelChangedEventArgs(player, pair.Key, pair.Value.Old, pair.Value.New));
+ events.LevelChanged.Raise(new LevelChangedEventArgs(player, skill, value.Old, value.New));
}
// raise player inventory changed
if (playerState.Inventory.IsChanged)
{
- var inventory = playerState.Inventory;
+ SnapshotItemListDiff inventory = playerState.Inventory;
if (this.Monitor.IsVerbose)
this.Monitor.Log("Events: player inventory changed.");
@@ -1070,7 +1066,8 @@ namespace StardewModdingAPI.Framework
// update mod translation helpers
foreach (IModMetadata mod in this.ModRegistry.GetAll())
{
- mod.Translations.SetLocale(locale, languageCode);
+ TranslationHelper translations = mod.Translations!; // not null at this point
+ translations.SetLocale(locale, languageCode);
foreach (ContentPack contentPack in mod.GetFakeContentPacks())
contentPack.TranslationImpl.SetLocale(locale, languageCode);
@@ -1117,7 +1114,7 @@ namespace StardewModdingAPI.Framework
break;
case LoadStage.Loaded:
- // override chatbox
+ // override chat box
Game1.onScreenMenus.Remove(Game1.chatBox);
Game1.onScreenMenus.Add(Game1.chatBox = new SChatBox(this.LogManager.MonitorForGame));
break;
@@ -1182,7 +1179,7 @@ namespace StardewModdingAPI.Framework
/// <param name="id">The content pack ID.</param>
/// <param name="verb">The verb phrase indicating what action will be performed, like 'load assets' or 'edit assets'.</param>
/// <returns>Returns the content pack metadata if valid, else <c>null</c>.</returns>
- private IModMetadata GetOnBehalfOfContentPack(IModMetadata mod, string id, string verb)
+ private IModMetadata? GetOnBehalfOfContentPack(IModMetadata mod, string? id, string verb)
{
if (id == null)
return null;
@@ -1190,7 +1187,7 @@ namespace StardewModdingAPI.Framework
string errorPrefix = $"Can't {verb} on behalf of content pack ID '{id}'";
// get target mod
- IModMetadata onBehalfOf = this.ModRegistry.Get(id);
+ IModMetadata? onBehalfOf = this.ModRegistry.Get(id);
if (onBehalfOf == null)
{
mod.LogAsModOnce($"{errorPrefix}: there's no content pack installed with that ID.", LogLevel.Warn);
@@ -1198,7 +1195,7 @@ namespace StardewModdingAPI.Framework
}
// make sure it's a content pack for the requesting mod
- if (!onBehalfOf.IsContentPack || !string.Equals(onBehalfOf.Manifest?.ContentPackFor?.UniqueID, mod.Manifest.UniqueID))
+ if (!onBehalfOf.IsContentPack || !string.Equals(onBehalfOf.Manifest.ContentPackFor?.UniqueID, mod.Manifest.UniqueID))
{
mod.LogAsModOnce($"{errorPrefix}: that isn't a content pack for this mod.", LogLevel.Warn);
return null;
@@ -1232,7 +1229,7 @@ namespace StardewModdingAPI.Framework
modIDs.Remove(message.FromModID); // don't send a broadcast back to the sender
// raise events
- this.EventManager.ModMessageReceived.Raise(new ModMessageReceivedEventArgs(message, this.Toolkit.JsonHelper), mod => mod != null && modIDs.Contains(mod.Manifest.UniqueID));
+ this.EventManager.ModMessageReceived.Raise(new ModMessageReceivedEventArgs(message, this.Toolkit.JsonHelper), mod => modIDs.Contains(mod.Manifest.UniqueID));
}
/// <summary>Constructor a content manager to read game content files.</summary>
@@ -1241,6 +1238,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
if (this.ContentCore == null)
{
this.ContentCore = new ContentCoordinator(
@@ -1293,21 +1291,21 @@ namespace StardewModdingAPI.Framework
// detect issues
bool hasObjectIssues = false;
void LogIssue(int id, string issue) => this.Monitor.Log($@"Detected issue: item #{id} in Content\Data\ObjectInformation.xnb is invalid ({issue}).");
- foreach (KeyValuePair<int, string> entry in Game1.objectInformation)
+ foreach ((int id, string? fieldsStr) in Game1.objectInformation)
{
// must not be empty
- if (string.IsNullOrWhiteSpace(entry.Value))
+ if (string.IsNullOrWhiteSpace(fieldsStr))
{
- LogIssue(entry.Key, "entry is empty");
+ LogIssue(id, "entry is empty");
hasObjectIssues = true;
continue;
}
// require core fields
- string[] fields = entry.Value.Split('/');
+ string[] fields = fieldsStr.Split('/');
if (fields.Length < SObject.objectInfoDescriptionIndex + 1)
{
- LogIssue(entry.Key, "too few fields for an object");
+ LogIssue(id, "too few fields for an object");
hasObjectIssues = true;
continue;
}
@@ -1318,7 +1316,7 @@ namespace StardewModdingAPI.Framework
case "Cooking":
if (fields.Length < SObject.objectInfoBuffDurationIndex + 1)
{
- LogIssue(entry.Key, "too few fields for a cooking item");
+ LogIssue(id, "too few fields for a cooking item");
hasObjectIssues = true;
}
break;
@@ -1366,7 +1364,7 @@ namespace StardewModdingAPI.Framework
string[] installedNames = registryKeys
.SelectMany(registryKey =>
{
- using RegistryKey key = Registry.LocalMachine.OpenSubKey(registryKey);
+ using RegistryKey? key = Registry.LocalMachine.OpenSubKey(registryKey);
if (key == null)
return Array.Empty<string>();
@@ -1374,9 +1372,9 @@ namespace StardewModdingAPI.Framework
.GetSubKeyNames()
.Select(subkeyName =>
{
- using RegistryKey subkey = key.OpenSubKey(subkeyName);
- string displayName = (string)subkey?.GetValue("DisplayName");
- string displayVersion = (string)subkey?.GetValue("DisplayVersion");
+ using RegistryKey? subkey = key.OpenSubKey(subkeyName);
+ string? displayName = (string?)subkey?.GetValue("DisplayName");
+ string? displayVersion = (string?)subkey?.GetValue("DisplayVersion");
if (displayName != null && displayVersion != null && displayName.EndsWith($" {displayVersion}"))
displayName = displayName.Substring(0, displayName.Length - displayVersion.Length - 1);
@@ -1386,6 +1384,7 @@ namespace StardewModdingAPI.Framework
.ToArray();
})
.Where(name => name != null && (name.Contains("MSI Afterburner") || name.Contains("RivaTuner")))
+ .Select(name => name!)
.Distinct()
.OrderBy(name => name)
.ToArray();
@@ -1418,14 +1417,14 @@ namespace StardewModdingAPI.Framework
// check SMAPI version
{
- ISemanticVersion updateFound = null;
- string updateUrl = null;
+ ISemanticVersion? updateFound = null;
+ string? updateUrl = null;
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 ?? Constants.HomePageUrl;
+ updateUrl = response.SuggestedUpdate?.Url;
// log message
if (updateFound != null)
@@ -1451,7 +1450,7 @@ namespace StardewModdingAPI.Framework
// show update message on next launch
if (updateFound != null)
- this.LogManager.WriteUpdateMarker(updateFound.ToString(), updateUrl);
+ this.LogManager.WriteUpdateMarker(updateFound.ToString(), updateUrl ?? Constants.HomePageUrl);
}
// check mod versions
@@ -1485,12 +1484,12 @@ namespace StardewModdingAPI.Framework
foreach (IModMetadata mod in mods.OrderBy(p => p.DisplayName))
{
// link to update-check data
- if (!mod.HasID() || !results.TryGetValue(mod.Manifest.UniqueID, out ModEntryModel result))
+ if (!mod.HasID() || !results.TryGetValue(mod.Manifest.UniqueID, out ModEntryModel? result))
continue;
mod.SetUpdateData(result);
// handle errors
- if (result.Errors != null && result.Errors.Any())
+ if (result.Errors.Any())
{
errors.AppendLine(result.Errors.Length == 1
? $" {mod.DisplayName}: {result.Errors[0]}"
@@ -1512,13 +1511,8 @@ namespace StardewModdingAPI.Framework
{
this.Monitor.Newline();
this.Monitor.Log($"You can update {updates.Count} mod{(updates.Count != 1 ? "s" : "")}:", LogLevel.Alert);
- foreach (var entry in updates)
- {
- IModMetadata mod = entry.Item1;
- ISemanticVersion newVersion = entry.Item2;
- string newUrl = entry.Item3;
+ foreach ((IModMetadata mod, ISemanticVersion newVersion, string newUrl) in updates)
this.Monitor.Log($" {mod.DisplayName} {newVersion}: {newUrl}", LogLevel.Alert);
- }
}
else
this.Monitor.Log(" All mods up to date.");
@@ -1571,9 +1565,8 @@ namespace StardewModdingAPI.Framework
// load mods
foreach (IModMetadata mod in mods)
{
- if (!this.TryLoadMod(mod, mods, modAssemblyLoader, proxyFactory, jsonHelper, contentCore, modDatabase, suppressUpdateChecks, out ModFailReason? failReason, out string errorPhrase, out string errorDetails))
+ if (!this.TryLoadMod(mod, mods, modAssemblyLoader, proxyFactory, jsonHelper, contentCore, modDatabase, suppressUpdateChecks, out ModFailReason? failReason, out string? errorPhrase, out string? errorDetails))
{
- failReason ??= ModFailReason.LoadFailed;
mod.SetStatus(ModMetadataStatus.Failed, failReason.Value, errorPhrase, errorDetails);
skippedMods.Add(mod);
}
@@ -1599,7 +1592,7 @@ namespace StardewModdingAPI.Framework
foreach (IModMetadata metadata in loadedMods)
{
// add interceptors
- if (metadata.Mod.Helper is ModHelper helper)
+ if (metadata.Mod?.Helper is ModHelper helper)
{
// ReSharper disable SuspiciousTypeConversion.Global
if (metadata.Mod is IAssetEditor editor)
@@ -1636,8 +1629,8 @@ namespace StardewModdingAPI.Framework
// call entry method
try
{
- IMod mod = metadata.Mod;
- mod.Entry(mod.Helper);
+ IMod mod = metadata.Mod!;
+ mod.Entry(mod.Helper!);
}
catch (Exception ex)
{
@@ -1647,7 +1640,7 @@ namespace StardewModdingAPI.Framework
// get mod API
try
{
- object api = metadata.Mod.GetApi();
+ object? api = metadata.Mod!.GetApi();
if (api != null && !api.GetType().IsPublic)
{
api = null;
@@ -1676,7 +1669,8 @@ namespace StardewModdingAPI.Framework
/// <param name="added">The interceptors that were added.</param>
/// <param name="removed">The interceptors that were removed.</param>
/// <param name="list">A list of interceptors to update for the change.</param>
- private void OnAssetInterceptorsChanged<T>(IModMetadata mod, IEnumerable<T> added, IEnumerable<T> removed, IList<ModLinked<T>> list)
+ private void OnAssetInterceptorsChanged<T>(IModMetadata mod, IEnumerable<T>? added, IEnumerable<T>? removed, IList<ModLinked<T>> list)
+ where T : notnull
{
foreach (T interceptor in added ?? Array.Empty<T>())
{
@@ -1705,7 +1699,7 @@ namespace StardewModdingAPI.Framework
/// <param name="errorReasonPhrase">The user-facing reason phrase explaining why the mod couldn't be loaded (if applicable).</param>
/// <param name="errorDetails">More detailed details about the error intended for developers (if any).</param>
/// <returns>Returns whether the mod was successfully loaded.</returns>
- private bool TryLoadMod(IModMetadata mod, IModMetadata[] mods, AssemblyLoader assemblyLoader, InterfaceProxyFactory proxyFactory, JsonHelper jsonHelper, ContentCoordinator contentCore, ModDatabase modDatabase, HashSet<string> suppressUpdateChecks, out ModFailReason? failReason, out string errorReasonPhrase, out string errorDetails)
+ private bool TryLoadMod(IModMetadata mod, IModMetadata[] mods, AssemblyLoader assemblyLoader, InterfaceProxyFactory proxyFactory, JsonHelper jsonHelper, ContentCoordinator contentCore, ModDatabase modDatabase, HashSet<string> suppressUpdateChecks, [NotNullWhen(false)] out ModFailReason? failReason, out string? errorReasonPhrase, out string? errorDetails)
{
errorDetails = null;
@@ -1714,6 +1708,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
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
@@ -1721,21 +1716,22 @@ namespace StardewModdingAPI.Framework
}
// add warning for missing update key
- if (mod.HasID() && !suppressUpdateChecks.Contains(mod.Manifest.UniqueID) && !mod.HasValidUpdateKeys())
+ if (mod.HasID() && !suppressUpdateChecks.Contains(mod.Manifest!.UniqueID) && !mod.HasValidUpdateKeys())
mod.SetWarning(ModWarning.NoUpdateKeys);
// validate status
if (mod.Status == ModMetadataStatus.Failed)
{
this.Monitor.Log($" Failed: {mod.ErrorDetails ?? mod.Error}");
- failReason = mod.FailReason;
+ failReason = mod.FailReason ?? ModFailReason.LoadFailed;
errorReasonPhrase = mod.Error;
return false;
}
+ IManifest manifest = mod.Manifest!;
// validate dependencies
// Although dependencies are validated before mods are loaded, a dependency may have failed to load.
- foreach (IManifestDependency dependency in mod.Manifest.Dependencies.Where(p => p.IsRequired))
+ foreach (IManifestDependency dependency in manifest.Dependencies.Where(p => p.IsRequired))
{
if (this.ModRegistry.Get(dependency.UniqueID) == null)
{
@@ -1751,7 +1747,6 @@ namespace StardewModdingAPI.Framework
// load as content pack
if (mod.IsContentPack)
{
- IManifest manifest = mod.Manifest;
IMonitor monitor = this.LogManager.GetMonitor(mod.DisplayName);
CaseInsensitivePathCache relativePathCache = this.ContentCore.GetCaseInsensitivePathCache(mod.DirectoryPath);
GameContentHelper gameContentHelper = new(this.ContentCore, manifest.UniqueID, mod.DisplayName, monitor, this.Reflection);
@@ -1770,8 +1765,7 @@ namespace StardewModdingAPI.Framework
else
{
// get mod info
- IManifest manifest = mod.Manifest;
- string assemblyPath = Path.Combine(mod.DirectoryPath, manifest.EntryDll);
+ string assemblyPath = Path.Combine(mod.DirectoryPath, manifest.EntryDll!);
// load mod
Assembly modAssembly;
@@ -1782,7 +1776,7 @@ namespace StardewModdingAPI.Framework
}
catch (IncompatibleInstructionException) // details already in trace logs
{
- string[] updateUrls = new[] { modDatabase.GetModPageUrlFor(manifest.UniqueID), "https://smapi.io/mods" }.Where(p => p != null).ToArray();
+ string[] updateUrls = new[] { modDatabase.GetModPageUrlFor(manifest.UniqueID), "https://smapi.io/mods" }.Where(p => p != null).ToArray()!;
errorReasonPhrase = $"it's no longer compatible. Please check for a new version at {string.Join(" or ", updateUrls)}";
failReason = ModFailReason.Incompatible;
return false;
@@ -1808,7 +1802,7 @@ namespace StardewModdingAPI.Framework
try
{
// get mod instance
- if (!this.TryLoadModEntry(modAssembly, out Mod modEntry, out errorReasonPhrase))
+ if (!this.TryLoadModEntry(modAssembly, out Mod? modEntry, out errorReasonPhrase))
{
failReason = ModFailReason.LoadFailed;
return false;
@@ -1822,8 +1816,8 @@ namespace StardewModdingAPI.Framework
return this.ModRegistry
.GetAll(assemblyMods: false)
- .Where(p => p.IsContentPack && mod.HasID(p.Manifest.ContentPackFor.UniqueID))
- .Select(p => p.ContentPack)
+ .Where(p => p.IsContentPack && mod.HasID(p.Manifest.ContentPackFor!.UniqueID))
+ .Select(p => p.ContentPack!)
.ToArray();
}
@@ -1890,7 +1884,7 @@ namespace StardewModdingAPI.Framework
/// <param name="mod">The loaded instance.</param>
/// <param name="error">The error indicating why loading failed (if applicable).</param>
/// <returns>Returns whether the mod entry class was successfully loaded.</returns>
- private bool TryLoadModEntry(Assembly modAssembly, out Mod mod, out string error)
+ private bool TryLoadModEntry(Assembly modAssembly, [NotNullWhen(true)] out Mod? mod, [NotNullWhen(false)] out string? error)
{
mod = null;
@@ -1908,7 +1902,7 @@ namespace StardewModdingAPI.Framework
}
// get implementation
- mod = (Mod)modAssembly.CreateInstance(modEntries[0].ToString());
+ mod = (Mod?)modAssembly.CreateInstance(modEntries[0].ToString());
if (mod == null)
{
error = "its entry class couldn't be instantiated.";
@@ -1954,7 +1948,7 @@ namespace StardewModdingAPI.Framework
metadata.LogAsMod($" - {error}", LogLevel.Warn);
}
- metadata.Translations.SetTranslations(translations);
+ metadata.Translations!.SetTranslations(translations);
}
// fake content packs
@@ -1997,7 +1991,7 @@ namespace StardewModdingAPI.Framework
string locale = Path.GetFileNameWithoutExtension(file.Name.ToLower().Trim());
try
{
- if (!jsonHelper.ReadJsonFileIfExists(file.FullName, out IDictionary<string, string> data) || data == null)
+ if (!jsonHelper.ReadJsonFileIfExists(file.FullName, out IDictionary<string, string>? data))
{
errors.Add($"{file.Name} file couldn't be read"); // mainly happens when the file is corrupted or empty
continue;
@@ -2016,8 +2010,8 @@ namespace StardewModdingAPI.Framework
foreach (string locale in translations.Keys.ToArray())
{
// handle duplicates
- HashSet<string> keys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
- HashSet<string> duplicateKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+ HashSet<string> keys = new(StringComparer.OrdinalIgnoreCase);
+ HashSet<string> duplicateKeys = new(StringComparer.OrdinalIgnoreCase);
foreach (string key in translations[locale].Keys.ToArray())
{
if (!keys.Add(key))
@@ -2107,5 +2101,15 @@ namespace StardewModdingAPI.Framework
return null;
}
+
+
+ /*********
+ ** Private types
+ *********/
+ /// <summary>A queued console command to run during the update loop.</summary>
+ /// <param name="Command">The command which can handle the input.</param>
+ /// <param name="Name">The parsed command name.</param>
+ /// <param name="Args">The parsed command arguments.</param>
+ private readonly record struct QueuedCommand(Command Command, string Name, string[] Args);
}
}
diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs
index de3c25a5..c0e8ee81 100644
--- a/src/SMAPI/Framework/SMultiplayer.cs
+++ b/src/SMAPI/Framework/SMultiplayer.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.IO;
@@ -61,7 +59,7 @@ namespace StardewModdingAPI.Framework
private readonly PerScreen<IDictionary<long, MultiplayerPeer>> PeersImpl = new(() => new Dictionary<long, MultiplayerPeer>());
/// <summary>The backing field for <see cref="HostPeer"/>.</summary>
- private readonly PerScreen<MultiplayerPeer> HostPeerImpl = new();
+ private readonly PerScreen<MultiplayerPeer?> HostPeerImpl = new();
/*********
@@ -71,7 +69,7 @@ namespace StardewModdingAPI.Framework
public IDictionary<long, MultiplayerPeer> Peers => this.PeersImpl.Value;
/// <summary>The metadata for the host player, if the current player is a farmhand.</summary>
- public MultiplayerPeer HostPeer
+ public MultiplayerPeer? HostPeer
{
get => this.HostPeerImpl.Value;
private set => this.HostPeerImpl.Value = value;
@@ -115,13 +113,13 @@ namespace StardewModdingAPI.Framework
{
case LidgrenClient:
{
- string address = this.Reflection.GetField<string>(client, "address").GetValue();
+ string address = this.Reflection.GetField<string>(client, "address").GetValue() ?? throw new InvalidOperationException("Can't initialize base networking client: no valid address found.");
return new SLidgrenClient(address, this.OnClientProcessingMessage, this.OnClientSendingMessage);
}
case GalaxyNetClient:
{
- GalaxyID address = this.Reflection.GetField<GalaxyID>(client, "lobbyId").GetValue();
+ GalaxyID address = this.Reflection.GetField<GalaxyID>(client, "lobbyId").GetValue() ?? throw new InvalidOperationException("Can't initialize GOG networking client: no valid address found.");
return new SGalaxyNetClient(address, this.OnClientProcessingMessage, this.OnClientSendingMessage);
}
@@ -139,13 +137,13 @@ namespace StardewModdingAPI.Framework
{
case LidgrenServer:
{
- IGameServer gameServer = this.Reflection.GetField<IGameServer>(server, "gameServer").GetValue();
+ IGameServer gameServer = this.Reflection.GetField<IGameServer>(server, "gameServer").GetValue() ?? throw new InvalidOperationException("Can't initialize base networking client: the required 'gameServer' field wasn't found.");
return new SLidgrenServer(gameServer, this, this.OnServerProcessingMessage);
}
case GalaxyNetServer:
{
- IGameServer gameServer = this.Reflection.GetField<IGameServer>(server, "gameServer").GetValue();
+ IGameServer gameServer = this.Reflection.GetField<IGameServer>(server, "gameServer").GetValue() ?? throw new InvalidOperationException("Can't initialize GOG networking client: the required 'gameServer' field wasn't found.");
return new SGalaxyNetServer(gameServer, this, this.OnServerProcessingMessage);
}
@@ -194,7 +192,7 @@ namespace StardewModdingAPI.Framework
case (byte)MessageType.ModContext:
{
// parse message
- RemoteContextModel model = this.ReadContext(message.Reader);
+ RemoteContextModel? model = this.ReadContext(message.Reader);
this.Monitor.Log($"Received context for farmhand {message.FarmerID} running {(model != null ? $"SMAPI {model.ApiVersion} with {model.Mods.Length} mods" : "vanilla")}.");
// store peer
@@ -290,7 +288,7 @@ namespace StardewModdingAPI.Framework
case (byte)MessageType.ModContext:
{
// parse message
- RemoteContextModel model = this.ReadContext(message.Reader);
+ RemoteContextModel? model = this.ReadContext(message.Reader);
this.Monitor.Log($"Received context for {(model?.IsHost == true ? "host" : "farmhand")} {message.FarmerID} running {(model != null ? $"SMAPI {model.ApiVersion} with {model.Mods.Length} mods" : "vanilla")}.");
// store peer
@@ -334,7 +332,7 @@ namespace StardewModdingAPI.Framework
case (byte)MessageType.PlayerIntroduction:
{
// store peer
- if (!this.Peers.TryGetValue(message.FarmerID, out MultiplayerPeer peer))
+ if (!this.Peers.TryGetValue(message.FarmerID, out MultiplayerPeer? peer))
{
peer = new MultiplayerPeer(
playerID: message.FarmerID,
@@ -367,7 +365,7 @@ namespace StardewModdingAPI.Framework
{
foreach (long playerID in this.disconnectingFarmers)
{
- if (this.Peers.TryGetValue(playerID, out MultiplayerPeer peer))
+ if (this.Peers.TryGetValue(playerID, out MultiplayerPeer? peer))
{
this.Monitor.Log($"Player quit: {playerID}");
this.Peers.Remove(playerID);
@@ -384,7 +382,7 @@ namespace StardewModdingAPI.Framework
/// <param name="fromModID">The unique ID of the mod sending the message.</param>
/// <param name="toModIDs">The mod IDs which should receive the message on the destination computers, or <c>null</c> for all mods. Specifying mod IDs is recommended to improve performance, unless it's a general-purpose broadcast.</param>
/// <param name="toPlayerIDs">The <see cref="Farmer.UniqueMultiplayerID" /> values for the players who should receive the message, or <c>null</c> for all players. If you don't need to broadcast to all players, specifying player IDs is recommended to reduce latency.</param>
- public void BroadcastModMessage<TMessage>(TMessage message, string messageType, string fromModID, string[] toModIDs, long[] toPlayerIDs)
+ public void BroadcastModMessage<TMessage>(TMessage message, string messageType, string fromModID, string[]? toModIDs, long[]? toPlayerIDs)
{
// validate input
if (message == null)
@@ -488,13 +486,13 @@ namespace StardewModdingAPI.Framework
/// <summary>Read the metadata context for a player.</summary>
/// <param name="reader">The stream reader.</param>
- private RemoteContextModel ReadContext(BinaryReader reader)
+ private RemoteContextModel? ReadContext(BinaryReader reader)
{
string data = reader.ReadString();
RemoteContextModel model = this.JsonHelper.Deserialize<RemoteContextModel>(data);
return model.ApiVersion != null
? model
- : null; // no data available for unmodded players
+ : null; // no data available for vanilla players
}
/// <summary>Receive a mod message sent from another player's mods.</summary>
@@ -515,12 +513,15 @@ namespace StardewModdingAPI.Framework
// forward to other players
if (Context.IsMainPlayer && playerIDs.Any(p => p != Game1.player.UniqueMultiplayerID))
{
- ModMessageModel newModel = new(model);
foreach (long playerID in playerIDs)
{
- if (playerID != Game1.player.UniqueMultiplayerID && playerID != model.FromPlayerID && this.Peers.TryGetValue(playerID, out MultiplayerPeer peer))
+ if (playerID != Game1.player.UniqueMultiplayerID && playerID != model.FromPlayerID && this.Peers.TryGetValue(playerID, out MultiplayerPeer? peer))
{
- newModel.ToPlayerIDs = new[] { peer.PlayerID };
+ ModMessageModel newModel = new(model)
+ {
+ ToPlayerIDs = new[] { peer.PlayerID }
+ };
+
this.Monitor.VerboseLog($" Forwarding message to player {peer.PlayerID}.");
peer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, peer.PlayerID, this.JsonHelper.Serialize(newModel, Formatting.None)));
}
@@ -546,22 +547,20 @@ namespace StardewModdingAPI.Framework
/// <summary>Get the fields to include in a context sync message sent to other players.</summary>
private object[] GetContextSyncMessageFields()
{
- RemoteContextModel model = new()
- {
- IsHost = Context.IsWorldReady && Context.IsMainPlayer,
- Platform = Constants.TargetPlatform,
- ApiVersion = Constants.ApiVersion,
- GameVersion = Constants.GameVersion,
- Mods = this.ModRegistry
+ RemoteContextModel model = new(
+ isHost: Context.IsWorldReady && Context.IsMainPlayer,
+ platform: Constants.TargetPlatform,
+ apiVersion: Constants.ApiVersion,
+ gameVersion: Constants.GameVersion,
+ mods: this.ModRegistry
.GetAll()
- .Select(mod => new RemoteContextModModel
- {
- ID = mod.Manifest.UniqueID,
- Name = mod.Manifest.Name,
- Version = mod.Manifest.Version
- })
+ .Select(mod => new RemoteContextModModel(
+ id: mod.Manifest.UniqueID,
+ name: mod.Manifest.Name,
+ version: mod.Manifest.Version
+ ))
.ToArray()
- };
+ );
return new object[] { this.JsonHelper.Serialize(model, Formatting.None) };
}
@@ -573,21 +572,19 @@ namespace StardewModdingAPI.Framework
if (!peer.HasSmapi)
return new object[] { "{}" };
- RemoteContextModel model = new()
- {
- IsHost = peer.IsHost,
- Platform = peer.Platform.Value,
- ApiVersion = peer.ApiVersion,
- GameVersion = peer.GameVersion,
- Mods = peer.Mods
- .Select(mod => new RemoteContextModModel
- {
- ID = mod.ID,
- Name = mod.Name,
- Version = mod.Version
- })
+ RemoteContextModel model = new(
+ isHost: peer.IsHost,
+ platform: peer.Platform.Value,
+ apiVersion: peer.ApiVersion,
+ gameVersion: peer.GameVersion,
+ mods: peer.Mods
+ .Select(mod => new RemoteContextModModel(
+ id: mod.ID,
+ name: mod.Name,
+ version: mod.Version
+ ))
.ToArray()
- };
+ );
return new object[] { this.JsonHelper.Serialize(model, Formatting.None) };
}
diff --git a/src/SMAPI/Framework/Serialization/KeybindConverter.cs b/src/SMAPI/Framework/Serialization/KeybindConverter.cs
index f3bab20d..539f1291 100644
--- a/src/SMAPI/Framework/Serialization/KeybindConverter.cs
+++ b/src/SMAPI/Framework/Serialization/KeybindConverter.cs
@@ -53,13 +53,13 @@ namespace StardewModdingAPI.Framework.Serialization
if (objectType == typeof(Keybind))
{
- return Keybind.TryParse(str, out Keybind parsed, out string[] errors)
+ return Keybind.TryParse(str, out Keybind? parsed, out string[] errors)
? parsed
: throw new SParseException($"Can't parse {nameof(Keybind)} from invalid value '{str}' (path: {path}).\n{string.Join("\n", errors)}");
}
else
{
- return KeybindList.TryParse(str, out KeybindList parsed, out string[] errors)
+ return KeybindList.TryParse(str, out KeybindList? parsed, out string[] errors)
? parsed
: throw new SParseException($"Can't parse {nameof(KeybindList)} from invalid value '{str}' (path: {path}).\n{string.Join("\n", errors)}");
}
diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableWatcher.cs
index 4f94294c..5f76fe0a 100644
--- a/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableWatcher.cs
+++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableWatcher.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
@@ -42,7 +40,8 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
{
this.GetValue = getValue;
this.Comparer = comparer;
- this.PreviousValue = getValue();
+ this.CurrentValue = getValue();
+ this.PreviousValue = this.CurrentValue;
}
/// <summary>Update the current value if needed.</summary>
diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs
index 367eafea..5433ac8e 100644
--- a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs
+++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs
@@ -1,7 +1,6 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewModdingAPI.Enums;
using StardewModdingAPI.Framework.StateTracking.Comparers;
@@ -23,7 +22,7 @@ namespace StardewModdingAPI.Framework.StateTracking
private IDictionary<Item, int> CurrentInventory;
/// <summary>The player's last valid location.</summary>
- private GameLocation LastValidLocation;
+ private GameLocation? LastValidLocation;
/// <summary>The underlying watchers.</summary>
private readonly List<IWatcher> Watchers = new();
@@ -36,7 +35,7 @@ namespace StardewModdingAPI.Framework.StateTracking
public Farmer Player { get; }
/// <summary>The player's current location.</summary>
- public IValueWatcher<GameLocation> LocationWatcher { get; }
+ public IValueWatcher<GameLocation?> LocationWatcher { get; }
/// <summary>Tracks changes to the player's skill levels.</summary>
public IDictionary<SkillType, IValueWatcher<int>> SkillWatchers { get; }
@@ -51,7 +50,8 @@ namespace StardewModdingAPI.Framework.StateTracking
{
// init player data
this.Player = player;
- this.PreviousInventory = this.GetInventory();
+ this.CurrentInventory = this.GetInventory();
+ this.PreviousInventory = new Dictionary<Item, int>(this.CurrentInventory);
// init trackers
this.LocationWatcher = WatcherFactory.ForReference(this.GetCurrentLocation);
@@ -95,7 +95,7 @@ namespace StardewModdingAPI.Framework.StateTracking
/// <summary>Get the player's current location, ignoring temporary null values.</summary>
/// <remarks>The game will set <see cref="Character.currentLocation"/> to null in some cases, e.g. when they're a secondary player in multiplayer and transition to a location that hasn't been synced yet. While that's happening, this returns the player's last valid location instead.</remarks>
- public GameLocation GetCurrentLocation()
+ public GameLocation? GetCurrentLocation()
{
return this.Player.currentLocation ?? this.LastValidLocation;
}
@@ -103,7 +103,7 @@ namespace StardewModdingAPI.Framework.StateTracking
/// <summary>Get the inventory changes since the last update, if anything changed.</summary>
/// <param name="changes">The inventory changes, or <c>null</c> if nothing changed.</param>
/// <returns>Returns whether anything changed.</returns>
- public bool TryGetInventoryChanges(out SnapshotItemListDiff changes)
+ public bool TryGetInventoryChanges([NotNullWhen(true)] out SnapshotItemListDiff? changes)
{
IDictionary<Item, int> current = this.GetInventory();
@@ -124,7 +124,7 @@ namespace StardewModdingAPI.Framework.StateTracking
public void Dispose()
{
this.PreviousInventory.Clear();
- this.CurrentInventory?.Clear();
+ this.CurrentInventory.Clear();
foreach (IWatcher watcher in this.Watchers)
watcher.Dispose();
diff --git a/src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs b/src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs
index bf81a35e..6a24ec30 100644
--- a/src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs
+++ b/src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -47,17 +45,18 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots
public PlayerSnapshot(Farmer player)
{
this.Player = player;
+ this.Inventory = this.EmptyItemListDiff;
}
/// <summary>Update the tracked values.</summary>
/// <param name="watcher">The player watcher to snapshot.</param>
public void Update(PlayerTracker watcher)
{
- this.Location.Update(watcher.LocationWatcher);
- foreach (var pair in this.Skills)
- pair.Value.Update(watcher.SkillWatchers[pair.Key]);
+ this.Location.Update(watcher.LocationWatcher!);
+ foreach ((SkillType skill, var value) in this.Skills)
+ value.Update(watcher.SkillWatchers[skill]);
- this.Inventory = watcher.TryGetInventoryChanges(out SnapshotItemListDiff itemChanges)
+ this.Inventory = watcher.TryGetInventoryChanges(out SnapshotItemListDiff? itemChanges)
? itemChanges
: this.EmptyItemListDiff;
}
diff --git a/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs b/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs
index 5f0ecfa0..b5fc1f57 100644
--- a/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs
+++ b/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
// This temporary utility fixes an esoteric issue in XNA Framework where deserialization depends on
// the order of fields returned by Type.GetFields, but that order changes after Harmony/MonoMod use
// reflection to access the fields due to an issue in .NET Framework.
@@ -7,15 +5,15 @@
//
// This will be removed when Harmony/MonoMod are updated to incorporate the fix.
//
-// Special thanks to 0x0ade for submitting this worokaround! Copy/pasted and adapted from MonoMod.
+// Special thanks to 0x0ade for submitting this workaround! Copy/pasted and adapted from MonoMod.
using System;
-using System.Reflection;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using HarmonyLib;
-using System.Reflection.Emit;
// ReSharper disable once CheckNamespace -- Temporary hotfix submitted by the MonoMod author.
namespace MonoMod.Utils
@@ -26,33 +24,33 @@ namespace MonoMod.Utils
{
// .NET Framework can break member ordering if using Module.Resolve* on certain members.
- private static object[] _NoArgs = Array.Empty<object>();
- private static object[] _CacheGetterArgs = { /* MemberListType.All */ 0, /* name apparently always null? */ null };
+ private static readonly object[] _NoArgs = Array.Empty<object>();
+ private static readonly object?[] _CacheGetterArgs = { /* MemberListType.All */ 0, /* name apparently always null? */ null };
- private static Type t_RuntimeModule =
+ private static readonly Type? t_RuntimeModule =
typeof(Module).Assembly
.GetType("System.Reflection.RuntimeModule");
- private static PropertyInfo p_RuntimeModule_RuntimeType =
+ private static readonly PropertyInfo? p_RuntimeModule_RuntimeType =
typeof(Module).Assembly
.GetType("System.Reflection.RuntimeModule")
?.GetProperty("RuntimeType", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
- private static Type t_RuntimeType =
+ private static readonly Type? t_RuntimeType =
typeof(Type).Assembly
.GetType("System.RuntimeType");
- private static PropertyInfo p_RuntimeType_Cache =
+ private static readonly PropertyInfo? p_RuntimeType_Cache =
typeof(Type).Assembly
.GetType("System.RuntimeType")
?.GetProperty("Cache", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
- private static MethodInfo m_RuntimeTypeCache_GetFieldList =
+ private static readonly MethodInfo? m_RuntimeTypeCache_GetFieldList =
typeof(Type).Assembly
.GetType("System.RuntimeType+RuntimeTypeCache")
?.GetMethod("GetFieldList", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
- private static MethodInfo m_RuntimeTypeCache_GetPropertyList =
+ private static readonly MethodInfo? m_RuntimeTypeCache_GetPropertyList =
typeof(Type).Assembly
.GetType("System.RuntimeType+RuntimeTypeCache")
?.GetMethod("GetPropertyList", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
@@ -65,37 +63,37 @@ namespace MonoMod.Utils
harmony.Patch(
original: typeof(Harmony).Assembly
- .GetType("HarmonyLib.MethodBodyReader")
+ .GetType("HarmonyLib.MethodBodyReader", throwOnError: true)!
.GetMethod("ReadOperand", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance),
transpiler: new HarmonyMethod(typeof(MiniMonoModHotfix), nameof(ResolveTokenFix))
);
harmony.Patch(
original: typeof(MonoMod.Utils.ReflectionHelper).Assembly
- .GetType("MonoMod.Utils.DynamicMethodDefinition+<>c__DisplayClass3_0")
+ .GetType("MonoMod.Utils.DynamicMethodDefinition+<>c__DisplayClass3_0", throwOnError: true)!
.GetMethod("<_CopyMethodToDefinition>g__ResolveTokenAs|1", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance),
transpiler: new HarmonyMethod(typeof(MiniMonoModHotfix), nameof(ResolveTokenFix))
);
}
- private static IEnumerable<CodeInstruction> ResolveTokenFix(IEnumerable<CodeInstruction> instrs)
+ private static IEnumerable<CodeInstruction> ResolveTokenFix(IEnumerable<CodeInstruction> instructions)
{
- MethodInfo getdecl = typeof(MiniMonoModHotfix).GetMethod(nameof(GetRealDeclaringType));
- MethodInfo fixup = typeof(MiniMonoModHotfix).GetMethod(nameof(FixReflectionCache));
+ MethodInfo getRealDeclaringType = typeof(MiniMonoModHotfix).GetMethod(nameof(MiniMonoModHotfix.GetRealDeclaringType)) ?? throw new InvalidOperationException($"Can't get required method {nameof(MiniMonoModHotfix)}.{nameof(GetRealDeclaringType)}");
+ MethodInfo fixReflectionCache = typeof(MiniMonoModHotfix).GetMethod(nameof(MiniMonoModHotfix.FixReflectionCache)) ?? throw new InvalidOperationException($"Can't get required method {nameof(MiniMonoModHotfix)}.{nameof(FixReflectionCache)}");
- foreach (CodeInstruction instr in instrs)
+ foreach (CodeInstruction instruction in instructions)
{
- yield return instr;
+ yield return instruction;
- if (instr.operand is MethodInfo called)
+ if (instruction.operand is MethodInfo called)
{
switch (called.Name)
{
case "ResolveType":
// type.FixReflectionCache();
yield return new CodeInstruction(OpCodes.Dup);
- yield return new CodeInstruction(OpCodes.Call, fixup);
+ yield return new CodeInstruction(OpCodes.Call, fixReflectionCache);
break;
case "ResolveMember":
@@ -103,15 +101,15 @@ namespace MonoMod.Utils
case "ResolveField":
// member.GetRealDeclaringType().FixReflectionCache();
yield return new CodeInstruction(OpCodes.Dup);
- yield return new CodeInstruction(OpCodes.Call, getdecl);
- yield return new CodeInstruction(OpCodes.Call, fixup);
+ yield return new CodeInstruction(OpCodes.Call, getRealDeclaringType);
+ yield return new CodeInstruction(OpCodes.Call, fixReflectionCache);
break;
}
}
}
}
- public static Type GetModuleType(this Module module)
+ public static Type? GetModuleType(this Module? module)
{
// Sadly we can't blindly resolve type 0x02000001 as the runtime throws ArgumentException.
@@ -120,22 +118,21 @@ namespace MonoMod.Utils
// .NET
if (p_RuntimeModule_RuntimeType != null)
- return (Type)p_RuntimeModule_RuntimeType.GetValue(module, _NoArgs);
+ return (Type?)p_RuntimeModule_RuntimeType.GetValue(module, _NoArgs);
// The hotfix doesn't apply to Mono anyway, thus that's not copied over.
return null;
}
- public static Type GetRealDeclaringType(this MemberInfo member)
- => member.DeclaringType ?? member.Module.GetModuleType();
+ public static Type? GetRealDeclaringType(this MemberInfo member)
+ {
+ return member.DeclaringType ?? member.Module.GetModuleType();
+ }
- public static void FixReflectionCache(this Type type)
+ public static void FixReflectionCache(this Type? type)
{
- if (t_RuntimeType == null ||
- p_RuntimeType_Cache == null ||
- m_RuntimeTypeCache_GetFieldList == null ||
- m_RuntimeTypeCache_GetPropertyList == null)
+ if (t_RuntimeType == null || p_RuntimeType_Cache == null || m_RuntimeTypeCache_GetFieldList == null || m_RuntimeTypeCache_GetPropertyList == null)
return;
for (; type != null; type = type.DeclaringType)
@@ -145,21 +142,17 @@ namespace MonoMod.Utils
if (!t_RuntimeType.IsInstanceOfType(type))
continue;
- CacheFixEntry entry = _CacheFixed.GetValue(type, rt => {
- CacheFixEntry entryNew = new();
- object cache;
- Array properties, fields;
-
+ CacheFixEntry entry = _CacheFixed.GetValue(type, rt =>
+ {
// All RuntimeTypes MUST have a cache, the getter is non-virtual, it creates on demand and asserts non-null.
- entryNew.Cache = cache = p_RuntimeType_Cache.GetValue(rt, _NoArgs);
- entryNew.Properties = properties = _GetArray(cache, m_RuntimeTypeCache_GetPropertyList);
- entryNew.Fields = fields = _GetArray(cache, m_RuntimeTypeCache_GetFieldList);
+ object cache = MiniMonoModHotfix.p_RuntimeType_Cache.GetValue(rt, MiniMonoModHotfix._NoArgs)!;
+ Array properties = MiniMonoModHotfix._GetArray(cache, MiniMonoModHotfix.m_RuntimeTypeCache_GetPropertyList);
+ Array fields = MiniMonoModHotfix._GetArray(cache, MiniMonoModHotfix.m_RuntimeTypeCache_GetFieldList);
_FixReflectionCacheOrder<PropertyInfo>(properties);
_FixReflectionCacheOrder<FieldInfo>(fields);
- entryNew.NeedsVerify = false;
- return entryNew;
+ return new CacheFixEntry(cache, properties, fields, needsVerify: false);
});
if (entry.NeedsVerify && !_Verify(entry, type))
@@ -177,44 +170,43 @@ namespace MonoMod.Utils
private static bool _Verify(CacheFixEntry entry, Type type)
{
- object cache;
- Array properties, fields;
-
// The cache can sometimes be invalidated.
// TODO: Figure out if only the arrays get replaced or if the entire cache object gets replaced!
- if (entry.Cache != (cache = p_RuntimeType_Cache.GetValue(type, _NoArgs)))
+ object cache = p_RuntimeType_Cache!.GetValue(type, _NoArgs)!;
+ if (entry.Cache != cache)
{
entry.Cache = cache;
- entry.Properties = _GetArray(cache, m_RuntimeTypeCache_GetPropertyList);
- entry.Fields = _GetArray(cache, m_RuntimeTypeCache_GetFieldList);
+ entry.Properties = _GetArray(cache, m_RuntimeTypeCache_GetPropertyList!);
+ entry.Fields = _GetArray(cache, m_RuntimeTypeCache_GetFieldList!);
return false;
}
- else if (entry.Properties != (properties = _GetArray(cache, m_RuntimeTypeCache_GetPropertyList)))
+
+ Array properties = _GetArray(cache, m_RuntimeTypeCache_GetPropertyList!);
+ if (entry.Properties != properties)
{
entry.Properties = properties;
- entry.Fields = _GetArray(cache, m_RuntimeTypeCache_GetFieldList);
+ entry.Fields = _GetArray(cache, m_RuntimeTypeCache_GetFieldList!);
return false;
-
}
- else if (entry.Fields != (fields = _GetArray(cache, m_RuntimeTypeCache_GetFieldList)))
+
+ Array fields = _GetArray(cache, m_RuntimeTypeCache_GetFieldList!);
+ if (entry.Fields != fields)
{
entry.Fields = fields;
return false;
}
- else
- {
- // Cache should still be the same, no re-fix necessary.
- return true;
- }
+
+ // Cache should still be the same, no re-fix necessary.
+ return true;
}
private static Array _GetArray(object cache, MethodInfo getter)
{
// Get and discard once, otherwise we might not be getting the actual backing array.
getter.Invoke(cache, _CacheGetterArgs);
- return (Array)getter.Invoke(cache, _CacheGetterArgs);
+ return (Array)getter.Invoke(cache, _CacheGetterArgs)!;
}
private static void _FixReflectionCacheOrder<T>(Array orig) where T : MemberInfo
@@ -222,7 +214,7 @@ namespace MonoMod.Utils
// Sort using a short-lived list.
List<T> list = new List<T>(orig.Length);
for (int i = 0; i < orig.Length; i++)
- list.Add((T)orig.GetValue(i));
+ list.Add((T)orig.GetValue(i)!);
list.Sort((a, b) => a.MetadataToken - b.MetadataToken);
@@ -232,10 +224,18 @@ namespace MonoMod.Utils
private class CacheFixEntry
{
- public object Cache;
+ public object? Cache;
public Array Properties;
public Array Fields;
public bool NeedsVerify;
+
+ public CacheFixEntry(object? cache, Array properties, Array fields, bool needsVerify)
+ {
+ this.Cache = cache;
+ this.Properties = properties;
+ this.Fields = fields;
+ this.NeedsVerify = needsVerify;
+ }
}
}
}